Skip to content

Commit 501c775

Browse files
SamuelHassinerichard-julien
authored andcommitted
[backend] Improve hub/children synchronization pattern (#6253)
Co-authored-by: Julien Richard <julien.richard@filigran.io>
1 parent 50904fb commit 501c775

File tree

10 files changed

+72
-57
lines changed

10 files changed

+72
-57
lines changed

opencti-platform/opencti-front/src/schema/relay.schema.graphql

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7522,7 +7522,7 @@ type ExternalReferenceEditMutations {
75227522
relationAdd(input: StixRefRelationshipAddInput!): StixRefRelationship
75237523
relationDelete(fromId: StixRef!, relationship_type: String!): ExternalReference
75247524
askEnrichment(connectorId: ID!): Work
7525-
importPush(file: Upload!, version: String, noTriggerImport: Boolean): File
7525+
importPush(file: Upload!, version: DateTime, noTriggerImport: Boolean): File
75267526
}
75277527

75287528
type KillChainPhaseEditMutations {
@@ -7542,7 +7542,7 @@ type StixCoreObjectEditMutations {
75427542
restrictionOrganizationAdd(organizationId: ID!): StixCoreObject
75437543
restrictionOrganizationDelete(organizationId: ID!): StixCoreObject
75447544
askEnrichment(connectorId: ID!): Work
7545-
importPush(file: Upload!, version: String, noTriggerImport: Boolean): File
7545+
importPush(file: Upload!, version: DateTime, noTriggerImport: Boolean): File
75467546
exportAsk(format: String!, exportType: String!, maxMarkingDefinition: String): [File!]
75477547
exportPush(file: Upload!): Boolean
75487548
}
@@ -7563,7 +7563,7 @@ type StixDomainObjectEditMutations {
75637563
relationAdd(input: StixRefRelationshipAddInput!): StixRefRelationship
75647564
relationsAdd(input: StixRefRelationshipsAddInput!): StixDomainObject
75657565
relationDelete(toId: StixRef!, relationship_type: String): StixDomainObject
7566-
importPush(file: Upload!, version: String, noTriggerImport: Boolean): File
7566+
importPush(file: Upload!, version: DateTime, noTriggerImport: Boolean): File
75677567
exportAsk(format: String!, exportType: String!, maxMarkingDefinition: String): [File!]
75687568
exportPush(file: Upload!): Boolean
75697569
stixDomainObjectFileEdit(input: StixDomainObjectFileEditInput): StixDomainObject
@@ -7796,7 +7796,7 @@ type StixCyberObservableEditMutations {
77967796
relationsAdd(input: StixRefRelationshipsAddInput!): StixCyberObservable
77977797
relationDelete(toId: StixRef!, relationship_type: String!): StixCyberObservable
77987798
promote: StixCyberObservable
7799-
importPush(file: Upload!, version: String, noTriggerImport: Boolean): File
7799+
importPush(file: Upload!, version: DateTime, noTriggerImport: Boolean): File
78007800
exportAsk(format: String!, exportType: String!, maxMarkingDefinition: String): [File!]
78017801
exportPush(file: Upload!): Boolean
78027802
}

opencti-platform/opencti-graphql/config/schema/opencti.graphql

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12014,7 +12014,7 @@ type ExternalReferenceEditMutations {
1201412014
relationAdd(input: StixRefRelationshipAddInput!): StixRefRelationship @auth(for: [KNOWLEDGE_KNUPDATE])
1201512015
relationDelete(fromId: StixRef!, relationship_type: String!): ExternalReference @auth(for: [KNOWLEDGE_KNUPDATE])
1201612016
askEnrichment(connectorId: ID!): Work @auth(for: [KNOWLEDGE_KNENRICHMENT])
12017-
importPush(file: Upload!, version: String, noTriggerImport: Boolean): File @auth(for: [KNOWLEDGE_KNUPLOAD])
12017+
importPush(file: Upload!, version: DateTime, noTriggerImport: Boolean): File @auth(for: [KNOWLEDGE_KNUPLOAD])
1201812018
}
1201912019
type KillChainPhaseEditMutations {
1202012020
delete: ID
@@ -12035,7 +12035,7 @@ type StixCoreObjectEditMutations {
1203512035
restrictionOrganizationAdd(organizationId: ID!): StixCoreObject @auth(for: [KNOWLEDGE_KNUPDATE_KNORGARESTRICT])
1203612036
restrictionOrganizationDelete(organizationId: ID!): StixCoreObject @auth(for: [KNOWLEDGE_KNUPDATE_KNORGARESTRICT])
1203712037
askEnrichment(connectorId: ID!): Work @auth(for: [KNOWLEDGE_KNENRICHMENT])
12038-
importPush(file: Upload!, version: String, noTriggerImport: Boolean): File @auth(for: [KNOWLEDGE_KNUPLOAD])
12038+
importPush(file: Upload!, version: DateTime, noTriggerImport: Boolean): File @auth(for: [KNOWLEDGE_KNUPLOAD])
1203912039
exportAsk(format: String!, exportType: String!, maxMarkingDefinition: String): [File!]
1204012040
@auth(for: [KNOWLEDGE_KNGETEXPORT_KNASKEXPORT])
1204112041
exportPush(file: Upload!): Boolean @auth(for: [CONNECTORAPI])
@@ -12060,7 +12060,7 @@ type StixDomainObjectEditMutations {
1206012060
relationAdd(input: StixRefRelationshipAddInput!): StixRefRelationship @auth(for: [KNOWLEDGE_KNUPDATE])
1206112061
relationsAdd(input: StixRefRelationshipsAddInput!): StixDomainObject @auth(for: [KNOWLEDGE_KNUPDATE])
1206212062
relationDelete(toId: StixRef!, relationship_type: String): StixDomainObject @auth(for: [KNOWLEDGE_KNUPDATE])
12063-
importPush(file: Upload!, version: String, noTriggerImport: Boolean): File @auth(for: [KNOWLEDGE_KNUPLOAD])
12063+
importPush(file: Upload!, version: DateTime, noTriggerImport: Boolean): File @auth(for: [KNOWLEDGE_KNUPLOAD])
1206412064
exportAsk(format: String!, exportType: String!, maxMarkingDefinition: String): [File!]
1206512065
@auth(for: [KNOWLEDGE_KNGETEXPORT_KNASKEXPORT])
1206612066
exportPush(file: Upload!): Boolean @auth(for: [CONNECTORAPI])
@@ -12290,7 +12290,7 @@ type StixCyberObservableEditMutations {
1229012290
relationsAdd(input: StixRefRelationshipsAddInput!): StixCyberObservable @auth(for: [KNOWLEDGE_KNUPDATE])
1229112291
relationDelete(toId: StixRef!, relationship_type: String!): StixCyberObservable @auth(for: [KNOWLEDGE_KNUPDATE])
1229212292
promote: StixCyberObservable @auth(for: [KNOWLEDGE_KNUPDATE])
12293-
importPush(file: Upload!, version: String, noTriggerImport: Boolean): File @auth(for: [KNOWLEDGE_KNUPLOAD])
12293+
importPush(file: Upload!, version: DateTime, noTriggerImport: Boolean): File @auth(for: [KNOWLEDGE_KNUPLOAD])
1229412294
exportAsk(format: String!, exportType: String!, maxMarkingDefinition: String): [File!]
1229512295
@auth(for: [KNOWLEDGE_KNGETEXPORT_KNASKEXPORT])
1229612296
exportPush(file: Upload!): Boolean @auth(for: [CONNECTORAPI])

opencti-platform/opencti-graphql/src/database/file-storage.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { defaultProvider } from '@aws-sdk/credential-provider-node';
77
import { getDefaultRoleAssumerWithWebIdentity } from '@aws-sdk/client-sts';
88
import mime from 'mime-types';
99
import conf, { booleanConf, ENABLED_FILE_INDEX_MANAGER, logApp } from '../config/conf';
10-
import { now, sinceNowInMinutes, truncate } from '../utils/format';
10+
import { now, sinceNowInMinutes, truncate, utcDate } from '../utils/format';
1111
import { DatabaseError, FunctionalError, UnsupportedError } from '../config/errors';
1212
import { createWork, deleteWorkForFile, deleteWorkForSource } from '../domain/work';
1313
import { isNotEmptyField } from './utils';
@@ -328,7 +328,7 @@ export const upload = async (context, user, filePath, fileUpload, opts) => {
328328
const key = `${filePath}/${truncatedFileName}`;
329329
const currentFile = await documentFindById(context, user, key);
330330
if (currentFile) {
331-
if (currentFile.metaData?.version === metadata.version) {
331+
if (utcDate(currentFile.metaData.version).isSameOrAfter(utcDate(metadata.version))) {
332332
return { upload: currentFile, untouched: true };
333333
}
334334
if (errorOnExisting) {

opencti-platform/opencti-graphql/src/database/middleware.js

Lines changed: 42 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1359,8 +1359,9 @@ const innerUpdateAttribute = (instance, rawInput) => {
13591359
}
13601360
return input;
13611361
};
1362-
const prepareAttributesForUpdate = (instance, elements, upsert) => {
1362+
const prepareAttributesForUpdate = async (context, user, instance, elements, upsert) => {
13631363
const instanceType = instance.entity_type;
1364+
const platformStatuses = await getEntitiesListFromCache(context, user, ENTITY_TYPE_STATUS);
13641365
return elements.map((input) => {
13651366
// Dynamic cases, attributes not defined in the schema
13661367
if (input.key.startsWith(RULE_PREFIX) || input.key.startsWith(REL_INDEX_PREFIX)) {
@@ -1371,6 +1372,37 @@ const prepareAttributesForUpdate = (instance, elements, upsert) => {
13711372
if (!def) {
13721373
throw UnsupportedError('Cant prepare attribute for update', { type: instance.entity_type, name: input.key });
13731374
}
1375+
// Specific case for Label
1376+
if (input.key === VALUE_FIELD && instanceType === ENTITY_TYPE_LABEL) {
1377+
return {
1378+
key: input.key,
1379+
value: input.value.map((v) => v.toLowerCase())
1380+
};
1381+
}
1382+
// Specific case for name in aliased entities
1383+
// If name change already inside aliases, name must be kep untouched
1384+
if (upsert && input.key === NAME_FIELD && isStixObjectAliased(instanceType)) {
1385+
const aliasField = resolveAliasesField(instanceType).name;
1386+
const normalizeAliases = instance[aliasField] ? instance[aliasField].map((e) => normalizeName(e)) : [];
1387+
const name = normalizeName(input.value.at(0));
1388+
if ((normalizeAliases).includes(name)) {
1389+
return null;
1390+
}
1391+
}
1392+
// Aliases can't have the same name as entity name and an already existing normalized alias
1393+
if (input.key === ATTRIBUTE_ALIASES || input.key === ATTRIBUTE_ALIASES_OPENCTI) {
1394+
const filteredValues = input.value.filter((e) => normalizeName(e) !== normalizeName(instance.name));
1395+
const uniqAliases = R.uniqBy((e) => normalizeName(e), filteredValues);
1396+
return { key: input.key, value: uniqAliases };
1397+
}
1398+
// For upsert, workflow cant be reset or setup on un-existing workflow
1399+
if (input.key === X_WORKFLOW_ID && upsert) {
1400+
const workflowId = R.head(input.value);
1401+
const workflowStatus = workflowId ? platformStatuses.find((p) => p.id === workflowId) : workflowId;
1402+
if (isEmptyField(workflowStatus)) { // If workflow is not found, remove the input
1403+
return null;
1404+
}
1405+
}
13741406
// Check integer
13751407
if (def.type === 'numeric') {
13761408
return {
@@ -1409,29 +1441,6 @@ const prepareAttributesForUpdate = (instance, elements, upsert) => {
14091441
};
14101442
}
14111443
}
1412-
// Specific case for Label
1413-
if (input.key === VALUE_FIELD && instanceType === ENTITY_TYPE_LABEL) {
1414-
return {
1415-
key: input.key,
1416-
value: input.value.map((v) => v.toLowerCase())
1417-
};
1418-
}
1419-
// Specific case for name in aliased entities
1420-
// If name change already inside aliases, name must be kep untouched
1421-
if (upsert && input.key === NAME_FIELD && isStixObjectAliased(instanceType)) {
1422-
const aliasField = resolveAliasesField(instanceType).name;
1423-
const normalizeAliases = instance[aliasField] ? instance[aliasField].map((e) => normalizeName(e)) : [];
1424-
const name = normalizeName(input.value.at(0));
1425-
if ((normalizeAliases).includes(name)) {
1426-
return null;
1427-
}
1428-
}
1429-
// Aliases can't have the same name as entity name and an already existing normalized alias
1430-
if (input.key === ATTRIBUTE_ALIASES || input.key === ATTRIBUTE_ALIASES_OPENCTI) {
1431-
const filteredValues = input.value.filter((e) => normalizeName(e) !== normalizeName(instance.name));
1432-
const uniqAliases = R.uniqBy((e) => normalizeName(e), filteredValues);
1433-
return { key: input.key, value: uniqAliases };
1434-
}
14351444
// No need to rework the input
14361445
return input;
14371446
}).filter((i) => isNotEmptyField(i));
@@ -1465,7 +1474,7 @@ const updateAttributeRaw = async (context, user, instance, inputs, opts = {}) =>
14651474
const elements = Array.isArray(inputs) ? inputs : [inputs];
14661475
const instanceType = instance.entity_type;
14671476
// Prepare attributes
1468-
const preparedElements = prepareAttributesForUpdate(instance, elements, upsert);
1477+
const preparedElements = await prepareAttributesForUpdate(context, user, instance, elements, upsert);
14691478
// region Check date range
14701479
const inputKeys = elements.map((i) => i.key);
14711480
if (inputKeys.includes(START_TIME) || inputKeys.includes(STOP_TIME)) {
@@ -2356,8 +2365,9 @@ const upsertElement = async (context, user, element, type, basePatch, opts = {})
23562365
const updatePatch = { ...basePatch };
23572366
// Handle attributes updates
23582367
if (isNotEmptyField(basePatch.stix_id) || isNotEmptyField(basePatch.x_opencti_stix_ids)) {
2368+
const compareIds = [element.standard_id, generateStandardId(type, basePatch)];
23592369
const ids = [...(basePatch.x_opencti_stix_ids || [])];
2360-
if (isNotEmptyField(basePatch.stix_id) && basePatch.stix_id !== element.standard_id) {
2370+
if (isNotEmptyField(basePatch.stix_id) && !compareIds.includes(basePatch.stix_id)) {
23612371
ids.push(basePatch.stix_id);
23622372
}
23632373
if (ids.length > 0) {
@@ -3081,7 +3091,8 @@ const createEntityRaw = async (context, user, rawInput, type, opts = {}) => {
30813091
const standardId = resolvedInput.standard_id || generateStandardId(type, resolvedInput);
30823092
// Check if the entity exists, must be done with SYSTEM USER to really find it.
30833093
const existingEntities = [];
3084-
const existingByIdsPromise = internalFindByIds(context, SYSTEM_USER, participantIds, { type });
3094+
const finderIds = [...participantIds, ...(context.previousStandard ? [context.previousStandard] : [])];
3095+
const existingByIdsPromise = internalFindByIds(context, SYSTEM_USER, finderIds, { type });
30853096
// Hash are per definition keys.
30863097
// When creating a hash, we can check all hashes to update or merge the result
30873098
// Generating multiple standard ids could be a solution but to complex to implements
@@ -3151,8 +3162,8 @@ const createEntityRaw = async (context, user, rawInput, type, opts = {}) => {
31513162
const concurrentAliases = R.flatten(R.map((c) => [c[key], c.name], concurrentEntities));
31523163
const normedAliases = R.uniq(concurrentAliases.map((c) => normalizeName(c)));
31533164
const filteredAliases = R.filter((i) => !normedAliases.includes(normalizeName(i)), resolvedInput[key] || []);
3154-
const inputAliases = { ...resolvedInput, [key]: filteredAliases };
3155-
return upsertElement(context, user, existingByStandard, type, inputAliases, { ...opts, locks: participantIds });
3165+
const resolvedAliases = { ...resolvedInput, [key]: filteredAliases };
3166+
return upsertElement(context, user, existingByStandard, type, resolvedAliases, { ...opts, locks: participantIds });
31563167
}
31573168
if (resolvedInput.update === true) {
31583169
// The new one is new reference, merge all found entities
@@ -3165,7 +3176,8 @@ const createEntityRaw = async (context, user, rawInput, type, opts = {}) => {
31653176
}
31663177
if (resolvedInput.stix_id && !existingEntities.map((n) => getInstanceIds(n)).flat().includes(resolvedInput.stix_id)) {
31673178
const target = R.head(filteredEntities);
3168-
return upsertElement(context, user, target, type, { x_opencti_stix_ids: [...target.x_opencti_stix_ids, resolvedInput.stix_id] }, { ...opts, locks: participantIds });
3179+
const resolvedStixIds = { ...target, x_opencti_stix_ids: [...target.x_opencti_stix_ids, resolvedInput.stix_id] };
3180+
return upsertElement(context, user, target, type, resolvedStixIds, { ...opts, locks: participantIds });
31693181
}
31703182
// If not we dont know what to do, just throw an exception.
31713183
throw UnsupportedError('Cant upsert entity. Too many entities resolved', { input, entityIds });

opencti-platform/opencti-graphql/src/domain/connector.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ export const createSyncHttpUri = (sync, state, testMode) => {
151151
return `${httpBase(uri)}stream/${stream}`;
152152
}
153153
const from = isEmptyField(state) ? '0-0' : state;
154-
const recover = sync.recover ?? sync.created_at;
154+
const recover = sync.recover ?? now();
155155
let streamUri = `${httpBase(uri)}stream/${stream}?from=${from}&listen-delete=${del}&no-dependencies=${dep}`;
156156
if (recover) {
157157
streamUri += `&recover=${recover}`;

opencti-platform/opencti-graphql/src/domain/stixCoreObject.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,7 @@ export const stixCoreObjectImportPush = async (context, user, id, file, args = {
385385
const isAutoExternal = !entitySetting ? false : entitySetting.platform_entity_files_ref;
386386
const filePath = `import/${previous.entity_type}/${internalId}`;
387387
// 01. Upload the file
388-
const meta = { version: fileVersion };
388+
const meta = { version: fileVersion?.toISOString() };
389389
if (isAutoExternal) {
390390
const key = `${filePath}/${filename}`;
391391
meta.external_reference_id = generateStandardId(ENTITY_TYPE_EXTERNAL_REFERENCE, { url: `/storage/get/${key}` });

opencti-platform/opencti-graphql/src/generated/graphql.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6759,7 +6759,7 @@ export type ExternalReferenceEditMutationsFieldPatchArgs = {
67596759
export type ExternalReferenceEditMutationsImportPushArgs = {
67606760
file: Scalars['Upload']['input'];
67616761
noTriggerImport?: InputMaybe<Scalars['Boolean']['input']>;
6762-
version?: InputMaybe<Scalars['String']['input']>;
6762+
version?: InputMaybe<Scalars['DateTime']['input']>;
67636763
};
67646764

67656765

@@ -21808,7 +21808,7 @@ export type StixCoreObjectEditMutationsExportPushArgs = {
2180821808
export type StixCoreObjectEditMutationsImportPushArgs = {
2180921809
file: Scalars['Upload']['input'];
2181021810
noTriggerImport?: InputMaybe<Scalars['Boolean']['input']>;
21811-
version?: InputMaybe<Scalars['String']['input']>;
21811+
version?: InputMaybe<Scalars['DateTime']['input']>;
2181221812
};
2181321813

2181421814

@@ -22381,7 +22381,7 @@ export type StixCyberObservableEditMutationsFieldPatchArgs = {
2238122381
export type StixCyberObservableEditMutationsImportPushArgs = {
2238222382
file: Scalars['Upload']['input'];
2238322383
noTriggerImport?: InputMaybe<Scalars['Boolean']['input']>;
22384-
version?: InputMaybe<Scalars['String']['input']>;
22384+
version?: InputMaybe<Scalars['DateTime']['input']>;
2238522385
};
2238622386

2238722387

@@ -22673,7 +22673,7 @@ export type StixDomainObjectEditMutationsFieldPatchArgs = {
2267322673
export type StixDomainObjectEditMutationsImportPushArgs = {
2267422674
file: Scalars['Upload']['input'];
2267522675
noTriggerImport?: InputMaybe<Scalars['Boolean']['input']>;
22676-
version?: InputMaybe<Scalars['String']['input']>;
22676+
version?: InputMaybe<Scalars['DateTime']['input']>;
2267722677
};
2267822678

2267922679

opencti-platform/opencti-graphql/src/graphql/graphql.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ const createApolloServer = () => {
7878
executeContext.req = req;
7979
executeContext.res = res;
8080
executeContext.synchronizedUpsert = req.headers['synchronized-upsert'] === 'true';
81+
executeContext.previousStandard = req.headers['previous-standard'];
8182
executeContext.workId = req.headers['opencti-work-id'];
8283
try {
8384
const user = await authenticateUserFromRequest(executeContext, req, res);

opencti-platform/opencti-graphql/src/manager/syncManager.js

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
import * as R from 'ramda';
21
import EventSource from 'eventsource';
32
import { clearIntervalAsync, setIntervalAsync } from 'set-interval-async/fixed';
4-
import * as jsonpatch from 'fast-json-patch';
53
import conf, { booleanConf, getPlatformHttpProxyAgent, logApp } from '../config/conf';
64
import { executionContext, SYSTEM_USER } from '../utils/access';
75
import { TYPE_LOCK_ERROR } from '../config/errors';
@@ -80,13 +78,9 @@ const syncManagerInstance = (syncId) => {
8078
};
8179
const transformDataWithReverseIdAndFilesData = async (sync, httpClient, data, context) => {
8280
const { uri } = sync;
83-
let processingData = data;
81+
const processingData = { ...data };
8482
// Reverse patch the id if modified
85-
const idOperations = (context?.reverse_patch ?? []).filter((patch) => patch.path === '/id');
86-
if (idOperations.length > 0) {
87-
const { newDocument: stixPreviousID } = jsonpatch.applyPatch(R.clone(data), idOperations);
88-
processingData = stixPreviousID;
89-
}
83+
const idOperation = (context?.reverse_patch ?? []).find((patch) => patch.path === '/id');
9084
// Handle file enrichment
9185
const entityFiles = processingData.extensions[STIX_EXT_OCTI].files ?? [];
9286
for (let index = 0; index < entityFiles.length; index += 1) {
@@ -95,7 +89,7 @@ const syncManagerInstance = (syncId) => {
9589
const response = await httpClient.get(`${httpBase(uri)}${fileUri.substring(fileUri.indexOf('storage/get'))}`);
9690
entityFile.data = Buffer.from(response.data, 'utf-8').toString('base64');
9791
}
98-
return processingData;
92+
return { data: processingData, previous_standard: idOperation?.value };
9993
};
10094
const saveCurrentState = async (context, type, sync, eventId) => {
10195
const currentTime = new Date().getTime();
@@ -139,11 +133,17 @@ const syncManagerInstance = (syncId) => {
139133
if (eventType === 'heartbeat') {
140134
await saveCurrentState(context, eventType, sync, eventId);
141135
} else {
142-
const syncData = await transformDataWithReverseIdAndFilesData(sync, httpClient, data, eventContext);
136+
const { data: syncData, previous_standard } = await transformDataWithReverseIdAndFilesData(sync, httpClient, data, eventContext);
143137
const enrichedEvent = JSON.stringify({ id: eventId, type: eventType, data: syncData, context: eventContext });
144138
const content = Buffer.from(enrichedEvent, 'utf-8').toString('base64');
145139
// Applicant_id should be a userId coming from synchronizer
146-
await pushToSync({ type: 'event', synchronized, update: true, applicant_id: sync.user_id ?? OPENCTI_SYSTEM_UUID, content });
140+
await pushToSync({
141+
type: 'event',
142+
synchronized,
143+
previous_standard,
144+
update: true,
145+
applicant_id: sync.user_id ?? OPENCTI_SYSTEM_UUID,
146+
content });
147147
await saveCurrentState(context, 'event', sync, eventId);
148148
}
149149
} catch (e) {

0 commit comments

Comments
 (0)