From 681c73af62b090aef4a226078e4401b05647bf22 Mon Sep 17 00:00:00 2001 From: Eric Masseran Date: Tue, 2 Dec 2025 17:47:34 +0100 Subject: [PATCH 01/24] Fix str vs UUID usage --- opentaxii/persistence/api.py | 15 ++-- opentaxii/persistence/sqldb/api.py | 18 ++-- opentaxii/taxii2/entities.py | 111 +++++++++++++----------- opentaxii/taxii2/http.py | 15 +++- tests/taxii2/test_taxii2_collection.py | 26 ++++-- tests/taxii2/test_taxii2_collections.py | 30 ++++--- tests/taxii2/test_taxii2_objects.py | 25 ++++-- tests/taxii2/test_taxii2_sqldb.py | 51 +++++++---- tests/taxii2/test_taxii2_status.py | 6 +- tests/taxii2/utils.py | 88 ++++++++++--------- 10 files changed, 231 insertions(+), 154 deletions(-) diff --git a/opentaxii/persistence/api.py b/opentaxii/persistence/api.py index d5782f33..103351d3 100644 --- a/opentaxii/persistence/api.py +++ b/opentaxii/persistence/api.py @@ -1,4 +1,5 @@ import datetime +import uuid from typing import Dict, List, Optional, Tuple from opentaxii.taxii2.entities import (ApiRoot, Collection, Job, @@ -310,7 +311,7 @@ def get_collection( def get_manifest( self, - collection_id: str, + collection_id: uuid.UUID, limit: Optional[int] = None, added_after: Optional[datetime.datetime] = None, next_kwargs: Optional[Dict] = None, @@ -323,7 +324,7 @@ def get_manifest( def get_objects( self, - collection_id: str, + collection_id: uuid.UUID, limit: Optional[int] = None, added_after: Optional[datetime.datetime] = None, next_kwargs: Optional[Dict] = None, @@ -334,12 +335,14 @@ def get_objects( ) -> Tuple[List[STIXObject], bool, Optional[str]]: raise NotImplementedError - def add_objects(self, api_root_id: str, collection_id: str, objects: List[Dict]) -> Job: + def add_objects( + self, api_root_id: str, collection_id: uuid.UUID, objects: List[Dict] + ) -> Job: raise NotImplementedError def get_object( self, - collection_id: str, + collection_id: uuid.UUID, object_id: str, limit: Optional[int] = None, added_after: Optional[datetime.datetime] = None, @@ -356,7 +359,7 @@ def get_object( def delete_object( self, - collection_id: str, + collection_id: uuid.UUID, object_id: str, match_version: Optional[List[str]] = None, match_spec_version: Optional[List[str]] = None, @@ -365,7 +368,7 @@ def delete_object( def get_versions( self, - collection_id: str, + collection_id: uuid.UUID, object_id: str, limit: Optional[int] = None, added_after: Optional[datetime.datetime] = None, diff --git a/opentaxii/persistence/sqldb/api.py b/opentaxii/persistence/sqldb/api.py index d41f6515..e7641574 100644 --- a/opentaxii/persistence/sqldb/api.py +++ b/opentaxii/persistence/sqldb/api.py @@ -738,7 +738,7 @@ def add_collection( is_public_write=collection.is_public_write, ) - def _objects_query(self, collection_id: str, ordered: bool) -> Query: + def _objects_query(self, collection_id: uuid.UUID, ordered: bool) -> Query: query = self.db.session.query(taxii2models.STIXObject).filter( taxii2models.STIXObject.collection_id == collection_id, ) @@ -785,7 +785,7 @@ def _apply_match_type( def _apply_match_version( self, query: Query, - collection_id: str, + collection_id: uuid.UUID, match_version: Optional[List[str]] = None, ) -> Query: if match_version is None: @@ -874,7 +874,7 @@ def _apply_limit( def _filtered_objects_query( self, - collection_id: str, + collection_id: uuid.UUID, limit: Optional[int] = None, added_after: Optional[datetime.datetime] = None, next_kwargs: Optional[Dict] = None, @@ -896,7 +896,7 @@ def _filtered_objects_query( def get_manifest( self, - collection_id: str, + collection_id: uuid.UUID, limit: Optional[int] = None, added_after: Optional[datetime.datetime] = None, next_kwargs: Optional[Dict] = None, @@ -938,7 +938,7 @@ def get_manifest( def get_objects( self, - collection_id: str, + collection_id: uuid.UUID, limit: Optional[int] = None, added_after: Optional[datetime.datetime] = None, next_kwargs: Optional[Dict] = None, @@ -982,7 +982,7 @@ def get_objects( ) def add_objects( - self, api_root_id: str, collection_id: str, objects: List[Dict] + self, api_root_id: str, collection_id: uuid.UUID, objects: List[Dict] ) -> entities.Job: job = taxii2models.Job( api_root_id=api_root_id, @@ -1047,7 +1047,7 @@ def add_objects( def get_object( self, - collection_id: str, + collection_id: uuid.UUID, object_id: str, limit: Optional[int] = None, added_after: Optional[datetime.datetime] = None, @@ -1108,7 +1108,7 @@ def get_object( def delete_object( self, - collection_id: str, + collection_id: uuid.UUID, object_id: str, match_version: Optional[List[str]] = None, match_spec_version: Optional[List[str]] = None, @@ -1126,7 +1126,7 @@ def delete_object( def get_versions( self, - collection_id: str, + collection_id: uuid.UUID, object_id: str, limit: Optional[int] = None, added_after: Optional[datetime.datetime] = None, diff --git a/opentaxii/taxii2/entities.py b/opentaxii/taxii2/entities.py index e660f225..4671c43b 100644 --- a/opentaxii/taxii2/entities.py +++ b/opentaxii/taxii2/entities.py @@ -1,4 +1,6 @@ """Taxii2 entities.""" + +import uuid from datetime import datetime from typing import List, NamedTuple, Optional @@ -11,15 +13,20 @@ class ApiRoot(Entity): """ TAXII2 API Root entity. - :param str id: id of this API root - :param bool default: indicator of default api root, should only be True once - :param str title: human readable plain text name used to identify this API Root - :param str description: human readable plain text description for this API Root - :param bool is_public: whether this is a publicly readable API root + :param id: id of this API root + :param default: indicator of default api root, should only be True once + :param title: human readable plain text name used to identify this API Root + :param description: human readable plain text description for this API Root + :param is_public: whether this is a publicly readable API root """ def __init__( - self, id: str, default: bool, title: str, description: str, is_public: bool + self, + id: uuid.UUID, + default: bool, + title: str, + description: str | None, + is_public: bool, ): """Initialize ApiRoot.""" self.id = id @@ -33,22 +40,25 @@ class Collection(Entity): """ TAXII2 Collection entity. - :param str id: id of this collection - :param str api_root_id: id of the :class:`ApiRoot` this collection belongs to - :param str title: human readable plain text name used to identify this collection - :param str description: human readable plain text description for this collection - :param str alias: human readable collection name that can be used on systems to alias a collection id - :param bool is_public: whether this is a publicly readable collection - :param bool is_public_write: whether this is a publicly writable collection + :param id: id of this collection + :param api_root_id: id of the :class:`ApiRoot` this collection belongs to + :param title: human readable plain text name used to identify this + collection + :param description: human readable plain text description for this + collection + :param alias: human readable collection name that can be used on systems to + alias a collection id + :param is_public: whether this is a publicly readable collection + :param is_public_write: whether this is a publicly writable collection """ def __init__( self, - id: str, - api_root_id: str, + id: uuid.UUID, + api_root_id: uuid.UUID, title: str, description: str, - alias: str, + alias: str | None, is_public: bool, is_public_write: bool, ): @@ -84,19 +94,19 @@ class STIXObject(Entity): """ TAXII2 STIXObject entity. - :param str id: id of this stix object - :param str collection_id: id of the :class:`Collection` this stix object belongs to - :param str type: type of this stix object - :param str spec_version: stix version this object matches - :param datetime date_added: the date and time this object was added - :param datetime version: the version of this object - :param dict serialized_data: the payload of this object + :param id: id of this stix object + :param collection_id: id of the :class:`Collection` this stix object belongs to + :param type: type of this stix object + :param spec_version: stix version this object matches + :param date_added: the date and time this object was added + :param version: the version of this object + :param serialized_data: the payload of this object """ def __init__( self, id: str, - collection_id: str, + collection_id: uuid.UUID, type: str, spec_version: str, date_added: datetime, @@ -119,10 +129,10 @@ class ManifestRecord(Entity): This is a cut-down version of :class:`STIXObject`, for efficiency. - :param str id: id of this stix object - :param datetime date_added: the date and time this object was added - :param datetime version: the version of this object - :param str spec_version: stix version this object matches + :param id: id of this stix object + :param date_added: the date and time this object was added + :param version: the version of this object + :param spec_version: stix version this object matches """ def __init__( @@ -163,19 +173,20 @@ class JobDetail(Entity): """ TAXII2 JobDetail entity, part of "status resource" in taxii2 docs. - :param str id: id of this job detail - :param str job_id: id of the job this detail belongs to - :param str stix_id: id of the :class:`STIXObject` this detail tracks - :param datetime version: the version of this object - :param str message: message indicating more information about the object being created, - its pending state, or why the object failed to be created. - :param str status: status of this job + :param id: id of this job detail + :param job_id: id of the job this detail belongs to + :param stix_id: id of the :class:`STIXObject` this detail tracks + :param version: the version of this object + :param message: message indicating more information about the object + being created, its pending state, or why the object failed to be + created. + :param status: status of this job """ def __init__( self, - id: str, - job_id: str, + id: uuid.UUID, + job_id: uuid.UUID, stix_id: str, version: datetime, message: str, @@ -207,22 +218,24 @@ class Job(Entity): """ TAXII2 Job entity, called a "status resource" in taxii2 docs. - :param str id: id of this job - :param str api_root_id: id of the :class:`ApiRoot` this collection belongs to - :param str status: status of this job - :param datetime request_timestamp: the datetime of the request that this status resource is monitoring - :param datetime completed_timestamp: the datetime of the completion of this job (used for cleanup) - :param int total_count: the total number of stix objects in this job - :param int success_count: the number of successful stix objects in this job - :param int failure_count: the number of failed stix objects in this job - :param int pending_count: the number of pending stix objects in this job - :param dict details: the details per status of this job + :param id: id of this job + :param api_root_id: id of the :class:`ApiRoot` this collection belongs to + :param status: status of this job + :param request_timestamp: the datetime of the request that this status + resource is monitoring + :param completed_timestamp: the datetime of the completion of this job (used + for cleanup) + :param total_count: the total number of stix objects in this job + :param success_count: the number of successful stix objects in this job + :param failure_count: the number of failed stix objects in this job + :param pending_count: the number of pending stix objects in this job + :param details: the details per status of this job """ def __init__( self, - id: str, - api_root_id: str, + id: uuid.UUID, + api_root_id: uuid.UUID, status: str, request_timestamp: datetime, completed_timestamp: Optional[datetime] = None, diff --git a/opentaxii/taxii2/http.py b/opentaxii/taxii2/http.py index a0408d33..5af5d060 100644 --- a/opentaxii/taxii2/http.py +++ b/opentaxii/taxii2/http.py @@ -1,14 +1,25 @@ """Taxii2 http helper functions.""" + import json +import uuid from typing import Dict, Optional from flask import Response, make_response -def make_taxii2_response(data, status: Optional[int] = 200, extra_headers: Optional[Dict] = None) -> Response: +class UuidJSONEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, uuid.UUID): + return str(obj) + return super().default(obj) + + +def make_taxii2_response( + data, status: Optional[int] = 200, extra_headers: Optional[Dict] = None +) -> Response: """Turn input data into valid taxii2 response.""" if not isinstance(data, str): - data = json.dumps(data) + data = json.dumps(data, cls=UuidJSONEncoder) response = make_response((data, status)) response.content_type = "application/taxii+json;version=2.1" response.headers.update(extra_headers or {}) diff --git a/tests/taxii2/test_taxii2_collection.py b/tests/taxii2/test_taxii2_collection.py index 061a6055..77938b34 100644 --- a/tests/taxii2/test_taxii2_collection.py +++ b/tests/taxii2/test_taxii2_collection.py @@ -19,7 +19,7 @@ 200, {"Content-Type": "application/taxii+json;version=2.1"}, { - "id": COLLECTIONS[0].id, + "id": str(COLLECTIONS[0].id), "title": "0Read only", "description": "Read only description", "can_read": True, @@ -36,7 +36,7 @@ 200, {"Content-Type": "application/taxii+json;version=2.1"}, { - "id": COLLECTIONS[4].id, + "id": str(COLLECTIONS[4].id), "title": "4No description", "can_read": True, "can_write": True, @@ -52,7 +52,7 @@ 200, {"Content-Type": "application/taxii+json;version=2.1"}, { - "id": COLLECTIONS[5].id, + "id": str(COLLECTIONS[5].id), "title": "5With alias", "description": "With alias description", "alias": "this-is-an-alias", @@ -70,7 +70,7 @@ 200, {"Content-Type": "application/taxii+json;version=2.1"}, { - "id": COLLECTIONS[5].id, + "id": str(COLLECTIONS[5].id), "title": "5With alias", "description": "With alias description", "alias": "this-is-an-alias", @@ -189,7 +189,9 @@ def test_collection( }, ): func = getattr(authenticated_client, method) - response = func(f"/taxii2/{api_root_id}/collections/{collection_id}/", headers=headers) + response = func( + f"/taxii2/{api_root_id}/collections/{collection_id}/", headers=headers + ) assert response.status_code == expected_status assert { key: response.headers.get(key) for key in expected_headers @@ -281,7 +283,15 @@ def test_collection_unauthenticated( ], ) def test_add_collection( - app, api_root_id, title, description, alias, is_public, is_public_write, db_api_roots, db_collections + app, + api_root_id, + title, + description, + alias, + is_public, + is_public_write, + db_api_roots, + db_collections, ): collection = app.taxii_server.servers.taxii2.persistence.api.add_collection( api_root_id=api_root_id, @@ -292,7 +302,7 @@ def test_add_collection( is_public_write=is_public_write, ) assert collection.id is not None - assert str(collection.api_root_id) == api_root_id + assert collection.api_root_id == api_root_id assert collection.title == title assert collection.description == description assert collection.alias == alias @@ -305,7 +315,7 @@ def test_add_collection( .filter(taxii2models.Collection.id == collection.id) .one() ) - assert str(db_collection.api_root_id) == api_root_id + assert db_collection.api_root_id == api_root_id assert db_collection.title == title assert db_collection.description == description assert db_collection.alias == alias diff --git a/tests/taxii2/test_taxii2_collections.py b/tests/taxii2/test_taxii2_collections.py index e478361b..46ded5b5 100644 --- a/tests/taxii2/test_taxii2_collections.py +++ b/tests/taxii2/test_taxii2_collections.py @@ -3,10 +3,16 @@ from uuid import uuid4 import pytest -from tests.taxii2.utils import (API_ROOTS, COLLECTIONS, GET_API_ROOT_MOCK, - GET_COLLECTIONS_MOCK, config_noop, - server_mapping_noop, - server_mapping_remove_fields) + +from tests.taxii2.utils import ( + API_ROOTS, + COLLECTIONS, + GET_API_ROOT_MOCK, + GET_COLLECTIONS_MOCK, + config_noop, + server_mapping_noop, + server_mapping_remove_fields, +) @pytest.mark.parametrize( @@ -32,7 +38,7 @@ { "collections": [ { - "id": COLLECTIONS[0].id, + "id": str(COLLECTIONS[0].id), "title": "0Read only", "description": "Read only description", "can_read": True, @@ -40,7 +46,7 @@ "media_types": ["application/stix+json;version=2.1"], }, { - "id": COLLECTIONS[1].id, + "id": str(COLLECTIONS[1].id), "title": "1Write only", "description": "Write only description", "can_read": False, @@ -48,7 +54,7 @@ "media_types": ["application/stix+json;version=2.1"], }, { - "id": COLLECTIONS[2].id, + "id": str(COLLECTIONS[2].id), "title": "2Read/Write", "description": "Read/Write description", "can_read": True, @@ -56,7 +62,7 @@ "media_types": ["application/stix+json;version=2.1"], }, { - "id": COLLECTIONS[3].id, + "id": str(COLLECTIONS[3].id), "title": "3No permissions", "description": "No permissions description", "can_read": False, @@ -64,14 +70,14 @@ "media_types": ["application/stix+json;version=2.1"], }, { - "id": COLLECTIONS[4].id, + "id": str(COLLECTIONS[4].id), "title": "4No description", "can_read": True, "can_write": True, "media_types": ["application/stix+json;version=2.1"], }, { - "id": COLLECTIONS[5].id, + "id": str(COLLECTIONS[5].id), "title": "5With alias", "description": "With alias description", "alias": "this-is-an-alias", @@ -80,7 +86,7 @@ "media_types": ["application/stix+json;version=2.1"], }, { - "id": COLLECTIONS[6].id, + "id": str(COLLECTIONS[6].id), "title": "6Public", "description": "public description", "can_read": True, @@ -88,7 +94,7 @@ "media_types": ["application/stix+json;version=2.1"], }, { - "id": COLLECTIONS[7].id, + "id": str(COLLECTIONS[7].id), "title": "7Publicwrite", "description": "public write description", "can_read": False, diff --git a/tests/taxii2/test_taxii2_objects.py b/tests/taxii2/test_taxii2_objects.py index c5a97010..5f5a3d31 100644 --- a/tests/taxii2/test_taxii2_objects.py +++ b/tests/taxii2/test_taxii2_objects.py @@ -5,11 +5,22 @@ from uuid import uuid4 import pytest + from opentaxii.taxii2.utils import taxii2_datetimeformat -from tests.taxii2.utils import (ADD_OBJECTS_MOCK, API_ROOTS, COLLECTIONS, - GET_COLLECTION_MOCK, GET_JOB_AND_DETAILS_MOCK, - GET_NEXT_PARAM, GET_OBJECTS_MOCK, JOBS, NOW, - STIX_OBJECTS, config_noop, config_override) +from tests.taxii2.utils import ( + ADD_OBJECTS_MOCK, + API_ROOTS, + COLLECTIONS, + GET_COLLECTION_MOCK, + GET_JOB_AND_DETAILS_MOCK, + GET_NEXT_PARAM, + GET_OBJECTS_MOCK, + JOBS, + NOW, + STIX_OBJECTS, + config_noop, + config_override, +) from tests.utils import SKIP @@ -775,7 +786,7 @@ 202, {"Content-Type": "application/taxii+json;version=2.1"}, { - "id": JOBS[0].id, + "id": str(JOBS[0].id), "status": JOBS[0].status, "request_timestamp": taxii2_datetimeformat(JOBS[0].request_timestamp), "total_count": 4, @@ -1217,7 +1228,7 @@ def test_objects( assert response.status_code == expected_status if method == "post" and expected_status == 202: add_objects_mock.assert_called_once_with( - api_root_id=API_ROOTS[0].id, + api_root_id=str(API_ROOTS[0].id), collection_id=COLLECTIONS[5].id, objects=post_data["objects"], ) @@ -1318,7 +1329,7 @@ def test_objects_unauthenticated( assert response.status_code == expected_status_code if method == "post" and expected_status_code == 202: add_objects_mock.assert_called_once_with( - api_root_id=API_ROOTS[0].id, + api_root_id=str(API_ROOTS[0].id), collection_id=COLLECTIONS[7].id, objects=kwargs["json"]["objects"], ) diff --git a/tests/taxii2/test_taxii2_sqldb.py b/tests/taxii2/test_taxii2_sqldb.py index c993720b..94914be8 100644 --- a/tests/taxii2/test_taxii2_sqldb.py +++ b/tests/taxii2/test_taxii2_sqldb.py @@ -2,16 +2,27 @@ from uuid import uuid4 import pytest + from opentaxii.persistence.sqldb.taxii2models import Job, JobDetail, STIXObject from opentaxii.taxii2 import entities from opentaxii.taxii2.utils import DATETIMEFORMAT -from tests.taxii2.utils import (API_ROOTS, API_ROOTS_WITH_DEFAULT, - API_ROOTS_WITHOUT_DEFAULT, COLLECTIONS, - GET_API_ROOT_MOCK, GET_COLLECTION_MOCK, - GET_COLLECTIONS_MOCK, GET_JOB_AND_DETAILS_MOCK, - GET_MANIFEST_MOCK, GET_OBJECT_MOCK, - GET_OBJECTS_MOCK, GET_VERSIONS_MOCK, JOBS, NOW, - STIX_OBJECTS) +from tests.taxii2.utils import ( + API_ROOTS, + API_ROOTS_WITH_DEFAULT, + API_ROOTS_WITHOUT_DEFAULT, + COLLECTIONS, + GET_API_ROOT_MOCK, + GET_COLLECTION_MOCK, + GET_COLLECTIONS_MOCK, + GET_JOB_AND_DETAILS_MOCK, + GET_MANIFEST_MOCK, + GET_OBJECT_MOCK, + GET_OBJECTS_MOCK, + GET_VERSIONS_MOCK, + JOBS, + NOW, + STIX_OBJECTS, +) @pytest.mark.parametrize( @@ -51,8 +62,8 @@ def test_get_api_roots(taxii2_sqldb_api, db_api_roots): ], ) def test_get_api_root(taxii2_sqldb_api, db_api_roots, api_root_id): - response = taxii2_sqldb_api.get_api_root(api_root_id) - assert response == GET_API_ROOT_MOCK(api_root_id) + response = taxii2_sqldb_api.get_api_root(str(api_root_id)) + assert response == GET_API_ROOT_MOCK(str(api_root_id)) @pytest.mark.parametrize( @@ -86,8 +97,8 @@ def test_get_api_root(taxii2_sqldb_api, db_api_roots, api_root_id): ], ) def test_get_job_and_details(taxii2_sqldb_api, db_jobs, api_root_id, job_id): - response = taxii2_sqldb_api.get_job_and_details(api_root_id, job_id) - assert response == GET_JOB_AND_DETAILS_MOCK(api_root_id, job_id) + response = taxii2_sqldb_api.get_job_and_details(str(api_root_id), str(job_id)) + assert response == GET_JOB_AND_DETAILS_MOCK(str(api_root_id), str(job_id)) @pytest.mark.parametrize( @@ -108,8 +119,8 @@ def test_get_job_and_details(taxii2_sqldb_api, db_jobs, api_root_id, job_id): ], ) def test_get_collections(taxii2_sqldb_api, db_collections, api_root_id): - response = taxii2_sqldb_api.get_collections(api_root_id) - assert response == GET_COLLECTIONS_MOCK(api_root_id) + response = taxii2_sqldb_api.get_collections(str(api_root_id)) + assert response == GET_COLLECTIONS_MOCK(str(api_root_id)) @pytest.mark.parametrize( @@ -150,8 +161,12 @@ def test_get_collections(taxii2_sqldb_api, db_collections, api_root_id): def test_get_collection( taxii2_sqldb_api, db_collections, api_root_id, collection_id_or_alias ): - response = taxii2_sqldb_api.get_collection(api_root_id, collection_id_or_alias) - assert response == GET_COLLECTION_MOCK(api_root_id, collection_id_or_alias) + response = taxii2_sqldb_api.get_collection( + str(api_root_id), str(collection_id_or_alias) + ) + assert response == GET_COLLECTION_MOCK( + str(api_root_id), str(collection_id_or_alias) + ) @pytest.mark.parametrize( @@ -864,7 +879,7 @@ def test_add_objects( assert isinstance(job.completed_timestamp, datetime.datetime) # Check database state db_job = taxii2_sqldb_api.db.session.query(Job).one() - assert str(db_job.api_root_id) == api_root_id + assert db_job.api_root_id == api_root_id assert db_job.status == "complete" assert isinstance(db_job.request_timestamp, datetime.datetime) assert isinstance(db_job.completed_timestamp, datetime.datetime) @@ -881,7 +896,7 @@ def test_add_objects( .one() ) assert db_obj.id == obj["id"] - assert str(db_obj.collection_id) == collection_id + assert db_obj.collection_id == collection_id assert db_obj.type == obj["type"] assert db_obj.spec_version == obj["spec_version"] assert isinstance(db_obj.date_added, datetime.datetime) @@ -1249,7 +1264,7 @@ def test_delete_object( match_spec_version=match_spec_version, ) assert set( - (str(db_obj.collection_id), db_obj.id, db_obj.version) + (db_obj.collection_id, db_obj.id, db_obj.version) for db_obj in taxii2_sqldb_api.db.session.query(STIXObject).all() ) == set((obj.collection_id, obj.id, obj.version) for obj in expected_objects) diff --git a/tests/taxii2/test_taxii2_status.py b/tests/taxii2/test_taxii2_status.py index 660345b8..92afb07e 100644 --- a/tests/taxii2/test_taxii2_status.py +++ b/tests/taxii2/test_taxii2_status.py @@ -34,7 +34,7 @@ 200, {"Content-Type": "application/taxii+json;version=2.1"}, { - "id": JOBS[0].id, + "id": str(JOBS[0].id), "status": JOBS[0].status, "request_timestamp": taxii2_datetimeformat(JOBS[0].request_timestamp), "total_count": 4, @@ -77,7 +77,7 @@ 200, {"Content-Type": "application/taxii+json;version=2.1"}, { - "id": JOBS[3].id, + "id": str(JOBS[3].id), "status": JOBS[3].status, "request_timestamp": taxii2_datetimeformat(JOBS[3].request_timestamp), "total_count": 0, @@ -97,7 +97,7 @@ 200, {"Content-Type": "application/taxii+json;version=2.1"}, { - "id": JOBS[6].id, + "id": str(JOBS[6].id), "status": JOBS[6].status, "request_timestamp": taxii2_datetimeformat(JOBS[6].request_timestamp), "total_count": 6, diff --git a/tests/taxii2/utils.py b/tests/taxii2/utils.py index a3c68039..04ee43ef 100644 --- a/tests/taxii2/utils.py +++ b/tests/taxii2/utils.py @@ -1,22 +1,28 @@ import base64 import datetime from typing import Dict, List, Optional -from uuid import uuid4 +from uuid import UUID, uuid4 from opentaxii.server import ServerMapping -from opentaxii.taxii2.entities import (ApiRoot, Collection, Job, JobDetail, - ManifestRecord, STIXObject, - VersionRecord) +from opentaxii.taxii2.entities import ( + ApiRoot, + Collection, + Job, + JobDetail, + ManifestRecord, + STIXObject, + VersionRecord, +) from opentaxii.taxii2.utils import DATETIMEFORMAT, taxii2_datetimeformat API_ROOTS_WITH_DEFAULT = ( - ApiRoot(str(uuid4()), True, "first title", "first description", False), - ApiRoot(str(uuid4()), False, "second title", "second description", True), + ApiRoot(uuid4(), True, "first title", "first description", False), + ApiRoot(uuid4(), False, "second title", "second description", True), ) API_ROOTS_WITHOUT_DEFAULT = ( - ApiRoot(str(uuid4()), False, "first title", "first description", False), - ApiRoot(str(uuid4()), False, "second title", "second description", True), - ApiRoot(str(uuid4()), False, "third title", None, False), + ApiRoot(uuid4(), False, "first title", "first description", False), + ApiRoot(uuid4(), False, "second title", "second description", True), + ApiRoot(uuid4(), False, "third title", None, False), ) API_ROOTS = API_ROOTS_WITHOUT_DEFAULT NOW = datetime.datetime.now(datetime.timezone.utc) @@ -24,14 +30,14 @@ for api_root in API_ROOTS: JOBS = JOBS + ( Job( - str(uuid4()), + uuid4(), api_root.id, "complete", NOW, NOW - datetime.timedelta(hours=24, minutes=1), ), Job( - str(uuid4()), + uuid4(), api_root.id, "pending", NOW, @@ -40,7 +46,7 @@ ) JOBS = JOBS + ( Job( - str(uuid4()), + uuid4(), API_ROOTS[0].id, "pending", NOW, @@ -55,7 +61,7 @@ JOBS[0].details.success.extend( [ JobDetail( - id=str(uuid4()), + id=uuid4(), job_id=JOBS[0].id, stix_id="indicator--c410e480-e42b-47d1-9476-85307c12bcbf", version=datetime.datetime.strptime( @@ -70,7 +76,7 @@ JOBS[0].details.failure.extend( [ JobDetail( - id=str(uuid4()), + id=uuid4(), job_id=JOBS[0].id, stix_id="malware--664fa29d-bf65-4f28-a667-bdb76f29ec98", version=datetime.datetime.strptime( @@ -85,7 +91,7 @@ JOBS[0].details.pending.extend( [ JobDetail( - id=str(uuid4()), + id=uuid4(), job_id=JOBS[0].id, stix_id="indicator--252c7c11-daf2-42bd-843b-be65edca9f61", version=datetime.datetime.strptime( @@ -95,7 +101,7 @@ status="pending", ), JobDetail( - id=str(uuid4()), + id=uuid4(), job_id=JOBS[0].id, stix_id="relationship--045585ad-a22f-4333-af33-bfd503a683b5", version=datetime.datetime.strptime( @@ -111,7 +117,7 @@ COLLECTIONS = ( Collection( - str(uuid4()), + uuid4(), API_ROOTS[0].id, "0Read only", "Read only description", @@ -120,7 +126,7 @@ False, ), Collection( - str(uuid4()), + uuid4(), API_ROOTS[0].id, "1Write only", "Write only description", @@ -129,7 +135,7 @@ False, ), Collection( - str(uuid4()), + uuid4(), API_ROOTS[0].id, "2Read/Write", "Read/Write description", @@ -138,7 +144,7 @@ False, ), Collection( - str(uuid4()), + uuid4(), API_ROOTS[0].id, "3No permissions", "No permissions description", @@ -146,9 +152,9 @@ False, False, ), - Collection(str(uuid4()), API_ROOTS[0].id, "4No description", "", None, False, False), + Collection(uuid4(), API_ROOTS[0].id, "4No description", "", None, False, False), Collection( - str(uuid4()), + uuid4(), API_ROOTS[0].id, "5With alias", "With alias description", @@ -157,7 +163,7 @@ False, ), Collection( - str(uuid4()), + uuid4(), API_ROOTS[0].id, "6Public", "public description", @@ -166,7 +172,7 @@ False, ), Collection( - str(uuid4()), + uuid4(), API_ROOTS[0].id, "7Publicwrite", "public write description", @@ -175,7 +181,7 @@ True, ), ) -STIX_OBJECTS = ( +STIX_OBJECTS: tuple[STIXObject, ...] = ( STIXObject( f"indicator--{str(uuid4())}", COLLECTIONS[5].id, @@ -303,17 +309,17 @@ def process_match_version(match_version): return id_version_combos -def GET_API_ROOT_MOCK(api_root_id): +def GET_API_ROOT_MOCK(api_root_id: str): for api_root in API_ROOTS: - if api_root.id == api_root_id: + if str(api_root.id) == api_root_id: return api_root return None -def GET_JOB_AND_DETAILS_MOCK(api_root_id, job_id): +def GET_JOB_AND_DETAILS_MOCK(api_root_id: str, job_id: str): job_response = None for job in JOBS: - if job.api_root_id == api_root_id and job.id == job_id: + if str(job.api_root_id) == api_root_id and str(job.id) == job_id: job_response = job break return job_response @@ -322,15 +328,15 @@ def GET_JOB_AND_DETAILS_MOCK(api_root_id, job_id): def GET_COLLECTIONS_MOCK(api_root_id): response = [] for collection in COLLECTIONS: - if collection.api_root_id == api_root_id: + if str(collection.api_root_id) == api_root_id: response.append(collection) return response -def GET_COLLECTION_MOCK(api_root_id, collection_id_or_alias): +def GET_COLLECTION_MOCK(api_root_id: str, collection_id_or_alias: str): for collection in COLLECTIONS: - if collection.api_root_id == api_root_id and ( - collection.id == collection_id_or_alias + if str(collection.api_root_id) == api_root_id and ( + str(collection.id) == collection_id_or_alias or collection.alias == collection_id_or_alias ): return collection @@ -377,7 +383,7 @@ def GET_NEXT_PARAM(kwargs: Dict) -> str: def GET_OBJECTS_MOCK( - collection_id: str, + collection_id: UUID, limit: Optional[int] = None, added_after: Optional[datetime.datetime] = None, next_kwargs: Optional[Dict] = None, @@ -426,7 +432,7 @@ def GET_OBJECTS_MOCK( def GET_OBJECT_MOCK( - collection_id: str, + collection_id: UUID, object_id: str, limit: Optional[int] = None, added_after: Optional[datetime.datetime] = None, @@ -480,7 +486,7 @@ def ADD_OBJECTS_MOCK(api_root_id: str, collection_id: str, objects: List[Dict]): def DELETE_OBJECT_MOCK( - collection_id: str, + collection_id: UUID, object_id: str, match_version: Optional[List[str]] = None, match_spec_version: Optional[List[str]] = None, @@ -489,7 +495,7 @@ def DELETE_OBJECT_MOCK( def GET_VERSIONS_MOCK( - collection_id: str, + collection_id: UUID, object_id: str, limit: Optional[int] = None, added_after: Optional[datetime.datetime] = None, @@ -506,9 +512,11 @@ def GET_VERSIONS_MOCK( match_version=["all"], ) return ( - [VersionRecord(obj.date_added, obj.version) for obj in versions] - if versions is not None - else None, + ( + [VersionRecord(obj.date_added, obj.version) for obj in versions] + if versions is not None + else None + ), more, ) From c169762e2dcf81ea533640ffa432701efcd391f6 Mon Sep 17 00:00:00 2001 From: Eric Masseran Date: Tue, 2 Dec 2025 17:48:23 +0100 Subject: [PATCH 02/24] Fix some typing issues --- opentaxii/persistence/api.py | 3 ++- opentaxii/persistence/manager.py | 11 ++++++++--- opentaxii/persistence/sqldb/api.py | 2 +- opentaxii/server.py | 2 ++ 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/opentaxii/persistence/api.py b/opentaxii/persistence/api.py index 103351d3..d16a2722 100644 --- a/opentaxii/persistence/api.py +++ b/opentaxii/persistence/api.py @@ -268,8 +268,9 @@ class OpenTAXII2PersistenceAPI: Stub, pending implementation. """ + @staticmethod - def get_next_param(self, kwargs: Dict) -> str: + def get_next_param(kwargs: Dict) -> str: """ Get value for `next` based on :class:`Dict` instance. diff --git a/opentaxii/persistence/manager.py b/opentaxii/persistence/manager.py index 8f3649d3..470076ba 100644 --- a/opentaxii/persistence/manager.py +++ b/opentaxii/persistence/manager.py @@ -1,5 +1,5 @@ import datetime -from typing import Dict, List, Optional, Tuple +from typing import TYPE_CHECKING, Dict, List, Optional, Tuple import structlog from opentaxii.local import context @@ -15,9 +15,14 @@ log = structlog.getLogger(__name__) +if TYPE_CHECKING: + from opentaxii.persistence.api import OpenTAXII2PersistenceAPI + from opentaxii.server import TAXII2Server + class BasePersistenceManager: - pass + def __init__(self, server, api): + raise NotImplementedError class Taxii1PersistenceManager(BasePersistenceManager): @@ -394,7 +399,7 @@ class Taxii2PersistenceManager(BasePersistenceManager): instance of persistence API class """ - def __init__(self, server, api): + def __init__(self, server: "TAXII2Server", api: "OpenTAXII2PersistenceAPI"): self.server = server self.api = api diff --git a/opentaxii/persistence/sqldb/api.py b/opentaxii/persistence/sqldb/api.py index e7641574..1fad95e6 100644 --- a/opentaxii/persistence/sqldb/api.py +++ b/opentaxii/persistence/sqldb/api.py @@ -882,7 +882,7 @@ def _filtered_objects_query( match_type: Optional[List[str]] = None, match_version: Optional[List[str]] = None, match_spec_version: Optional[List[str]] = None, - ordered: Optional[bool] = True, + ordered: bool = True, ) -> Tuple[Query, bool]: query = self._objects_query(collection_id, ordered) query = self._apply_added_after(query, added_after) diff --git a/opentaxii/server.py b/opentaxii/server.py index 4b848f7a..2d222245 100644 --- a/opentaxii/server.py +++ b/opentaxii/server.py @@ -433,6 +433,8 @@ def get_endpoint(self, relative_path: str) -> Optional[Callable[[], Response]]: if endpoint: return functools.partial(self.handle_request, endpoint) + return None + def check_authentication(self, endpoint: Callable[[], Response]): """Check if account is authenticated, unless endpoint handles that itself.""" if endpoint.func.handles_own_auth: From 2aade0a8508514e5284fa98cc2158ae53c7246aa Mon Sep 17 00:00:00 2001 From: Eric Masseran Date: Wed, 3 Dec 2025 11:51:07 +0100 Subject: [PATCH 03/24] Fix types --- opentaxii/auth/__init__.py | 5 + opentaxii/auth/sqldb/api.py | 70 ++++--- opentaxii/auth/sqldb/models.py | 12 +- opentaxii/common/sqldb.py | 2 +- opentaxii/config.py | 4 +- opentaxii/middleware.py | 9 +- opentaxii/persistence/__init__.py | 10 +- opentaxii/persistence/api.py | 27 ++- opentaxii/persistence/manager.py | 47 +++-- opentaxii/persistence/sqldb/taxii2models.py | 6 +- opentaxii/server.py | 212 +++++++++++++------- opentaxii/taxii/services/__init__.py | 11 +- opentaxii/taxii2/validation.py | 29 ++- opentaxii/utils.py | 15 +- 14 files changed, 292 insertions(+), 167 deletions(-) diff --git a/opentaxii/auth/__init__.py b/opentaxii/auth/__init__.py index fa78ca89..7586727f 100644 --- a/opentaxii/auth/__init__.py +++ b/opentaxii/auth/__init__.py @@ -1,3 +1,8 @@ # flake8: noqa from .api import OpenTAXIIAuthAPI from .manager import AuthManager + +__all__ = ( + "AuthManager", + "OpenTAXIIAuthAPI", +) diff --git a/opentaxii/auth/sqldb/api.py b/opentaxii/auth/sqldb/api.py index 29808341..319cb454 100644 --- a/opentaxii/auth/sqldb/api.py +++ b/opentaxii/auth/sqldb/api.py @@ -1,47 +1,53 @@ from datetime import datetime, timedelta +from typing import Optional import jwt import structlog +from sqlalchemy.orm import exc + from opentaxii.auth import OpenTAXIIAuthAPI from opentaxii.common.sqldb import BaseSQLDatabaseAPI from opentaxii.entities import Account as AccountEntity -from sqlalchemy.orm import exc from .models import Account, Base -__all__ = ['SQLDatabaseAPI'] +__all__ = ["SQLDatabaseAPI"] log = structlog.getLogger(__name__) class SQLDatabaseAPI(BaseSQLDatabaseAPI, OpenTAXIIAuthAPI): - """Naive SQL database implementation of OpenTAXII Auth API. - - Implementation will work with any DB supported by SQLAlchemy package. - - :param str db_connection: a string that indicates database dialect and - connection arguments that will be passed directly - to :func:`~sqlalchemy.engine.create_engine` method. - :param bool create_tables=False: if True, tables will be created in the DB. - :param str secret: secret string used for token generation - :param int token_ttl_secs: TTL for JWT token, in seconds. - :param engine_parameters=None: if defined, these arguments would be passed to sqlalchemy.create_engine - """ BASEMODEL = Base def __init__( - self, - db_connection, - create_tables=False, - secret=None, - token_ttl_secs=None, - **engine_parameters): + self, + db_connection: str, + create_tables: bool = False, + secret: Optional[str] = None, + token_ttl_secs: Optional[int] = None, + **engine_parameters + ): + """Naive SQL database implementation of OpenTAXII Auth API. + + Implementation will work with any DB supported by SQLAlchemy package. + + :param db_connection: a string that indicates database dialect and + connection arguments that will be passed directly to + :func:`~sqlalchemy.engine.create_engine` method. + :param create_tables=False: if True, tables will be created in the DB. + :param secret: secret string used for token generation + :param token_ttl_secs: TTL for JWT token, in seconds. + :param engine_parameters=None: if defined, these arguments would be passed + to sqlalchemy.create_engine + """ super().__init__(db_connection, create_tables, **engine_parameters) if not secret: - raise ValueError('Secret is not defined for %s.%s' % ( - self.__module__, self.__class__.__name__)) + raise ValueError( + "Secret is not defined for %s.%s" + % (self.__module__, self.__class__.__name__) + ) self.secret = secret self.token_ttl_secs = token_ttl_secs or 60 * 60 # 60min @@ -55,7 +61,9 @@ def authenticate(self, username, password): return self._generate_token(account.id, ttl=self.token_ttl_secs) def create_account(self, username, password, is_admin=False): - account = Account(username=username, is_admin=is_admin, permissions={}) + account = Account( # type: ignore[misc] + username=username, is_admin=is_admin, permissions={} + ) account.set_password(password) self.db.session.add(account) self.db.session.commit() @@ -71,7 +79,9 @@ def get_account(self, token): return account_to_account_entity(account) def delete_account(self, username): - account = self.db.session.query(Account).filter_by(username=username).one_or_none() + account = ( + self.db.session.query(Account).filter_by(username=username).one_or_none() + ) if account: self.db.session.delete(account) self.db.session.commit() @@ -79,10 +89,15 @@ def delete_account(self, username): def get_accounts(self): return [ account_to_account_entity(account) - for account in self.db.session.query(Account).all()] + for account in self.db.session.query(Account).all() + ] def update_account(self, obj, password=None): - account = self.db.session.query(Account).filter_by(username=obj.username).one_or_none() + account = ( + self.db.session.query(Account) + .filter_by(username=obj.username) + .one_or_none() + ) if not account: account = Account(username=obj.username) self.db.session.add(account) @@ -120,4 +135,5 @@ def account_to_account_entity(account): id=account.id, username=account.username, is_admin=account.is_admin, - permissions=account.permissions) + permissions=account.permissions, + ) diff --git a/opentaxii/auth/sqldb/models.py b/opentaxii/auth/sqldb/models.py index ce512fe8..db39a30a 100644 --- a/opentaxii/auth/sqldb/models.py +++ b/opentaxii/auth/sqldb/models.py @@ -2,10 +2,7 @@ from sqlalchemy import schema, types from sqlalchemy.ext.declarative import declarative_base - -from werkzeug.security import ( - check_password_hash, generate_password_hash -) +from werkzeug.security import check_password_hash, generate_password_hash __all__ = ['Base', 'Account'] @@ -31,6 +28,7 @@ def set_password(self, password): self.password_hash = generate_password_hash(password) def is_password_valid(self, password): + assert self.password_hash is not None return check_password_hash(self.password_hash, password) @property @@ -42,6 +40,8 @@ def permissions(self, permissions): for collection_name, permission in permissions.items(): if permission not in ALL_PERMISSIONS: raise ValueError( - "Unknown permission '{}' specified for collection '{}'" - .format(permission, collection_name)) + "Unknown permission '{}' specified for collection '{}'".format( + permission, collection_name + ) + ) self._permissions = json.dumps(permissions) diff --git a/opentaxii/common/sqldb.py b/opentaxii/common/sqldb.py index 65720a1b..eba26ab2 100644 --- a/opentaxii/common/sqldb.py +++ b/opentaxii/common/sqldb.py @@ -3,7 +3,7 @@ from opentaxii.sqldb_helper import SQLAlchemyDB try: - from sqlalchemy.orm import DeclarativeMeta + from sqlalchemy.orm import DeclarativeMeta # type: ignore[attr-defined] except ImportError: from sqlalchemy.ext.declarative import DeclarativeMeta diff --git a/opentaxii/config.py b/opentaxii/config.py index b8f2f87a..3e5357bd 100644 --- a/opentaxii/config.py +++ b/opentaxii/config.py @@ -99,7 +99,7 @@ def _get_env_config(env=os.environ, optional_env_var=None): continue if key == optional_env_var: continue - key = key[len(ENV_VAR_PREFIX):].lstrip("_").lower() + key = key[len(ENV_VAR_PREFIX) :].lstrip("_").lower() value = yaml.safe_load(value) container = result @@ -112,7 +112,7 @@ def _get_env_config(env=os.environ, optional_env_var=None): @classmethod def _load_configs(cls, *configs): - result = dict() + result: dict = dict() for config in configs: # read content from path-like object if not isinstance(config, dict): diff --git a/opentaxii/middleware.py b/opentaxii/middleware.py index 02f8ebb1..16c6067e 100644 --- a/opentaxii/middleware.py +++ b/opentaxii/middleware.py @@ -2,8 +2,7 @@ import structlog from flask import Flask, request -from marshmallow.exceptions import \ - ValidationError as MarshmallowValidationError +from marshmallow.exceptions import ValidationError as MarshmallowValidationError from werkzeug.exceptions import HTTPException from .exceptions import InvalidAuthHeader @@ -26,7 +25,7 @@ def create_app(server): """ app = Flask(__name__) - app.taxii_server = server + app.taxii_server = server # type: ignore[attr-defined] server.init_app(app) @@ -42,7 +41,9 @@ def create_app(server): app.register_error_handler(500, server.handle_internal_error) app.register_error_handler(StatusMessageException, server.handle_status_exception) app.register_error_handler(HTTPException, server.handle_http_exception) - app.register_error_handler(MarshmallowValidationError, server.handle_validation_exception) + app.register_error_handler( + MarshmallowValidationError, server.handle_validation_exception + ) app.before_request(functools.partial(create_context_before_request, server)) app.after_request(cleanup_context) return app diff --git a/opentaxii/persistence/__init__.py b/opentaxii/persistence/__init__.py index e02c81c3..ad7157a0 100644 --- a/opentaxii/persistence/__init__.py +++ b/opentaxii/persistence/__init__.py @@ -1,4 +1,10 @@ # flake8: noqa from .api import OpenTAXII2PersistenceAPI, OpenTAXIIPersistenceAPI -from .manager import (BasePersistenceManager, Taxii1PersistenceManager, - Taxii2PersistenceManager) +from .manager import Taxii1PersistenceManager, Taxii2PersistenceManager + +__all__ = ( + "OpenTAXII2PersistenceAPI", + "OpenTAXIIPersistenceAPI", + "Taxii1PersistenceManager", + "Taxii2PersistenceManager", +) diff --git a/opentaxii/persistence/api.py b/opentaxii/persistence/api.py index d16a2722..1dc2fc7c 100644 --- a/opentaxii/persistence/api.py +++ b/opentaxii/persistence/api.py @@ -2,9 +2,14 @@ import uuid from typing import Dict, List, Optional, Tuple -from opentaxii.taxii2.entities import (ApiRoot, Collection, Job, - ManifestRecord, STIXObject, - VersionRecord) +from opentaxii.taxii2.entities import ( + ApiRoot, + Collection, + Job, + ManifestRecord, + STIXObject, + VersionRecord, +) class OpenTAXIIPersistenceAPI: @@ -30,6 +35,15 @@ def create_service(self, service_entity): """ raise NotImplementedError() + def update_service(self, obj): + raise NotImplementedError() + + def delete_service(self, service_id): + raise NotImplementedError() + + def set_collection_services(self, collection_id, service_ids): + raise NotImplementedError() + def create_collection(self, collection_entity): """Create a collection. @@ -269,6 +283,9 @@ class OpenTAXII2PersistenceAPI: Stub, pending implementation. """ + def init_app(self, app): + pass + @staticmethod def get_next_param(kwargs: Dict) -> str: """ @@ -297,9 +314,7 @@ def get_api_roots(self) -> List[ApiRoot]: def get_api_root(self, api_root_id: str) -> Optional[ApiRoot]: raise NotImplementedError - def get_job_and_details( - self, api_root_id: str, job_id: str - ) -> Optional[Job]: + def get_job_and_details(self, api_root_id: str, job_id: str) -> Optional[Job]: raise NotImplementedError def get_collections(self, api_root_id: str) -> List[Collection]: diff --git a/opentaxii/persistence/manager.py b/opentaxii/persistence/manager.py index 470076ba..46caeee8 100644 --- a/opentaxii/persistence/manager.py +++ b/opentaxii/persistence/manager.py @@ -2,30 +2,39 @@ from typing import TYPE_CHECKING, Dict, List, Optional, Tuple import structlog + from opentaxii.local import context -from opentaxii.persistence.exceptions import (DoesNotExistError, - NoReadNoWritePermission, - NoReadPermission, - NoWritePermission) -from opentaxii.signals import (CONTENT_BLOCK_CREATED, INBOX_MESSAGE_CREATED, - SUBSCRIPTION_CREATED) -from opentaxii.taxii2.entities import (ApiRoot, Collection, Job, - ManifestRecord, STIXObject, - VersionRecord) +from opentaxii.persistence.exceptions import ( + DoesNotExistError, + NoReadNoWritePermission, + NoReadPermission, + NoWritePermission, +) +from opentaxii.signals import ( + CONTENT_BLOCK_CREATED, + INBOX_MESSAGE_CREATED, + SUBSCRIPTION_CREATED, +) +from opentaxii.taxii2.entities import ( + ApiRoot, + Collection, + Job, + ManifestRecord, + STIXObject, + VersionRecord, +) log = structlog.getLogger(__name__) if TYPE_CHECKING: - from opentaxii.persistence.api import OpenTAXII2PersistenceAPI - from opentaxii.server import TAXII2Server - - -class BasePersistenceManager: - def __init__(self, server, api): - raise NotImplementedError + from opentaxii.persistence.api import ( + OpenTAXII2PersistenceAPI, + OpenTAXIIPersistenceAPI, + ) + from opentaxii.server import TAXII1Server, TAXII2Server -class Taxii1PersistenceManager(BasePersistenceManager): +class Taxii1PersistenceManager: """Manager responsible for persisting and retrieving data. Manager uses API instance ``api`` for basic data CRUD operations and @@ -35,7 +44,7 @@ class Taxii1PersistenceManager(BasePersistenceManager): instance of persistence API class """ - def __init__(self, server, api): + def __init__(self, server: "TAXII1Server", api: "OpenTAXIIPersistenceAPI"): self.server = server self.api = api @@ -389,7 +398,7 @@ def delete_content_blocks( return count -class Taxii2PersistenceManager(BasePersistenceManager): +class Taxii2PersistenceManager: """Manager responsible for persisting and retrieving data. Manager uses API instance ``api`` for basic data CRUD operations and diff --git a/opentaxii/persistence/sqldb/taxii2models.py b/opentaxii/persistence/sqldb/taxii2models.py index ead4333d..3a09d2f5 100644 --- a/opentaxii/persistence/sqldb/taxii2models.py +++ b/opentaxii/persistence/sqldb/taxii2models.py @@ -1,14 +1,16 @@ """Database models for taxii2 entities.""" + import datetime import uuid import sqlalchemy -from opentaxii.persistence.sqldb.common import GUID, UTCDateTime -from opentaxii.taxii2 import entities from sqlalchemy import literal from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import relationship +from opentaxii.persistence.sqldb.common import GUID, UTCDateTime +from opentaxii.taxii2 import entities + Base = declarative_base() diff --git a/opentaxii/server.py b/opentaxii/server.py index 2d222245..8a57cb25 100644 --- a/opentaxii/server.py +++ b/opentaxii/server.py @@ -5,26 +5,36 @@ try: from re import Pattern except ImportError: - from typing.re import Pattern + from typing.re import Pattern # type: ignore[no-redef] -from typing import Callable, ClassVar, NamedTuple, Optional, Tuple, Type +from typing import ClassVar, NamedTuple, Optional, Protocol, Tuple, Type import structlog from flask import Flask, Response, request -from werkzeug.exceptions import (Forbidden, MethodNotAllowed, NotAcceptable, - NotFound, RequestEntityTooLarge, Unauthorized, - UnsupportedMediaType) - -from opentaxii.persistence.exceptions import (DoesNotExistError, - NoReadNoWritePermission, - NoReadPermission, - NoWritePermission) +from werkzeug.exceptions import ( + Forbidden, + MethodNotAllowed, + NotAcceptable, + NotFound, + RequestEntityTooLarge, + Unauthorized, + UnsupportedMediaType, +) + +from opentaxii.persistence.exceptions import ( + DoesNotExistError, + NoReadNoWritePermission, + NoReadPermission, + NoWritePermission, +) from opentaxii.taxii2.utils import taxii2_datetimeformat -from opentaxii.taxii2.validation import (validate_delete_filter_params, - validate_envelope, - validate_list_filter_params, - validate_object_filter_params, - validate_versions_filter_params) +from opentaxii.taxii2.validation import ( + validate_delete_filter_params, + validate_envelope, + validate_list_filter_params, + validate_object_filter_params, + validate_versions_filter_params, +) from opentaxii.utils import register_handler from .auth import AuthManager @@ -32,20 +42,26 @@ from .entities import Account from .exceptions import UnauthorizedException from .local import context -from .persistence import (BasePersistenceManager, Taxii1PersistenceManager, - Taxii2PersistenceManager) +from .persistence import Taxii1PersistenceManager, Taxii2PersistenceManager from .taxii2.http import make_taxii2_response -from .taxii.bindings import (ALL_PROTOCOL_BINDINGS, MESSAGE_BINDINGS, - SERVICE_BINDINGS) -from .taxii.exceptions import (FailureStatus, StatusMessageException, - raise_failure) -from .taxii.http import (HTTP_ALLOW, HTTP_X_TAXII_CONTENT_TYPES, - get_content_type, get_http_headers, - make_taxii_response, validate_request_headers, - validate_request_headers_post_parse, - validate_response_headers) -from .taxii.services import (CollectionManagementService, DiscoveryService, - InboxService, PollService) +from .taxii.bindings import ALL_PROTOCOL_BINDINGS, MESSAGE_BINDINGS, SERVICE_BINDINGS +from .taxii.exceptions import FailureStatus, StatusMessageException, raise_failure +from .taxii.http import ( + HTTP_ALLOW, + HTTP_X_TAXII_CONTENT_TYPES, + get_content_type, + get_http_headers, + make_taxii_response, + validate_request_headers, + validate_request_headers_post_parse, + validate_response_headers, +) +from .taxii.services import ( + CollectionManagementService, + DiscoveryService, + InboxService, + PollService, +) from .taxii.services.abstract import TAXIIService from .taxii.status import process_status_exception from .taxii.utils import configure_libtaxii_xml_parser, parse_message @@ -56,27 +72,39 @@ anonymous_full_access = Account(id=None, username=None, permissions={}, is_admin=True) +class EndpointFunc(Protocol): + registered_url_re: Pattern + registered_valid_methods: Tuple[str, ...] + registered_valid_accept_mimetypes: Tuple[str, ...] + registered_valid_content_types: Tuple[str, ...] + handles_own_auth: bool + + def __call__(self, **kwargs) -> Response: ... + + +class Endpoint(Protocol): + """The result of functools.partial""" + + func: EndpointFunc + server: "BaseTAXIIServer" + + def __call__(self) -> Response: ... + + class BaseTAXIIServer: """ Base class for common functionality in taxii* servers. """ - PERSISTENCE_MANAGER_CLASS: ClassVar[Type[BasePersistenceManager]] - ENDPOINT_MAPPING: Tuple[(Pattern, Callable[[], Response])] + ENDPOINT_MAPPING: Tuple[Tuple[Pattern, EndpointFunc], ...] app: Flask config: dict - - def __init__(self, config: dict): - self.config = config - self.persistence = self.PERSISTENCE_MANAGER_CLASS( - server=self, api=initialize_api(config["persistence_api"]) - ) - self.setup_endpoint_mapping() + persistence: Taxii1PersistenceManager | Taxii2PersistenceManager def setup_endpoint_mapping(self): mapping = [] for attr_name in self.__dir__(): - attr = getattr(self, attr_name) + attr: EndpointFunc = getattr(self, attr_name) if hasattr(attr, "registered_url_re"): mapping.append((attr.registered_url_re, attr)) if mapping: @@ -87,15 +115,9 @@ def init_app(self, app: Flask): self.app = app self.persistence.api.init_app(app) - def get_domain(self, service_id): - """Get domain either from request handler or config.""" - dynamic_domain = self.persistence.get_domain(service_id) - domain = dynamic_domain or self.config.get("domain") - return domain - - def get_endpoint(self, relative_path: str) -> Optional[Callable[[], Response]]: + def get_endpoint(self, relative_path: str) -> Optional[Endpoint]: """Get first endpoint matching relative_path.""" - return + raise NotImplementedError def handle_internal_error(self, error): """ @@ -103,7 +125,7 @@ def handle_internal_error(self, error): Placeholder for subclasses to implement. """ - return + raise NotImplementedError def handle_status_exception(self, error): """ @@ -111,7 +133,7 @@ def handle_status_exception(self, error): Placeholder for subclasses to implement. """ - return + raise NotImplementedError def handle_http_exception(self, error): return error.get_response() @@ -122,7 +144,7 @@ def handle_validation_exception(self, error): Placeholder for subclasses to implement. """ - return + raise NotImplementedError def raise_unauthorized(self): """ @@ -149,10 +171,14 @@ class TAXII1Server(BaseTAXIIServer): "collection_management": CollectionManagementService, "poll": PollService, } - PERSISTENCE_MANAGER_CLASS = Taxii1PersistenceManager + persistence: Taxii1PersistenceManager def __init__(self, config: dict): - super().__init__(config) + self.config = config + self.persistence = Taxii1PersistenceManager( + server=self, api=initialize_api(config["persistence_api"]) + ) + self.setup_endpoint_mapping() signal_hooks = config["hooks"] if signal_hooks: importlib.import_module(signal_hooks) @@ -198,11 +224,21 @@ def check_allowed_methods(self): if request.method not in valid_methods: raise MethodNotAllowed(valid_methods=valid_methods) - def get_endpoint(self, relative_path: str) -> Optional[Callable[[], Response]]: + def get_endpoint(self, relative_path: str) -> Optional[Endpoint]: """Get first endpoint matching relative_path.""" for endpoint in self.get_services(): if endpoint.path == relative_path: - return functools.partial(self.handle_request, endpoint=endpoint) + return functools.partial( # type: ignore[return-value] + self.handle_request, endpoint=endpoint + ) + + return None + + def get_domain(self, service_id): + """Get domain either from request handler or config.""" + dynamic_domain = self.persistence.get_domain(service_id) + domain = dynamic_domain or self.config.get("domain") + return domain def get_services(self, service_ids=None): """Get services registered with this TAXII server instance. @@ -265,11 +301,12 @@ def get_services_for_collection(self, collection, service_type): # Sync services for collection with registered services for this server return self.get_services(ids_for_type) - def handle_request(self, endpoint: TAXIIService): + def handle_request(self, endpoint: TAXIIService) -> Response: """ Handle request and return appropriate response. - Process :class:`TAXIIService` with either :meth:`_process_with_service` or :meth:`_process_options_request`. + Process :class:`TAXIIService` with either :meth:`_process_with_service` + or :meth:`_process_options_request`. """ self.check_allowed_methods() if endpoint.authentication_required and context.account is None: @@ -286,7 +323,7 @@ def handle_request(self, endpoint: TAXIIService): if request.method == "POST": return self._process_with_service(endpoint) - if request.method == "OPTIONS": + else: # OPTIONS return self._process_options_request(endpoint) @staticmethod @@ -312,7 +349,7 @@ def _process_with_service(cls, service) -> Response: if "application/xml" not in request.accept_mimetypes: raise_failure( "The specified values of Accept is not supported: {}".format( - ", ".join((request.accept_mimetypes or [])) + ", ".join((request.accept_mimetypes or [])) # type: ignore[arg-type] ) ) @@ -389,7 +426,14 @@ class TAXII2Server(BaseTAXIIServer): Stub, implementation pending. """ - PERSISTENCE_MANAGER_CLASS = Taxii2PersistenceManager + persistence: Taxii2PersistenceManager + + def __init__(self, config: dict): + self.config = config + self.persistence = Taxii2PersistenceManager( + server=self, api=initialize_api(config["persistence_api"]) + ) + self.setup_endpoint_mapping() def handle_http_exception(self, error): """Return JSON instead of HTML for HTTP errors.""" @@ -423,7 +467,7 @@ def raise_unauthorized(self): """ raise Unauthorized() - def get_endpoint(self, relative_path: str) -> Optional[Callable[[], Response]]: + def get_endpoint(self, relative_path: str) -> Optional[Endpoint]: endpoint = None for regex, handler in self.ENDPOINT_MAPPING: match = regex.match(relative_path) @@ -431,11 +475,13 @@ def get_endpoint(self, relative_path: str) -> Optional[Callable[[], Response]]: endpoint = functools.partial(handler, **match.groupdict()) break if endpoint: - return functools.partial(self.handle_request, endpoint) + return functools.partial( # type: ignore[return-value] + self.handle_request, endpoint # type: ignore[arg-type] + ) return None - def check_authentication(self, endpoint: Callable[[], Response]): + def check_authentication(self, endpoint: Endpoint): """Check if account is authenticated, unless endpoint handles that itself.""" if endpoint.func.handles_own_auth: # Endpoint will handle auth checks itself @@ -451,11 +497,11 @@ def check_content_length(self): ]: # untestable with flask raise RequestEntityTooLarge() - def check_headers(self, endpoint: Callable[[], Response]): + def check_headers(self, endpoint: Endpoint): if not any( [ - valid_accept_mimetype in request.accept_mimetypes - for valid_accept_mimetype in endpoint.func.registered_valid_accept_mimetypes + accept_mimetype in request.accept_mimetypes + for accept_mimetype in endpoint.func.registered_valid_accept_mimetypes ] ): raise NotAcceptable() @@ -465,11 +511,11 @@ def check_headers(self, endpoint: Callable[[], Response]): ): raise UnsupportedMediaType() - def check_allowed_methods(self, endpoint: Callable[[], Response]): + def check_allowed_methods(self, endpoint: Endpoint): if request.method not in endpoint.func.registered_valid_methods: raise MethodNotAllowed(valid_methods=endpoint.func.registered_valid_methods) - def handle_request(self, endpoint: Callable[[], Response]): + def handle_request(self, endpoint: Endpoint) -> Response: self.check_authentication(endpoint) self.check_content_length() self.check_allowed_methods(endpoint) @@ -546,7 +592,7 @@ def collections_handler(self, api_root_id): if context.account is None and not api_root.is_public: raise Unauthorized() collections = self.persistence.get_collections(api_root_id=api_root_id) - response = {} + response: dict = {} if collections: response["collections"] = [] for collection in collections: @@ -565,10 +611,11 @@ def collections_handler(self, api_root_id): return make_taxii2_response(response) @register_handler( - r"^/taxii2/(?P[^/]+)/collections/(?P[^/]+)/$", + r"^/taxii2/(?P[^/]+)" + r"/collections/(?P[^/]+)/$", handles_own_auth=True, ) - def collection_handler(self, api_root_id, collection_id_or_alias): + def collection_handler(self, api_root_id, collection_id_or_alias: str): try: collection = self.persistence.get_collection( api_root_id=api_root_id, collection_id_or_alias=collection_id_or_alias @@ -596,7 +643,8 @@ def collection_handler(self, api_root_id, collection_id_or_alias): return make_taxii2_response(response) @register_handler( - r"^/taxii2/(?P[^/]+)/collections/(?P[^/]+)/manifest/$", + r"^/taxii2/(?P[^/]+)" + r"/collections/(?P[^/]+)/manifest/$", handles_own_auth=True, ) def manifest_handler(self, api_root_id, collection_id_or_alias): @@ -612,14 +660,15 @@ def manifest_handler(self, api_root_id, collection_id_or_alias): raise Unauthorized() raise NotFound() if manifest: - response = { + response: dict = { "more": more, "objects": [ { "id": obj.id, "date_added": taxii2_datetimeformat(obj.date_added), "version": taxii2_datetimeformat(obj.version), - "media_type": f"application/stix+json;version={obj.spec_version}", + "media_type": "application/stix+json;version=" + + obj.spec_version, } for obj in manifest ], @@ -641,7 +690,8 @@ def manifest_handler(self, api_root_id, collection_id_or_alias): ) @register_handler( - r"^/taxii2/(?P[^/]+)/collections/(?P[^/]+)/objects/$", + r"^/taxii2/(?P[^/]+)" + r"/collections/(?P[^/]+)/objects/$", ("GET", "POST"), valid_content_types=("application/taxii+json;version=2.1",), handles_own_auth=True, @@ -710,7 +760,7 @@ def objects_post_handler(self, api_root_id, collection_id_or_alias): raise Unauthorized() raise NotFound() response = job.as_taxii2_dict() - headers = {} + headers: dict = {} return make_taxii2_response( response, 202, @@ -718,7 +768,9 @@ def objects_post_handler(self, api_root_id, collection_id_or_alias): ) @register_handler( - r"^/taxii2/(?P[^/]+)/collections/(?P[^/]+)/objects/(?P[^/]+)/$", + r"^/taxii2/(?P[^/]+)" + r"/collections/(?P[^/]+)" + r"/objects/(?P[^/]+)/$", ("GET", "DELETE"), handles_own_auth=True, ) @@ -799,7 +851,8 @@ def object_delete_handler(self, api_root_id, collection_id_or_alias, object_id): @register_handler( ( - r"^/taxii2/(?P[^/]+)/collections/(?P[^/]+)" + r"^/taxii2/(?P[^/]+)" + r"/collections/(?P[^/]+)" r"/objects/(?P[^/]+)/versions/$" ), handles_own_auth=True, @@ -864,7 +917,7 @@ class TAXIIServer: def __init__(self, config: ServerConfig): self.config = config - servers_kwargs = { + servers_kwargs: dict = { "taxii1": None, "taxii2": None, } @@ -907,7 +960,7 @@ def is_basic_auth_supported(self): """Check if basic auth is a supported feature.""" return self.config.get("support_basic_auth", False) - def get_endpoint(self, relative_path: str) -> Optional[Callable[[], Response]]: + def get_endpoint(self, relative_path: str) -> Optional[Endpoint]: """Get first endpoint matching relative_path.""" for server in self.real_servers: endpoint = server.get_endpoint(relative_path) @@ -915,6 +968,8 @@ def get_endpoint(self, relative_path: str) -> Optional[Callable[[], Response]]: endpoint.server = server return endpoint + return None + def handle_request(self, relative_path: str) -> Response: """Dispatch request to appropriate taxii* server.""" relative_path = "/" + relative_path @@ -954,6 +1009,7 @@ def raise_unauthorized(self): if endpoint: server = endpoint.server else: + assert self.servers.taxii1 is not None server = self.servers.taxii1 context.taxiiserver = server return server.raise_unauthorized() diff --git a/opentaxii/taxii/services/__init__.py b/opentaxii/taxii/services/__init__.py index b7b83cbd..860c87ac 100644 --- a/opentaxii/taxii/services/__init__.py +++ b/opentaxii/taxii/services/__init__.py @@ -1,6 +1,13 @@ # flake8: noqa -from .inbox import InboxService -from .discovery import DiscoveryService from .collection_management import CollectionManagementService +from .discovery import DiscoveryService +from .inbox import InboxService from .poll import PollService + +__all__ = [ + "InboxService", + "DiscoveryService", + "CollectionManagementService", + "PollService", +] diff --git a/opentaxii/taxii2/validation.py b/opentaxii/taxii2/validation.py index 07a8e3b5..cd393649 100644 --- a/opentaxii/taxii2/validation.py +++ b/opentaxii/taxii2/validation.py @@ -1,22 +1,24 @@ """Taxii2 validation functions.""" + import datetime import json +from typing import Mapping from marshmallow import Schema, fields +from stix2 import parse +from stix2.exceptions import STIXError + from opentaxii.persistence.api import OpenTAXII2PersistenceAPI from opentaxii.taxii2.exceptions import ValidationError from opentaxii.taxii2.utils import DATETIMEFORMAT -from stix2 import parse -from stix2.exceptions import STIXError -from werkzeug.datastructures import ImmutableMultiDict -def validate_envelope(json_data: str, allow_custom: bool = False) -> None: +def validate_envelope(json_data: str | bytes, allow_custom: bool = False) -> None: """ Validate if ``json_data`` is a valid taxii2 envelope. - :param str json_data: the data to check - :param bool allow_custom: if true, allow non-standard stix types + :param json_data: the data to check + :param allow_custom: if true, allow non-standard stix types """ if not json_data: raise ValidationError("No data") @@ -51,7 +53,11 @@ class Taxii2Next(fields.Field): def _deserialize(self, value, attr, data, **kwargs): value = super()._deserialize(value, attr, data, **kwargs) try: - value = self.parent.persistence_api.parse_next_param(value) + value = ( + self.parent.persistence_api.parse_next_param( # type:ignore[union-attr] + value + ) + ) except: # noqa raise ValidationError("Not a valid value.") return value @@ -85,6 +91,7 @@ def _deserialize(self, value, attr, data, **kwargs): class PersistenceApiMxin: """Store persistence api on schema instance, to reference in `Taxii2Next`""" + def __init__(self, persistence_api: OpenTAXII2PersistenceAPI, *args, **kwargs): self.persistence_api = persistence_api super().__init__(*args, **kwargs) @@ -129,7 +136,7 @@ class DeleteFilterParamsSchema(Schema): def validate_object_filter_params( - filter_params: ImmutableMultiDict, persistence_api: OpenTAXII2PersistenceAPI + filter_params: Mapping, persistence_api: OpenTAXII2PersistenceAPI ) -> dict: """Validate and load filter params for the object endpoint.""" parsed_params = ObjectFilterParamsSchema(persistence_api).load(filter_params) @@ -137,7 +144,7 @@ def validate_object_filter_params( def validate_list_filter_params( - filter_params: ImmutableMultiDict, persistence_api: OpenTAXII2PersistenceAPI + filter_params: Mapping, persistence_api: OpenTAXII2PersistenceAPI ) -> dict: """Validate and load filter params for the list endpoint.""" parsed_params = ListFilterParamsSchema(persistence_api).load(filter_params) @@ -145,14 +152,14 @@ def validate_list_filter_params( def validate_versions_filter_params( - filter_params: ImmutableMultiDict, persistence_api: OpenTAXII2PersistenceAPI + filter_params: Mapping, persistence_api: OpenTAXII2PersistenceAPI ) -> dict: """Validate and load filter params for the versions endpoint.""" parsed_params = VersionFilterParamsSchema(persistence_api).load(filter_params) return parsed_params -def validate_delete_filter_params(filter_params: ImmutableMultiDict) -> dict: +def validate_delete_filter_params(filter_params: Mapping) -> dict: """Validate and load filter params for the delete endpoint.""" parsed_params = DeleteFilterParamsSchema().load(filter_params) return parsed_params diff --git a/opentaxii/utils.py b/opentaxii/utils.py index 43e2e45a..dc0ea7e8 100644 --- a/opentaxii/utils.py +++ b/opentaxii/utils.py @@ -313,20 +313,21 @@ def sync_accounts(server, accounts): def register_handler( url_re: str, - valid_methods: Optional[Tuple[str]] = None, - valid_accept_mimetypes: Optional[Tuple[str]] = None, - valid_content_types: Optional[Tuple[str]] = None, + valid_methods: Optional[Tuple[str, ...]] = None, + valid_accept_mimetypes: Optional[Tuple[str, ...]] = None, + valid_content_types: Optional[Tuple[str, ...]] = None, handles_own_auth: bool = False, ): """ Register decorated method as handler function for `url_re`. - :param str url_re: The regex to trigger the handler on - :param list valid_methods: The list of methods to accept for this handler, defaults to ("GET",) - :param list valid_accept_mimetypes: + :param url_re: The regex to trigger the handler on + :param valid_methods: The list of methods to accept for this handler, + defaults to ("GET",) + :param valid_accept_mimetypes: The list of accepted mimetypes to accept for this handler, defaults to ("application/taxii+json;version=2.1",) - :param list valid_content_types: + :param valid_content_types: The list of content types to accept for this handler, defaults to ("application/json",) """ From fc67a2bc3f42dd78f8700c9a9855531036eac7fa Mon Sep 17 00:00:00 2001 From: Eric Masseran Date: Wed, 3 Dec 2025 11:57:24 +0100 Subject: [PATCH 04/24] Add mypy in env and CI --- .github/workflows/mypy.yml | 17 +++++++++++++++++ pyproject.toml | 25 +++++++++++++++++++++++++ requirements-dev.txt | 5 +++++ 3 files changed, 47 insertions(+) create mode 100644 .github/workflows/mypy.yml diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml new file mode 100644 index 00000000..24c607fe --- /dev/null +++ b/.github/workflows/mypy.yml @@ -0,0 +1,17 @@ +name: Check mypy + +on: + - pull_request +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python 3.9 + uses: actions/setup-python@v5 + with: + python-version: 3.9 + - name: Install deps + run: pip install -r requirements.txt -r requirements-dev.txt + - name: Run mypy + run: mypy diff --git a/pyproject.toml b/pyproject.toml index d25ff27c..6533836e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,3 +10,28 @@ ignore-semiprivate = true omit-covered-files = true verbose = 0 exclude = ["tests"] + +[tool.mypy] +plugins = ["sqlmypy"] + +ignore_missing_imports = true +no_implicit_optional = true +show_error_codes = true +warn_redundant_casts = true +warn_unused_configs = true +warn_unused_ignores = true +check_untyped_defs = true +follow_imports = "silent" + +files = [ + "opentaxii/", +] + +exclude = """ +(?x)( + ^opentaxii/persistence/sqldb/api\\.py$ + | ^opentaxii/taxii/.+ + | ^opentaxii/utils\\.py$ + | ^opentaxii/common/entities\\.py$ +) +""" diff --git a/requirements-dev.txt b/requirements-dev.txt index 16a1ba0f..5de045fd 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -5,4 +5,9 @@ pytest-pythonpath flake8 ipdb factory-boy>=3.2.1 +mypy==1.19.0 +types-six==1.17.0.20251009 +sqlalchemy-stubs==0.4 +types-PyYAML==6.0.12.20250915 +types-pytz==2025.2.0.20251108 -r requirements-interrogate.txt From cc9f59a64dd2f6d09a1e355cc0607565e3870159 Mon Sep 17 00:00:00 2001 From: Eric Masseran Date: Wed, 3 Dec 2025 11:58:53 +0100 Subject: [PATCH 05/24] Fix compatibility with python 3.8 --- opentaxii/taxii2/entities.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opentaxii/taxii2/entities.py b/opentaxii/taxii2/entities.py index 4671c43b..3b379726 100644 --- a/opentaxii/taxii2/entities.py +++ b/opentaxii/taxii2/entities.py @@ -25,7 +25,7 @@ def __init__( id: uuid.UUID, default: bool, title: str, - description: str | None, + description: Optional[str], is_public: bool, ): """Initialize ApiRoot.""" @@ -58,7 +58,7 @@ def __init__( api_root_id: uuid.UUID, title: str, description: str, - alias: str | None, + alias: Optional[str], is_public: bool, is_public_write: bool, ): From df4c0880efbaebc9db42ed187ee72041eff833c5 Mon Sep 17 00:00:00 2001 From: Eric Masseran Date: Wed, 3 Dec 2025 12:00:23 +0100 Subject: [PATCH 06/24] Docker compose doc update --- docs/installation/docker.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/installation/docker.rst b/docs/installation/docker.rst index b2e76bbc..5b1112c3 100644 --- a/docs/installation/docker.rst +++ b/docs/installation/docker.rst @@ -120,11 +120,12 @@ TAXII 2 instance with Compose Checkout the configuration at: :github-file:`examples/docker-compose-taxii2.yml `. -To add dummy data, you can execute: - .. code-block:: shell - # while the compose project is running + # Start + docker compose -f examples/docker-compose-taxii2.yml up + + # To add dummy data, run this while the compose project is running docker exec -i examples-opentaxii-1 bash < examples/taxii2/data-setup.sh Full Example with Compose From e51c6ae2e43d003b70228edb771da5df40c8d146 Mon Sep 17 00:00:00 2001 From: Eric Masseran Date: Wed, 3 Dec 2025 15:25:07 +0100 Subject: [PATCH 07/24] Use UUID for api_root_id --- opentaxii/persistence/api.py | 14 ++++--- opentaxii/persistence/manager.py | 21 +++++----- opentaxii/persistence/sqldb/api.py | 10 ++--- opentaxii/server.py | 62 ++++++++++++++++++----------- tests/taxii2/test_taxii2_objects.py | 4 +- tests/taxii2/test_taxii2_sqldb.py | 60 ++++++++++++++++------------ tests/taxii2/utils.py | 20 +++++----- tests/test_cli.py | 22 +++++----- 8 files changed, 119 insertions(+), 94 deletions(-) diff --git a/opentaxii/persistence/api.py b/opentaxii/persistence/api.py index 1dc2fc7c..8d6b76f8 100644 --- a/opentaxii/persistence/api.py +++ b/opentaxii/persistence/api.py @@ -311,17 +311,19 @@ def get_api_roots(self) -> List[ApiRoot]: """ raise NotImplementedError - def get_api_root(self, api_root_id: str) -> Optional[ApiRoot]: + def get_api_root(self, api_root_id: uuid.UUID) -> Optional[ApiRoot]: raise NotImplementedError - def get_job_and_details(self, api_root_id: str, job_id: str) -> Optional[Job]: + def get_job_and_details( + self, api_root_id: uuid.UUID, job_id: uuid.UUID + ) -> Optional[Job]: raise NotImplementedError - def get_collections(self, api_root_id: str) -> List[Collection]: + def get_collections(self, api_root_id: uuid.UUID) -> List[Collection]: raise NotImplementedError def get_collection( - self, api_root_id: str, collection_id_or_alias: str + self, api_root_id: uuid.UUID, collection_id_or_alias: str ) -> Optional[Collection]: raise NotImplementedError @@ -352,7 +354,7 @@ def get_objects( raise NotImplementedError def add_objects( - self, api_root_id: str, collection_id: uuid.UUID, objects: List[Dict] + self, api_root_id: uuid.UUID, collection_id: uuid.UUID, objects: List[Dict] ) -> Job: raise NotImplementedError @@ -390,7 +392,7 @@ def get_versions( added_after: Optional[datetime.datetime] = None, next_kwargs: Optional[Dict] = None, match_spec_version: Optional[List[str]] = None, - ) -> Tuple[List[VersionRecord], bool]: + ) -> Tuple[Optional[List[VersionRecord]], bool]: """ Get all versions of single object from database. diff --git a/opentaxii/persistence/manager.py b/opentaxii/persistence/manager.py index 46caeee8..67428a85 100644 --- a/opentaxii/persistence/manager.py +++ b/opentaxii/persistence/manager.py @@ -1,4 +1,5 @@ import datetime +import uuid from typing import TYPE_CHECKING, Dict, List, Optional, Tuple import structlog @@ -428,23 +429,23 @@ def get_api_roots(self) -> Tuple[Optional[ApiRoot], List[ApiRoot]]: break return (default_api_root, api_roots) - def get_api_root(self, api_root_id: str) -> ApiRoot: + def get_api_root(self, api_root_id: uuid.UUID) -> ApiRoot: api_root = self.api.get_api_root(api_root_id=api_root_id) if api_root is None: raise DoesNotExistError() return api_root - def get_job_and_details(self, api_root_id: str, job_id: str) -> Job: + def get_job_and_details(self, api_root_id: uuid.UUID, job_id: uuid.UUID) -> Job: job = self.api.get_job_and_details(api_root_id=api_root_id, job_id=job_id) if job is None: raise DoesNotExistError() return job - def get_collections(self, api_root_id: str) -> List[Collection]: + def get_collections(self, api_root_id: uuid.UUID) -> List[Collection]: return self.api.get_collections(api_root_id=api_root_id) def get_collection( - self, api_root_id: str, collection_id_or_alias: str + self, api_root_id: uuid.UUID, collection_id_or_alias: str ) -> Collection: collection = self.api.get_collection( api_root_id=api_root_id, collection_id_or_alias=collection_id_or_alias @@ -455,7 +456,7 @@ def get_collection( def get_manifest( self, - api_root_id: str, + api_root_id: uuid.UUID, collection_id_or_alias: str, limit: Optional[int] = None, added_after: Optional[datetime.datetime] = None, @@ -483,7 +484,7 @@ def get_manifest( def get_objects( self, - api_root_id: str, + api_root_id: uuid.UUID, collection_id_or_alias: str, limit: Optional[int] = None, added_after: Optional[datetime.datetime] = None, @@ -511,7 +512,7 @@ def get_objects( def add_objects( self, - api_root_id: str, + api_root_id: uuid.UUID, collection_id_or_alias: str, data: Dict, ) -> Job: @@ -529,7 +530,7 @@ def add_objects( def get_object( self, - api_root_id: str, + api_root_id: uuid.UUID, collection_id_or_alias: str, object_id: str, limit: Optional[int] = None, @@ -558,7 +559,7 @@ def get_object( def delete_object( self, - api_root_id: str, + api_root_id: uuid.UUID, collection_id_or_alias: str, object_id: str, match_version: Optional[List[str]] = None, @@ -586,7 +587,7 @@ def delete_object( def get_versions( self, - api_root_id: str, + api_root_id: uuid.UUID, collection_id_or_alias: str, object_id: str, limit: Optional[int] = None, diff --git a/opentaxii/persistence/sqldb/api.py b/opentaxii/persistence/sqldb/api.py index 1fad95e6..8431d66b 100644 --- a/opentaxii/persistence/sqldb/api.py +++ b/opentaxii/persistence/sqldb/api.py @@ -532,7 +532,7 @@ def get_api_roots(self) -> List[entities.ApiRoot]: for obj in query.all() ] - def get_api_root(self, api_root_id: str) -> Optional[entities.ApiRoot]: + def get_api_root(self, api_root_id: uuid.UUID) -> Optional[entities.ApiRoot]: api_root = ( self.db.session.query(taxii2models.ApiRoot) .filter(taxii2models.ApiRoot.id == api_root_id) @@ -616,7 +616,7 @@ def _job_and_details_to_entity( return job_entity def get_job_and_details( - self, api_root_id: str, job_id: str + self, api_root_id: uuid.UUID, job_id: uuid.UUID ) -> Optional[entities.Job]: job = ( self.db.session.query(taxii2models.Job) @@ -647,7 +647,7 @@ def job_cleanup(self) -> int: """ return taxii2models.Job.cleanup(self.db.session) - def get_collections(self, api_root_id: str) -> List[entities.Collection]: + def get_collections(self, api_root_id: uuid.UUID) -> List[entities.Collection]: query = ( self.db.session.query(taxii2models.Collection) .filter(taxii2models.Collection.api_root_id == api_root_id) @@ -667,7 +667,7 @@ def get_collections(self, api_root_id: str) -> List[entities.Collection]: ] def get_collection( - self, api_root_id: str, collection_id_or_alias: str + self, api_root_id: uuid.UUID, collection_id_or_alias: str ) -> Optional[entities.Collection]: id_or_alias_filter = taxii2models.Collection.alias == collection_id_or_alias try: @@ -982,7 +982,7 @@ def get_objects( ) def add_objects( - self, api_root_id: str, collection_id: uuid.UUID, objects: List[Dict] + self, api_root_id: uuid.UUID, collection_id: uuid.UUID, objects: List[Dict] ) -> entities.Job: job = taxii2models.Job( api_root_id=api_root_id, diff --git a/opentaxii/server.py b/opentaxii/server.py index 8a57cb25..92cc2b21 100644 --- a/opentaxii/server.py +++ b/opentaxii/server.py @@ -1,6 +1,7 @@ import functools import importlib import json +import uuid try: from re import Pattern @@ -539,9 +540,9 @@ def discovery_handler(self): return make_taxii2_response(response) @register_handler(r"^/taxii2/(?P[^/]+)/$", handles_own_auth=True) - def api_root_handler(self, api_root_id): + def api_root_handler(self, api_root_id: str): try: - api_root = self.persistence.get_api_root(api_root_id=api_root_id) + api_root = self.persistence.get_api_root(api_root_id=uuid.UUID(api_root_id)) except DoesNotExistError: if context.account is None: raise Unauthorized() @@ -561,9 +562,10 @@ def api_root_handler(self, api_root_id): r"^/taxii2/(?P[^/]+)/status/(?P[^/]+)/$", handles_own_auth=True, ) - def job_handler(self, api_root_id, job_id): + def job_handler(self, api_root_id: str, job_id: str): + api_root_uuid = uuid.UUID(api_root_id) try: - api_root = self.persistence.get_api_root(api_root_id=api_root_id) + api_root = self.persistence.get_api_root(api_root_id=api_root_uuid) except DoesNotExistError: if context.account is None: raise Unauthorized() @@ -572,7 +574,7 @@ def job_handler(self, api_root_id, job_id): raise Unauthorized() try: job = self.persistence.get_job_and_details( - api_root_id=api_root_id, job_id=job_id + api_root_id=api_root_uuid, job_id=uuid.UUID(job_id) ) except DoesNotExistError: raise NotFound() @@ -582,16 +584,17 @@ def job_handler(self, api_root_id, job_id): @register_handler( r"^/taxii2/(?P[^/]+)/collections/$", handles_own_auth=True ) - def collections_handler(self, api_root_id): + def collections_handler(self, api_root_id: str): + api_root_uuid = uuid.UUID(api_root_id) try: - api_root = self.persistence.get_api_root(api_root_id=api_root_id) + api_root = self.persistence.get_api_root(api_root_id=api_root_uuid) except DoesNotExistError: if context.account is None: raise Unauthorized() raise NotFound() if context.account is None and not api_root.is_public: raise Unauthorized() - collections = self.persistence.get_collections(api_root_id=api_root_id) + collections = self.persistence.get_collections(api_root_id=api_root_uuid) response: dict = {} if collections: response["collections"] = [] @@ -615,10 +618,11 @@ def collections_handler(self, api_root_id): r"/collections/(?P[^/]+)/$", handles_own_auth=True, ) - def collection_handler(self, api_root_id, collection_id_or_alias: str): + def collection_handler(self, api_root_id: str, collection_id_or_alias: str): try: collection = self.persistence.get_collection( - api_root_id=api_root_id, collection_id_or_alias=collection_id_or_alias + api_root_id=uuid.UUID(api_root_id), + collection_id_or_alias=collection_id_or_alias, ) except DoesNotExistError: if context.account is None: @@ -647,11 +651,11 @@ def collection_handler(self, api_root_id, collection_id_or_alias: str): r"/collections/(?P[^/]+)/manifest/$", handles_own_auth=True, ) - def manifest_handler(self, api_root_id, collection_id_or_alias): + def manifest_handler(self, api_root_id: str, collection_id_or_alias: str): filter_params = validate_list_filter_params(request.args, self.persistence.api) try: manifest, more = self.persistence.get_manifest( - api_root_id=api_root_id, + api_root_id=uuid.UUID(api_root_id), collection_id_or_alias=collection_id_or_alias, **filter_params, ) @@ -696,13 +700,14 @@ def manifest_handler(self, api_root_id, collection_id_or_alias): valid_content_types=("application/taxii+json;version=2.1",), handles_own_auth=True, ) - def objects_handler(self, api_root_id, collection_id_or_alias): + def objects_handler(self, api_root_id: str, collection_id_or_alias: str): + api_root_uuid = uuid.UUID(api_root_id) if request.method == "GET": - return self.objects_get_handler(api_root_id, collection_id_or_alias) + return self.objects_get_handler(api_root_uuid, collection_id_or_alias) if request.method == "POST": - return self.objects_post_handler(api_root_id, collection_id_or_alias) + return self.objects_post_handler(api_root_uuid, collection_id_or_alias) - def objects_get_handler(self, api_root_id, collection_id_or_alias): + def objects_get_handler(self, api_root_id: uuid.UUID, collection_id_or_alias: str): filter_params = validate_list_filter_params(request.args, self.persistence.api) try: objects, more, next_param = self.persistence.get_objects( @@ -745,7 +750,7 @@ def objects_get_handler(self, api_root_id, collection_id_or_alias): extra_headers=headers, ) - def objects_post_handler(self, api_root_id, collection_id_or_alias): + def objects_post_handler(self, api_root_id: uuid.UUID, collection_id_or_alias: str): validate_envelope( request.data, allow_custom=self.config.get("allow_custom_properties", True) ) @@ -774,17 +779,22 @@ def objects_post_handler(self, api_root_id, collection_id_or_alias): ("GET", "DELETE"), handles_own_auth=True, ) - def object_handler(self, api_root_id, collection_id_or_alias, object_id): + def object_handler( + self, api_root_id: str, collection_id_or_alias: str, object_id: str + ): + api_root_uuid = uuid.UUID(api_root_id) if request.method == "GET": return self.object_get_handler( - api_root_id, collection_id_or_alias, object_id + api_root_uuid, collection_id_or_alias, object_id ) if request.method == "DELETE": return self.object_delete_handler( - api_root_id, collection_id_or_alias, object_id + api_root_uuid, collection_id_or_alias, object_id ) - def object_get_handler(self, api_root_id, collection_id_or_alias, object_id): + def object_get_handler( + self, api_root_id: uuid.UUID, collection_id_or_alias: str, object_id: str + ): filter_params = validate_object_filter_params( request.args, self.persistence.api ) @@ -830,7 +840,9 @@ def object_get_handler(self, api_root_id, collection_id_or_alias, object_id): extra_headers=headers, ) - def object_delete_handler(self, api_root_id, collection_id_or_alias, object_id): + def object_delete_handler( + self, api_root_id: uuid.UUID, collection_id_or_alias: str, object_id: str + ): filter_params = validate_delete_filter_params(request.args) try: self.persistence.delete_object( @@ -857,13 +869,15 @@ def object_delete_handler(self, api_root_id, collection_id_or_alias, object_id): ), handles_own_auth=True, ) - def versions_handler(self, api_root_id, collection_id_or_alias, object_id): + def versions_handler( + self, api_root_id: str, collection_id_or_alias: str, object_id: str + ): filter_params = validate_versions_filter_params( request.args, self.persistence.api ) try: versions, more = self.persistence.get_versions( - api_root_id=api_root_id, + api_root_id=uuid.UUID(api_root_id), collection_id_or_alias=collection_id_or_alias, object_id=object_id, **filter_params, diff --git a/tests/taxii2/test_taxii2_objects.py b/tests/taxii2/test_taxii2_objects.py index 5f5a3d31..9f71ca3e 100644 --- a/tests/taxii2/test_taxii2_objects.py +++ b/tests/taxii2/test_taxii2_objects.py @@ -1228,7 +1228,7 @@ def test_objects( assert response.status_code == expected_status if method == "post" and expected_status == 202: add_objects_mock.assert_called_once_with( - api_root_id=str(API_ROOTS[0].id), + api_root_id=API_ROOTS[0].id, collection_id=COLLECTIONS[5].id, objects=post_data["objects"], ) @@ -1329,7 +1329,7 @@ def test_objects_unauthenticated( assert response.status_code == expected_status_code if method == "post" and expected_status_code == 202: add_objects_mock.assert_called_once_with( - api_root_id=str(API_ROOTS[0].id), + api_root_id=API_ROOTS[0].id, collection_id=COLLECTIONS[7].id, objects=kwargs["json"]["objects"], ) diff --git a/tests/taxii2/test_taxii2_sqldb.py b/tests/taxii2/test_taxii2_sqldb.py index 94914be8..91b9900b 100644 --- a/tests/taxii2/test_taxii2_sqldb.py +++ b/tests/taxii2/test_taxii2_sqldb.py @@ -3,6 +3,7 @@ import pytest +from opentaxii.persistence.sqldb.api import Taxii2SQLDatabaseAPI from opentaxii.persistence.sqldb.taxii2models import Job, JobDetail, STIXObject from opentaxii.taxii2 import entities from opentaxii.taxii2.utils import DATETIMEFORMAT @@ -39,7 +40,7 @@ ], indirect=["db_api_roots"], ) -def test_get_api_roots(taxii2_sqldb_api, db_api_roots): +def test_get_api_roots(taxii2_sqldb_api: Taxii2SQLDatabaseAPI, db_api_roots): response = taxii2_sqldb_api.get_api_roots() assert response == [api_root for api_root in db_api_roots] @@ -61,9 +62,11 @@ def test_get_api_roots(taxii2_sqldb_api, db_api_roots): ), ], ) -def test_get_api_root(taxii2_sqldb_api, db_api_roots, api_root_id): - response = taxii2_sqldb_api.get_api_root(str(api_root_id)) - assert response == GET_API_ROOT_MOCK(str(api_root_id)) +def test_get_api_root( + taxii2_sqldb_api: Taxii2SQLDatabaseAPI, db_api_roots, api_root_id +): + response = taxii2_sqldb_api.get_api_root(api_root_id) + assert response == GET_API_ROOT_MOCK(api_root_id) @pytest.mark.parametrize( @@ -96,9 +99,11 @@ def test_get_api_root(taxii2_sqldb_api, db_api_roots, api_root_id): ), ], ) -def test_get_job_and_details(taxii2_sqldb_api, db_jobs, api_root_id, job_id): - response = taxii2_sqldb_api.get_job_and_details(str(api_root_id), str(job_id)) - assert response == GET_JOB_AND_DETAILS_MOCK(str(api_root_id), str(job_id)) +def test_get_job_and_details( + taxii2_sqldb_api: Taxii2SQLDatabaseAPI, db_jobs, api_root_id, job_id +): + response = taxii2_sqldb_api.get_job_and_details(api_root_id, job_id) + assert response == GET_JOB_AND_DETAILS_MOCK(api_root_id, job_id) @pytest.mark.parametrize( @@ -118,9 +123,11 @@ def test_get_job_and_details(taxii2_sqldb_api, db_jobs, api_root_id, job_id): ), ], ) -def test_get_collections(taxii2_sqldb_api, db_collections, api_root_id): - response = taxii2_sqldb_api.get_collections(str(api_root_id)) - assert response == GET_COLLECTIONS_MOCK(str(api_root_id)) +def test_get_collections( + taxii2_sqldb_api: Taxii2SQLDatabaseAPI, db_collections, api_root_id +): + response = taxii2_sqldb_api.get_collections(api_root_id) + assert response == GET_COLLECTIONS_MOCK(api_root_id) @pytest.mark.parametrize( @@ -147,26 +154,25 @@ def test_get_collections(taxii2_sqldb_api, db_collections, api_root_id): id="wrong api root", ), pytest.param( - str(uuid4()), + uuid4(), COLLECTIONS[0].id, id="unknown api root", ), pytest.param( API_ROOTS[0].id, - str(uuid4()), + uuid4(), id="unknown collection id", ), ], ) def test_get_collection( - taxii2_sqldb_api, db_collections, api_root_id, collection_id_or_alias + taxii2_sqldb_api: Taxii2SQLDatabaseAPI, + db_collections, + api_root_id, + collection_id_or_alias, ): - response = taxii2_sqldb_api.get_collection( - str(api_root_id), str(collection_id_or_alias) - ) - assert response == GET_COLLECTION_MOCK( - str(api_root_id), str(collection_id_or_alias) - ) + response = taxii2_sqldb_api.get_collection(api_root_id, str(collection_id_or_alias)) + assert response == GET_COLLECTION_MOCK(api_root_id, str(collection_id_or_alias)) @pytest.mark.parametrize( @@ -432,7 +438,7 @@ def test_get_collection( ], ) def test_get_manifest( - taxii2_sqldb_api, + taxii2_sqldb_api: Taxii2SQLDatabaseAPI, db_stix_objects, collection_id, limit, @@ -728,7 +734,7 @@ def test_get_manifest( ], ) def test_get_objects( - taxii2_sqldb_api, + taxii2_sqldb_api: Taxii2SQLDatabaseAPI, db_stix_objects, collection_id, limit, @@ -835,7 +841,7 @@ def test_get_objects( ], ) def test_add_objects( - taxii2_sqldb_api, + taxii2_sqldb_api: Taxii2SQLDatabaseAPI, db_stix_objects, api_root_id, collection_id, @@ -1175,7 +1181,7 @@ def test_add_objects( ], ) def test_get_object( - taxii2_sqldb_api, + taxii2_sqldb_api: Taxii2SQLDatabaseAPI, db_stix_objects, collection_id, object_id, @@ -1249,7 +1255,7 @@ def test_get_object( ], ) def test_delete_object( - taxii2_sqldb_api, + taxii2_sqldb_api: Taxii2SQLDatabaseAPI, db_stix_objects, collection_id, object_id, @@ -1378,7 +1384,7 @@ def test_delete_object( ], ) def test_get_versions( - taxii2_sqldb_api, + taxii2_sqldb_api: Taxii2SQLDatabaseAPI, db_stix_objects, collection_id, object_id, @@ -1421,7 +1427,9 @@ def test_get_versions( ), ], ) -def test_next_param(taxii2_sqldb_api, stix_id, date_added, next_param): +def test_next_param( + taxii2_sqldb_api: Taxii2SQLDatabaseAPI, stix_id, date_added, next_param +): assert ( taxii2_sqldb_api.get_next_param({"id": stix_id, "date_added": date_added}) == next_param diff --git a/tests/taxii2/utils.py b/tests/taxii2/utils.py index 04ee43ef..c19d9ab3 100644 --- a/tests/taxii2/utils.py +++ b/tests/taxii2/utils.py @@ -309,33 +309,33 @@ def process_match_version(match_version): return id_version_combos -def GET_API_ROOT_MOCK(api_root_id: str): +def GET_API_ROOT_MOCK(api_root_id: UUID): for api_root in API_ROOTS: - if str(api_root.id) == api_root_id: + if api_root.id == api_root_id: return api_root return None -def GET_JOB_AND_DETAILS_MOCK(api_root_id: str, job_id: str): +def GET_JOB_AND_DETAILS_MOCK(api_root_id: UUID, job_id: UUID): job_response = None for job in JOBS: - if str(job.api_root_id) == api_root_id and str(job.id) == job_id: + if job.api_root_id == api_root_id and job.id == job_id: job_response = job break return job_response -def GET_COLLECTIONS_MOCK(api_root_id): +def GET_COLLECTIONS_MOCK(api_root_id: UUID): response = [] for collection in COLLECTIONS: - if str(collection.api_root_id) == api_root_id: + if collection.api_root_id == api_root_id: response.append(collection) return response -def GET_COLLECTION_MOCK(api_root_id: str, collection_id_or_alias: str): +def GET_COLLECTION_MOCK(api_root_id: UUID, collection_id_or_alias: str): for collection in COLLECTIONS: - if str(collection.api_root_id) == api_root_id and ( + if collection.api_root_id == api_root_id and ( str(collection.id) == collection_id_or_alias or collection.alias == collection_id_or_alias ): @@ -350,7 +350,7 @@ def STIX_OBJECT_FROM_MANIFEST(stix_id): def GET_MANIFEST_MOCK( - collection_id: str, + collection_id: UUID, limit: Optional[int] = None, added_after: Optional[datetime.datetime] = None, next_kwargs: Optional[Dict] = None, @@ -481,7 +481,7 @@ def GET_OBJECT_MOCK( return response, more, next_param -def ADD_OBJECTS_MOCK(api_root_id: str, collection_id: str, objects: List[Dict]): +def ADD_OBJECTS_MOCK(api_root_id: UUID, collection_id: str, objects: List[Dict]): return JOBS[0] diff --git a/tests/test_cli.py b/tests/test_cli.py index 0ef6eb4c..fcb1c251 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -399,13 +399,13 @@ def test_add_api_root( ["argv", "raises", "message", "stdout", "stderr", "expected_call"], [ pytest.param( - ["-r", API_ROOTS[0].id, "-t", "my new collection"], # argv + ["-r", str(API_ROOTS[0].id), "-t", "my new collection"], # argv False, # raises None, # message "", # stdout "", # stderr { - "api_root_id": API_ROOTS[0].id, + "api_root_id": str(API_ROOTS[0].id), "title": "my new collection", "description": None, "alias": None, @@ -417,7 +417,7 @@ def test_add_api_root( pytest.param( [ "-r", - API_ROOTS[0].id, + str(API_ROOTS[0].id), "-t", "my new collection", "-d", @@ -428,7 +428,7 @@ def test_add_api_root( "", # stdout "", # stderr { - "api_root_id": API_ROOTS[0].id, + "api_root_id": str(API_ROOTS[0].id), "title": "my new collection", "description": "my description", "alias": None, @@ -440,7 +440,7 @@ def test_add_api_root( pytest.param( [ "-r", - API_ROOTS[0].id, + str(API_ROOTS[0].id), "-t", "my new collection", "-d", @@ -453,7 +453,7 @@ def test_add_api_root( "", # stdout "", # stderr { - "api_root_id": API_ROOTS[0].id, + "api_root_id": str(API_ROOTS[0].id), "title": "my new collection", "description": "my description", "alias": "my-alias", @@ -463,13 +463,13 @@ def test_add_api_root( id="rootid, title, description, alias", ), pytest.param( - ["-r", API_ROOTS[0].id, "-t", "my new collection", "--public"], # argv + ["-r", str(API_ROOTS[0].id), "-t", "my new collection", "--public"], # argv False, # raises None, # message "", # stdout "", # stderr { - "api_root_id": API_ROOTS[0].id, + "api_root_id": str(API_ROOTS[0].id), "title": "my new collection", "description": None, "alias": None, @@ -481,7 +481,7 @@ def test_add_api_root( pytest.param( [ "-r", - API_ROOTS[0].id, + str(API_ROOTS[0].id), "-t", "my new collection", "--public-write", @@ -491,7 +491,7 @@ def test_add_api_root( "", # stdout "", # stderr { - "api_root_id": API_ROOTS[0].id, + "api_root_id": str(API_ROOTS[0].id), "title": "my new collection", "description": None, "alias": None, @@ -554,7 +554,7 @@ def test_add_collection( ) stderr = stderr.replace( "ROOTIDS", - ",".join([api_root.id for api_root in db_api_roots]), + ",".join([str(api_root.id) for api_root in db_api_roots]), ) with mock.patch("opentaxii.cli.persistence.app", app), mock.patch( "sys.argv", [""] + argv From 591c96139f9f4120e4c53b8e98935487cfa7d7b4 Mon Sep 17 00:00:00 2001 From: Eric Masseran Date: Wed, 3 Dec 2025 15:28:05 +0100 Subject: [PATCH 08/24] Add init files in tests/ and update imports Avoid issues to detect correct packages and allow relative imports --- tests/__init__.py | 0 tests/services/__init__.py | 0 tests/services/test_discovery.py | 5 ++-- tests/services/test_inbox.py | 19 +++++++++---- tests/services/test_poll.py | 28 +++++++++++++------ .../services/test_subscription_management.py | 28 ++++++++++++++----- tests/taxii2/__init__.py | 0 tests/test_auth.py | 8 +++--- tests/test_delete_content_blocks.py | 2 +- tests/test_http.py | 3 +- tests/test_server.py | 6 ++-- tests/utils.py | 16 +++++++---- 12 files changed, 78 insertions(+), 37 deletions(-) create mode 100644 tests/__init__.py create mode 100644 tests/services/__init__.py create mode 100644 tests/taxii2/__init__.py diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/services/__init__.py b/tests/services/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/services/test_discovery.py b/tests/services/test_discovery.py index 0286ef7a..f9efc50b 100644 --- a/tests/services/test_discovery.py +++ b/tests/services/test_discovery.py @@ -1,7 +1,8 @@ import pytest -from fixtures import INBOX_A, INBOX_B, INSTANCES_CONFIGURED, MESSAGE_ID from libtaxii.constants import SVC_INBOX -from utils import as_tm, prepare_headers + +from ..fixtures import INBOX_A, INBOX_B, INSTANCES_CONFIGURED, MESSAGE_ID +from ..utils import as_tm, prepare_headers @pytest.fixture(autouse=True) diff --git a/tests/services/test_inbox.py b/tests/services/test_inbox.py index da61850e..4a0f1b1d 100644 --- a/tests/services/test_inbox.py +++ b/tests/services/test_inbox.py @@ -1,13 +1,22 @@ import pytest -from fixtures import (COLLECTION_ONLY_STIX, COLLECTION_OPEN, COLLECTIONS_A, - COLLECTIONS_B, CONTENT, CONTENT_BINDING_SUBTYPE, - CUSTOM_CONTENT_BINDING, INVALID_CONTENT_BINDING, - MESSAGE_ID) from libtaxii import messages_10 as tm10 from libtaxii import messages_11 as tm11 from libtaxii.constants import CB_STIX_XML_111, ST_SUCCESS + from opentaxii.taxii import exceptions -from utils import as_tm, prepare_headers + +from ..fixtures import ( + COLLECTION_ONLY_STIX, + COLLECTION_OPEN, + COLLECTIONS_A, + COLLECTIONS_B, + CONTENT, + CONTENT_BINDING_SUBTYPE, + CUSTOM_CONTENT_BINDING, + INVALID_CONTENT_BINDING, + MESSAGE_ID, +) +from ..utils import as_tm, prepare_headers def make_content( diff --git a/tests/services/test_poll.py b/tests/services/test_poll.py index eb9adea0..fc0c94df 100644 --- a/tests/services/test_poll.py +++ b/tests/services/test_poll.py @@ -1,15 +1,27 @@ import pytest -from fixtures import (COLLECTION_DISABLED, COLLECTION_ONLY_STIX, - COLLECTION_OPEN, COLLECTION_STIX_AND_CUSTOM, - COLLECTIONS_B, CUSTOM_CONTENT_BINDING, MESSAGE_ID, - POLL_MAX_COUNT, POLL_RESULT_SIZE) from libtaxii import messages_10 as tm10 from libtaxii import messages_11 as tm11 -from libtaxii.constants import (ACT_SUBSCRIBE, CB_STIX_XML_111, RT_COUNT_ONLY, - RT_FULL) +from libtaxii.constants import ACT_SUBSCRIBE, CB_STIX_XML_111, RT_COUNT_ONLY, RT_FULL + from opentaxii.taxii import exceptions -from utils import (as_tm, persist_content, prepare_headers, - prepare_subscription_request) + +from ..fixtures import ( + COLLECTION_DISABLED, + COLLECTION_ONLY_STIX, + COLLECTION_OPEN, + COLLECTION_STIX_AND_CUSTOM, + COLLECTIONS_B, + CUSTOM_CONTENT_BINDING, + MESSAGE_ID, + POLL_MAX_COUNT, + POLL_RESULT_SIZE, +) +from ..utils import ( + as_tm, + persist_content, + prepare_headers, + prepare_subscription_request, +) @pytest.fixture(autouse=True) diff --git a/tests/services/test_subscription_management.py b/tests/services/test_subscription_management.py index a4a89335..fb1b70fe 100644 --- a/tests/services/test_subscription_management.py +++ b/tests/services/test_subscription_management.py @@ -1,12 +1,26 @@ import pytest -from fixtures import (COLLECTION_OPEN, COLLECTIONS_B, CUSTOM_CONTENT_BINDING, - SUBSCRIPTION_MESSAGE) -from libtaxii.constants import (ACT_PAUSE, ACT_RESUME, ACT_SUBSCRIBE, - ACT_UNSUBSCRIBE, CB_STIX_XML_111, RT_FULL, - SS_ACTIVE, SS_PAUSED, SS_UNSUBSCRIBED) +from libtaxii.constants import ( + ACT_PAUSE, + ACT_RESUME, + ACT_SUBSCRIBE, + ACT_UNSUBSCRIBE, + CB_STIX_XML_111, + RT_FULL, + SS_ACTIVE, + SS_PAUSED, + SS_UNSUBSCRIBED, +) + from opentaxii.taxii import exceptions -from utils import as_tm, prepare_headers -from utils import prepare_subscription_request as prepare_request + +from ..fixtures import ( + COLLECTION_OPEN, + COLLECTIONS_B, + CUSTOM_CONTENT_BINDING, + SUBSCRIPTION_MESSAGE, +) +from ..utils import as_tm, prepare_headers +from ..utils import prepare_subscription_request as prepare_request ASSIGNED_SERVICES = ['collection-management-A', 'poll-A'] diff --git a/tests/taxii2/__init__.py b/tests/taxii2/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_auth.py b/tests/test_auth.py index 98e958f6..a695c2e6 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -4,13 +4,13 @@ import pytest from libtaxii import messages_10 as tm10 from libtaxii import messages_11 as tm11 -from libtaxii.constants import (CB_STIX_XML_111, RT_FULL, ST_BAD_MESSAGE, - ST_UNAUTHORIZED) +from libtaxii.constants import CB_STIX_XML_111, RT_FULL, ST_BAD_MESSAGE, ST_UNAUTHORIZED + from opentaxii.taxii.http import HTTP_AUTHORIZATION from opentaxii.utils import sync_conf_dict_into_db -from fixtures import VID_TAXII_HTTP_10 -from utils import as_tm, is_headers_valid, prepare_headers +from .fixtures import VID_TAXII_HTTP_10 +from .utils import as_tm, is_headers_valid, prepare_headers INBOX_OPEN = dict( id='inbox-A', diff --git a/tests/test_delete_content_blocks.py b/tests/test_delete_content_blocks.py index 709088e5..0538ea85 100644 --- a/tests/test_delete_content_blocks.py +++ b/tests/test_delete_content_blocks.py @@ -2,7 +2,7 @@ import pytest -from fixtures import COLLECTION_OPEN, COLLECTIONS_A +from .fixtures import COLLECTION_OPEN, COLLECTIONS_A @pytest.mark.parametrize("with_messages", [True, False]) diff --git a/tests/test_http.py b/tests/test_http.py index 674c8292..c379d1b4 100644 --- a/tests/test_http.py +++ b/tests/test_http.py @@ -1,9 +1,10 @@ import pytest from libtaxii.constants import ST_BAD_MESSAGE, ST_FAILURE + from opentaxii.taxii.converters import dict_to_service_entity from opentaxii.taxii.http import HTTP_X_TAXII_SERVICES -from utils import as_tm, is_headers_valid, prepare_headers +from .utils import as_tm, is_headers_valid, prepare_headers INBOX = dict( id='inbox-A', diff --git a/tests/test_server.py b/tests/test_server.py index 9fc12806..99bb2bbe 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -1,13 +1,13 @@ import concurrent.futures import pytest -from opentaxii.persistence import (OpenTAXII2PersistenceAPI, - Taxii2PersistenceManager) + +from opentaxii.persistence import OpenTAXII2PersistenceAPI, Taxii2PersistenceManager from opentaxii.persistence.sqldb import Taxii2SQLDatabaseAPI from opentaxii.server import TAXII2Server from opentaxii.taxii.converters import dict_to_service_entity -from fixtures import DOMAIN +from .fixtures import DOMAIN INBOX = dict( id='inbox-A', diff --git a/tests/utils.py b/tests/utils.py index 98ff5b04..2b2f530a 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -3,15 +3,19 @@ import pytest from libtaxii import messages_10 as tm10 from libtaxii import messages_11 as tm11 + from opentaxii.taxii import entities -from opentaxii.taxii.http import (HTTP_ACCEPT, HTTP_CONTENT_XML, - TAXII_10_HTTP_HEADERS, - TAXII_10_HTTPS_HEADERS, - TAXII_11_HTTP_HEADERS, - TAXII_11_HTTPS_HEADERS) +from opentaxii.taxii.http import ( + HTTP_ACCEPT, + HTTP_CONTENT_XML, + TAXII_10_HTTP_HEADERS, + TAXII_10_HTTPS_HEADERS, + TAXII_11_HTTP_HEADERS, + TAXII_11_HTTPS_HEADERS, +) from opentaxii.taxii.utils import get_utc_now -from fixtures import CB_STIX_XML_111, CONTENT, MESSAGE, MESSAGE_ID +from .fixtures import CB_STIX_XML_111, CONTENT, MESSAGE, MESSAGE_ID JWT_RE = re.compile(r'[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*') From 274ed65cf3b692199211108c5a4508397cbd0503 Mon Sep 17 00:00:00 2001 From: Eric Masseran Date: Wed, 3 Dec 2025 15:28:32 +0100 Subject: [PATCH 09/24] Type tests/ --- .flake8 | 1 + pyproject.toml | 1 + tests/conftest.py | 33 ++- tests/fixtures.py | 83 +++--- tests/services/test_collection_management.py | 54 ++-- tests/services/test_discovery.py | 4 +- tests/services/test_inbox.py | 2 +- tests/services/test_poll.py | 97 +++---- .../services/test_subscription_management.py | 125 ++++----- tests/taxii2/test_taxii2_collection.py | 9 +- tests/taxii2/test_taxii2_objects.py | 2 +- tests/taxii2/utils.py | 12 +- tests/test_auth.py | 238 ++++++++---------- tests/test_config.py | 4 +- tests/test_http.py | 24 +- tests/test_server.py | 18 +- tests/utils.py | 1 + 17 files changed, 367 insertions(+), 341 deletions(-) diff --git a/.flake8 b/.flake8 index f649211e..9950d6d4 100644 --- a/.flake8 +++ b/.flake8 @@ -1,3 +1,4 @@ [flake8] max-line-length=120 exclude = docs/*,.tox/* +ignore = E203 \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 6533836e..6ac4290b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ follow_imports = "silent" files = [ "opentaxii/", + "tests/", ] exclude = """ diff --git a/tests/conftest.py b/tests/conftest.py index 56192d9a..93c708ca 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,18 +5,30 @@ import pytest from flask.testing import FlaskClient + from opentaxii.config import ServerConfig from opentaxii.local import context, release_context from opentaxii.middleware import create_app -from opentaxii.persistence.sqldb.taxii2models import (ApiRoot, Collection, Job, - JobDetail, STIXObject) +from opentaxii.persistence.sqldb.taxii2models import ( + ApiRoot, + Collection, + Job, + JobDetail, + STIXObject, +) from opentaxii.server import TAXIIServer from opentaxii.taxii.converters import dict_to_service_entity from opentaxii.taxii.http import HTTP_AUTHORIZATION from opentaxii.utils import configure_logging - -from tests.fixtures import (ACCOUNT, COLLECTIONS_B, DOMAIN, PASSWORD, SERVICES, - USERNAME, VALID_TOKEN) +from tests.fixtures import ( + ACCOUNT, + COLLECTIONS_B, + DOMAIN, + PASSWORD, + SERVICES, + USERNAME, + VALID_TOKEN, +) from tests.taxii2.utils import API_ROOTS, COLLECTIONS, JOBS, STIX_OBJECTS @@ -48,7 +60,6 @@ def dbconn(): except FileNotFoundError: pass - elif DBTYPE in ("mysql", "mariadb"): import MySQLdb @@ -61,7 +72,6 @@ def dbconn(): port = 3307 yield f"mysql+mysqldb://root:@127.0.0.1:{port}/test?charset=utf8" - elif DBTYPE == "postgres": import platform @@ -75,7 +85,6 @@ def dbconn(): def dbconn(): yield "postgresql+psycopg2://test:test@127.0.0.1:5432/test" - else: raise NotImplementedError(f"dbtype {DBTYPE} not supported") @@ -135,7 +144,7 @@ def anonymous_user(): def clean_db(dbconn): # drop and recreate db to provide clean state at beginning if DBTYPE == "sqlite": - filename = dbconn[len("sqlite:///"):] + filename = dbconn[len("sqlite:///") :] os.remove(filename) elif DBTYPE == "postgres": with psycopg2.connect( @@ -207,7 +216,7 @@ def transaction_app(dbconn, taxiiserver): connections.append(connection) sessions.append(manager.api.db.session) yield app - for (transaction, connection, session, manager) in zip( + for transaction, connection, session, manager in zip( transactions, connections, sessions, managers ): transaction.rollback() @@ -223,8 +232,8 @@ def truncate_app(dbconn): app = create_app(context.server) app.config["TESTING"] = True yield app - taxiiserver.servers.taxii1.persistence.api.db.engine.dispose() - taxiiserver.servers.taxii2.persistence.api.db.engine.dispose() + taxiiserver.servers.taxii1.persistence.api.db.engine.dispose() # type: ignore[union-attr] + taxiiserver.servers.taxii2.persistence.api.db.engine.dispose() # type: ignore[union-attr] @pytest.fixture() diff --git a/tests/fixtures.py b/tests/fixtures.py index 6fae04bd..b4f6dccb 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -1,7 +1,7 @@ from uuid import uuid4 -from libtaxii.constants import (CB_STIX_XML_111, VID_TAXII_HTTP_10, - VID_TAXII_HTTPS_10) +from libtaxii.constants import CB_STIX_XML_111, VID_TAXII_HTTP_10, VID_TAXII_HTTPS_10 + from opentaxii.entities import Account from opentaxii.taxii import entities @@ -17,7 +17,7 @@ destination_collection_required=False, address='/relative/path/inbox-a', accept_all_content=True, - protocol_bindings=PROTOCOL_BINDINGS + protocol_bindings=PROTOCOL_BINDINGS, ) INBOX_B = dict( @@ -27,7 +27,7 @@ destination_collection_required='yes', address='/relative/path/inbox-b', supported_content=[CB_STIX_XML_111, CUSTOM_CONTENT_BINDING], - protocol_bindings=PROTOCOL_BINDINGS + protocol_bindings=PROTOCOL_BINDINGS, ) DISCOVERY_A = dict( @@ -36,9 +36,14 @@ description='discovery-A description', address='/relative/path/discovery-a', advertised_services=[ - 'inbox-A', 'inbox-B', 'discovery-A', 'discovery-B', - 'collection-management-A', 'poll-A'], - protocol_bindings=PROTOCOL_BINDINGS + 'inbox-A', + 'inbox-B', + 'discovery-A', + 'discovery-B', + 'collection-management-A', + 'poll-A', + ], + protocol_bindings=PROTOCOL_BINDINGS, ) DISCOVERY_B = dict( @@ -46,7 +51,7 @@ type='discovery', description='External discovery-B service', address='http://something.com/absolute/path/discovery-b', - protocol_bindings=[VID_TAXII_HTTP_10] + protocol_bindings=[VID_TAXII_HTTP_10], ) SUBSCRIPTION_MESSAGE = 'message about subscription' @@ -57,7 +62,7 @@ description='Collection management description', address='/relative/path/collection-management', protocol_bindings=PROTOCOL_BINDINGS, - subscription_message=SUBSCRIPTION_MESSAGE + subscription_message=SUBSCRIPTION_MESSAGE, ) POLL_RESULT_SIZE = 20 @@ -70,23 +75,21 @@ address='/relative/path/poll', protocol_bindings=PROTOCOL_BINDINGS, max_result_size=POLL_RESULT_SIZE, - max_result_count=POLL_MAX_COUNT + max_result_count=POLL_MAX_COUNT, ) DOMAIN = 'www.some-example.local' -INTERNAL_SERVICES = [ - INBOX_A, INBOX_B, DISCOVERY_A, COLLECTION_MANAGEMENT, POLL] +INTERNAL_SERVICES = [INBOX_A, INBOX_B, DISCOVERY_A, COLLECTION_MANAGEMENT, POLL] SERVICES = INTERNAL_SERVICES + [DISCOVERY_B] -INSTANCES_CONFIGURED = sum(len(s['protocol_bindings']) for s in SERVICES) +INSTANCES_CONFIGURED = sum(len(s['protocol_bindings']) for s in SERVICES) # type: ignore MESSAGE_ID = '123' CONTENT = 'some-content' CONTENT_BINDINGS_ONLY_STIX = [CB_STIX_XML_111] -CONTENT_BINDINGS_STIX_AND_CUSTOM = ( - CONTENT_BINDINGS_ONLY_STIX + [CUSTOM_CONTENT_BINDING]) +CONTENT_BINDINGS_STIX_AND_CUSTOM = CONTENT_BINDINGS_ONLY_STIX + [CUSTOM_CONTENT_BINDING] CONTENT_BINDING_SUBTYPE = 'custom-subtype' MESSAGE = 'test-message' @@ -99,35 +102,33 @@ COLLECTIONS_A = [ - entities.CollectionEntity(**x) for x in - [{ - 'name': COLLECTION_OPEN, - 'available': True, - 'accept_all_content': True - }] + entities.CollectionEntity(**x) + for x in [{'name': COLLECTION_OPEN, 'available': True, 'accept_all_content': True}] ] COLLECTIONS_B = [ - entities.CollectionEntity(**x) for x in - [{ - 'name': COLLECTION_OPEN, - 'available': True, - 'accept_all_content': True, - 'type': entities.CollectionEntity.TYPE_SET - }, { - 'name': COLLECTION_ONLY_STIX, - 'available': True, - 'accept_all_content': False, - 'supported_content': CONTENT_BINDINGS_ONLY_STIX - }, { - 'name': COLLECTION_STIX_AND_CUSTOM, - 'available': True, - 'accept_all_content': False, - 'supported_content': CONTENT_BINDINGS_STIX_AND_CUSTOM - }, { - 'name': COLLECTION_DISABLED, - 'available': False - }] + entities.CollectionEntity(**x) + for x in [ + { + 'name': COLLECTION_OPEN, + 'available': True, + 'accept_all_content': True, + 'type': entities.CollectionEntity.TYPE_SET, + }, + { + 'name': COLLECTION_ONLY_STIX, + 'available': True, + 'accept_all_content': False, + 'supported_content': CONTENT_BINDINGS_ONLY_STIX, + }, + { + 'name': COLLECTION_STIX_AND_CUSTOM, + 'available': True, + 'accept_all_content': False, + 'supported_content': CONTENT_BINDINGS_STIX_AND_CUSTOM, + }, + {'name': COLLECTION_DISABLED, 'available': False}, + ] ] USERNAME = "some-username" diff --git a/tests/services/test_collection_management.py b/tests/services/test_collection_management.py index 61dd5d82..22323fc4 100644 --- a/tests/services/test_collection_management.py +++ b/tests/services/test_collection_management.py @@ -1,21 +1,31 @@ import pytest -from fixtures import (COLLECTION_DISABLED, COLLECTION_ONLY_STIX, - COLLECTION_OPEN, COLLECTION_STIX_AND_CUSTOM, - COLLECTIONS_B, MESSAGE_ID, SERVICES) + from opentaxii.taxii import entities -from utils import as_tm, persist_content, prepare_headers + +from ..fixtures import ( + COLLECTION_DISABLED, + COLLECTION_ONLY_STIX, + COLLECTION_OPEN, + COLLECTION_STIX_AND_CUSTOM, + COLLECTIONS_B, + MESSAGE_ID, + SERVICES, +) +from ..utils import as_tm, persist_content, prepare_headers ASSIGNED_SERVICES = ['collection-management-A', 'inbox-A', 'inbox-B', 'poll-A'] ASSIGNED_INBOX_INSTANCES = sum( len(s['protocol_bindings']) for s in SERVICES - if s['id'] in ASSIGNED_SERVICES and s['id'].startswith('inbox')) + if s['id'] in ASSIGNED_SERVICES and s['id'].startswith('inbox') +) ASSIGNED_SUBSCTRIPTION_INSTANCES = sum( len(s['protocol_bindings']) for s in SERVICES - if s['id'] in ASSIGNED_SERVICES and s['id'].startswith('collection-')) + if s['id'] in ASSIGNED_SERVICES and s['id'].startswith('collection-') +) @pytest.fixture(autouse=True) @@ -23,7 +33,8 @@ def prepare_server(server, services): for coll in COLLECTIONS_B: coll = server.servers.taxii1.persistence.create_collection(coll) server.servers.taxii1.persistence.set_collection_services( - coll.id, service_ids=ASSIGNED_SERVICES) + coll.id, service_ids=ASSIGNED_SERVICES + ) def prepare_request(version): @@ -47,8 +58,7 @@ def test_collections(server, version, https): names = [c.name for c in COLLECTIONS_B] if version == 11: - assert isinstance( - response, as_tm(version).CollectionInformationResponse) + assert isinstance(response, as_tm(version).CollectionInformationResponse) assert len(response.collection_informations) == len(COLLECTIONS_B) for c in response.collection_informations: @@ -111,18 +121,18 @@ def test_collection_supported_content(server, version, https): def get_coll(name): return next( - c for c in response.collection_informations - if c.collection_name == name) + c for c in response.collection_informations if c.collection_name == name + ) assert ( - get_coll(COLLECTION_OPEN).collection_type == - entities.CollectionEntity.TYPE_SET) + get_coll(COLLECTION_OPEN).collection_type + == entities.CollectionEntity.TYPE_SET + ) else: + def get_coll(name): - return next( - c for c in response.feed_informations - if c.feed_name == name) + return next(c for c in response.feed_informations if c.feed_name == name) assert len(get_coll(COLLECTION_OPEN).supported_contents) == 0 @@ -146,8 +156,10 @@ def test_collections_volume(server, https): response = service.process(headers, request) collection = next( - c for c in response.collection_informations - if c.collection_name == COLLECTION_OPEN) + c + for c in response.collection_informations + if c.collection_name == COLLECTION_OPEN + ) assert collection.collection_volume == 0 @@ -160,8 +172,10 @@ def test_collections_volume(server, https): response = service.process(headers, request) collection = next( - c for c in response.collection_informations - if c.collection_name == COLLECTION_OPEN) + c + for c in response.collection_informations + if c.collection_name == COLLECTION_OPEN + ) assert collection.collection_volume == blocks_amount diff --git a/tests/services/test_discovery.py b/tests/services/test_discovery.py index f9efc50b..f09b8c41 100644 --- a/tests/services/test_discovery.py +++ b/tests/services/test_discovery.py @@ -39,9 +39,7 @@ def test_content_bindings_present(server, version, https): assert len(response.service_instances) == INSTANCES_CONFIGURED assert response.in_response_to == MESSAGE_ID - inboxes = [ - s for s in response.service_instances - if s.service_type == SVC_INBOX] + inboxes = [s for s in response.service_instances if s.service_type == SVC_INBOX] assert len(inboxes) == 4 diff --git a/tests/services/test_inbox.py b/tests/services/test_inbox.py index 4a0f1b1d..e042251a 100644 --- a/tests/services/test_inbox.py +++ b/tests/services/test_inbox.py @@ -71,7 +71,7 @@ def prepare_server(server, services): .filter_by(name=coll.name) .one() ) - service_ids = {s.id for s in coll.services} | {service} + service_ids = {s.id for s in coll.services} | {service} # type:ignore server.servers.taxii1.persistence.set_collection_services( coll.id, service_ids=service_ids diff --git a/tests/services/test_poll.py b/tests/services/test_poll.py index fc0c94df..6f7bb59a 100644 --- a/tests/services/test_poll.py +++ b/tests/services/test_poll.py @@ -30,12 +30,14 @@ def prepare_server(server, services): for coll in COLLECTIONS_B: coll = server.servers.taxii1.persistence.create_collection(coll) server.servers.taxii1.persistence.set_collection_services( - coll.id, service_ids=services) + coll.id, service_ids=services + ) return server -def prepare_request(collection_name, version, count_only=False, - bindings=[], subscription_id=None): +def prepare_request( + collection_name, version, count_only=False, bindings=[], subscription_id=None +): if version == 11: content_bindings = [tm11.ContentBinding(b) for b in bindings] @@ -43,14 +45,14 @@ def prepare_request(collection_name, version, count_only=False, poll_parameters = None else: poll_parameters = tm11.PollParameters( - response_type=( - RT_FULL if not count_only else RT_COUNT_ONLY), - content_bindings=content_bindings) + response_type=(RT_FULL if not count_only else RT_COUNT_ONLY), + content_bindings=content_bindings, + ) return tm11.PollRequest( message_id=MESSAGE_ID, collection_name=collection_name, subscription_id=subscription_id, - poll_parameters=poll_parameters + poll_parameters=poll_parameters, ) elif version == 10: content_bindings = bindings @@ -58,7 +60,7 @@ def prepare_request(collection_name, version, count_only=False, message_id=MESSAGE_ID, feed_name=collection_name, content_bindings=content_bindings, - subscription_id=subscription_id + subscription_id=subscription_id, ) @@ -67,14 +69,19 @@ def prepare_fulfilment_request(collection_name, result_id, part_number): message_id=MESSAGE_ID, collection_name=collection_name, result_id=result_id, - result_part_number=part_number + result_part_number=part_number, ) -@pytest.mark.parametrize(("https", "version", "count_blocks"), [ - (True, 11, True), (False, 11, False), - (True, 10, False), (False, 10, False), -]) +@pytest.mark.parametrize( + ("https", "version", "count_blocks"), + [ + (True, 11, True), + (False, 11, False), + (True, 10, False), + (False, 10, False), + ], +) def test_poll_empty_response(server, version, https, count_blocks): server.servers.taxii1.config['count_blocks_in_poll_responses'] = count_blocks @@ -82,8 +89,7 @@ def test_poll_empty_response(server, version, https, count_blocks): service = server.servers.taxii1.get_service('poll-A') headers = prepare_headers(version, https) - request = prepare_request( - collection_name=COLLECTION_OPEN, version=version) + request = prepare_request(collection_name=COLLECTION_OPEN, version=version) if version == 11: response = service.process(headers, request) @@ -105,15 +111,14 @@ def test_poll_empty_response(server, version, https, count_blocks): @pytest.mark.parametrize( - ("https", "version"), - [(True, 11), (False, 11), (True, 10), (False, 10)]) + ("https", "version"), [(True, 11), (False, 11), (True, 10), (False, 10)] +) def test_poll_collection_not_available(server, version, https): service = server.servers.taxii1.get_service('poll-A') headers = prepare_headers(version, https) - request = prepare_request( - collection_name=COLLECTION_DISABLED, version=version) + request = prepare_request(collection_name=COLLECTION_DISABLED, version=version) with pytest.raises(exceptions.StatusMessageException): service.process(headers, request) @@ -125,14 +130,17 @@ def test_poll_get_content(server, version, https): service = server.servers.taxii1.get_service('poll-A') original = persist_content( - server.servers.taxii1.persistence, COLLECTION_ONLY_STIX, - service.id, binding=CB_STIX_XML_111) + server.servers.taxii1.persistence, + COLLECTION_ONLY_STIX, + service.id, + binding=CB_STIX_XML_111, + ) # wrong collection headers = prepare_headers(version, https) request = prepare_request( - collection_name=COLLECTION_STIX_AND_CUSTOM, - version=version) + collection_name=COLLECTION_STIX_AND_CUSTOM, version=version + ) response = service.process(headers, request) @@ -141,9 +149,7 @@ def test_poll_get_content(server, version, https): # right collection headers = prepare_headers(version, https) - request = prepare_request( - collection_name=COLLECTION_ONLY_STIX, - version=version) + request = prepare_request(collection_name=COLLECTION_ONLY_STIX, version=version) response = service.process(headers, request) @@ -159,7 +165,9 @@ def test_poll_get_content(server, version, https): headers = prepare_headers(version, https) request = prepare_request( collection_name=COLLECTION_ONLY_STIX, - version=version, bindings=[CUSTOM_CONTENT_BINDING]) + version=version, + bindings=[CUSTOM_CONTENT_BINDING], + ) with pytest.raises(exceptions.StatusMessageException): service.process(headers, request) @@ -167,7 +175,8 @@ def test_poll_get_content(server, version, https): @pytest.mark.parametrize( ("https", "count_blocks"), - [(True, True), (False, True), (True, False), (False, False)]) + [(True, True), (False, True), (True, False), (False, False)], +) def test_poll_get_content_count(server, https, count_blocks): version = 11 server.servers.taxii1.config['count_blocks_in_poll_responses'] = count_blocks @@ -182,7 +191,8 @@ def test_poll_get_content_count(server, https, count_blocks): # count-only request request = prepare_request( - collection_name=COLLECTION_OPEN, count_only=True, version=version) + collection_name=COLLECTION_OPEN, count_only=True, version=version + ) response = service.process(headers, request) assert isinstance(response, tm11.PollResponse) @@ -198,7 +208,8 @@ def test_poll_get_content_count(server, https, count_blocks): @pytest.mark.parametrize( ("https", "count_blocks"), - [(True, True), (False, True), (True, False), (False, False)]) + [(True, True), (False, True), (True, False), (False, False)], +) def test_poll_max_count_max_size(server, https, count_blocks): version = 11 @@ -214,8 +225,9 @@ def test_poll_max_count_max_size(server, https, count_blocks): headers = prepare_headers(version, https) # count-only request - request = prepare_request(collection_name=COLLECTION_OPEN, - count_only=True, version=version) + request = prepare_request( + collection_name=COLLECTION_OPEN, count_only=True, version=version + ) response = service.process(headers, request) assert isinstance(response, tm11.PollResponse) @@ -245,7 +257,8 @@ def test_poll_max_count_max_size(server, https, count_blocks): @pytest.mark.parametrize( ("https", "count_blocks"), - [(True, True), (False, True), (True, False), (False, False)]) + [(True, True), (False, True), (True, False), (False, False)], +) def test_poll_fulfilment_request(server, https, count_blocks): server.servers.taxii1.config['count_blocks_in_poll_responses'] = count_blocks version = 11 @@ -277,8 +290,7 @@ def test_poll_fulfilment_request(server, https, count_blocks): # poll fullfilment request result_id = response.result_id part_number = 2 - request = prepare_fulfilment_request( - COLLECTION_OPEN, result_id, part_number) + request = prepare_fulfilment_request(COLLECTION_OPEN, result_id, part_number) response = service.process(headers, request) assert isinstance(response, tm11.PollResponse) @@ -297,8 +309,7 @@ def test_poll_fulfilment_request(server, https, count_blocks): # poll fullfilment request over the top result_id = response.result_id part_number = 3 - request = prepare_fulfilment_request( - COLLECTION_OPEN, result_id, part_number) + request = prepare_fulfilment_request(COLLECTION_OPEN, result_id, part_number) response = service.process(headers, request) assert isinstance(response, tm11.PollResponse) @@ -334,13 +345,12 @@ def test_subscribe_and_poll(server, version, https): params = dict( response_type=RT_COUNT_ONLY, - content_bindings=[CB_STIX_XML_111, CUSTOM_CONTENT_BINDING]) + content_bindings=[CB_STIX_XML_111, CUSTOM_CONTENT_BINDING], + ) subs_request = prepare_subscription_request( - collection=collection, - action=ACT_SUBSCRIBE, - version=version, - params=params) + collection=collection, action=ACT_SUBSCRIBE, version=version, params=params + ) subs_response = subs_service.process(headers, subs_request) @@ -355,7 +365,8 @@ def test_subscribe_and_poll(server, version, https): collection_name=collection, count_only=False, subscription_id=subscription.subscription_id, - version=version) + version=version, + ) poll_response = poll_service.process(headers, poll_request) diff --git a/tests/services/test_subscription_management.py b/tests/services/test_subscription_management.py index fb1b70fe..fa493e86 100644 --- a/tests/services/test_subscription_management.py +++ b/tests/services/test_subscription_management.py @@ -30,7 +30,8 @@ def prepare_server(server, services): for coll in COLLECTIONS_B: coll = server.servers.taxii1.persistence.create_collection(coll) server.servers.taxii1.persistence.set_collection_services( - coll.id, service_ids=ASSIGNED_SERVICES) + coll.id, service_ids=ASSIGNED_SERVICES + ) return server @@ -45,24 +46,20 @@ def test_subscribe(server, version, https): params = dict( response_type=RT_FULL, - content_bindings=[CB_STIX_XML_111, CUSTOM_CONTENT_BINDING] + content_bindings=[CB_STIX_XML_111, CUSTOM_CONTENT_BINDING], ) request = prepare_request( - collection=COLLECTION_OPEN, action=ACT_SUBSCRIBE, - version=version, params=params) + collection=COLLECTION_OPEN, action=ACT_SUBSCRIBE, version=version, params=params + ) response = service.process(headers, request) if version == 11: - assert isinstance( - response, - as_tm(version).ManageCollectionSubscriptionResponse) + assert isinstance(response, as_tm(version).ManageCollectionSubscriptionResponse) assert response.collection_name == COLLECTION_OPEN else: - assert isinstance( - response, - as_tm(version).ManageFeedSubscriptionResponse) + assert isinstance(response, as_tm(version).ManageFeedSubscriptionResponse) assert response.feed_name == COLLECTION_OPEN assert response.message == SUBSCRIPTION_MESSAGE @@ -74,22 +71,19 @@ def test_subscribe(server, version, https): # 1 poll service * 2 protocol bindings assert len(subs.poll_instances) == 2 - assert ( - subs.poll_instances[0].poll_address == - poll_service.get_absolute_address( - subs.poll_instances[0].poll_protocol)) + assert subs.poll_instances[0].poll_address == poll_service.get_absolute_address( + subs.poll_instances[0].poll_protocol + ) if version == 11: assert subs.status == SS_ACTIVE response_bindings = [ - b.binding_id - for b in subs.subscription_parameters.content_bindings] + b.binding_id for b in subs.subscription_parameters.content_bindings + ] assert response_bindings == params['content_bindings'] - assert ( - subs.subscription_parameters.response_type == - params['response_type']) + assert subs.subscription_parameters.response_type == params['response_type'] @pytest.mark.parametrize("https", [True, False]) @@ -103,19 +97,17 @@ def test_subscribe_pause_resume(server, https): params = dict( response_type=RT_FULL, - content_bindings=[CB_STIX_XML_111, CUSTOM_CONTENT_BINDING] + content_bindings=[CB_STIX_XML_111, CUSTOM_CONTENT_BINDING], ) # Subscribing request = prepare_request( - collection=COLLECTION_OPEN, action=ACT_SUBSCRIBE, - version=version, params=params) + collection=COLLECTION_OPEN, action=ACT_SUBSCRIBE, version=version, params=params + ) response = service.process(headers, request) - assert isinstance( - response, - as_tm(version).ManageCollectionSubscriptionResponse) + assert isinstance(response, as_tm(version).ManageCollectionSubscriptionResponse) assert response.collection_name == COLLECTION_OPEN assert len(response.subscription_instances) == 1 @@ -124,19 +116,21 @@ def test_subscribe_pause_resume(server, https): assert subs.status == SS_ACTIVE assert ( - server.servers.taxii1.persistence.get_subscription(subs.subscription_id).status == - SS_ACTIVE) + server.servers.taxii1.persistence.get_subscription(subs.subscription_id).status + == SS_ACTIVE + ) # Pausing request = prepare_request( - collection=COLLECTION_OPEN, action=ACT_PAUSE, - subscription_id=subs.subscription_id, version=version) + collection=COLLECTION_OPEN, + action=ACT_PAUSE, + subscription_id=subs.subscription_id, + version=version, + ) response = service.process(headers, request) - assert isinstance( - response, - as_tm(version).ManageCollectionSubscriptionResponse) + assert isinstance(response, as_tm(version).ManageCollectionSubscriptionResponse) assert response.collection_name == COLLECTION_OPEN assert len(response.subscription_instances) == 1 @@ -146,19 +140,21 @@ def test_subscribe_pause_resume(server, https): assert subs.subscription_id assert subs.status == SS_PAUSED assert ( - server.servers.taxii1.persistence.get_subscription(subs.subscription_id).status == - SS_PAUSED) + server.servers.taxii1.persistence.get_subscription(subs.subscription_id).status + == SS_PAUSED + ) # Resume request = prepare_request( - collection=COLLECTION_OPEN, action=ACT_RESUME, - subscription_id=subs.subscription_id, version=version) + collection=COLLECTION_OPEN, + action=ACT_RESUME, + subscription_id=subs.subscription_id, + version=version, + ) response = service.process(headers, request) - assert isinstance( - response, - as_tm(version).ManageCollectionSubscriptionResponse) + assert isinstance(response, as_tm(version).ManageCollectionSubscriptionResponse) assert response.collection_name == COLLECTION_OPEN assert len(response.subscription_instances) == 1 @@ -168,8 +164,9 @@ def test_subscribe_pause_resume(server, https): assert subs.subscription_id assert subs.status == SS_ACTIVE assert ( - server.servers.taxii1.persistence.get_subscription(subs.subscription_id).status == - SS_ACTIVE) + server.servers.taxii1.persistence.get_subscription(subs.subscription_id).status + == SS_ACTIVE + ) @pytest.mark.parametrize("https", [True, False]) @@ -182,13 +179,12 @@ def test_pause_resume_wrong_id(server, https): # Subscribing request = prepare_request( - collection=COLLECTION_OPEN, action=ACT_SUBSCRIBE, - version=version) + collection=COLLECTION_OPEN, action=ACT_SUBSCRIBE, version=version + ) response = service.process(headers, request) - assert isinstance( - response, as_tm(version).ManageCollectionSubscriptionResponse) + assert isinstance(response, as_tm(version).ManageCollectionSubscriptionResponse) assert response.collection_name == COLLECTION_OPEN assert len(response.subscription_instances) == 1 @@ -200,15 +196,21 @@ def test_pause_resume_wrong_id(server, https): # Pausing with wrong subscription ID with pytest.raises(exceptions.StatusMessageException): request = prepare_request( - collection=COLLECTION_OPEN, action=ACT_PAUSE, - subscription_id="RANDOM-WRONG-SUBSCRIPTION", version=version) + collection=COLLECTION_OPEN, + action=ACT_PAUSE, + subscription_id="RANDOM-WRONG-SUBSCRIPTION", + version=version, + ) response = service.process(headers, request) # Resuming with wrong subscription ID with pytest.raises(exceptions.StatusMessageException): request = prepare_request( - collection=COLLECTION_OPEN, action=ACT_RESUME, - subscription_id="RANDOM-WRONG-SUBSCRIPTION", version=version) + collection=COLLECTION_OPEN, + action=ACT_RESUME, + subscription_id="RANDOM-WRONG-SUBSCRIPTION", + version=version, + ) response = service.process(headers, request) @@ -221,13 +223,13 @@ def test_unsubscribe(server, version, https): params = dict( response_type=RT_FULL, - content_bindings=[CB_STIX_XML_111, CUSTOM_CONTENT_BINDING] + content_bindings=[CB_STIX_XML_111, CUSTOM_CONTENT_BINDING], ) # Subscribing request = prepare_request( - collection=COLLECTION_OPEN, action=ACT_SUBSCRIBE, - version=version, params=params) + collection=COLLECTION_OPEN, action=ACT_SUBSCRIBE, version=version, params=params + ) response = service.process(headers, request) @@ -242,8 +244,11 @@ def test_unsubscribe(server, version, https): # return valid response INVALID_ID = "RANDOM-WRONG-SUBSCRIPTION" request = prepare_request( - collection=COLLECTION_OPEN, action=ACT_UNSUBSCRIBE, - subscription_id=INVALID_ID, version=version) + collection=COLLECTION_OPEN, + action=ACT_UNSUBSCRIBE, + subscription_id=INVALID_ID, + version=version, + ) response = service.process(headers, request) assert len(response.subscription_instances) == 1 @@ -252,8 +257,11 @@ def test_unsubscribe(server, version, https): # Unsubscribing with valid subscription ID request = prepare_request( - collection=COLLECTION_OPEN, action=ACT_UNSUBSCRIBE, - subscription_id=subscription_id, version=version) + collection=COLLECTION_OPEN, + action=ACT_UNSUBSCRIBE, + subscription_id=subscription_id, + version=version, + ) response = service.process(headers, request) assert len(response.subscription_instances) == 1 @@ -264,5 +272,6 @@ def test_unsubscribe(server, version, https): assert subs.status == SS_UNSUBSCRIBED assert ( - server.servers.taxii1.persistence.get_subscription(subscription_id).status == - SS_UNSUBSCRIBED) + server.servers.taxii1.persistence.get_subscription(subscription_id).status + == SS_UNSUBSCRIBED + ) diff --git a/tests/taxii2/test_taxii2_collection.py b/tests/taxii2/test_taxii2_collection.py index 77938b34..267f0e01 100644 --- a/tests/taxii2/test_taxii2_collection.py +++ b/tests/taxii2/test_taxii2_collection.py @@ -3,9 +3,14 @@ from uuid import uuid4 import pytest + from opentaxii.persistence.sqldb import taxii2models -from tests.taxii2.utils import (API_ROOTS, COLLECTIONS, GET_API_ROOT_MOCK, - GET_COLLECTION_MOCK) +from tests.taxii2.utils import ( + API_ROOTS, + COLLECTIONS, + GET_API_ROOT_MOCK, + GET_COLLECTION_MOCK, +) @pytest.mark.parametrize( diff --git a/tests/taxii2/test_taxii2_objects.py b/tests/taxii2/test_taxii2_objects.py index 9f71ca3e..9cf6fd10 100644 --- a/tests/taxii2/test_taxii2_objects.py +++ b/tests/taxii2/test_taxii2_objects.py @@ -1295,7 +1295,7 @@ def test_objects_unauthenticated( "add_objects", side_effect=ADD_OBJECTS_MOCK, ) as add_objects_mock: - kwargs = { + kwargs: dict = { "headers": { "Accept": "application/taxii+json;version=2.1", "Content-Type": "application/taxii+json;version=2.1", diff --git a/tests/taxii2/utils.py b/tests/taxii2/utils.py index c19d9ab3..733b1c1a 100644 --- a/tests/taxii2/utils.py +++ b/tests/taxii2/utils.py @@ -1,6 +1,6 @@ import base64 import datetime -from typing import Dict, List, Optional +from typing import Dict, List, Optional, Tuple from uuid import UUID, uuid4 from opentaxii.server import ServerMapping @@ -26,7 +26,7 @@ ) API_ROOTS = API_ROOTS_WITHOUT_DEFAULT NOW = datetime.datetime.now(datetime.timezone.utc) -JOBS = tuple() +JOBS: Tuple[Job, ...] = tuple() for api_root in API_ROOTS: JOBS = JOBS + ( Job( @@ -286,7 +286,7 @@ def process_match_version(match_version): if match_version is None: match_version = ["last"] - versions_per_id = {} + versions_per_id: dict = {} for stix_obj in STIX_OBJECTS: if stix_obj.id not in versions_per_id: versions_per_id[stix_obj.id] = [] @@ -393,7 +393,7 @@ def GET_OBJECTS_MOCK( match_spec_version: Optional[List[str]] = None, ): id_version_combos = process_match_version(match_version) - response = [] + response: List = [] more = False for stix_object in STIX_OBJECTS: if ( @@ -441,7 +441,7 @@ def GET_OBJECT_MOCK( match_spec_version: Optional[List[str]] = None, ): id_version_combos = process_match_version(match_version) - response = [] + response: List = [] more = False at_least_one = False for stix_object in STIX_OBJECTS: @@ -477,7 +477,7 @@ def GET_OBJECT_MOCK( else: next_param = None if not at_least_one: - response = None + response = None # type: ignore[assignment] return response, more, next_param diff --git a/tests/test_auth.py b/tests/test_auth.py index a695c2e6..81a4bfb8 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -18,7 +18,8 @@ description='inboxA description', address='/path/inbox', destination_collection_required=True, - authentication_required=False) + authentication_required=False, +) INBOX_CLOSED = dict( id='inbox-A', @@ -26,7 +27,8 @@ description='inboxA description', address='/path/inbox', destination_collection_required=True, - authentication_required=True) + authentication_required=True, +) DISCOVERY = dict( id='discovery-A', @@ -35,7 +37,8 @@ address='/path/discovery', advertised_services=['inbox-A', 'poll-A'], protocol_bindings=[VID_TAXII_HTTP_10], - authentication_required=True) + authentication_required=True, +) POLL_CLOSED = dict( id='poll-A', @@ -45,7 +48,8 @@ protocol_bindings=[VID_TAXII_HTTP_10], authentication_required=True, max_result_size=100, - max_result_count=10) + max_result_count=10, +) POLL_OPEN = dict( id='poll-B', @@ -55,40 +59,45 @@ protocol_bindings=[VID_TAXII_HTTP_10], authentication_required=False, # <- open for all max_result_size=100, - max_result_count=10) + max_result_count=10, +) CONTENT = 'inbox-message-content' COLLECTIONS = [ - {'name': 'collection-1', - 'available': True, - 'accept_all_content': True, - 'type': 'DATA_FEED', - 'service_ids': ['discovery-A', 'inbox-A', 'poll-A', 'poll-B']}, - {'name': 'collection-2', - 'available': True, - 'accept_all_content': True, - 'type': 'DATA_FEED', - 'service_ids': ['discovery-A', 'inbox-A', 'poll-A', 'poll-B']}] + { + 'name': 'collection-1', + 'available': True, + 'accept_all_content': True, + 'type': 'DATA_FEED', + 'service_ids': ['discovery-A', 'inbox-A', 'poll-A', 'poll-B'], + }, + { + 'name': 'collection-2', + 'available': True, + 'accept_all_content': True, + 'type': 'DATA_FEED', + 'service_ids': ['discovery-A', 'inbox-A', 'poll-A', 'poll-B'], + }, +] USERNAME = 'some-username' PASSWORD = 'some-password' ACCOUNTS = [ - {'username': 'johnny', - 'password': 'johnny', - 'permissions': { - 'collection-1': 'read', - 'collection-2': 'modify'}}, - {'username': 'billy', - 'password': 'billy', - 'permissions': { - 'collection-1': 'modify'}}, - {'username': 'wally', - 'password': 'wally', - 'is_admin': True}, - {'username': USERNAME, - 'password': PASSWORD}] + { + 'username': 'johnny', + 'password': 'johnny', + 'permissions': {'collection-1': 'read', 'collection-2': 'modify'}, + }, + { + 'username': 'billy', + 'password': 'billy', + 'permissions': {'collection-1': 'modify'}, + }, + {'username': 'wally', 'password': 'wally', 'is_admin': True}, + {'username': USERNAME, 'password': PASSWORD}, +] MESSAGE_ID = '123' @@ -100,11 +109,11 @@ def auth_fixtures(server): sync_conf_dict_into_db( server, config={ - 'services': [ - INBOX_OPEN, INBOX_CLOSED, - DISCOVERY, POLL_OPEN, POLL_CLOSED], + 'services': [INBOX_OPEN, INBOX_CLOSED, DISCOVERY, POLL_OPEN, POLL_CLOSED], 'collections': COLLECTIONS, - 'accounts': ACCOUNTS}) + 'accounts': ACCOUNTS, + }, + ) assert len(server.servers.taxii1.persistence.get_services()) == 4 assert len(server.servers.taxii1.persistence.get_collections()) == len(COLLECTIONS) @@ -113,6 +122,7 @@ def auth_fixtures(server): @pytest.fixture() def test_account(server): from opentaxii.entities import Account + account = Account(id=None, username=USERNAME, permissions={}) server.auth.update_account(account, PASSWORD) @@ -125,7 +135,8 @@ def test_unauthorized_request(app, client, version, https): INBOX_OPEN['address'], data='invalid-body', headers=prepare_headers(version, https), - base_url=base_url) + base_url=base_url, + ) assert response.status_code == 200 assert is_headers_valid(response.headers, version, https) @@ -134,6 +145,7 @@ def test_unauthorized_request(app, client, version, https): assert message.status_type == ST_UNAUTHORIZED from opentaxii import context + assert not hasattr(context, 'account') @@ -143,23 +155,18 @@ def test_get_token(client, version, https): base_url = '%s://localhost' % ('https' if https else 'http') # Invalid credentials response = client.post( - AUTH_PATH, - data={'username': 'dummy', 'password': 'wrong'}, - base_url=base_url) + AUTH_PATH, data={'username': 'dummy', 'password': 'wrong'}, base_url=base_url + ) assert response.status_code == 401 # Invalid auth data - response = client.post( - AUTH_PATH, - data={'other': 'somethind'}, - base_url=base_url) + response = client.post(AUTH_PATH, data={'other': 'somethind'}, base_url=base_url) assert response.status_code == 400 # Valid credentials as form data response = client.post( - AUTH_PATH, - data={'username': USERNAME, 'password': PASSWORD}, - base_url=base_url) + AUTH_PATH, data={'username': USERNAME, 'password': PASSWORD}, base_url=base_url + ) assert response.status_code == 200 @@ -171,7 +178,8 @@ def test_get_token(client, version, https): AUTH_PATH, data=json.dumps({'username': USERNAME, 'password': PASSWORD}), base_url=base_url, - content_type='application/json') + content_type='application/json', + ) assert response.status_code == 200 @@ -187,9 +195,8 @@ def test_get_token_and_send_request(client, version, https): # Get valid token response = client.post( - AUTH_PATH, - data={'username': USERNAME, 'password': PASSWORD}, - base_url=base_url) + AUTH_PATH, data={'username': USERNAME, 'password': PASSWORD}, base_url=base_url + ) assert response.status_code == 200 @@ -203,10 +210,8 @@ def test_get_token_and_send_request(client, version, https): # Get correct response for invalid body response = client.post( - INBOX_OPEN['address'], - data='invalid-body', - headers=headers, - base_url=base_url) + INBOX_OPEN['address'], data='invalid-body', headers=headers, base_url=base_url + ) assert response.status_code == 200 assert is_headers_valid(response.headers, version, https) @@ -220,10 +225,8 @@ def test_get_token_and_send_request(client, version, https): # Get correct response for valid request response = client.post( - DISCOVERY['address'], - data=request.to_xml(), - headers=headers, - base_url=base_url) + DISCOVERY['address'], data=request.to_xml(), headers=headers, base_url=base_url + ) assert response.status_code == 200 assert is_headers_valid(response.headers, version=version, https=https) @@ -233,12 +236,12 @@ def test_get_token_and_send_request(client, version, https): assert isinstance(message, as_tm(version).DiscoveryResponse) from opentaxii import context + assert not hasattr(context, 'account') def basic_auth_token(username, password): - return base64.b64encode( - '{}:{}'.format(username, password).encode('utf-8')) + return base64.b64encode('{}:{}'.format(username, password).encode('utf-8')) @pytest.mark.parametrize("https", [True, False]) @@ -247,17 +250,16 @@ def test_request_with_basic_auth(client, version, https): base_url = '%s://localhost' % ('https' if https else 'http') basic_auth_header = 'Basic {}'.format( - basic_auth_token(USERNAME, PASSWORD).decode('utf-8')) + basic_auth_token(USERNAME, PASSWORD).decode('utf-8') + ) headers = prepare_headers(version, https) headers[HTTP_AUTHORIZATION] = basic_auth_header # Get correct response for invalid body response = client.post( - INBOX_OPEN['address'], - data='invalid-body', - headers=headers, - base_url=base_url) + INBOX_OPEN['address'], data='invalid-body', headers=headers, base_url=base_url + ) assert response.status_code == 200 assert is_headers_valid(response.headers, version, https) @@ -271,10 +273,7 @@ def test_request_with_basic_auth(client, version, https): # Get correct response for valid request response = client.post( - DISCOVERY['address'], - data=request.to_xml(), - headers=headers, - base_url=base_url + DISCOVERY['address'], data=request.to_xml(), headers=headers, base_url=base_url ) assert response.status_code == 200 @@ -285,6 +284,7 @@ def test_request_with_basic_auth(client, version, https): assert isinstance(message, as_tm(version).DiscoveryResponse) from opentaxii import context + assert not hasattr(context, 'account') @@ -299,10 +299,8 @@ def test_invalid_basic_auth_request(client, version, https): request = as_tm(version).DiscoveryRequest(message_id=MESSAGE_ID) response = client.post( - DISCOVERY['address'], - data=request.to_xml(), - headers=headers, - base_url=base_url) + DISCOVERY['address'], data=request.to_xml(), headers=headers, base_url=base_url + ) assert response.status_code == 200 assert is_headers_valid(response.headers, version, https) @@ -324,10 +322,8 @@ def test_invalid_auth_header_request(client, version, https): request = as_tm(version).DiscoveryRequest(message_id=MESSAGE_ID) response = client.post( - DISCOVERY['address'], - data=request.to_xml(), - headers=headers, - base_url=base_url) + DISCOVERY['address'], data=request.to_xml(), headers=headers, base_url=base_url + ) assert response.status_code == 200 assert is_headers_valid(response.headers, version, https) @@ -340,7 +336,8 @@ def prepare_url_headers(version, https, username, password): base_url = '%s://localhost' % ('https' if https else 'http') headers = prepare_headers(version, https) basic_auth_header = 'Basic {}'.format( - basic_auth_token(username, password).decode('utf-8')) + basic_auth_token(username, password).decode('utf-8') + ) headers[HTTP_AUTHORIZATION] = basic_auth_header return base_url, headers @@ -351,14 +348,11 @@ def test_collection_access_private_poll(client, version, https): # POLL_CLOSED collection allowed read access url, headers = prepare_url_headers(version, https, 'johnny', 'johnny') - request = prepare_poll_request( - 'collection-1', version, bindings=[CB_STIX_XML_111]) + request = prepare_poll_request('collection-1', version, bindings=[CB_STIX_XML_111]) response = client.post( - POLL_CLOSED['address'], - data=request.to_xml(), - headers=headers, - base_url=url) + POLL_CLOSED['address'], data=request.to_xml(), headers=headers, base_url=url + ) assert response.status_code == 200 assert is_headers_valid(response.headers, version, https) message = as_tm(version).get_message_from_xml(response.data) @@ -366,14 +360,11 @@ def test_collection_access_private_poll(client, version, https): # POLL_CLOSED collection disallowed read access url, headers = prepare_url_headers(version, https, 'billy', 'billy') - request = prepare_poll_request( - 'collection-2', version, bindings=[CB_STIX_XML_111]) + request = prepare_poll_request('collection-2', version, bindings=[CB_STIX_XML_111]) response = client.post( - POLL_CLOSED['address'], - data=request.to_xml(), - headers=headers, - base_url=url) + POLL_CLOSED['address'], data=request.to_xml(), headers=headers, base_url=url + ) assert response.status_code == 200 assert is_headers_valid(response.headers, version, https) message = as_tm(version).get_message_from_xml(response.data) @@ -381,14 +372,11 @@ def test_collection_access_private_poll(client, version, https): # POLL_CLOSED collection admin access url, headers = prepare_url_headers(version, https, 'wally', 'wally') - request = prepare_poll_request( - 'collection-2', version, bindings=[CB_STIX_XML_111]) + request = prepare_poll_request('collection-2', version, bindings=[CB_STIX_XML_111]) response = client.post( - POLL_CLOSED['address'], - data=request.to_xml(), - headers=headers, - base_url=url) + POLL_CLOSED['address'], data=request.to_xml(), headers=headers, base_url=url + ) assert response.status_code == 200 assert is_headers_valid(response.headers, version, https) message = as_tm(version).get_message_from_xml(response.data) @@ -401,15 +389,12 @@ def test_collection_access_private_inbox(client, version, https): # INBOX read-only collection access url, headers = prepare_url_headers(version, https, 'johnny', 'johnny') request = prepare_inbox_message( - version, - dest_collection='collection-1', - blocks=[make_inbox_content(version)]) + version, dest_collection='collection-1', blocks=[make_inbox_content(version)] + ) response = client.post( - INBOX_CLOSED['address'], - data=request.to_xml(), - headers=headers, - base_url=url) + INBOX_CLOSED['address'], data=request.to_xml(), headers=headers, base_url=url + ) assert response.status_code == 200 assert is_headers_valid(response.headers, version, https) message = as_tm(version).get_message_from_xml(response.data) @@ -417,9 +402,7 @@ def test_collection_access_private_inbox(client, version, https): if version == 11: assert message.status_type == 'UNAUTHORIZED' - assert ( - message.message == - 'User can not write to collection collection-1') + assert message.message == 'User can not write to collection collection-1' else: # Because in TAXII 1.0 destination collection can not be specified # so it impossible to verify access @@ -427,15 +410,12 @@ def test_collection_access_private_inbox(client, version, https): # INBOX modify collection access request = prepare_inbox_message( - version, - dest_collection='collection-2', - blocks=[make_inbox_content(version)]) + version, dest_collection='collection-2', blocks=[make_inbox_content(version)] + ) response = client.post( - INBOX_CLOSED['address'], - data=request.to_xml(), - headers=headers, - base_url=url) + INBOX_CLOSED['address'], data=request.to_xml(), headers=headers, base_url=url + ) assert response.status_code == 200 assert is_headers_valid(response.headers, version, https) message = as_tm(version).get_message_from_xml(response.data) @@ -445,15 +425,12 @@ def test_collection_access_private_inbox(client, version, https): # INBOX modify collection access url, headers = prepare_url_headers(version, https, 'wally', 'wally') request = prepare_inbox_message( - version, - dest_collection='collection-2', - blocks=[make_inbox_content(version)]) + version, dest_collection='collection-2', blocks=[make_inbox_content(version)] + ) response = client.post( - INBOX_CLOSED['address'], - data=request.to_xml(), - headers=headers, - base_url=url) + INBOX_CLOSED['address'], data=request.to_xml(), headers=headers, base_url=url + ) assert response.status_code == 200 assert is_headers_valid(response.headers, version, https) message = as_tm(version).get_message_from_xml(response.data) @@ -461,8 +438,7 @@ def test_collection_access_private_inbox(client, version, https): assert message.status_type == 'SUCCESS' -def prepare_poll_request( - collection_name, version, bindings=[], subscription_id=None): +def prepare_poll_request(collection_name, version, bindings=[], subscription_id=None): if version == 11: content_bindings = [tm11.ContentBinding(b) for b in bindings] @@ -470,41 +446,39 @@ def prepare_poll_request( poll_parameters = None else: poll_parameters = tm11.PollParameters( - response_type=RT_FULL, - content_bindings=content_bindings) + response_type=RT_FULL, content_bindings=content_bindings + ) return tm11.PollRequest( message_id=MESSAGE_ID, collection_name=collection_name, subscription_id=subscription_id, - poll_parameters=poll_parameters) + poll_parameters=poll_parameters, + ) elif version == 10: content_bindings = bindings return tm10.PollRequest( message_id=MESSAGE_ID, feed_name=collection_name, content_bindings=content_bindings, - subscription_id=subscription_id) + subscription_id=subscription_id, + ) -def make_inbox_content( - version, content_binding=CB_STIX_XML_111, content=CONTENT): +def make_inbox_content(version, content_binding=CB_STIX_XML_111, content=CONTENT): if version == 10: return tm10.ContentBlock(content_binding, content) elif version == 11: - return tm11.ContentBlock( - tm11.ContentBinding(content_binding), content) + return tm11.ContentBlock(tm11.ContentBinding(content_binding), content) else: raise ValueError('Unknown TAXII message version: %s' % version) def prepare_inbox_message(version, blocks=None, dest_collection=None): if version == 10: - inbox_message = tm10.InboxMessage( - message_id=MESSAGE_ID, content_blocks=blocks) + inbox_message = tm10.InboxMessage(message_id=MESSAGE_ID, content_blocks=blocks) elif version == 11: - inbox_message = tm11.InboxMessage( - message_id=MESSAGE_ID, content_blocks=blocks) + inbox_message = tm11.InboxMessage(message_id=MESSAGE_ID, content_blocks=blocks) if dest_collection: inbox_message.destination_collection_names.append(dest_collection) else: diff --git a/tests/test_config.py b/tests/test_config.py index 0a085726..33945ed4 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,7 +1,9 @@ import os import tempfile +from typing import Tuple import pytest + from opentaxii.config import ServerConfig BACKWARDS_COMPAT_CONFIG = """ @@ -168,7 +170,7 @@ def test_custom_config_file(config_file_name_expected_value): deprecation_warning, taxii2_only_warning, ) = config_file_name_expected_value - warning_classes = (UserWarning,) + warning_classes: Tuple = (UserWarning,) if deprecation_warning or taxii2_only_warning: warning_classes += (DeprecationWarning,) expected_warnings = {"Ignoring invalid configuration item 'dummy'."} diff --git a/tests/test_http.py b/tests/test_http.py index c379d1b4..57682198 100644 --- a/tests/test_http.py +++ b/tests/test_http.py @@ -15,7 +15,8 @@ accept_all_content=True, protocol_bindings=[ 'urn:taxii.mitre.org:protocol:http:1.0', - 'urn:taxii.mitre.org:protocol:https:1.0'] + 'urn:taxii.mitre.org:protocol:https:1.0', + ], ) DISCOVERY = dict( @@ -24,7 +25,7 @@ description='discoveryA description', address='/relative/discovery', advertised_services=['inbox-A', 'discovery-A', 'discovery-B'], - protocol_bindings=['urn:taxii.mitre.org:protocol:http:1.0'] + protocol_bindings=['urn:taxii.mitre.org:protocol:http:1.0'], ) DISCOVERY_NOT_AVAILABLE = dict( @@ -34,18 +35,20 @@ address='/relative/discovery-b', advertised_services=['inbox-A', 'discovery-A'], protocol_bindings=['urn:taxii.mitre.org:protocol:http:1.0'], - available=False + available=False, ) SERVICES = [INBOX, DISCOVERY, DISCOVERY_NOT_AVAILABLE] -INSTANCES_CONFIGURED = sum(len(s['protocol_bindings']) for s in SERVICES) +INSTANCES_CONFIGURED = sum(len(s['protocol_bindings']) for s in SERVICES) # type: ignore MESSAGE_ID = '123' @pytest.fixture(autouse=True) def local_services(server): for service in SERVICES: - server.servers.taxii1.persistence.update_service(dict_to_service_entity(service)) + server.servers.taxii1.persistence.update_service( + dict_to_service_entity(service) + ) def test_root_get(client): @@ -72,7 +75,7 @@ def test_status_message_response(client, version, https): INBOX['address'], data='invalid-body', headers=prepare_headers(version, https), - base_url=base_url + base_url=base_url, ) assert response.status_code == 200 @@ -94,7 +97,7 @@ def test_successful_response(client, version, https): DISCOVERY['address'], data=request.to_xml(), headers=prepare_headers(version=version, https=https), - base_url=base_url + base_url=base_url, ) assert response.status_code == 200 @@ -117,10 +120,7 @@ def test_post_parse_verification(client, version, https): base_url = '%s://localhost' % ('https' if https else 'http') response = client.post( - DISCOVERY['address'], - data=request.to_xml(), - headers=headers, - base_url=base_url + DISCOVERY['address'], data=request.to_xml(), headers=headers, base_url=base_url ) assert response.status_code == 200 @@ -145,7 +145,7 @@ def test_services_available(client, version, https): DISCOVERY_NOT_AVAILABLE['address'], data=request.to_xml(), headers=headers, - base_url=base_url + base_url=base_url, ) assert response.status_code == 200 diff --git a/tests/test_server.py b/tests/test_server.py index 99bb2bbe..86a19f22 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -18,7 +18,8 @@ accept_all_content='yes', protocol_bindings=[ 'urn:taxii.mitre.org:protocol:http:1.0', - 'urn:taxii.mitre.org:protocol:https:1.0'], + 'urn:taxii.mitre.org:protocol:https:1.0', + ], ) DISCOVERY = dict( @@ -27,7 +28,7 @@ description='discoveryA description', address='/relative/discovery', advertised_services=['inboxA', 'discoveryA'], - protocol_bindings=['urn:taxii.mitre.org:protocol:http:1.0'] + protocol_bindings=['urn:taxii.mitre.org:protocol:http:1.0'], ) DISCOVERY_EXTERNAL = dict( @@ -35,7 +36,7 @@ type='discovery', description='discoveryB description', address='http://example.com/a/b/c', - protocol_bindings=['urn:taxii.mitre.org:protocol:http:1.0'] + protocol_bindings=['urn:taxii.mitre.org:protocol:http:1.0'], ) INTERNAL_SERVICES = [INBOX, DISCOVERY] @@ -45,19 +46,18 @@ @pytest.fixture() def local_services(server): for service in SERVICES: - server.servers.taxii1.persistence.update_service(dict_to_service_entity(service)) + server.servers.taxii1.persistence.update_service( + dict_to_service_entity(service) + ) def test_services_configured(server, local_services): assert len(server.servers.taxii1.get_services()) == len(SERVICES) - with_paths = [ - s for s in server.servers.taxii1.get_services() - if s.path] + with_paths = [s for s in server.servers.taxii1.get_services() if s.path] assert len(with_paths) == len(INTERNAL_SERVICES) - assert all([ - p.address.startswith(DOMAIN) for p in with_paths]) + assert all([p.address.startswith(DOMAIN) for p in with_paths]) def test_taxii2_configured(server): diff --git a/tests/utils.py b/tests/utils.py index 2b2f530a..da3e5dc8 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -168,4 +168,5 @@ def assert_str_equal_no_formatting(str1, str2): class SKIP: """Used as signalling value to skip check""" + pass From 836f8e97d4c14842f8b1528cb97cd8c32358ea40 Mon Sep 17 00:00:00 2001 From: Eric Masseran Date: Wed, 3 Dec 2025 15:28:54 +0100 Subject: [PATCH 10/24] Type opentaxii/persistence/sqldb/api.py --- opentaxii/persistence/sqldb/api.py | 38 +++++++++++++++++------------- pyproject.toml | 3 +-- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/opentaxii/persistence/sqldb/api.py b/opentaxii/persistence/sqldb/api.py index 8431d66b..dd097dbf 100644 --- a/opentaxii/persistence/sqldb/api.py +++ b/opentaxii/persistence/sqldb/api.py @@ -3,7 +3,7 @@ import json import uuid from functools import reduce -from typing import Dict, List, Optional, Tuple +from typing import Dict, List, Optional, Tuple, no_type_check import six import structlog @@ -48,7 +48,8 @@ class SQLDatabaseAPI(BaseSQLDatabaseAPI, OpenTAXIIPersistenceAPI): :param bool create_tables=False: if True, tables will be created in the DB. - :param engine_parameters=None: if defined, these arguments would be passed to sqlalchemy.create_engine + :param engine_parameters=None: if defined, these arguments would be passed + to sqlalchemy.create_engine """ BASEMODEL = Base @@ -70,7 +71,9 @@ def update_service(self, obj): service.type = obj.type service.properties = obj.properties else: - service = Service(id=obj.id, type=obj.type, properties=obj.properties) + service = Service( # type: ignore[misc] + id=obj.id, type=obj.type, properties=obj.properties + ) self.db.session.add(service) self.db.session.commit() return conv.to_service_entity(service) @@ -441,7 +444,7 @@ def delete_content_blocks( content_blocks_query = ( self.db.session.query(ContentBlock.id) - .join(DataCollection.content_blocks) + .join(DataCollection.content_blocks) # type: ignore[attr-defined] .filter(DataCollection.id == collection.id) .filter(ContentBlock.timestamp_label > start_time) ) @@ -553,7 +556,7 @@ def add_api_root( self, title: str, description: Optional[str] = None, - default: Optional[bool] = False, + default: bool = False, is_public: bool = False, api_root_id: Optional[str] = None, ) -> entities.ApiRoot: @@ -588,6 +591,7 @@ def add_api_root( is_public=is_public, ) + @no_type_check # taxii2models.Job has too many None allowance def _job_and_details_to_entity( self, job: taxii2models.Job, job_details: List[taxii2models.JobDetail] ) -> entities.Job: @@ -708,12 +712,14 @@ def add_collection( """ Add a new collection. - :param str api_root_id: ID of the api root the new collection is part of - :param str title: Title of the new collection - :param str description: [Optional] Description of the new collection - :param str alias: [Optional] Alias of the new collection - :param bool is_public: [Optional] Whether collection should be publicly readable - :param bool is_public_write: [Optional] Whether collection should be publicly writable + :param api_root_id: ID of the api root the new collection is part of + :param title: Title of the new collection + :param description: [Optional] Description of the new collection + :param alias: [Optional] Alias of the new collection + :param is_public: [Optional] Whether collection should be publicly + readable + :param is_public_write: [Optional] Whether collection should be + publicly writable :return: The added Collection entity. """ @@ -730,9 +736,9 @@ def add_collection( return entities.Collection( id=collection.id, - api_root_id=collection.api_root_id, + api_root_id=collection.api_root_id, # type: ignore[arg-type] title=collection.title, - description=collection.description, + description=collection.description, # type: ignore[arg-type] alias=collection.alias, is_public=collection.is_public, is_public_write=collection.is_public_write, @@ -1037,8 +1043,8 @@ def add_objects( ) job_details.append(job_detail) self.db.session.add(job_detail) - job.total_count += 1 - job.success_count += 1 + job.total_count += 1 # type: ignore[operator] + job.success_count += 1 # type: ignore[operator] job.status = "complete" job.completed_timestamp = datetime.datetime.now(datetime.timezone.utc) self.db.session.commit() @@ -1132,7 +1138,7 @@ def get_versions( added_after: Optional[datetime.datetime] = None, next_kwargs: Optional[Dict] = None, match_spec_version: Optional[List[str]] = None, - ) -> Tuple[List[entities.VersionRecord], bool]: + ) -> Tuple[Optional[List[entities.VersionRecord]], bool]: """ Get all versions of single object from database. diff --git a/pyproject.toml b/pyproject.toml index 6ac4290b..849e1c53 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,8 +30,7 @@ files = [ exclude = """ (?x)( - ^opentaxii/persistence/sqldb/api\\.py$ - | ^opentaxii/taxii/.+ + ^opentaxii/taxii/.+ | ^opentaxii/utils\\.py$ | ^opentaxii/common/entities\\.py$ ) From e04db23f7b6eeb7b4f27abbe098cbd7543692850 Mon Sep 17 00:00:00 2001 From: Eric Masseran Date: Wed, 3 Dec 2025 15:42:11 +0100 Subject: [PATCH 11/24] Ignore false positive flake8 errors --- .flake8 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.flake8 b/.flake8 index 9950d6d4..9968b3a7 100644 --- a/.flake8 +++ b/.flake8 @@ -1,4 +1,4 @@ [flake8] max-line-length=120 exclude = docs/*,.tox/* -ignore = E203 \ No newline at end of file +ignore = E203, W503, W504, E704 From fd5b910173bf010344f241cebd455e863bff280e Mon Sep 17 00:00:00 2001 From: Eric Masseran Date: Wed, 3 Dec 2025 15:42:42 +0100 Subject: [PATCH 12/24] Drop python 3.8 and 3.9 support --- .github/workflows/interrogate.yml | 4 ++-- .github/workflows/mypy.yml | 4 ++-- .github/workflows/tox.yml | 2 +- CHANGES.rst | 2 +- docs/installation/installation.rst | 2 +- setup.py | 2 -- tox.ini | 2 -- 7 files changed, 7 insertions(+), 11 deletions(-) diff --git a/.github/workflows/interrogate.yml b/.github/workflows/interrogate.yml index 25866572..174e40f0 100644 --- a/.github/workflows/interrogate.yml +++ b/.github/workflows/interrogate.yml @@ -15,10 +15,10 @@ jobs: with: ref: ${{ github.base_ref }} path: base - - name: Set up Python 3.9 + - name: Set up Python uses: actions/setup-python@v5 with: - python-version: 3.9 + python-version: 3.10 - name: Install interrogate run: pip install -r head/requirements-interrogate.txt - name: Run interrogate on base branch for baseline diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml index 24c607fe..6801979e 100644 --- a/.github/workflows/mypy.yml +++ b/.github/workflows/mypy.yml @@ -7,10 +7,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Set up Python 3.9 + - name: Set up Python uses: actions/setup-python@v5 with: - python-version: 3.9 + python-version: 3.10 - name: Install deps run: pip install -r requirements.txt -r requirements-dev.txt - name: Run mypy diff --git a/.github/workflows/tox.yml b/.github/workflows/tox.yml index 1d47ec97..a2a925ba 100644 --- a/.github/workflows/tox.yml +++ b/.github/workflows/tox.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-22.04 strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "pypy-3.10"] + python-version: ["3.10", "3.11", "pypy-3.10"] services: mariadb: image: mariadb:10.11 diff --git a/CHANGES.rst b/CHANGES.rst index 0bc5961c..8653f7cd 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -3,7 +3,7 @@ Changelog 0.10.0 (unreleased) ------------------- -* Drop EOL Python 3.6 and 3.7 support +* Drop EOL Python 3.6, 3.7, 3.8 and 3.9 support * Add Python 3.11 support * Remove mypy_extensions dependency * Fix returned "spec_version" was equal to the "type" #237 (@meetghodasara-crest) diff --git a/docs/installation/installation.rst b/docs/installation/installation.rst index 0610cbbd..195aa6c8 100644 --- a/docs/installation/installation.rst +++ b/docs/installation/installation.rst @@ -7,7 +7,7 @@ Installation Install Python -------------- -OpenTAXII works with Python versions 3.8 - 3.11. You can download Python +OpenTAXII works with Python versions >=3.10. You can download Python `here `_ or install it with your operating system’s package manager. diff --git a/setup.py b/setup.py index 01e11cdd..22eda993 100644 --- a/setup.py +++ b/setup.py @@ -53,8 +53,6 @@ def get_file_contents(filename): 'Intended Audience :: Information Technology', 'License :: OSI Approved :: BSD License', 'Programming Language :: Python', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: Implementation :: PyPy', diff --git a/tox.ini b/tox.ini index 87223999..35d61f90 100644 --- a/tox.ini +++ b/tox.ini @@ -4,8 +4,6 @@ envlist = py{38,39,310,311,py3}-sqlalchemy{13,14}-werkzeug{lt21,gte21}-{sqlite,m [gh-actions] python = - 3.8: py38 - 3.9: py39 3.10: py310 3.11: py311 pypy-3.10: pypy3 From dd95a8fefd625ed1b6e5835f0b800e1ec6739139 Mon Sep 17 00:00:00 2001 From: Eric Masseran Date: Wed, 3 Dec 2025 15:42:57 +0100 Subject: [PATCH 13/24] Fix types --- opentaxii/server.py | 4 ++-- opentaxii/taxii2/validation.py | 4 ++-- tests/fixtures.py | 14 +++++++------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/opentaxii/server.py b/opentaxii/server.py index 92cc2b21..e5b43cf9 100644 --- a/opentaxii/server.py +++ b/opentaxii/server.py @@ -8,7 +8,7 @@ except ImportError: from typing.re import Pattern # type: ignore[no-redef] -from typing import ClassVar, NamedTuple, Optional, Protocol, Tuple, Type +from typing import ClassVar, NamedTuple, Optional, Protocol, Tuple, Type, Union import structlog from flask import Flask, Response, request @@ -100,7 +100,7 @@ class BaseTAXIIServer: ENDPOINT_MAPPING: Tuple[Tuple[Pattern, EndpointFunc], ...] app: Flask config: dict - persistence: Taxii1PersistenceManager | Taxii2PersistenceManager + persistence: Union[Taxii1PersistenceManager, Taxii2PersistenceManager] def setup_endpoint_mapping(self): mapping = [] diff --git a/opentaxii/taxii2/validation.py b/opentaxii/taxii2/validation.py index cd393649..36d81ef2 100644 --- a/opentaxii/taxii2/validation.py +++ b/opentaxii/taxii2/validation.py @@ -2,7 +2,7 @@ import datetime import json -from typing import Mapping +from typing import Mapping, Union from marshmallow import Schema, fields from stix2 import parse @@ -13,7 +13,7 @@ from opentaxii.taxii2.utils import DATETIMEFORMAT -def validate_envelope(json_data: str | bytes, allow_custom: bool = False) -> None: +def validate_envelope(json_data: Union[str, bytes], allow_custom: bool = False) -> None: """ Validate if ``json_data`` is a valid taxii2 envelope. diff --git a/tests/fixtures.py b/tests/fixtures.py index b4f6dccb..17958341 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -10,7 +10,7 @@ CUSTOM_CONTENT_BINDING = 'custom:content:binding' INVALID_CONTENT_BINDING = 'invalid:content:binding' -INBOX_A = dict( +INBOX_A: dict = dict( id='inbox-A', type='inbox', description='inbox-A description', @@ -20,7 +20,7 @@ protocol_bindings=PROTOCOL_BINDINGS, ) -INBOX_B = dict( +INBOX_B: dict = dict( id='inbox-B', type='inbox', description='inbox-B description', @@ -30,7 +30,7 @@ protocol_bindings=PROTOCOL_BINDINGS, ) -DISCOVERY_A = dict( +DISCOVERY_A: dict = dict( id='discovery-A', type='discovery', description='discovery-A description', @@ -46,7 +46,7 @@ protocol_bindings=PROTOCOL_BINDINGS, ) -DISCOVERY_B = dict( +DISCOVERY_B: dict = dict( id='discovery-B', type='discovery', description='External discovery-B service', @@ -56,7 +56,7 @@ SUBSCRIPTION_MESSAGE = 'message about subscription' -COLLECTION_MANAGEMENT = dict( +COLLECTION_MANAGEMENT: dict = dict( id='collection-management-A', type='collection_management', description='Collection management description', @@ -68,7 +68,7 @@ POLL_RESULT_SIZE = 20 POLL_MAX_COUNT = 15 -POLL = dict( +POLL: dict = dict( id='poll-A', type='poll', description='Poll service description', @@ -83,7 +83,7 @@ INTERNAL_SERVICES = [INBOX_A, INBOX_B, DISCOVERY_A, COLLECTION_MANAGEMENT, POLL] SERVICES = INTERNAL_SERVICES + [DISCOVERY_B] -INSTANCES_CONFIGURED = sum(len(s['protocol_bindings']) for s in SERVICES) # type: ignore +INSTANCES_CONFIGURED = sum(len(s['protocol_bindings']) for s in SERVICES) MESSAGE_ID = '123' CONTENT = 'some-content' From 7bdbff482a0d1d961548780b24006841dd2f1ece Mon Sep 17 00:00:00 2001 From: Eric Masseran Date: Wed, 3 Dec 2025 15:53:34 +0100 Subject: [PATCH 14/24] Use correct 3.10 version --- .github/workflows/interrogate.yml | 2 +- .github/workflows/mypy.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/interrogate.yml b/.github/workflows/interrogate.yml index 174e40f0..21bebc51 100644 --- a/.github/workflows/interrogate.yml +++ b/.github/workflows/interrogate.yml @@ -18,7 +18,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: 3.10 + python-version: "3.10" - name: Install interrogate run: pip install -r head/requirements-interrogate.txt - name: Run interrogate on base branch for baseline diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml index 6801979e..a24bfec1 100644 --- a/.github/workflows/mypy.yml +++ b/.github/workflows/mypy.yml @@ -10,7 +10,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: 3.10 + python-version: "3.10" - name: Install deps run: pip install -r requirements.txt -r requirements-dev.txt - name: Run mypy From 2e10f8ec83237287a3e348b29aba2cd6da496ed1 Mon Sep 17 00:00:00 2001 From: Eric Masseran Date: Wed, 3 Dec 2025 15:54:50 +0100 Subject: [PATCH 15/24] Update python version for publishing --- .github/workflows/publish.yml | 4 ++-- Dockerfile | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e849dbca..25509e69 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -11,10 +11,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Set up Python 3.9 + - name: Set up Python uses: actions/setup-python@v5 with: - python-version: 3.9 + python-version: "3.10" - name: Install pypa/build run: >- python -m diff --git a/Dockerfile b/Dockerfile index 82486a69..3d5d759b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.9 AS build +FROM python:3.10 AS build LABEL maintainer="EclecticIQ " RUN apt-get update \ @@ -13,7 +13,7 @@ COPY . /opentaxii RUN /venv/bin/pip install /opentaxii -FROM python:3.9-slim AS prod +FROM python:3.10-slim AS prod LABEL maintainer="EclecticIQ " COPY --from=build /venv /venv From 6ddd43e20d7b23fe4ddc55ec2026752ffb48b3c5 Mon Sep 17 00:00:00 2001 From: Eric Masseran Date: Wed, 3 Dec 2025 15:57:57 +0100 Subject: [PATCH 16/24] Remove sqlalchemy 1.3 --- tox.ini | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 35d61f90..058bbe73 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] skipsdist = True -envlist = py{38,39,310,311,py3}-sqlalchemy{13,14}-werkzeug{lt21,gte21}-{sqlite,mysql,mariadb,postgres} +envlist = py{310,311,py3}-sqlalchemy14-werkzeug{lt21,gte21}-{sqlite,mysql,mariadb,postgres} [gh-actions] python = @@ -16,7 +16,6 @@ deps = mysql,mariadb: -rrequirements-dev-mysql.txt postgres-!pypy3: -rrequirements-dev-postgres.txt postgres-pypy3: -rrequirements-dev-postgres-pypy.txt - sqlalchemy13: sqlalchemy>=1.3,<1.4 sqlalchemy14: sqlalchemy>=1.4,<1.5 werkzeuglt21: werkzeug<2.1 werkzeuggte21: werkzeug>=2.1 From 74a157a465afadfd52b6e00954c13c82c874bcc1 Mon Sep 17 00:00:00 2001 From: Eric Masseran Date: Wed, 3 Dec 2025 16:07:46 +0100 Subject: [PATCH 17/24] Do not install mypy for Pypy env --- requirements-dev.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 5de045fd..bc50cb39 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -5,7 +5,9 @@ pytest-pythonpath flake8 ipdb factory-boy>=3.2.1 -mypy==1.19.0 +# Pypy is incompatible with latest mypy version +# https://github.com/python/mypy/issues/20329 +mypy==1.19.0; platform_python_implementation != 'PyPy' types-six==1.17.0.20251009 sqlalchemy-stubs==0.4 types-PyYAML==6.0.12.20250915 From b37fb3b14ddc550b63d2f8629c42230c6a31ddc4 Mon Sep 17 00:00:00 2001 From: Eric Masseran Date: Wed, 3 Dec 2025 16:11:44 +0100 Subject: [PATCH 18/24] Add some docstrings --- opentaxii/persistence/api.py | 3 +++ opentaxii/persistence/sqldb/api.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/opentaxii/persistence/api.py b/opentaxii/persistence/api.py index 8d6b76f8..c9d869c5 100644 --- a/opentaxii/persistence/api.py +++ b/opentaxii/persistence/api.py @@ -36,12 +36,15 @@ def create_service(self, service_entity): raise NotImplementedError() def update_service(self, obj): + """Update service. To implement in subclass""" raise NotImplementedError() def delete_service(self, service_id): + """Delete service. To implement in subclass""" raise NotImplementedError() def set_collection_services(self, collection_id, service_ids): + """Set collection's services. To implement in subclass""" raise NotImplementedError() def create_collection(self, collection_entity): diff --git a/opentaxii/persistence/sqldb/api.py b/opentaxii/persistence/sqldb/api.py index dd097dbf..afefac33 100644 --- a/opentaxii/persistence/sqldb/api.py +++ b/opentaxii/persistence/sqldb/api.py @@ -652,6 +652,7 @@ def job_cleanup(self) -> int: return taxii2models.Job.cleanup(self.db.session) def get_collections(self, api_root_id: uuid.UUID) -> List[entities.Collection]: + """Get a list of collections from the database""" query = ( self.db.session.query(taxii2models.Collection) .filter(taxii2models.Collection.api_root_id == api_root_id) @@ -673,6 +674,7 @@ def get_collections(self, api_root_id: uuid.UUID) -> List[entities.Collection]: def get_collection( self, api_root_id: uuid.UUID, collection_id_or_alias: str ) -> Optional[entities.Collection]: + """Get a collection from the database""" id_or_alias_filter = taxii2models.Collection.alias == collection_id_or_alias try: uuid.UUID(collection_id_or_alias) From f4bfb48e8292f28f4f3f9257295e00ed41b5b2e4 Mon Sep 17 00:00:00 2001 From: Eric Masseran Date: Wed, 3 Dec 2025 16:21:51 +0100 Subject: [PATCH 19/24] Fix pypy installation --- requirements-dev.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index bc50cb39..a2ad5e9b 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,7 +7,8 @@ ipdb factory-boy>=3.2.1 # Pypy is incompatible with latest mypy version # https://github.com/python/mypy/issues/20329 -mypy==1.19.0; platform_python_implementation != 'PyPy' +mypy==1.19.0;platform_python_implementation != 'PyPy' +mypy<=1.19.0;platform_python_implementation == 'PyPy' types-six==1.17.0.20251009 sqlalchemy-stubs==0.4 types-PyYAML==6.0.12.20250915 From ec1a03f4df0251c44f43c3c1566f5e25f7adf835 Mon Sep 17 00:00:00 2001 From: Eric Masseran Date: Wed, 3 Dec 2025 16:22:03 +0100 Subject: [PATCH 20/24] Force sqlalchemy 1.4 at least --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7cdd2555..391a2b83 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ libtaxii>=1.1.111 lxml>=4.3.1 pyyaml>=3.11 flask>=0.10.1 -sqlalchemy>=1.1.2 +sqlalchemy>=1.4 structlog>=18.1.0 blinker>=1.4 pyjwt>=1.4.0 From d7274dd8f055d994bcc163fca6b455c305929f4a Mon Sep 17 00:00:00 2001 From: Eric Masseran Date: Wed, 3 Dec 2025 16:37:28 +0100 Subject: [PATCH 21/24] Better tox config to install deps --- requirements-dev.txt | 10 ++-------- requirements-dev-mysql.txt => requirements-mysql.txt | 0 ...ostgres-pypy.txt => requirements-postgres-pypy.txt | 0 ...ents-dev-postgres.txt => requirements-postgres.txt | 0 requirements-test.txt | 4 ++++ tox.ini | 11 +++++++---- 6 files changed, 13 insertions(+), 12 deletions(-) rename requirements-dev-mysql.txt => requirements-mysql.txt (100%) rename requirements-dev-postgres-pypy.txt => requirements-postgres-pypy.txt (100%) rename requirements-dev-postgres.txt => requirements-postgres.txt (100%) create mode 100644 requirements-test.txt diff --git a/requirements-dev.txt b/requirements-dev.txt index a2ad5e9b..85b70c78 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,14 +1,8 @@ -r requirements.txt -pytest-cov -pytest>=4.6 -pytest-pythonpath +-r requirements-test.txt flake8 ipdb -factory-boy>=3.2.1 -# Pypy is incompatible with latest mypy version -# https://github.com/python/mypy/issues/20329 -mypy==1.19.0;platform_python_implementation != 'PyPy' -mypy<=1.19.0;platform_python_implementation == 'PyPy' +mypy==1.19.0 types-six==1.17.0.20251009 sqlalchemy-stubs==0.4 types-PyYAML==6.0.12.20250915 diff --git a/requirements-dev-mysql.txt b/requirements-mysql.txt similarity index 100% rename from requirements-dev-mysql.txt rename to requirements-mysql.txt diff --git a/requirements-dev-postgres-pypy.txt b/requirements-postgres-pypy.txt similarity index 100% rename from requirements-dev-postgres-pypy.txt rename to requirements-postgres-pypy.txt diff --git a/requirements-dev-postgres.txt b/requirements-postgres.txt similarity index 100% rename from requirements-dev-postgres.txt rename to requirements-postgres.txt diff --git a/requirements-test.txt b/requirements-test.txt new file mode 100644 index 00000000..3c94f223 --- /dev/null +++ b/requirements-test.txt @@ -0,0 +1,4 @@ +pytest-cov +pytest>=4.6 +pytest-pythonpath +factory-boy>=3.2.1 diff --git a/tox.ini b/tox.ini index 058bbe73..33263334 100644 --- a/tox.ini +++ b/tox.ini @@ -12,10 +12,13 @@ python = commands = py.test --cov {envsitepackagesdir}/opentaxii {posargs} deps = - sqlite: -rrequirements-dev.txt - mysql,mariadb: -rrequirements-dev-mysql.txt - postgres-!pypy3: -rrequirements-dev-postgres.txt - postgres-pypy3: -rrequirements-dev-postgres-pypy.txt + !pypy3: -rrequirements-dev.txt + # does not support recent mypy + # https://github.com/python/mypy/issues/20329 + pypy3: -rrequirements-test.txt + mysql,mariadb: -rrequirements-mysql.txt + postgres-!pypy3: -rrequirements-postgres.txt + postgres-pypy3: -rrequirements-postgres-pypy.txt sqlalchemy14: sqlalchemy>=1.4,<1.5 werkzeuglt21: werkzeug<2.1 werkzeuggte21: werkzeug>=2.1 From becfa7acb7f1d4b49907868d1e10444fef781e98 Mon Sep 17 00:00:00 2001 From: Eric Masseran Date: Wed, 3 Dec 2025 17:08:26 +0100 Subject: [PATCH 22/24] Clean requirements --- requirements-mysql.txt | 1 - requirements-postgres-pypy.txt | 1 - requirements-postgres.txt | 1 - 3 files changed, 3 deletions(-) diff --git a/requirements-mysql.txt b/requirements-mysql.txt index ae2018da..c9dc3f58 100644 --- a/requirements-mysql.txt +++ b/requirements-mysql.txt @@ -1,2 +1 @@ --r requirements-dev.txt mysqlclient>=2.0.3 diff --git a/requirements-postgres-pypy.txt b/requirements-postgres-pypy.txt index 58317d38..102e1160 100644 --- a/requirements-postgres-pypy.txt +++ b/requirements-postgres-pypy.txt @@ -1,2 +1 @@ --r requirements-dev.txt psycopg2cffi>=2.9.0 diff --git a/requirements-postgres.txt b/requirements-postgres.txt index 452418fc..1197662d 100644 --- a/requirements-postgres.txt +++ b/requirements-postgres.txt @@ -1,2 +1 @@ --r requirements-dev.txt psycopg2-binary>=2.9.1 From 80e6f1a3c813db66febe017a0c7abf9ab4ec9c0f Mon Sep 17 00:00:00 2001 From: Eric Masseran Date: Wed, 3 Dec 2025 17:27:55 +0100 Subject: [PATCH 23/24] Install base dep for pypy --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 33263334..49c718da 100644 --- a/tox.ini +++ b/tox.ini @@ -16,6 +16,7 @@ deps = # does not support recent mypy # https://github.com/python/mypy/issues/20329 pypy3: -rrequirements-test.txt + pypy3: -rrequirements.txt mysql,mariadb: -rrequirements-mysql.txt postgres-!pypy3: -rrequirements-postgres.txt postgres-pypy3: -rrequirements-postgres-pypy.txt From 7e74b487c22b9ca3d4cad0c9d9809f4fc0184940 Mon Sep 17 00:00:00 2001 From: Eric Masseran Date: Mon, 8 Dec 2025 11:45:04 +0100 Subject: [PATCH 24/24] Fix merging --- tests/test_auth.py | 1 - tests/utils.py | 1 - 2 files changed, 2 deletions(-) diff --git a/tests/test_auth.py b/tests/test_auth.py index 3a4830a6..81a4bfb8 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -2,7 +2,6 @@ import json import pytest -from fixtures import VID_TAXII_HTTP_10 from libtaxii import messages_10 as tm10 from libtaxii import messages_11 as tm11 from libtaxii.constants import CB_STIX_XML_111, RT_FULL, ST_BAD_MESSAGE, ST_UNAUTHORIZED diff --git a/tests/utils.py b/tests/utils.py index d7e74603..da3e5dc8 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,7 +1,6 @@ import re import pytest -from fixtures import CB_STIX_XML_111, CONTENT, MESSAGE, MESSAGE_ID from libtaxii import messages_10 as tm10 from libtaxii import messages_11 as tm11