diff --git a/opencti-platform/opencti-graphql/src/database/middleware.js b/opencti-platform/opencti-graphql/src/database/middleware.js index bb31fcf8a4e9..e06bf3855e92 100644 --- a/opencti-platform/opencti-graphql/src/database/middleware.js +++ b/opencti-platform/opencti-graphql/src/database/middleware.js @@ -22,6 +22,7 @@ import { buildPagination, computeAverage, extractIdsFromStoreObject, + extractObjectsRestrictionsFromInputs, fillTimeSeries, INDEX_DRAFT_OBJECTS, INDEX_INFERRED_RELATIONSHIPS, @@ -2200,7 +2201,8 @@ export const updateAttributeMetaResolved = async (context, user, initial, inputs message: opts.commitMessage, external_references: references.map((ref) => convertExternalReferenceToStix(ref)) } : undefined; - const event = await storeUpdateEvent(context, user, initial, updatedInstance, message, { ...opts, commit }); + const relatedRestrictions = extractObjectsRestrictionsFromInputs(updatedInputs, initial.entity_type); + const event = await storeUpdateEvent(context, user, initial, updatedInstance, message, { ...opts, commit, related_restrictions: relatedRestrictions }); return { element: updatedInstance, event, isCreation: false }; } // Return updated element after waiting for it. diff --git a/opencti-platform/opencti-graphql/src/database/redis.ts b/opencti-platform/opencti-graphql/src/database/redis.ts index 003c02a1f57b..a0ae2226474c 100644 --- a/opencti-platform/opencti-graphql/src/database/redis.ts +++ b/opencti-platform/opencti-graphql/src/database/redis.ts @@ -524,7 +524,8 @@ export const buildStixUpdateEvent = (user: AuthUser, previousStix: StixCoreObjec commit: opts.commit, context: { patch, - reverse_patch: previousPatch + reverse_patch: previousPatch, + related_restrictions: opts.related_restrictions, } }; }; diff --git a/opencti-platform/opencti-graphql/src/database/utils.js b/opencti-platform/opencti-graphql/src/database/utils.js index 1479065be927..e8e9cee966df 100644 --- a/opencti-platform/opencti-graphql/src/database/utils.js +++ b/opencti-platform/opencti-graphql/src/database/utils.js @@ -4,16 +4,17 @@ import { Promise } from 'bluebird'; import { DatabaseError, UnsupportedError } from '../config/errors'; import { isHistoryObject, isInternalObject } from '../schema/internalObject'; import { isStixMetaObject } from '../schema/stixMetaObject'; -import { isStixDomainObject } from '../schema/stixDomainObject'; +import { isStixDomainObject, isStixDomainObjectContainer } from '../schema/stixDomainObject'; import { isStixCyberObservable } from '../schema/stixCyberObservable'; import { isInternalRelationship } from '../schema/internalRelationship'; import { isStixCoreRelationship } from '../schema/stixCoreRelationship'; import { isStixSightingRelationship } from '../schema/stixSightingRelationship'; import conf from '../config/conf'; import { now } from '../utils/format'; -import { isStixRefRelationship } from '../schema/stixRefRelationship'; +import { isStixRefRelationship, RELATION_OBJECT_MARKING } from '../schema/stixRefRelationship'; import { schemaAttributesDefinition } from '../schema/schema-attributes'; import { getDraftContext } from '../utils/draftContext'; +import { INPUT_OBJECTS } from '../schema/general'; export const ES_INDEX_PREFIX = conf.get('elasticsearch:index_prefix') || 'opencti'; const rabbitmqPrefix = conf.get('rabbitmq:queue_prefix'); @@ -326,6 +327,21 @@ export const extractIdsFromStoreObject = (instance) => { return ids; }; +export const extractObjectsRestrictionsFromInputs = (inputs, entityType) => { + const markings = []; + if (isStixDomainObjectContainer(entityType)) { + inputs.forEach((input) => { + if (input && input.key === INPUT_OBJECTS && input.value?.length > 0) { + const objectMarking = input.value.flatMap((value) => value[RELATION_OBJECT_MARKING] ?? []); + markings.push(...objectMarking); + } + }); + } + return { + markings + }; +}; + export const isObjectPathTargetMultipleAttribute = (instance, object_path) => { const preparedPath = object_path.startsWith('/') ? object_path : `/${object_path}`; const pathArray = preparedPath.split('/').filter((p) => isNotEmptyField(p)); diff --git a/opencti-platform/opencti-graphql/src/manager/historyManager.ts b/opencti-platform/opencti-graphql/src/manager/historyManager.ts index 736dcd1ca000..4941649dd9e2 100644 --- a/opencti-platform/opencti-graphql/src/manager/historyManager.ts +++ b/opencti-platform/opencti-graphql/src/manager/historyManager.ts @@ -122,6 +122,11 @@ export const buildHistoryElementsFromEvents = async (context:AuthContext, events .map((stixId) => markingsById.get(stixId)?.internal_id) .filter((o) => isNotEmptyField(o)) as string[]; eventMarkingRefs.push(...previousMarkingRefs); + // Get related restrictions (e.g. markings of added objects in a container) + if (updateEvent.context.related_restrictions) { + const relatedMarkings = updateEvent.context.related_restrictions.markings ?? []; + eventMarkingRefs.push(...relatedMarkings); + } } if (stix.type === STIX_TYPE_RELATION) { const rel: StixRelation = stix as StixRelation; diff --git a/opencti-platform/opencti-graphql/src/types/event.d.ts b/opencti-platform/opencti-graphql/src/types/event.d.ts index 3dddf84b4ba5..5448345c2385 100644 --- a/opencti-platform/opencti-graphql/src/types/event.d.ts +++ b/opencti-platform/opencti-graphql/src/types/event.d.ts @@ -19,6 +19,7 @@ interface CreateEventOpts extends EventOpts { interface UpdateEventOpts extends EventOpts { commit?: CommitContext | undefined; + related_restrictions?: { markings: string[] }; } interface RelationCreation { @@ -52,6 +53,7 @@ interface UpdateEvent extends StreamDataEvent { context: { patch: Array; reverse_patch: Array; + related_restrictions?: { markings: string[] }; }; } diff --git a/opencti-platform/opencti-graphql/tests/01-unit/database/utils-test.ts b/opencti-platform/opencti-graphql/tests/01-unit/database/utils-test.ts new file mode 100644 index 000000000000..583e5d864da2 --- /dev/null +++ b/opencti-platform/opencti-graphql/tests/01-unit/database/utils-test.ts @@ -0,0 +1,153 @@ +import { describe, expect, it } from 'vitest'; +import { extractObjectsRestrictionsFromInputs } from '../../../src/database/utils'; +import { ENTITY_TYPE_CONTAINER_REPORT, ENTITY_TYPE_MALWARE } from '../../../src/schema/stixDomainObject'; + +const inputs = [ + { + key: 'objects', + operation: 'add', + value: [ + { + _id: '4c688965-fd97-40ea-9af0-967030eb06a5', + _index: 'opencti_stix_domain_objects-000001', + aliases: [], + base_type: 'ENTITY', + confidence: 100, + created: '2024 -11-08T10:30:44.343Z', + 'created-by': 'bc9fe33d-e694-4604-abc1-82f2e99cd00a', + created_at: '2024-12-02T13:55:39.981Z', + creator_id: [ + '549e078a-41df-43aa-8e0f-ba961b16d0c8' + ], + description: '', + entity_type: 'Intrusion-Set', + 'external-reference': [], + first_seen: '1970-01-01T00:00:00.000Z', + goals: ['Military Advantage'], + i_aliases_ids: [], + id: '4c688965-fd97-40ea-9af0-967030eb06a5', + internal_id: '4c688965-fd97-40ea-9af0-967030eb06a5', + lang: 'en', + last_seen: '5138-11-16T09:46:40.000Z', + modified: '2024-12-02T13:55:40.064Z', + name: 'APT29', + 'object-label': [ + 'debcc53e-9515-4107-bbdc-8eb8084f7527' + ], + 'object-marking': [ + 'fa7fa933-7b65-463f-ac5e-aa33b2a36ce8', + '056276ff-26dc-4774-a439-a36253a96939' + ], + parent_types: [ + 'Basic-Object', + 'Stix-Object', + 'Stix-Core-Object', + 'Stix-Domain-Object' + ], + primary_motivation: 'Espionage', + 'rel_created-by.internal_id': [ + 'bc9fe33d-e694-4604-abc1-82f2e99cd00a' + ], + 'rel_external-reference.internal_id': [], + 'rel_object-label.internal_id': [ + 'debcc53e-9515-4107-bbdc-8eb8084f7527' + ], + 'rel_object-marking.internal_id': [ + 'fa7fa933-7b65-463f-ac5e-aa33b2a36ce8', + '056276ff-26dc-4774-a439-a36253a96939' + ], + resource_level: null, + revoked: false, + 'secondary_motivation s': [ + 'Military/Security/Diplomatic', + 'Ethnic/nationalist', + 'Ideological/Religious' + ], + sort: [ + 1733147739981 + ], + standard_id: 'intrusion-set--36319194-19e1-50ac-9163-778b56a1bf12', + updated_at: '2024-12-02T13:55:40.064Z', + x_opencti_stix_ids: [], + x_opencti_workflow_id: null + } + ] + } +]; + +const relInputs = [ + { + key: 'objects', + operation: 'add', + value: [ + { + _id: '23c0c086-afee-45e5-b276-872948997816', + _index: 'opencti_stix_core_relationships-000001', + base_type: 'RELATION', + confidence: 100, + created: '2024-12-0 6T08:41:59.270Z', + created_at: '2024-12-06T08:41:59.270Z', + creator_id: [ + '88ec0c6a-13ce-5e39-b486-354fe4a7084f' + ], + description: '', + entity_type: 'related-to', + from: null, + fromId: 'fd3259cb-f219-4cd6-85fe-0df16ffef185', + fromName: 'AlienVault', + fromRole: 'related-to_from', + fromType: 'Organization', + id: '23c0c086-afee-45e5-b276-872948997816', + internal_id: '23c0c086-afee-45e5-b276-872948997816', + lang: 'en', + modified: '2024-12-06T08:41:59.290Z', + 'object-marking': [ + 'eaccd139-ec2e-48d9-b2ef-a17ba6e7e938' + ], + parent_types: [ + 'basic-relationship', + 'stix-relationship', + 'stix-core-relationship' + ], + 'rel_object-marking.internal_id': [ + 'eaccd139-ec2e-48d9-b2ef-a17ba6e7e938' + ], + relationship_type: 'related-to', + revoked: false, + sort: [ + 1733474519270 + ], + source_ref: 'identity--temporary', + standard_id: 'relationship--54af1a95-b0e8-53d6-8c0c-074f57e9d58c', + start_time: '2024-12-06T08:40:55.000Z', + stop_time: '2024-12-06T08:41:55.000Z', + target_ref: 'malware--temporary', + to: null, + toId: 'd9162b45-55dd-403b-906b-a16edf74ebff', + toName: 'HAMMERTOSS', + toRole: 'related-to_to', + toType: 'Malware', + updated_at: '2024-12-06T08:41:59.290Z', + x_opencti_stix_ids: [] + } + ] + } +]; + +describe('extractObjectsRestrictionsFromInputs testing', () => { + it('should add inputs object-marking in stream when adding entity to a report', () => { + const relatedRestrictions = extractObjectsRestrictionsFromInputs(inputs, ENTITY_TYPE_CONTAINER_REPORT); + const expected = { markings: ['fa7fa933-7b65-463f-ac5e-aa33b2a36ce8', '056276ff-26dc-4774-a439-a36253a96939'] }; + expect(relatedRestrictions).toEqual(expected); + }); + it('should add inputs object-marking in stream when adding relationship to a report', () => { + const relatedRestrictions = extractObjectsRestrictionsFromInputs(relInputs, ENTITY_TYPE_CONTAINER_REPORT); + const expected = { markings: ['eaccd139-ec2e-48d9-b2ef-a17ba6e7e938'] }; + expect(relatedRestrictions).toEqual(expected); + }); + it('should not add inputs object-marking in stream if entity is not container', () => { + const relatedRestrictions = extractObjectsRestrictionsFromInputs(inputs, ENTITY_TYPE_MALWARE); + const expected = { markings: [] }; + expect(relatedRestrictions).toEqual(expected); + }); +}); diff --git a/opencti-platform/opencti-graphql/tests/02-integration/04-manager/historyManager-test.js b/opencti-platform/opencti-graphql/tests/02-integration/04-manager/historyManager-test.js index d810fa3b5aa9..588257126d5b 100644 --- a/opencti-platform/opencti-graphql/tests/02-integration/04-manager/historyManager-test.js +++ b/opencti-platform/opencti-graphql/tests/02-integration/04-manager/historyManager-test.js @@ -116,6 +116,94 @@ const eventWithGrantedRefsOnly = { } }; +const eventWithRelatedRestriction = { + id: '1733757875310-0', + event: 'update', + data: { + version: '4', + type: 'update', + scope: 'external', + message: 'adds `AlienVault` in `Contains`', + origin: { + socket: 'query', + ip: '::1', + user_id: '88ec0c6a-13ce-5e39-b486-354fe4a7084f', + group_ids: [ + 'eeacced8-cf03-4fd3-bfdb-7398839ef015' + ], + organization_ids: [], + user_metadata: {}, + referer: 'http://localhost:3000/dashboard' + }, + data: { + id: 'report--50ddc6fe-2a84-5c9a-904d-f964a94d1ff7', + spec_version: '2.1', + type: 'report', + extensions: { + 'extension-definition--ea279b3e-5c71-4632-ac08-831c66a786ba': { + extension_type: 'property-extension', + id: 'e6babfee-aa64-4e3a-9c67-1a163c178ca0', + type: 'Report', + created_at: '2024-11-18T09:43:49.623Z', + updated_at: '2024-12-09T15:24:28.686Z', + is_inferred: false, + creator_ids: [ + '88ec0c6a-13ce-5e39-b486-354fe4a7084f' + ], + assignee_ids: [ + '71432b4c-34d3-42dc-9b3d-5622b02b4954' + ], + workflow_id: 'a4b90e6f-06ae-461a-8dac-666cdb4a5ae7' + } + }, + created: '2024-11-18T09:43:40.000Z', + modified: '2024-12-09T14:18:43.125Z', + revoked: false, + confidence: 100, + lang: 'en', + object_marking_refs: [ + 'marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9' + ], + name: 'Report test', + description: 'description', + report_types: [ + 'threat-report' + ], + published: '2024-11-18T09:43:40.000Z', + object_refs: [ + 'relationship--aefe7518-a66e-5412-a26d-8a5f5eb74fe9', + 'campaign--bce98eb5-25a9-5ba7-b4a0-b160a79d0de7', + 'grouping--a5c0cace-a530-58ae-907f-eb0d8e41913f', + 'threat-actor--fd6b0e6f-96e0-568d-ba24-8a140d0428cd', + 'malware--3b0ff6e4-58fc-5312-aa59-ae755dc10189', + 'intrusion-set--36319194-19e1-50ac-9163-778b56a1bf12', + 'malware--4cc56e92-fc59-500e-966d-bcd32538c248', + 'identity--e52b2fa3-2af0-5e53-ad38-17d54b3d61cb' + ] + }, + context: { + patch: [ + { + op: 'add', + path: '/object_refs/7', + value: 'identity--e52b2fa3-2af0-5e53-ad38-17d54b3d61cb' + } + ], + reverse_patch: [ + { + op: 'remove', + path: '/object_refs/7' + } + ], + related_restrictions: { + markings: [ + '4584aeee-10b6-47a7-808e-603440642285' + ] + }, + } + } +}; + describe('History manager test resolveGrantedRefsIds', () => { it('should return empty map if granted refs ids are present', async () => { const organizationByIdsMap = await resolveGrantedRefsIds(testContext, [eventWithGrantedRefIds]); @@ -158,4 +246,18 @@ describe('history manager test buildHistoryElementsFromEvents', () => { expect(historyElement.organization_ids).toEqual(eventWithGrantedRefsOnly.data.origin.organization_ids); expect(historyElement['rel_granted.internal_id'].length).toEqual(1); }); + it('should build history with markins for related entities', async () => { + const historyElements = await buildHistoryElementsFromEvents(testContext, [eventWithRelatedRestriction]); + + expect(historyElements.length).toEqual(1); + const historyElement = historyElements[0]; + expect(historyElement.internal_id).toEqual(eventWithRelatedRestriction.id); + expect(historyElement._index).toEqual(INDEX_HISTORY); + expect(historyElement.entity_type).toEqual(ENTITY_TYPE_HISTORY); + expect(historyElement.event_type).toEqual('mutation'); + expect(historyElement.event_scope).toEqual(eventWithRelatedRestriction.event); + expect(historyElement.user_id).toEqual(eventWithRelatedRestriction.data.origin.user_id); + expect(historyElement.group_ids).toEqual(eventWithRelatedRestriction.data.origin.group_ids); + expect(historyElement['rel_object-marking.internal_id'].length).toEqual(2); + }); });