Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ workflows:
- test
filters:
tags:
only: /[0-9]+(\.[0-9]+)+(\.[0-9]+)*/
only: /[0-9]+(\.[0-9]+)+(\.[0-9]+)*(-lts\.[0-9]*)?/
branches:
ignore: /.*/
- build:
Expand Down
3 changes: 2 additions & 1 deletion ci-requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
isort ==7.0.0
black ==26.1.0
pytest ==8.4.2
pytest ==8.4.2
uv
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ This validator:
The SDK introduces `DeprecatedField`, a helper built on top of `pydantic.Field`, allowing deprecation metadata to be declared directly on fields.

Supported metadata includes:
- `deprecated`: deprecation flag or message (already present on pydanctic.Field)
- `deprecated`: deprecation flag or message (already present on pydantic.Field)
- `new_namespace`: destination namespace
- `new_namespaced_var`: destination variable name
- `new_value_factory`: optional value transformation function
Expand Down Expand Up @@ -108,9 +108,14 @@ Variable migration supports:

### Deprecated variable inside a configuration model

Preferred: using `DeprecatedField`:

```python
from pydantic import Field
from connectors_sdk import BaseConfigModel, DeprecatedField

class MyConfig(BaseConfigModel):
old_var: SkipValidation[int] = DeprecatedField(
old_var: int = DeprecatedField(
deprecated="Use new_var instead",
new_namespaced_var="new_var",
new_value_factory=lambda x: x * 60, # Optional transformation
Expand All @@ -119,11 +124,33 @@ class MyConfig(BaseConfigModel):
new_var: int = Field(description="New variable")
```

Alternative: using `Deprecate` annotation metadata:

```python
from typing import Annotated
from pydantic import Field
from connectors_sdk import BaseConfigModel, Deprecate

class MyConfig(BaseConfigModel):
old_var: Annotated[
int,
Deprecate(
new_namespaced_var="new_var",
new_value_factory=lambda x: x * 60, # Optional transformation
removal_date="2026-12-31", # Optional informative removal deadline
),
]
new_var: int = Field(description="New variable")
```

### Deprecated namespace at connector settings level

```python
from pydantic import Field
from connectors_sdk import BaseConnectorSettings, DeprecatedField

class ConnectorSettings(BaseConnectorSettings):
old_namespace: SkipValidation[MyConfig] = DeprecatedField(
old_namespace: MyConfig = DeprecatedField(
deprecated="Use new_namespace instead",
new_namespace="new_namespace",
removal_date="2026-12-31", # Optional informative removal deadline
Expand Down Expand Up @@ -166,3 +193,16 @@ If both old and new settings are present, new settings take precedence
This change introduces a declarative, centralized framework for configuration deprecation in connectors.

By moving migration logic into the SDK and driving it through field metadata, it eliminates duplicated code, enforces a single validated configuration schema, and provides a clear and explicit deprecation path for users.

---

## Update (2026-03-13)

This TDR is amended to clarify the recommended public API for field deprecation declarations.

- `DeprecatedField` is the preferred and documented way to mark configuration fields as deprecated.
- `Deprecate` exists as a lower-level annotation and is available for direct `Annotated[...]` usage if preferred.
- `DeprecatedField` uses `Deprecate` metadata under the hood in `BaseConnectorSettings`, so both declaration syntaxes behave the same.
- `pydantic.SkipValidation` remains supported for backward compatibility, but it is no longer required when using `DeprecatedField`/`Deprecate`.

This update is a documentation clarification only (code examples have been updated). It does not change migration behavior.
2 changes: 2 additions & 0 deletions connectors-sdk/connectors_sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
BaseStreamConnectorConfig,
)
from connectors_sdk.settings.deprecations import (
Deprecate,
DeprecatedField,
)
from connectors_sdk.settings.exceptions import (
Expand All @@ -40,5 +41,6 @@
"DatetimeFromIsoString",
"ListFromString",
# Deprecations
"Deprecate",
"DeprecatedField",
]
4 changes: 4 additions & 0 deletions connectors-sdk/connectors_sdk/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from connectors_sdk.models.city import City
from connectors_sdk.models.country import Country
from connectors_sdk.models.domain_name import DomainName
from connectors_sdk.models.email_address import EmailAddress
from connectors_sdk.models.external_reference import ExternalReference
from connectors_sdk.models.file import File
from connectors_sdk.models.hostname import Hostname
Expand All @@ -35,6 +36,7 @@
from connectors_sdk.models.threat_actor_group import ThreatActorGroup
from connectors_sdk.models.tlp_marking import TLPMarking
from connectors_sdk.models.url import URL
from connectors_sdk.models.user_account import UserAccount
from connectors_sdk.models.vulnerability import Vulnerability
from connectors_sdk.models.x509_certificate import X509Certificate

Expand All @@ -53,6 +55,7 @@
"City",
"Country",
"DomainName",
"EmailAddress",
"ExternalReference",
"File",
"Hostname",
Expand All @@ -76,6 +79,7 @@
"ThreatActorGroup",
"TLPMarking",
"URL",
"UserAccount",
"Vulnerability",
"X509Certificate",
]
33 changes: 33 additions & 0 deletions connectors-sdk/connectors_sdk/models/email_address.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""EmailAddress."""

from connectors_sdk.models.base_observable_entity import BaseObservableEntity
from connectors_sdk.models.reference import Reference
from connectors_sdk.models.user_account import UserAccount
from pydantic import Field
from stix2.v21 import EmailAddress as Stix2EmailAddress


class EmailAddress(BaseObservableEntity):
"""Represent an email address observable on OpenCTI."""

value: str = Field(
description="The email address value.",
min_length=1,
)
display_name: str | None = Field(
description="The display name of the email address.",
default=None,
)
belongs_to: UserAccount | Reference | None = Field(
description="The user account associated with the email address.",
default=None,
)

def to_stix2_object(self) -> Stix2EmailAddress:
"""Make stix object."""
return Stix2EmailAddress(
value=self.value,
display_name=self.display_name,
belongs_to_ref=self.belongs_to.id if self.belongs_to else None,
**self._common_stix2_properties(),
)
20 changes: 20 additions & 0 deletions connectors-sdk/connectors_sdk/models/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from enum import StrEnum

__all__ = [
"AccountType",
"AttackMotivation",
"AttackResourceLevel",
"CvssSeverity",
Expand Down Expand Up @@ -47,6 +48,25 @@ def _missing_(cls: type[_PermissiveEnum], value: object) -> _PermissiveEnum:
return obj


class AccountType(_PermissiveEnum):
"""Account Type Open Vocabulary.

See https://docs.oasis-open.org/cti/stix/v2.1/os/stix-v2.1-os.html#_k2b7lkt45f0i
"""

FACEBOOK = "facebook"
LDAP = "ldap"
NIS = "nis"
OPENID = "openid"
RADIUS = "radius"
SKYPE = "skype"
TACACS = "tacacs"
TWITTER = "twitter"
UNIX = "unix"
WINDOWS_LOCAL = "windows-local"
WINDOWS_DOMAIN = "windows-domain"


class AttackMotivation(_PermissiveEnum):
"""Attack Motivation Open Vocabulary.

Expand Down
88 changes: 88 additions & 0 deletions connectors-sdk/connectors_sdk/models/user_account.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
"""UserAccount model."""

from datetime import datetime

from connectors_sdk.models.base_observable_entity import BaseObservableEntity
from connectors_sdk.models.enums import AccountType
from pydantic import Field
from stix2.v21 import UserAccount as Stix2UserAccount


class UserAccount(BaseObservableEntity):
"""Represent a user account observable on OpenCTI."""

user_id: str | None = Field(
description="Identifier of the account in the system (for example UID, GUID, account name, or email address).",
default=None,
)
credential: str | None = Field(
description="Cleartext credential for the account (intended for malware-analysis metadata, not for sharing PII).",
default=None,
)
account_login: str | None = Field(
description="Account login used by the user to sign in when different from user_id.",
default=None,
)
account_type: AccountType | None = Field(
description="Type of account (for example unix, windows-local, windows-domain, twitter).",
default=None,
)
display_name: str | None = Field(
description="Display name of the account shown in user interfaces.",
default=None,
)
is_service_account: bool | None = Field(
description="Whether the account is associated with a service or system process rather than an individual.",
default=None,
)
is_privileged: bool | None = Field(
description="Whether the account has elevated privileges.",
default=None,
)
can_escalate_privs: bool | None = Field(
description="Whether the account can escalate privileges.",
default=None,
)
is_disabled: bool | None = Field(
description="Whether the account is disabled.",
default=None,
)
account_created: datetime | None = Field(
description="When the account was created.",
default=None,
)
account_expires: datetime | None = Field(
description="When the account expires.",
default=None,
)
credential_last_changed: datetime | None = Field(
description="When the account credential was last changed.",
default=None,
)
account_first_login: datetime | None = Field(
description="When the account was first accessed.",
default=None,
)
account_last_login: datetime | None = Field(
description="When the account was last accessed.",
default=None,
)

def to_stix2_object(self) -> Stix2UserAccount:
"""Make stix object."""
return Stix2UserAccount(
user_id=self.user_id,
account_login=self.account_login,
account_type=self.account_type.value if self.account_type else None,
display_name=self.display_name,
is_service_account=self.is_service_account,
is_privileged=self.is_privileged,
can_escalate_privs=self.can_escalate_privs,
is_disabled=self.is_disabled,
account_created=self.account_created,
account_expires=self.account_expires,
credential_last_changed=self.credential_last_changed,
account_first_login=self.account_first_login,
account_last_login=self.account_last_login,
**self._common_stix2_properties(),
)
Loading
Loading