Skip to content

Commit 99fd2bb

Browse files
authored
[connectors-sdk] Compute BaseIdentifiedObject.id on validation and cache its value (#6034)
1 parent e480e85 commit 99fd2bb

File tree

3 files changed

+51
-79
lines changed

3 files changed

+51
-79
lines changed

connectors-sdk/connectors_sdk/models/base_identified_entity.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,12 @@
1010
from connectors_sdk.models.external_reference import ExternalReference
1111
from connectors_sdk.models.reference import Reference
1212
from connectors_sdk.models.tlp_marking import TLPMarking
13-
from pydantic import (
14-
Field,
15-
PrivateAttr,
16-
)
13+
from pydantic import Field
1714

1815

1916
class BaseIdentifiedEntity(BaseIdentifiedObject, ABC):
2017
"""Base class that can be identified thanks to a stix-like id."""
2118

22-
_stix2_id: str | None = PrivateAttr(default=None)
23-
2419
author: BaseAuthorEntity | Reference | None = Field(
2520
default=None,
2621
description="The Author reporting this Observable.",
Lines changed: 36 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
"""BaseIdentifiedObject."""
22

3-
from __future__ import annotations
4-
53
import warnings
64
from abc import ABC
7-
from typing import Any
85

6+
import stix2
97
from connectors_sdk.models.base_object import BaseObject
108
from pydantic import (
119
PrivateAttr,
@@ -19,37 +17,34 @@ class BaseIdentifiedObject(BaseObject, ABC):
1917

2018
_stix2_id: str | None = PrivateAttr(default=None)
2119

22-
def model_post_init(self, context__: Any) -> None:
23-
"""Define the post initialization method, automatically called after __init__ in a pydantic model initialization.
20+
@computed_field # type: ignore[prop-decorator]
21+
# Typing known issue : see https://docs.pydantic.dev/2.3/usage/computed_fields/ (consulted on 2025-06-06)
22+
@property
23+
def id(self) -> str:
24+
"""Return the unique identifier of the entity.
25+
The value is computed from the STIX2 object representation of the entity, and then cached for future use.
2426
2527
Notes:
26-
This allows a last modification of the pydantic Model before it is validated.
27-
28-
Args:
29-
context__(Any): The pydantic context used by pydantic framework.
30-
31-
References:
32-
https://docs.pydantic.dev/latest/api/base_model/#pydantic.BaseModel.model_parametrized_name [consulted on
33-
October 4th, 2024]
34-
28+
The decorator `@computed_field` is used to indicate that this property
29+
must be considered as a field by pydantic, and then included in the model **serialization** processes.
30+
This field is **not** validated by pydantic, neither during the model initialization, nor during the model update.
3531
"""
36-
_ = context__ # Unused parameter, but required by pydantic
3732
if self._stix2_id is None:
38-
self._stix2_id = self.id
33+
stix_object: stix2.v21._STIXBase21 = self.to_stix2_object()
34+
stix_id = stix_object.get("id")
3935

40-
@computed_field # type: ignore[prop-decorator]
41-
# known issue : see https://docs.pydantic.dev/2.3/usage/computed_fields/ (consulted on 2025-06-06)
42-
@property
43-
def id(self) -> str:
44-
"""Return the unique identifier of the entity."""
45-
stix_id: str = self.to_stix2_object().get("id", "")
46-
self._stix2_id = stix_id
47-
return stix_id
36+
# The 'id' property is required and must be a non-empty string for model validation
37+
if not (isinstance(stix_id, str) and stix_id.strip()):
38+
raise ValueError("The 'id' property can't be set.")
39+
40+
self._stix2_id = stix_id
41+
42+
return self._stix2_id
4843

4944
# https://github.com/pydantic/pydantic/discussions/10098
5045
@model_validator(mode="after")
51-
def _check_id(self) -> BaseIdentifiedObject:
52-
"""Ensure the id is correctly set and alert if it has changed.
46+
def _compute_stix_id(self) -> "BaseIdentifiedObject":
47+
"""Compute STIX ID on validation (instance creation or re-assignments).
5348
5449
Raises:
5550
ValueError: If the id is not set.
@@ -73,19 +68,23 @@ def _check_id(self) -> BaseIdentifiedObject:
7368
'identity--011fe1ae-7b92-4779-9eb5-7be2aeffb9e1'
7469
7570
"""
76-
if self._stix2_id is None or self._stix2_id == "":
77-
raise ValueError("The 'id' property must be set.")
78-
79-
if self._stix2_id != self.id:
80-
# define message before the warning to avoid self._stix2_id has already changed in the main thread
81-
message = (
82-
f"The 'id' property has changed from to {self.id}. "
83-
"This may lead to unexpected behavior in the OpenCTI platform."
84-
)
71+
previous_stix2_id = self._stix2_id
72+
self._stix2_id = None # Reset cached id
73+
current_stix2_id = self.id # Compute and cache the new id
74+
75+
if previous_stix2_id is None:
76+
# If the previous id was not set, just set it without warning
77+
return self
78+
79+
if previous_stix2_id != current_stix2_id:
80+
# Warn on `_stix2_id` change after fields re-assignment
8581
warnings.warn(
86-
message=message,
82+
message=(
83+
f"The 'id' property has changed from {previous_stix2_id} to {current_stix2_id}. "
84+
"This may lead to unexpected behavior in the connector or the OpenCTI platform."
85+
),
8786
category=UserWarning,
8887
stacklevel=2,
8988
)
90-
self._stix2_id = self.id # Update the internal id to the current one
89+
9190
return self

internal-enrichment/censys-enrichment/src/censys_enrichment/converter.py

Lines changed: 14 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
from connectors_sdk.models import (
1313
AdministrativeArea,
1414
AutonomousSystem,
15-
BaseIdentifiedEntity,
1615
BaseObject,
1716
City,
1817
Country,
@@ -22,35 +21,14 @@
2221
Note,
2322
Organization,
2423
OrganizationAuthor,
24+
Reference,
2525
Region,
2626
Relationship,
2727
Software,
2828
TLPMarking,
2929
X509Certificate,
3030
)
3131
from connectors_sdk.models.enums import HashAlgorithm, RelationshipType, TLPLevel
32-
from pydantic import Field
33-
34-
35-
class EmbeddedIdentifiedStixObject(BaseIdentifiedEntity):
36-
"""Embedded Identified STIX Object representation.
37-
38-
This class encapsulates a STIX object with an id as a dictionary and provides
39-
access to the object without copying or modifying the original data.
40-
41-
Use when you only need to read or forward the STIX object, not alter it.
42-
"""
43-
44-
stix_object: dict[str, Any] = Field()
45-
46-
@property
47-
def id(self) -> str:
48-
"""Return the STIX object's ID."""
49-
return self.stix_object["id"]
50-
51-
def to_stix2_object(self) -> dict[str, Any]:
52-
"""Return the STIX2 object representation."""
53-
return self.stix_object
5432

5533

5634
class Converter:
@@ -60,7 +38,7 @@ def __init__(self) -> None:
6038
self._common_props = {"author": self.author, "markings": [self.marking]}
6139

6240
def _generate_city(
63-
self, observable: EmbeddedIdentifiedStixObject, name: str | None
41+
self, observable: Reference, name: str | None
6442
) -> Generator[BaseObject, None, None]:
6543
if not name:
6644
return
@@ -80,7 +58,7 @@ def _generate_city(
8058
]
8159

8260
def _generate_country(
83-
self, observable: EmbeddedIdentifiedStixObject, name: str | None
61+
self, observable: Reference, name: str | None
8462
) -> Generator[BaseObject, None, Country | None]:
8563
if not name:
8664
return None
@@ -101,7 +79,7 @@ def _generate_country(
10179
return country
10280

10381
def _generate_region(
104-
self, observable: EmbeddedIdentifiedStixObject, name: str | None
82+
self, observable: Reference, name: str | None
10583
) -> Generator[BaseObject, None, None]:
10684
if not name:
10785
return
@@ -122,7 +100,7 @@ def _generate_region(
122100

123101
def _generate_administrative_area(
124102
self,
125-
observable: EmbeddedIdentifiedStixObject,
103+
observable: Reference,
126104
name: str | None,
127105
coordinates: Coordinates | None,
128106
) -> Generator[BaseObject, None, None]:
@@ -154,7 +132,7 @@ def _generate_administrative_area(
154132
]
155133

156134
def _generate_hostnames(
157-
self, observable: EmbeddedIdentifiedStixObject, dns: HostDNS | None
135+
self, observable: Reference, dns: HostDNS | None
158136
) -> Generator[BaseObject, None, None]:
159137
if not dns:
160138
return
@@ -176,7 +154,7 @@ def _generate_hostnames(
176154

177155
def _generate_organization(
178156
self,
179-
observable: EmbeddedIdentifiedStixObject,
157+
observable: Reference,
180158
name: str | None,
181159
) -> Generator[BaseObject, None, Organization | None]:
182160
if not name:
@@ -199,7 +177,7 @@ def _generate_organization(
199177

200178
def _generate_autonomous_system(
201179
self,
202-
observable: EmbeddedIdentifiedStixObject,
180+
observable: Reference,
203181
number: int | None,
204182
name: str | None,
205183
description: str | None,
@@ -226,7 +204,7 @@ def _generate_autonomous_system(
226204

227205
def _generate_software(
228206
self,
229-
observable: EmbeddedIdentifiedStixObject,
207+
observable: Reference,
230208
name: str | None,
231209
vendor: str | None,
232210
cpe: str | None,
@@ -317,7 +295,7 @@ def _generate_certificate(
317295

318296
def _generate_note(
319297
self,
320-
observable: EmbeddedIdentifiedStixObject,
298+
observable: Reference,
321299
content: str | None,
322300
publication_date: str | None,
323301
port: int | None,
@@ -335,7 +313,7 @@ def _generate_note(
335313
)
336314

337315
def _generate_services(
338-
self, observable: EmbeddedIdentifiedStixObject, services: list[Service] | None
316+
self, observable: Reference, services: list[Service] | None
339317
) -> Generator[BaseObject, None, None]:
340318
for service in services or []:
341319
for software in service.software or []:
@@ -363,7 +341,7 @@ def _generate_services(
363341
)
364342

365343
def _generate_ip(
366-
self, observable: EmbeddedIdentifiedStixObject, ip: str
344+
self, observable: Reference, ip: str
367345
) -> Generator[BaseObject, None, None | IPV4Address | IPV6Address]:
368346
ip_version = ipaddress.ip_network(ip, strict=False).version
369347
if ip_version == 4:
@@ -384,7 +362,7 @@ def _generate_ip(
384362
def generate_octi_objects(
385363
self, stix_entity: dict[str, Any], data: Host
386364
) -> Generator[BaseObject, None, None]:
387-
observable = EmbeddedIdentifiedStixObject(stix_object=stix_entity)
365+
observable = Reference(id=stix_entity.get("id"))
388366

389367
yield from [
390368
self.author,
@@ -461,7 +439,7 @@ def generate_octi_objects_from_hosts(
461439
) -> Generator[BaseObject, None, None]:
462440
for host in hosts:
463441
ip_stix = yield from self._generate_ip(
464-
observable=EmbeddedIdentifiedStixObject(stix_object=stix_entity),
442+
observable=Reference(id=stix_entity.get("id")),
465443
ip=host.ip,
466444
)
467445
yield from self.generate_octi_objects(

0 commit comments

Comments
 (0)