From e05fb29f10bb3d26f3b408b4e7739616b43d16c6 Mon Sep 17 00:00:00 2001 From: Eric McGinnis Date: Wed, 16 Sep 2026 12:03:30 -0700 Subject: [PATCH 1/2] Update all dependencies in pyproject.toml to latest compatible versions - Bump pydantic, PyYAML, xmltodict, attackcti, questionary, docker, splunk-sdk, tqdm, pygit2, tyro, gitpython, and rich to latest. - Bump dev dependency ruff to latest. - setuptools kept below 81 (pinned floor raised to 80.10.2) since newer releases drop pkg_resources, which pycvesearch requires. - splunk-sdk kept at 2.1.1 since 3.x requires Python >=3.13, which is outside this project's supported Python range. - Fix new ruff lint/format findings (None-union ordering, one formatting change) surfaced by the ruff version bump. Co-Authored-By: Claude Code --- .../actions/detection_testing/GitService.py | 12 ++++--- .../DetectionTestingInfrastructure.py | 2 +- .../detection_abstract.py | 2 +- contentctl/objects/base_test.py | 2 +- contentctl/objects/base_test_result.py | 4 +-- contentctl/objects/data_source.py | 10 +++--- contentctl/objects/drilldown.py | 4 +-- contentctl/objects/lookup.py | 6 ++-- contentctl/objects/mitre_attack_enrichment.py | 8 ++--- pyproject.toml | 32 ++++++++----------- 10 files changed, 40 insertions(+), 42 deletions(-) diff --git a/contentctl/actions/detection_testing/GitService.py b/contentctl/actions/detection_testing/GitService.py index 5beb916d..64cc0b4f 100644 --- a/contentctl/actions/detection_testing/GitService.py +++ b/contentctl/actions/detection_testing/GitService.py @@ -147,11 +147,13 @@ def getChanges(self, target_branch: str) -> List[Detection]: # Filter to find the Lookup Object the references this CSV matched = list( filter( - lambda x: isinstance(x, CSVLookup) - and not isinstance( - x, RuntimeCSV - ) # RuntimeCSV is not used directly by any content - and x.filename == decoded_path, + lambda x: ( + isinstance(x, CSVLookup) + and not isinstance( + x, RuntimeCSV + ) # RuntimeCSV is not used directly by any content + and x.filename == decoded_path + ), self.director.lookups, ) ) diff --git a/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py b/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py index e3ce2ac7..8a97a15d 100644 --- a/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +++ b/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py @@ -149,7 +149,7 @@ def setup(self): # Init the list of setup functions we always need primary_setup_functions: list[ - tuple[Callable[[], None | client.Service], str] + tuple[Callable[[], client.Service | None], str] ] = [ (self.start, "Starting"), (self.get_conn, "Waiting for App Installation"), diff --git a/contentctl/objects/abstract_security_content_objects/detection_abstract.py b/contentctl/objects/abstract_security_content_objects/detection_abstract.py index d0c2e785..9ca35e2b 100644 --- a/contentctl/objects/abstract_security_content_objects/detection_abstract.py +++ b/contentctl/objects/abstract_security_content_objects/detection_abstract.py @@ -117,7 +117,7 @@ def severity(self) -> RiskSeverity: f"Error getting severity - risk_score must be between 0-100, but was actually {self.risk_score}" ) - explanation: None | str = Field( + explanation: str | None = Field( default=None, exclude=True, # Don't serialize this value when dumping the object description="Provide an explanation to be included " diff --git a/contentctl/objects/base_test.py b/contentctl/objects/base_test.py index 5cd174b8..3d14f577 100644 --- a/contentctl/objects/base_test.py +++ b/contentctl/objects/base_test.py @@ -39,7 +39,7 @@ class BaseTest(BaseModel, ABC): latest_time: Union[str, None] = None # The test result - result: Union[None, BaseTestResult] = None + result: Union[BaseTestResult, None] = None @abstractmethod def skip(self, message: str) -> None: diff --git a/contentctl/objects/base_test_result.py b/contentctl/objects/base_test_result.py index 8ddd003c..f9e64f5e 100644 --- a/contentctl/objects/base_test_result.py +++ b/contentctl/objects/base_test_result.py @@ -38,7 +38,7 @@ class BaseTestResult(BaseModel): """ # Message for the result - message: Union[None, str] = None + message: Union[str, None] = None # Any exception that was raised (may be None) exception: Union[Exception, None] = None @@ -53,7 +53,7 @@ class BaseTestResult(BaseModel): job_content: Union[Record, None] = None # The Splunk endpoint URL - sid_link: Union[None, str] = None + sid_link: Union[str, None] = None # Needed to allow for embedding of Exceptions in the model model_config = ConfigDict(validate_assignment=True, arbitrary_types_allowed=True) diff --git a/contentctl/objects/data_source.py b/contentctl/objects/data_source.py index 715d6fdf..f39dc214 100644 --- a/contentctl/objects/data_source.py +++ b/contentctl/objects/data_source.py @@ -19,14 +19,14 @@ class DataSource(SecurityContentObject): source: str = Field(...) sourcetype: str = Field(...) separator: Optional[str] = None - separator_value: None | str = None + separator_value: str | None = None configuration: Optional[str] = None supported_TA: list[TA] = [] - fields: None | list = None - field_mappings: None | list = None + fields: list | None = None + field_mappings: list | None = None mitre_components: list[str] = [] - convert_to_log_source: None | list = None - example_log: None | str = None + convert_to_log_source: list | None = None + example_log: str | None = None output_fields: list[str] = [] status: ContentStatus = ContentStatus.production diff --git a/contentctl/objects/drilldown.py b/contentctl/objects/drilldown.py index 11d7fe46..1fc64be7 100644 --- a/contentctl/objects/drilldown.py +++ b/contentctl/objects/drilldown.py @@ -22,14 +22,14 @@ class Drilldown(BaseModel): description="The text of a drilldown search. This must be valid SPL.", min_length=1, ) - earliest_offset: None | str = Field( + earliest_offset: str | None = Field( ..., description="Earliest offset time for the drilldown search. " f"The most common value for this field is '{EARLIEST_OFFSET}', " "but it is NOT the default value and must be supplied explicitly.", min_length=1, ) - latest_offset: None | str = Field( + latest_offset: str | None = Field( ..., description="Latest offset time for the driolldown search. " f"The most common value for this field is '{LATEST_OFFSET}', " diff --git a/contentctl/objects/lookup.py b/contentctl/objects/lookup.py index 93f38033..2759aaff 100644 --- a/contentctl/objects/lookup.py +++ b/contentctl/objects/lookup.py @@ -100,11 +100,11 @@ class Lookup(SecurityContentObject, abc.ABC): match_type: list[Annotated[str, Field(pattern=r"(^WILDCARD|CIDR)\(.+\)$")]] = Field( default=[] ) - min_matches: None | NonNegativeInt = Field(default=None) - max_matches: None | Annotated[NonNegativeInt, Field(ge=1, le=1000)] = Field( + min_matches: NonNegativeInt | None = Field(default=None) + max_matches: Annotated[NonNegativeInt, Field(ge=1, le=1000)] | None = Field( default=None ) - case_sensitive_match: None | bool = Field(default=None) + case_sensitive_match: bool | None = Field(default=None) status: ContentStatus = ContentStatus.production @field_validator("status", mode="after") diff --git a/contentctl/objects/mitre_attack_enrichment.py b/contentctl/objects/mitre_attack_enrichment.py index 8f1c4c7a..7bed32b3 100644 --- a/contentctl/objects/mitre_attack_enrichment.py +++ b/contentctl/objects/mitre_attack_enrichment.py @@ -41,9 +41,9 @@ class AttackGroupType(StrEnum): class MitreExternalReference(BaseModel): model_config = ConfigDict(extra="forbid") source_name: str - external_id: None | str = None - url: None | HttpUrl = None - description: None | str = None + external_id: str | None = None + url: HttpUrl | None = None + description: str | None = None class MitreAttackGroup(BaseModel): @@ -58,7 +58,7 @@ class MitreAttackGroup(BaseModel): group_id: str id: str matrix: list[AttackGroupMatrix] - mitre_attack_spec_version: None | str + mitre_attack_spec_version: str | None mitre_version: str # assume that if the deprecated field is not present, then the group is not deprecated mitre_deprecated: bool diff --git a/pyproject.toml b/pyproject.toml index 5c5fe2e4..3f066d02 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,31 +14,27 @@ contentctl = 'contentctl.contentctl:main' [tool.poetry.dependencies] python = "^3.11,<3.14" -pydantic = "~2.9.2" -PyYAML = "^6.0.2" +pydantic = "~2.13.5" +PyYAML = "^6.0.3" requests = ">=2.34.2" pycvesearch = "^1.2" -xmltodict = ">=0.13,<0.15" -attackcti = ">=0.5.4,<0.6" +xmltodict = "^1.0.4" +attackcti = "^0.6.4" Jinja2 = "^3.1.6" -questionary = "^2.0.1" -docker = "^7.1.0" -splunk-sdk = "^2.0.2" +questionary = "^2.1.1" +docker = "^7.2.0" +splunk-sdk = "^2.1.1" semantic-version = "^2.10.0" bottle = ">=0.12.25,<0.14.0" -tqdm = "^4.66.5" -pygit2 = "^1.15.1" -#We are pinned to this version of tyro because 0.9.23 and above -#have an issue when parsing an extremely large number of files -#(in our testing great than 130) when using the mode:selected -#--mode.files command. -tyro = "^0.9.2,<0.9.23" -gitpython = "^3.1.49" -setuptools = "<81" -rich = "^14.0.0" +tqdm = "^4.70.1" +pygit2 = "^1.20.1" +tyro = "^1.0.16" +gitpython = "^3.1.62" +setuptools = ">=80.10.2,<81" +rich = "^15.0.0" [tool.poetry.group.dev.dependencies] -ruff = "^0.12.10" +ruff = "^0.16.8" [build-system] requires = ["poetry-core>=1.0.0"] From f68940e7773f70ad3557ef005b2632fd9f4c44f4 Mon Sep 17 00:00:00 2001 From: Eric McGinnis Date: Wed, 16 Sep 2026 12:11:32 -0700 Subject: [PATCH 2/2] Revert pydantic to 2.9.2 to fix CI failures CI (contentctl validate/build) failed with "Story is not fully defined; you should define Detection, then call Story.model_rebuild()." on pydantic 2.13.5. Reverting to 2.9.2 and adding a comment explaining why. attackcti is reverted alongside it since 0.6.x requires pydantic>=2.12.5. Co-Authored-By: Claude Code --- pyproject.toml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3f066d02..eb7fc37e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,12 +14,18 @@ contentctl = 'contentctl.contentctl:main' [tool.poetry.dependencies] python = "^3.11,<3.14" -pydantic = "~2.13.5" +# Pinned to 2.9.2. Newer pydantic versions (tested through 2.13.5) fail to +# resolve the forward reference from Story to Detection, raising +# "`Story` is not fully defined; you should define `Detection`, then call +# `Story.model_rebuild()`." during `contentctl validate`/`build`. +pydantic = "~2.9.2" PyYAML = "^6.0.3" requests = ">=2.34.2" pycvesearch = "^1.2" xmltodict = "^1.0.4" -attackcti = "^0.6.4" +# attackcti 0.6.x requires pydantic>=2.12.5, which conflicts with our +# pydantic pin above, so this stays capped below 0.6. +attackcti = ">=0.5.4,<0.6" Jinja2 = "^3.1.6" questionary = "^2.1.1" docker = "^7.2.0"