From bcc6ea12328ed252c76ce108a99376f26db61b3e Mon Sep 17 00:00:00 2001 From: Thomas Chopitea Date: Sun, 27 Sep 2026 12:08:28 +0000 Subject: [PATCH 1/2] Stop sending dfiq_type in DFIQ create and patch payloads yeti#1340 removed dfiq_type from NewDFIQRequest, PatchDFIQRequest and DFIQValidateRequest -- the API reads the type from the YAML or the object. Those models set extra="forbid", so every call to new_dfiq_from_yaml, patch_dfiq_from_yaml and patch_dfiq now gets a 422. The dfiq_type parameters stay in the two from_yaml signatures and raise a DeprecationWarning instead of being removed: dfiq_type is the first positional argument of both, so dropping it would silently rebind existing positional callers' arguments. patch_dfiq took the value from dfiq_object["type"], so it needs no signature change. Version bumped to 2.3.1 so the release can be cut without a second commit on main. Fixes #26. --- pyproject.toml | 2 +- tests/api.py | 9 ++++----- yeti/api.py | 26 +++++++++++++++++++++----- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4056d31..8050ea8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "yeti-python" -version = "2.3.0" +version = "2.3.1" description = "Python bindings for the Yeti API" authors = ["tomchop"] license = "Apache" diff --git a/tests/api.py b/tests/api.py index 4a90d9f..6374734 100644 --- a/tests/api.py +++ b/tests/api.py @@ -239,12 +239,12 @@ def test_new_dfiq_from_yaml(self, mock_post): mock_response.content = b'{"id": "new_dfiq"}' mock_post.return_value = mock_response - result = self.api.new_dfiq_from_yaml("type", "yaml_content") + with self.assertWarns(DeprecationWarning): + result = self.api.new_dfiq_from_yaml("type", "yaml_content") self.assertEqual(result, {"id": "new_dfiq"}) mock_post.assert_called_with( "http://fake-url/api/v2/dfiq/from_yaml", json={ - "dfiq_type": "type", "dfiq_yaml": "yaml_content", }, ) @@ -255,12 +255,12 @@ def test_patch_dfiq_from_yaml(self, mock_patch): mock_response.content = b'{"id": "patched_dfiq"}' mock_patch.return_value = mock_response - result = self.api.patch_dfiq_from_yaml("type", "yaml_content", 1) + with self.assertWarns(DeprecationWarning): + result = self.api.patch_dfiq_from_yaml("type", "yaml_content", 1) self.assertEqual(result, {"id": "patched_dfiq"}) mock_patch.assert_called_with( "http://fake-url/api/v2/dfiq/1", json={ - "dfiq_type": "type", "dfiq_yaml": "yaml_content", }, ) @@ -279,7 +279,6 @@ def test_patch_dfiq(self, mock_patch): "http://fake-url/api/v2/dfiq/1", json={ "dfiq_object": {"name": "patched_dfiq", "type": "question", "id": 1}, - "dfiq_type": "question", }, ) diff --git a/yeti/api.py b/yeti/api.py index f6d9596..59f8aad 100644 --- a/yeti/api.py +++ b/yeti/api.py @@ -3,6 +3,7 @@ import json import logging import urllib.parse +import warnings from typing import Any, Sequence import requests @@ -20,6 +21,11 @@ OIDC_CALLBACK_ENDPOINT = "/api/v2/auth/oidc-callback-token" API_TOKEN_ENDPOINT = "/api/v2/auth/api-token" +DFIQ_TYPE_DEPRECATION = ( + "dfiq_type is ignored: Yeti infers the DFIQ type from the payload. The " + "parameter will be removed in the next major version." +) + SUPPORTED_IOC_TYPES = [ "generic", @@ -702,9 +708,14 @@ def get_multiple_dfiq( return json.loads(response)["dfiq"] def new_dfiq_from_yaml(self, dfiq_type: str, dfiq_yaml: str) -> YetiObject: - """Creates a new DFIQ object in Yeti from a YAML string.""" + """Creates a new DFIQ object in Yeti from a YAML string. + + Args: + dfiq_type: Ignored. Yeti reads the type from the YAML. + dfiq_yaml: The DFIQ object, as YAML. + """ + warnings.warn(DFIQ_TYPE_DEPRECATION, DeprecationWarning, stacklevel=2) params = { - "dfiq_type": dfiq_type, "dfiq_yaml": dfiq_yaml, } response = self.do_request( @@ -718,9 +729,15 @@ def patch_dfiq_from_yaml( dfiq_yaml: str, yeti_id: int, ) -> YetiObject: - """Patches a DFIQ object in Yeti from a YAML string.""" + """Patches a DFIQ object in Yeti from a YAML string. + + Args: + dfiq_type: Ignored. Yeti reads the type from the YAML. + dfiq_yaml: The DFIQ object, as YAML. + yeti_id: The ID of the DFIQ object to patch. + """ + warnings.warn(DFIQ_TYPE_DEPRECATION, DeprecationWarning, stacklevel=2) params = { - "dfiq_type": dfiq_type, "dfiq_yaml": dfiq_yaml, } response = self.do_request( @@ -731,7 +748,6 @@ def patch_dfiq_from_yaml( def patch_dfiq(self, dfiq_object: dict[str, Any]) -> YetiObject: """Patches a DFIQ object in Yeti.""" params = { - "dfiq_type": dfiq_object["type"], "dfiq_object": dfiq_object, } response = self.do_request( From e829cfe99381ba4327c135387f3462c59e22d505 Mon Sep 17 00:00:00 2001 From: Thomas Chopitea Date: Sun, 27 Sep 2026 13:02:52 +0000 Subject: [PATCH 2/2] Cover the DFIQ write methods in the e2e suite Nothing in tests/e2e.py called new_dfiq_from_yaml, patch_dfiq_from_yaml or patch_dfiq, so the 422 in #26 reached a release with a green e2e run. The new test walks a scenario through all three against the live API, which is the only place the request models' extra="forbid" is enforced -- the unit tests assert the payload we build, not what the API accepts. --- tests/e2e.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/e2e.py b/tests/e2e.py index 45c66e3..4409fde 100644 --- a/tests/e2e.py +++ b/tests/e2e.py @@ -1,6 +1,7 @@ import os import time import unittest +import uuid from yeti import errors from yeti.api import YetiApi @@ -204,6 +205,38 @@ def test_link_objects(self): neighbors["vertices"][f'entities/{malware["id"]}']["name"], "testMalware" ) + def test_dfiq_from_yaml_and_patch(self): + """Covers the three DFIQ write methods, whose payloads the API + validates with extra="forbid": an unexpected field is a 422.""" + self.api.auth_api_key(os.getenv("YETI_API_KEY")) + scenario_uuid = str(uuid.uuid4()) + scenario_yaml = f"""--- +name: testScenario +type: scenario +description: > + test +id: S1990 +uuid: {scenario_uuid} +dfiq_version: 1.1.0 +""" + + with self.assertWarns(DeprecationWarning): + scenario = self.api.new_dfiq_from_yaml("scenario", scenario_yaml) + self.assertEqual(scenario["name"], "testScenario") + self.assertEqual(scenario["uuid"], scenario_uuid) + + with self.assertWarns(DeprecationWarning): + patched = self.api.patch_dfiq_from_yaml( + "scenario", + scenario_yaml.replace(" test", " patched from yaml"), + scenario["id"], + ) + self.assertEqual(patched["description"].strip(), "patched from yaml") + + patched["description"] = "patched as an object" + patched = self.api.patch_dfiq(patched) + self.assertEqual(patched["description"], "patched as an object") + def test_new_tag(self): self.api.auth_api_key(os.getenv("YETI_API_KEY")) tag = self.api.new_tag("testTag", description="test")