Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
ffdd2cc
Update README with correct connector start path (#5392)
moin-loginsoft Dec 11, 2025
c67040a
[dragos] Update connector to be "manager_supported" (#5243)
Powlinett Dec 11, 2025
989147a
[Automation] Build and update manifest [ci build-manifest]
Filigran-Automation Dec 11, 2025
f55032d
[all] Release 6.9.0
Filigran-Automation Dec 12, 2025
07bdca7
[Email Intel Microsoft] Fix erroneous variable in readme (#4540)
adrienbroyere Dec 12, 2025
94be4b9
[composer] Fix scripts paths in Makefile (#5348)
Powlinett Dec 12, 2025
2c27772
[connectors] Replace rolling by latest per best practices and stable …
SamuelHassine Dec 12, 2025
7bc52d4
[connectors composer/manager] Prepare connectors for future migration…
Powlinett Dec 12, 2025
18e69d2
[connectors-sdk] Add `DatetimeFromIsoString` annotated type (#5093)
Powlinett Dec 12, 2025
962c4db
[recordedfuture] Map the 'last_seen' RF date as OCTI 'valid_from' whe…
romain-filigran Dec 12, 2025
9a2ff22
[all] Fix connectors-sdk version (#5402)
Powlinett Dec 12, 2025
419cbcb
[crowdstrike] Roll back indicator ingestion from (#5383)
CTIBurn0ut Dec 12, 2025
a9a28ac
[DigIntLab-DEP] Add DigIntLab Double Extorsion Platform connector (#5…
notdodo Dec 12, 2025
3788d10
[team-cymru-scout-search] Initial Commit (#5141)
mitchm101 Dec 12, 2025
ca409ea
[Automation] Build and update manifest [ci build-manifest]
Filigran-Automation Dec 12, 2025
744f4ee
fix: Fix typo in scope definition
jabesq Dec 15, 2025
b345a2c
chore: Set `manager_supported` flag to True
jabesq Dec 11, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ connectors_manifests:
sh ./shared/tools/composer/generate_connectors_manifests/generate_connectors_manifests.sh

connector_config_schema:
sh ./shared/tools/composer/generate_connectors_config_json_schemas/generate_connector_config_json_schema.sh
sh ./shared/tools/composer/generate_connectors_config_schemas/generate_connector_config_json_schema.sh
connectors_config_schemas:
sh ./shared/tools/composer/generate_connectors_config_json_schemas/generate_connectors_config_json_schemas.sh
sh ./shared/tools/composer/generate_connectors_config_schemas/generate_connectors_config_json_schemas.sh

global_manifest:
sh ./shared/tools/composer/generate_global_manifest/generate_global_manifest.sh
7 changes: 7 additions & 0 deletions connectors-sdk/connectors_sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

__version__ = "0.1.0"

from connectors_sdk.settings.annotated_types import (
DatetimeFromIsoString,
ListFromString,
)
from connectors_sdk.settings.base_settings import (
BaseConfigModel,
BaseConnectorSettings,
Expand Down Expand Up @@ -29,4 +33,7 @@
# Exceptions
"ConfigError",
"ConfigValidationError",
# Annotated types
"DatetimeFromIsoString",
"ListFromString",
]
1 change: 0 additions & 1 deletion connectors-sdk/connectors_sdk/core/__init__.py

This file was deleted.

11 changes: 0 additions & 11 deletions connectors-sdk/connectors_sdk/core/pydantic/__init__.py

This file was deleted.

25 changes: 0 additions & 25 deletions connectors-sdk/connectors_sdk/core/pydantic/parsers.py

This file was deleted.

36 changes: 0 additions & 36 deletions connectors-sdk/connectors_sdk/core/pydantic/serializers.py

This file was deleted.

44 changes: 0 additions & 44 deletions connectors-sdk/connectors_sdk/core/pydantic/types.py

This file was deleted.

177 changes: 177 additions & 0 deletions connectors-sdk/connectors_sdk/settings/annotated_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
"""Commonly used Pydantic types with custom validation and serialization logic."""

from datetime import datetime, timedelta, timezone
from typing import Annotated

from pydantic import (
BeforeValidator,
PlainSerializer,
SerializationInfo,
TypeAdapter,
)


def parse_comma_separated_list(value: str | list[str]) -> list[str]:
"""Coerce a comma-separated string into a list[str], trimming surrounding
whitespace for each element.
If the input is already a list[str], it is returned unchanged.

This is useful for values originating from environment variables or other
string-based sources (e.g., "a, b , c") and converts them to ["a", "b", "c"].

Parameters
- value: Either a string (e.g., "a,b,c") or a list[str].

Returns:
- A list of strings. For string inputs, the string is split on commas and each
token is stripped of leading/trailing whitespace.

Examples:
- "a, b ,c" -> ["a", "b", "c"]
- ["a", "b"] -> ["a", "b"]
"""
if isinstance(value, str):
return [string.strip() for string in value.split(",") if value]
return value


def serialize_list_of_strings(
value: list[str], info: SerializationInfo
) -> str | list[str]:
"""Serialize a list[str] as a comma-separated string when the Pydantic
serialization context requests "pycti" mode; otherwise, return the list
unchanged.

This serializer is intended for use with Pydantic v2 `PlainSerializer` and
is typically activated only during JSON serialization (`when_used="json"`),
so the in-memory Python value remains a `list[str]` while the JSON output
can be a single string when required by external systems.

Parameters
- value: The value to serialize. Expected to be a list of strings.
- info: Serialization context provided by Pydantic. If `info.context`
contains `{"mode": "pycti"}`, the list will be joined into a single
comma-separated string.

Returns:
- A comma-separated string if context mode is "pycti" and `value` is a list.
- The original value `value` unchanged in all other cases.

Notes:
- Joining does not insert spaces; e.g., ["a", "b", "c"] -> "a,b,c".
- If any element contains commas, those commas are not escaped.

Examples:
- info.context={"mode": "pycti"} and value=["e1", "e2"] -> "e1,e2"
- info.context is None or mode != "pycti" -> ["e1", "e2"]
"""
if info.context and info.context.get("mode") == "pycti":
return ",".join(value) # [ "e1", "e2", "e3" ] -> "e1,e2,e3"
return value


ListFromString = Annotated[
list[str], # Final type
BeforeValidator(parse_comma_separated_list),
PlainSerializer(serialize_list_of_strings, when_used="json"),
"""Annotated list[str] that:
- Validates: Accepts a comma-separated string (e.g., "a,b,c") or a list[str].
If a string is provided, it is split on commas and whitespace is trimmed for
each item.
- Serializes (JSON): When the Pydantic serialization context includes
{"mode": "pycti"}, the list is serialized as a single comma-separated string
(e.g., ["a","b"] -> "a,b"). Otherwise, it serializes as a JSON array by default.

Components
- BeforeValidator(parse_comma_separated_list): Converts input strings to list[str]
early in validation.
- PlainSerializer(serialize_list_of_strings, when_used="json"): Produces the "pycti"
string form only for JSON serialization.

Examples
- Validation:
from pydantic import BaseModel

class Model(BaseModel):
tags: ListFromString

Model.model_validate({"tags": "a, b , c"}).tags # -> ["a", "b", "c"]
Model.model_validate({"tags": ["x", "y"]}).tags # -> ["x", "y"]

- Serialization:
m = Model.model_validate({"tags": ["e1", "e2"]})
m.model_dump() # -> {'tags': ['e1', 'e2']}
m.model_dump_json() # -> {"tags":["e1","e2"]}
m.model_dump_json(context={"mode": "pycti"}) # -> {"tags":"e1,e2"}
""",
]


def parse_iso_string(value: str | datetime) -> datetime:
"""Convert ISO string into a datetime object.

Example:
> value = parse_iso_string("2023-10-01T00:00:00Z")
> print(value) # 2023-10-01 00:00:00+00:00

# If today is 2023-10-01:
> value = parse_iso_string("P30D")
> print(value) # 2023-09-01 00:00:00+00:00
"""
if not isinstance(value, str):
return value

try:
# Convert presumed ISO string to datetime object
parsed_datetime = datetime.fromisoformat(value)
if parsed_datetime.tzinfo:
return parsed_datetime.astimezone(tz=timezone.utc)
else:
return parsed_datetime.replace(tzinfo=timezone.utc)
except ValueError:
# If not a datetime ISO string, try to parse it as timedelta with pydantic first
duration = TypeAdapter(timedelta).validate_python(value)
# Then return a datetime minus the value
return datetime.now(timezone.utc) - duration


DatetimeFromIsoString = Annotated[
datetime,
BeforeValidator(parse_iso_string),
# Replace the default JSON serializer, in order to use +00:00 offset instead of Z prefix
PlainSerializer(datetime.isoformat, when_used="json"),
"""Annotated datetime that:
- Validates: Accepts an ISO-8601 string (datetime or duration).
If a datetime ISO-8601 string is provided, and no timezone is present, the string
is assumed to be UTC timezoned.
If a duration ISO-8601 string is provided, the returned datetime will be relative
to `datetime.now(timezone.utc)` at runtime.
- Serializes (JSON): Serializes a datetime object to an datetime ISO-8601 string.

Components
- BeforeValidator(parse_iso_string): Converts input strings to datetime
early in validation.
- PlainSerializer(datetime.isoformat, when_used="json"): Produces the datetime ISO-8601 string
for JSON serialization.

Examples
- Validation:
from pydantic import BaseModel

class Model(BaseModel):
start_date: DatetimeFromIsoString

Model.model_validate({
"start_date": "2023-10-01"
}).start_date # -> datetime(2023, 10, 01, 0, 0, tzinfo=timezone.utc)
Model.model_validate({
"start_date": datetime(2023, 10, 01, 0, 0, tzinfo=timezone.utc)
}).start_date # -> datetime(2023, 10, 01, 0, 0, tzinfo=timezone.utc)

- Serialization:
m = Model.model_validate({"start_date": datetime(2023, 10, 01, 0, 0, tzinfo=timezone.utc)})
m.model_dump() # -> {'start_date': datetime(2023, 10, 01, 0, 0, tzinfo=timezone.utc)}
m.model_dump_json() # -> {"start_date": "2023-10-01T00:00:00+00:00"}
m.model_dump_json(context={"mode": "pycti"}) # -> {"start_date": "2023-10-01T00:00:00+00:00"}
""",
]
2 changes: 1 addition & 1 deletion connectors-sdk/connectors_sdk/settings/base_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from typing import Any, Literal, Self

import __main__
from connectors_sdk.core.pydantic import ListFromString
from connectors_sdk.settings.annotated_types import ListFromString
from connectors_sdk.settings.exceptions import ConfigValidationError
from pydantic import (
BaseModel,
Expand Down
2 changes: 2 additions & 0 deletions connectors-sdk/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pydantic-settings = {version = ">= 2.9.1, <3"} # Actual minimal version used in
# octi-connectors = {path = "../.", extras = ["dev"], optional=true}
# This could be a project used to defined the common linter, prettifier and other rules in the future
black = {version = "25.12.0", optional=true} # Aligned with ci-requirements
freezegun = { version = ">=1.5.5, <2", optional=true}
isort = {version = "7.0.0", optional=true} # Aligned with ci-requirements
mypy = {version = "^1.15.0", optional=true}
ruff = {version = "^0.14.0", optional=true}
Expand All @@ -41,6 +42,7 @@ dev = [
"isort",
]
test = [
"freezegun",
"pytest",
"pip-audit",
"pytest-cov",
Expand Down
27 changes: 27 additions & 0 deletions connectors-sdk/tests/test_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# pragma: no cover # Do not compute coverage on test files
"""Offer tests for the root public API."""

import connectors_sdk as root_api


def test_root_public_api_is_valid():
"""Test that features are not removed by mistake."""
# Given the feature name
# Then it should all be present
imports = {
"BaseConnectorSettings",
"BaseConfigModel",
"BaseExternalImportConnectorConfig",
"BaseInternalEnrichmentConnectorConfig",
"BaseInternalExportFileConnectorConfig",
"BaseInternalImportFileConnectorConfig",
"BaseStreamConnectorConfig",
"ConfigError",
"ConfigValidationError",
"DatetimeFromIsoString",
"ListFromString",
}
missing = imports - set(root_api.__all__)
extra = set(root_api.__all__) - imports
assert not missing, f"Missing features in root public api: {missing}"
assert not extra, f"Unexpected features in root public api: {extra}"
Loading