From 31d780ed10435c6000d281647d5eff125d18dd41 Mon Sep 17 00:00:00 2001 From: Thomas Chopitea Date: Mon, 2 Jun 2025 18:07:35 +0000 Subject: [PATCH 01/23] Add more options to search params --- yeti/api.py | 88 +++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 76 insertions(+), 12 deletions(-) diff --git a/yeti/api.py b/yeti/api.py index ba375ed..a9fb405 100644 --- a/yeti/api.py +++ b/yeti/api.py @@ -177,25 +177,29 @@ def search_indicators( name: str | None = None, indicator_type: str | None = None, pattern: str | None = None, + description: str | None = None, tags: list[str] | None = None, + count: int = 100, + page: int = 0, ) -> list[YetiObject]: """Searches for an indicator in Yeti. - One of name or pattern must be provided. + One of name, indicator_type, pattern, description, or tags must be provided. Args: name: The name of the indicator to search for. indicator_type: The type of the indicator to search for. pattern: The pattern of the indicator to search for. + description: The description of the indicator to search for. tags: The tags of the indicator to search for. Returns: The response from the API; a list of dicts representing indicators. """ - if not any([name, indicator_type, pattern, tags]): + if not any([name, indicator_type, pattern, description, tags]): raise ValueError( - "You must provide one of name, indicator_type, pattern, or tags." + "You must provide one of name, indicator_type, pattern, description, or tags." ) query = {} @@ -203,11 +207,13 @@ def search_indicators( query["name"] = name if pattern: query["pattern"] = pattern + if description: + query["description"] = description if indicator_type: query["type"] = indicator_type if tags: query["tags"] = tags - params = {"query": query, "count": 0} + params = {"query": query, "count": count, "page": page} response = self.do_request( "POST", f"{self._url_root}/api/v2/indicators/search", @@ -237,8 +243,40 @@ def find_entity(self, name: str, type: str) -> YetiObject | None: raise return json.loads(response) - def search_entities(self, name: str) -> list[YetiObject]: - params = {"query": {"name": name}, "count": 0} + def search_entities( + self, + name: str | None = None, + entity_type: str | None = None, + description: str | None = None, + count: int = 100, + page: int = 0, + ) -> list[YetiObject]: + """Searches for entities in Yeti. + + One of name, type, or description must be provided. + + Args: + name: The name of the entity to search for (substring match). + entity_type: The type of the entity to search for. + description: The description of the entity to search for. + count: The number of results to return (default is 0, which means all). + page: The page of results to return (default is 0, which means the first page). + + Returns: + The response from the API; a list of dicts representing entities. + """ + if not any([name, type, description]): + raise ValueError("You must provide one of name, type, or description.") + + query = {} + if name: + query["name"] = name + if type: + query["type"] = type + if description: + query["description"] = description + + params = {"query": query, "count": count, "page": page} response = self.do_request( "POST", f"{self._url_root}/api/v2/entities/search", @@ -268,8 +306,14 @@ def find_observable(self, value: str, type: str) -> YetiObject | None: raise return json.loads(response) - def search_observables(self, value: str) -> list[YetiObject]: - """Searches for an observable in Yeti. + def search_observables( + self, + value: str, + count: int = 100, + page: int = 0, + tags: list[str] | None = None, + ) -> list[YetiObject]: + """Searches for observables in Yeti. Args: value: The value of the observable to search for. @@ -277,7 +321,11 @@ def search_observables(self, value: str) -> list[YetiObject]: Returns: The response from the API; a dict representing the observable. """ - params = {"query": {"value": value}, "count": 0} + query = {"value": value} + if tags: + query["tags"] = tags + params = {"query": query, "count": count, "page": page} + response = self.do_request( "POST", f"{self._url_root}/api/v2/observables/search", json_data=params ) @@ -423,7 +471,15 @@ def find_dfiq(self, name: str, dfiq_type: str) -> YetiObject | None: raise return json.loads(response) - def search_dfiq(self, name: str, dfiq_type: str | None = None) -> list[YetiObject]: + def search_dfiq( + self, + name: str, + dfiq_type: str | None = None, + dfiq_yaml: str | None = None, + dfiq_tags: list[str] | None = None, + count: int = 100, + page: int = 0, + ) -> list[YetiObject]: """Searches for a DFIQ in Yeti. Args: @@ -434,10 +490,18 @@ def search_dfiq(self, name: str, dfiq_type: str | None = None) -> list[YetiObjec Returns: The response from the API; a dict representing the DFIQ object. """ - query = {"name": name} + query = { + "name": name, + "filter_aliases": [["dfiq_tags", "list"], ["dfiq_id", "text"]], + } + if dfiq_type: query["type"] = dfiq_type - params = {"query": query, "count": 0} + if dfiq_yaml: + query["dfiq_yaml"] = dfiq_yaml + if dfiq_tags: + query["dfiq_tags"] = dfiq_tags + params = {"query": query, "count": count, "page": page} response = self.do_request( "POST", f"{self._url_root}/api/v2/dfiq/search", json_data=params ) From f82ca32cbb1219faf73ef8354c98190b00e67c7b Mon Sep 17 00:00:00 2001 From: Thomas Chopitea Date: Mon, 2 Jun 2025 18:18:11 +0000 Subject: [PATCH 02/23] Fix test --- tests/api.py | 47 +++++++++++++++++++++++++++++++++++++++-------- yeti/api.py | 4 ++-- 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/tests/api.py b/tests/api.py index 2fa8332..13bcb58 100644 --- a/tests/api.py +++ b/tests/api.py @@ -29,11 +29,21 @@ def test_search_indicators(self, mock_post): mock_response.content = b'{"indicators": [{"name": "test"}]}' mock_post.return_value = mock_response - result = self.api.search_indicators(name="test") + result = self.api.search_indicators( + name="test", description="test_description", tags=["tag1"] + ) self.assertEqual(result, [{"name": "test"}]) mock_post.assert_called_with( "http://fake-url/api/v2/indicators/search", - json={"query": {"name": "test"}, "count": 0}, + json={ + "query": { + "name": "test", + "description": "test_description", + "tags": ["tag1"], + }, + "count": 100, + "page": 0, + }, ) @patch("yeti.api.requests.Session.post") @@ -42,11 +52,17 @@ def test_search_entities(self, mock_post): mock_response.content = b'{"entities": [{"name": "test_entity"}]}' mock_post.return_value = mock_response - result = self.api.search_entities(name="test_entity") + result = self.api.search_entities( + name="test_entity", description="test_description" + ) self.assertEqual(result, [{"name": "test_entity"}]) mock_post.assert_called_with( "http://fake-url/api/v2/entities/search", - json={"query": {"name": "test_entity"}, "count": 0}, + json={ + "query": {"name": "test_entity", "description": "test_description"}, + "count": 100, + "page": 0, + }, ) @patch("yeti.api.requests.Session.post") @@ -55,11 +71,15 @@ def test_search_observables(self, mock_post): mock_response.content = b'{"observables": [{"value": "test_value"}]}' mock_post.return_value = mock_response - result = self.api.search_observables(value="test_value") + result = self.api.search_observables(value="test_value", tags=["tag1"]) self.assertEqual(result, [{"value": "test_value"}]) mock_post.assert_called_with( "http://fake-url/api/v2/observables/search", - json={"query": {"value": "test_value"}, "count": 0}, + json={ + "query": {"value": "test_value", "tags": ["tag1"]}, + "count": 100, + "page": 0, + }, ) @patch("yeti.api.requests.Session.post") @@ -121,11 +141,22 @@ def test_search_dfiq(self, mock_post): mock_response.content = b'{"dfiq": [{"name": "test_dfiq"}]}' mock_post.return_value = mock_response - result = self.api.search_dfiq(name="test_dfiq") + result = self.api.search_dfiq( + name="test_dfiq", dfiq_yaml="yaml_content", dfiq_tags=["tag1"] + ) self.assertEqual(result, [{"name": "test_dfiq"}]) mock_post.assert_called_with( "http://fake-url/api/v2/dfiq/search", - json={"query": {"name": "test_dfiq"}, "count": 0}, + json={ + "query": { + "name": "test_dfiq", + "dfiq_yaml": "yaml_content", + "dfiq_tags": ["tag1"], + "filter_aliases": [["dfiq_tags", "list"], ["dfiq_id", "text"]], + }, + "count": 100, + "page": 0, + }, ) @patch("yeti.api.requests.Session.post") diff --git a/yeti/api.py b/yeti/api.py index a9fb405..61d5b1a 100644 --- a/yeti/api.py +++ b/yeti/api.py @@ -271,8 +271,8 @@ def search_entities( query = {} if name: query["name"] = name - if type: - query["type"] = type + if entity_type: + query["type"] = entity_type if description: query["description"] = description From 631d774398d27174369686623433db0c9e471b4e Mon Sep 17 00:00:00 2001 From: Thomas Chopitea Date: Mon, 2 Jun 2025 18:18:23 +0000 Subject: [PATCH 03/23] Adjust e2e tests --- tests/e2e.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/e2e.py b/tests/e2e.py index 2f9a996..e7ade5d 100644 --- a/tests/e2e.py +++ b/tests/e2e.py @@ -48,7 +48,6 @@ def test_auth_refresh(self): self.api.search_indicators(name="test") def test_search_indicators(self): - self.api.auth_api_key(os.getenv("YETI_API_KEY")) self.api.auth_api_key(os.getenv("YETI_API_KEY")) self.api.new_indicator( { @@ -60,12 +59,13 @@ def test_search_indicators(self): } ) time.sleep(5) - result = self.api.search_indicators(name="testSear") + result = self.api.search_indicators( + name="testSear", description="test", tags=["testTag"] + ) self.assertEqual(len(result), 1, result) self.assertEqual(result[0]["name"], "testSearch") def test_find_indicator(self): - self.api.auth_api_key(os.getenv("YETI_API_KEY")) self.api.auth_api_key(os.getenv("YETI_API_KEY")) self.api.new_indicator( { From 7c7277d99ae2e9e3d66500139747e39b967e2a17 Mon Sep 17 00:00:00 2001 From: Thomas Chopitea Date: Mon, 2 Jun 2025 18:22:42 +0000 Subject: [PATCH 04/23] Attempt at fix test --- tests/e2e.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/e2e.py b/tests/e2e.py index e7ade5d..f6155b5 100644 --- a/tests/e2e.py +++ b/tests/e2e.py @@ -56,11 +56,12 @@ def test_search_indicators(self): "description": "test", "pattern": "test[0-9]", "diamond": "victim", + "tags": ["testTag"], } ) time.sleep(5) result = self.api.search_indicators( - name="testSear", description="test", tags=["testTag"] + name="testSear", description="tes", tags=["testTag"] ) self.assertEqual(len(result), 1, result) self.assertEqual(result[0]["name"], "testSearch") From 262ccafa8b91f6557ba5c72d33ae1ec2eaa949aa Mon Sep 17 00:00:00 2001 From: Thomas Chopitea Date: Mon, 2 Jun 2025 18:27:53 +0000 Subject: [PATCH 05/23] Fix test --- tests/e2e.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/e2e.py b/tests/e2e.py index f6155b5..39e822f 100644 --- a/tests/e2e.py +++ b/tests/e2e.py @@ -56,8 +56,8 @@ def test_search_indicators(self): "description": "test", "pattern": "test[0-9]", "diamond": "victim", - "tags": ["testTag"], - } + }, + tags=["testTag"], ) time.sleep(5) result = self.api.search_indicators( From d6c659859cb0d634b10375e5a842aa2b4519ad28 Mon Sep 17 00:00:00 2001 From: Thomas Chopitea Date: Mon, 2 Jun 2025 18:28:19 +0000 Subject: [PATCH 06/23] fix test --- tests/e2e.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e.py b/tests/e2e.py index 39e822f..8ae464e 100644 --- a/tests/e2e.py +++ b/tests/e2e.py @@ -57,7 +57,7 @@ def test_search_indicators(self): "pattern": "test[0-9]", "diamond": "victim", }, - tags=["testTag"], + tags=["testtag"], ) time.sleep(5) result = self.api.search_indicators( From b1eb085d7dde56a4a101d9922a9829b2f2a24c90 Mon Sep 17 00:00:00 2001 From: Thomas Chopitea Date: Mon, 2 Jun 2025 18:29:46 +0000 Subject: [PATCH 07/23] Fix docstrings --- yeti/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/yeti/api.py b/yeti/api.py index 61d5b1a..1dc438f 100644 --- a/yeti/api.py +++ b/yeti/api.py @@ -190,7 +190,7 @@ def search_indicators( name: The name of the indicator to search for. indicator_type: The type of the indicator to search for. pattern: The pattern of the indicator to search for. - description: The description of the indicator to search for. + description: The description of the indicator to search for. (substring match)) tags: The tags of the indicator to search for. Returns: @@ -258,7 +258,7 @@ def search_entities( Args: name: The name of the entity to search for (substring match). entity_type: The type of the entity to search for. - description: The description of the entity to search for. + description: The description of the entity to search for. (substring match) count: The number of results to return (default is 0, which means all). page: The page of results to return (default is 0, which means the first page). From 30552c218096e26d7d71a6d736e8842586863fbc Mon Sep 17 00:00:00 2001 From: Thomas Chopitea Date: Mon, 2 Jun 2025 18:31:53 +0000 Subject: [PATCH 08/23] Adjust some docstrings --- yeti/api.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/yeti/api.py b/yeti/api.py index 1dc438f..e89d72a 100644 --- a/yeti/api.py +++ b/yeti/api.py @@ -687,7 +687,6 @@ def link_objects( def search_graph( self, source: str, - graph: str, target_types: list[str], min_hops: int = 1, max_hops: int = 1, @@ -702,12 +701,11 @@ def search_graph( Args: source: The ID of the source object (as provided by Yeti) in the format - "/", such as 'dfiq/id'. - graph: The graph to search, such as 'links'. + "/", such as 'dfiq/12345'. target_types: The types of objects to search for. min_hops: The minimum number of hops to search. max_hops: The maximum number of hops to search. - direction: The direction to search. + direction: The direction to search. "inbound" or "outbound" or "both". include_original: Whether to include the source object in the results. Returns: @@ -716,7 +714,7 @@ def search_graph( params = { "count": 0, "source": source, - "graph": graph, + "graph": "links", "min_hops": min_hops, "max_hops": max_hops, "direction": direction, From 330f094cf12a33657cbe063776ac6f58f4bfe7a4 Mon Sep 17 00:00:00 2001 From: Thomas Chopitea Date: Mon, 2 Jun 2025 18:38:50 +0000 Subject: [PATCH 09/23] Fix test --- tests/e2e.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/e2e.py b/tests/e2e.py index 8ae464e..46b47e2 100644 --- a/tests/e2e.py +++ b/tests/e2e.py @@ -8,6 +8,10 @@ from yeti import errors from yeti.api import YetiApi +os.environ["YETI_ENDPOINT"] = "http://localhost:3000/" +YETI_API_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoibWNwIiwic3ViIjoieWV0aSIsInNjb3BlcyI6WyJhbGwiXSwiY3JlYXRlZCI6IjIwMjUtMDYtMDJUMTY6NDI6NTQuNTgxOTg2WiIsImV4cCI6bnVsbCwibGFzdF91c2VkIjpudWxsLCJlbmFibGVkIjp0cnVlLCJleHBpcmVkIjpmYWxzZX0.sEWAL2yGfh2Vo_7HIiyvF2d7xWEskvsN4K6FuhJGNH8" +os.environ["YETI_API_KEY"] = YETI_API_KEY + class YetiEndToEndTest(unittest.TestCase): def setUp(self): @@ -61,7 +65,7 @@ def test_search_indicators(self): ) time.sleep(5) result = self.api.search_indicators( - name="testSear", description="tes", tags=["testTag"] + name="testSear", description="tes", tags=["testtag"] ) self.assertEqual(len(result), 1, result) self.assertEqual(result[0]["name"], "testSearch") From e6bab730fff341623563101b2b6299e8b6bb52b3 Mon Sep 17 00:00:00 2001 From: Thomas Chopitea Date: Mon, 2 Jun 2025 18:42:29 +0000 Subject: [PATCH 10/23] Fix graphs --- tests/api.py | 4 ++-- yeti/api.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/api.py b/tests/api.py index 13bcb58..33a2f80 100644 --- a/tests/api.py +++ b/tests/api.py @@ -306,14 +306,14 @@ def test_search_graph(self, mock_post): mock_response.content = b'{"graph": "data"}' mock_post.return_value = mock_response - result = self.api.search_graph("source", "graph", ["type"]) + result = self.api.search_graph("source", ["type"]) self.assertEqual(result, {"graph": "data"}) mock_post.assert_called_with( "http://fake-url/api/v2/graph/search", json={ "count": 0, "source": "source", - "graph": "graph", + "graph": "links", "min_hops": 1, "max_hops": 1, "direction": "outbound", diff --git a/yeti/api.py b/yeti/api.py index e89d72a..fc151cc 100644 --- a/yeti/api.py +++ b/yeti/api.py @@ -688,6 +688,7 @@ def search_graph( self, source: str, target_types: list[str], + graph: str = "links", min_hops: int = 1, max_hops: int = 1, direction: str = "outbound", @@ -714,7 +715,7 @@ def search_graph( params = { "count": 0, "source": source, - "graph": "links", + "graph": graph, "min_hops": min_hops, "max_hops": max_hops, "direction": direction, From b62e084cd0d5fac17c84166cdee188b42815cba9 Mon Sep 17 00:00:00 2001 From: Thomas Chopitea Date: Mon, 2 Jun 2025 18:43:40 +0000 Subject: [PATCH 11/23] remove test api keys --- tests/e2e.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/e2e.py b/tests/e2e.py index 46b47e2..8164e28 100644 --- a/tests/e2e.py +++ b/tests/e2e.py @@ -8,10 +8,6 @@ from yeti import errors from yeti.api import YetiApi -os.environ["YETI_ENDPOINT"] = "http://localhost:3000/" -YETI_API_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoibWNwIiwic3ViIjoieWV0aSIsInNjb3BlcyI6WyJhbGwiXSwiY3JlYXRlZCI6IjIwMjUtMDYtMDJUMTY6NDI6NTQuNTgxOTg2WiIsImV4cCI6bnVsbCwibGFzdF91c2VkIjpudWxsLCJlbmFibGVkIjp0cnVlLCJleHBpcmVkIjpmYWxzZX0.sEWAL2yGfh2Vo_7HIiyvF2d7xWEskvsN4K6FuhJGNH8" -os.environ["YETI_API_KEY"] = YETI_API_KEY - class YetiEndToEndTest(unittest.TestCase): def setUp(self): From 31487b2b911ed086697bb3cca99c82093d550d41 Mon Sep 17 00:00:00 2001 From: Thomas Chopitea Date: Mon, 2 Jun 2025 19:55:57 +0000 Subject: [PATCH 12/23] Add count to graph search --- yeti/api.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/yeti/api.py b/yeti/api.py index fc151cc..bbb6d0b 100644 --- a/yeti/api.py +++ b/yeti/api.py @@ -693,6 +693,8 @@ def search_graph( max_hops: int = 1, direction: str = "outbound", include_original: bool = True, + count: int = 50, + page: int = 0, ) -> dict[str, Any]: """Searches the graph for objects related to a given object. @@ -713,7 +715,8 @@ def search_graph( The response from the API; a dict representing the graph. """ params = { - "count": 0, + "count": count, + "page": page, "source": source, "graph": graph, "min_hops": min_hops, From 9b5e9f553ebb2fbb610a81c8a905e21da2aee95d Mon Sep 17 00:00:00 2001 From: Thomas Chopitea Date: Mon, 2 Jun 2025 20:14:48 +0000 Subject: [PATCH 13/23] update docstring --- yeti/api.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/yeti/api.py b/yeti/api.py index bbb6d0b..d1ec574 100644 --- a/yeti/api.py +++ b/yeti/api.py @@ -703,16 +703,19 @@ def search_graph( for details. Args: - source: The ID of the source object (as provided by Yeti) in the format - "/", such as 'dfiq/12345'. - target_types: The types of objects to search for. - min_hops: The minimum number of hops to search. - max_hops: The maximum number of hops to search. - direction: The direction to search. "inbound" or "outbound" or "both". - include_original: Whether to include the source object in the results. + source: The ID of the source object (as provided by Yeti) in the format + "/", such as 'dfiq/12345'. + target_types: The types of objects to search for. + min_hops: The minimum number of hops to search. + max_hops: The maximum number of hops to search. + direction: The direction to search. "inbound" or "outbound" or "both". + include_original: Whether to include the source object in the results. + count: The number of results to return (default is 50). + page: The page of results to return (default is 0, which means the first page). Returns: - The response from the API; a dict representing the graph. + The response from the API; a dict representing the graph. If the number + of results is lower than the count, the search is complete. """ params = { "count": count, From 91b5515377f948b82daaa398727557c1fc3f6114 Mon Sep 17 00:00:00 2001 From: Thomas Chopitea Date: Mon, 2 Jun 2025 20:39:39 +0000 Subject: [PATCH 14/23] Add test for graph --- tests/e2e.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/e2e.py b/tests/e2e.py index 8164e28..adde5ae 100644 --- a/tests/e2e.py +++ b/tests/e2e.py @@ -82,3 +82,39 @@ def test_find_indicator(self): self.assertEqual(indicator["name"], "testGet") self.assertEqual(indicator["pattern"], "test[0-9]") + + def test_link_objects(self): + self.api.auth_api_key(os.getenv("YETI_API_KEY")) + indicator = self.api.new_indicator( + { + "name": "testLink", + "type": "regex", + "description": "test", + "pattern": "test[0-9]", + "diamond": "victim", + } + ) + malware = self.api.new_entity( + { + "name": "testMalware", + "type": "malware", + "description": "test", + } + ) + self.api.link_objects( + source=indicator, + target=malware, + link_type="indicates", + description="test link", + ) + + # get neighbors + neighbors = self.api.search_graph( + f'indicator/{indicator["id"]}', + target_types=["malware"], + include_original=False, + ) + self.assertEqual(len(neighbors["vertices"]), 1) + self.assertEqual( + neighbors["vertices"][f'entities/{malware["id"]}']["name"], "testMalware" + ) From 0215477a23b46aedbfc61d710baf2ad6163bb961 Mon Sep 17 00:00:00 2001 From: Thomas Chopitea Date: Mon, 2 Jun 2025 20:42:51 +0000 Subject: [PATCH 15/23] Fix unittests --- tests/api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/api.py b/tests/api.py index 33a2f80..90bedd9 100644 --- a/tests/api.py +++ b/tests/api.py @@ -311,7 +311,8 @@ def test_search_graph(self, mock_post): mock_post.assert_called_with( "http://fake-url/api/v2/graph/search", json={ - "count": 0, + "count": 50, + "page": 0, "source": "source", "graph": "links", "min_hops": 1, From caf75ee381db94a3dd2e282ffa377be51be14457 Mon Sep 17 00:00:00 2001 From: Thomas Chopitea Date: Mon, 2 Jun 2025 20:45:06 +0000 Subject: [PATCH 16/23] Fix some docstrings --- yeti/api.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/yeti/api.py b/yeti/api.py index d1ec574..3006749 100644 --- a/yeti/api.py +++ b/yeti/api.py @@ -190,7 +190,7 @@ def search_indicators( name: The name of the indicator to search for. indicator_type: The type of the indicator to search for. pattern: The pattern of the indicator to search for. - description: The description of the indicator to search for. (substring match)) + description: The description of the indicator to search for. (substring match) tags: The tags of the indicator to search for. Returns: @@ -259,13 +259,13 @@ def search_entities( name: The name of the entity to search for (substring match). entity_type: The type of the entity to search for. description: The description of the entity to search for. (substring match) - count: The number of results to return (default is 0, which means all). + count: The number of results to return (default is 100, which means all). page: The page of results to return (default is 0, which means the first page). Returns: The response from the API; a list of dicts representing entities. """ - if not any([name, type, description]): + if not any([name, entity_type, description]): raise ValueError("You must provide one of name, type, or description.") query = {} @@ -483,12 +483,16 @@ def search_dfiq( """Searches for a DFIQ in Yeti. Args: - name: The name of the DFIQ object to search for, e.g. "Suspicious DNS + name: The name of the DFIQ object to search for, e.g. "Suspicious DNS Query." - dfiq_type: The type of the DFIQ object to search for, e.g. "scenario". + dfiq_type: The type of the DFIQ object to search for, e.g. "scenario". + dfiq_yaml: The YAML content of the DFIQ object to search for. + dfiq_tags: The tags of the DFIQ object to search for. + count: The number of results to return (default is 100, which means all). + page: The page of results to return (default is 0, which means the first page). Returns: - The response from the API; a dict representing the DFIQ object. + The response from the API; a dict representing the DFIQ object. """ query = { "name": name, From e1f1f69d6fdf8d7ae1327ae37875e95ca10d536b Mon Sep 17 00:00:00 2001 From: Thomas Chopitea Date: Tue, 3 Jun 2025 14:28:16 +0000 Subject: [PATCH 17/23] Add match_observables method to YetiApi for advanced observable matching --- yeti/api.py | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/yeti/api.py b/yeti/api.py index 3006749..f524ad5 100644 --- a/yeti/api.py +++ b/yeti/api.py @@ -306,6 +306,50 @@ def find_observable(self, value: str, type: str) -> YetiObject | None: raise return json.loads(response) + def match_observables( + self, + observables: list[str], + add_tags: list[str] | None = None, + regex_match: bool = False, + add_type: str | None = None, + fetch_neighbors: bool = True, + add_unknown: bool = False, + ): + """Matches a list of observables against the Yeti data graph. + + This is a more complex method than `search_observables`, as it will + obtain information on entities related to the observables, matching + indicators, and bloom filter hits. + + Args: + observables: The list of observable values to match. + add_tags: Optional. The tags to add to the matched observables. + regex_match: Whether to use regex matching (default is False). + add_type: Optional. The type to add to the matched observables. + fetch_neighbors: Whether to fetch neighbors of the matched observables + (default is True). + add_unknown: Whether to add unknown observables (default is False). + + Returns: + The response from the API; a dict with 'entities' (entities related + to the observables), 'obseravbles' (with the relationship to their + entities), 'known' (list of known observables), 'matches' (for + observables that matched an indicator), and 'unknown' (set of + unknown observables). + """ + params = { + "observables": observables, + "add_tags": add_tags, + "regex_match": regex_match, + "add_type": add_type, + "fetch_neighbors": fetch_neighbors, + "add_unknown": add_unknown, + } + response = self.do_request( + "POST", f"{self._url_root}/api/v2/graph/match", json_data=params + ) + return json.loads(response)["observables"] + def search_observables( self, value: str, From c3067ce505af571dfef0c3decb989327dcbb7913 Mon Sep 17 00:00:00 2001 From: Thomas Chopitea Date: Tue, 3 Jun 2025 15:08:52 +0000 Subject: [PATCH 18/23] bugfix --- yeti/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yeti/api.py b/yeti/api.py index f524ad5..67263f3 100644 --- a/yeti/api.py +++ b/yeti/api.py @@ -339,7 +339,7 @@ def match_observables( """ params = { "observables": observables, - "add_tags": add_tags, + "add_tags": add_tags or [], "regex_match": regex_match, "add_type": add_type, "fetch_neighbors": fetch_neighbors, From 76914fab07548dfe2f37a6ab0968a6c5ae460029 Mon Sep 17 00:00:00 2001 From: Thomas Chopitea Date: Wed, 4 Jun 2025 09:52:02 +0000 Subject: [PATCH 19/23] Send back the whole match response --- yeti/api.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/yeti/api.py b/yeti/api.py index 67263f3..8d5f539 100644 --- a/yeti/api.py +++ b/yeti/api.py @@ -311,7 +311,7 @@ def match_observables( observables: list[str], add_tags: list[str] | None = None, regex_match: bool = False, - add_type: str | None = None, + add_type: str = "guess", fetch_neighbors: bool = True, add_unknown: bool = False, ): @@ -326,6 +326,8 @@ def match_observables( add_tags: Optional. The tags to add to the matched observables. regex_match: Whether to use regex matching (default is False). add_type: Optional. The type to add to the matched observables. + Default is "guess", which will try to guess the type based on the + observable value. fetch_neighbors: Whether to fetch neighbors of the matched observables (default is True). add_unknown: Whether to add unknown observables (default is False). @@ -348,7 +350,7 @@ def match_observables( response = self.do_request( "POST", f"{self._url_root}/api/v2/graph/match", json_data=params ) - return json.loads(response)["observables"] + return json.loads(response) def search_observables( self, From a05d980e5a5e6268d8c5ff237c61ff6d8beb7ae8 Mon Sep 17 00:00:00 2001 From: Thomas Chopitea Date: Thu, 5 Jun 2025 08:04:46 +0000 Subject: [PATCH 20/23] Add supported IOC types --- yeti/api.py | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/yeti/api.py b/yeti/api.py index 8d5f539..40357c2 100644 --- a/yeti/api.py +++ b/yeti/api.py @@ -21,6 +21,38 @@ API_TOKEN_ENDPOINT = "/api/v2/auth/api-token" +SUPPORTED_IOC_TYPES = [ + "generic", + "ipv6", + "ipv4", + "hostname", + "url", + "file", + "sha256", + "md5", + "sha1", + "asn", + "wallet", + "certificate", + "cidr", + "mac_address", + "command_line", + "registry_key", + "imphash", + "tlsh", + "ssdeep", + "email", + "path", + "container_image", + "docker_image", + "user_agent", + "user_account", + "iban", + "bic", + "auth_secret", +] + + # typedef for a Yeti Objects YetiObject = dict[str, Any] YetiLinkObject = dict[str, Any] @@ -670,7 +702,7 @@ def add_observables_bulk( Args: observables: The list of observables to add. Dictionaries should have a - 'value' (str) and a 'type' (str) key. See TACO_TYPE_MAPPING for a list + 'value' (str) and a 'type' (str) key. See SUPPORTED_IOC_TYPES for a list of supported types. tags: The tags to associate with all observables. From fde5de08f70e6db496f2dccb359a7b7d0f93c369 Mon Sep 17 00:00:00 2001 From: Thomas Chopitea Date: Thu, 5 Jun 2025 14:08:44 +0000 Subject: [PATCH 21/23] Bugfix --- yeti/api.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/yeti/api.py b/yeti/api.py index 40357c2..f22b981 100644 --- a/yeti/api.py +++ b/yeti/api.py @@ -574,7 +574,6 @@ def search_dfiq( """ query = { "name": name, - "filter_aliases": [["dfiq_tags", "list"], ["dfiq_id", "text"]], } if dfiq_type: @@ -583,7 +582,12 @@ def search_dfiq( query["dfiq_yaml"] = dfiq_yaml if dfiq_tags: query["dfiq_tags"] = dfiq_tags - params = {"query": query, "count": count, "page": page} + params = { + "query": query, + "count": count, + "page": page, + "filter_aliases": [["dfiq_tags", "list"], ["dfiq_id", "text"]], + } response = self.do_request( "POST", f"{self._url_root}/api/v2/dfiq/search", json_data=params ) From 8de1a06dee7cc5b3804c35acf8f976a1ba25fae5 Mon Sep 17 00:00:00 2001 From: Thomas Chopitea Date: Thu, 5 Jun 2025 14:14:23 +0000 Subject: [PATCH 22/23] Bugfix --- yeti/api.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/yeti/api.py b/yeti/api.py index f22b981..c879645 100644 --- a/yeti/api.py +++ b/yeti/api.py @@ -576,18 +576,19 @@ def search_dfiq( "name": name, } - if dfiq_type: - query["type"] = dfiq_type if dfiq_yaml: query["dfiq_yaml"] = dfiq_yaml if dfiq_tags: query["dfiq_tags"] = dfiq_tags + params = { "query": query, "count": count, "page": page, "filter_aliases": [["dfiq_tags", "list"], ["dfiq_id", "text"]], } + if dfiq_type: + params["type"] = dfiq_type response = self.do_request( "POST", f"{self._url_root}/api/v2/dfiq/search", json_data=params ) From 08376b3b1584b49cca69407230b2e023aa6e363c Mon Sep 17 00:00:00 2001 From: Thomas Chopitea Date: Sat, 7 Jun 2025 19:33:05 +0000 Subject: [PATCH 23/23] Adjust tests --- tests/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/api.py b/tests/api.py index 90bedd9..a44eaf6 100644 --- a/tests/api.py +++ b/tests/api.py @@ -152,9 +152,9 @@ def test_search_dfiq(self, mock_post): "name": "test_dfiq", "dfiq_yaml": "yaml_content", "dfiq_tags": ["tag1"], - "filter_aliases": [["dfiq_tags", "list"], ["dfiq_id", "text"]], }, "count": 100, + "filter_aliases": [["dfiq_tags", "list"], ["dfiq_id", "text"]], "page": 0, }, )