From bf721e99007b89f440771b836599661b38c46655 Mon Sep 17 00:00:00 2001 From: peritz Date: Thu, 16 Jan 2025 17:56:48 +0000 Subject: [PATCH 01/27] Add Role and Capability entity --- pycti/api/opencti_api_client.py | 28 ++ pycti/entities/opencti_capability.py | 44 ++ pycti/entities/opencti_role.py | 419 ++++++++++++++++++ .../entities/test_entity_crud.py | 24 +- tests/cases/entities.py | 30 ++ 5 files changed, 543 insertions(+), 2 deletions(-) create mode 100644 pycti/entities/opencti_capability.py create mode 100644 pycti/entities/opencti_role.py diff --git a/pycti/api/opencti_api_client.py b/pycti/api/opencti_api_client.py index a0c102b8a..814cc9033 100644 --- a/pycti/api/opencti_api_client.py +++ b/pycti/api/opencti_api_client.py @@ -61,6 +61,8 @@ from pycti.entities.opencti_tool import Tool from pycti.entities.opencti_vocabulary import Vocabulary from pycti.entities.opencti_vulnerability import Vulnerability +from pycti.entities.opencti_capability import Capability +from pycti.entities.opencti_role import Role from pycti.utils.opencti_logger import logger from pycti.utils.opencti_stix2 import OpenCTIStix2 from pycti.utils.opencti_stix2_utils import OpenCTIStix2Utils @@ -128,6 +130,7 @@ def __init__( # Configure logger self.logger_class = logger(log_level.upper(), json_logging) self.app_logger = self.logger_class("api") + self.admin_logger = self.logger_class("admin") # Define API self.api_token = token @@ -198,6 +201,10 @@ def __init__( self.grouping = Grouping(self) self.indicator = Indicator(self) + # Admin functionality + self.capability = Capability(self) + self.role = Role(self) + # Check if openCTI is available if perform_health_check and not self.health_check(): raise ValueError( @@ -623,6 +630,27 @@ def process_multiple_fields(self, data): if "importFiles" in data: data["importFiles"] = self.process_multiple(data["importFiles"]) data["importFilesIds"] = self.process_multiple_ids(data["importFiles"]) + + # Administrative data + if "groups" in data: + data["groups"] = self.process_multiple(data["group"]) + data["groupsIds"] = self.process_multiple_ids(data["groups"]) + if "objectOrganization" in data: + data["objectOrganization"] = self.process_multiple( + data["objectOrganization"] + ) + data["objectOrganizationIds"] = self.process_multiple_ids( + data["objectOrganization"] + ) + if "roles" in data: + data["roles"] = self.process_multiple(data["roles"]) + data["rolesIds"] = self.process_multiple_ids(data["roles"]) + if "capabilities" in data: + data["capabilities"] = self.process_multiple(data["capabilities"]) + data["capabilitiesIds"] = self.process_multiple_ids( + data["capabilities"] + ) + # See aliases of GraphQL query in stix_core_object method if "name_alt" in data: data["name"] = data["name_alt"] diff --git a/pycti/entities/opencti_capability.py b/pycti/entities/opencti_capability.py new file mode 100644 index 000000000..8d027a655 --- /dev/null +++ b/pycti/entities/opencti_capability.py @@ -0,0 +1,44 @@ +from typing import List, Dict + + +class Capability: + """Represents a role capability on the OpenCTI platform + + The dictionary representation of a Capability has the following form:: + + { + "id": "UUID", + "name": "Name of the capability, e.g. KNUPDATE", + "description": "Create/Update knowledge" + }. + """ + + def __init__(self, opencti): + self.opencti = opencti + self.properties = """ + id + entity_type + name + description + attribute_order + """ + + def list(self) -> List[Dict]: + self.opencti.logger.info("Listing capabilities") + query = ( + """ + query Capabilities { + capabilities(first: 500) { + edges { + node { + """ + + self.properties + + """ + } + } + } + } + """ + ) + result = self.opencti.query(query) + return self.opencti.process_multiple(result["data"]["capabilities"]) diff --git a/pycti/entities/opencti_role.py b/pycti/entities/opencti_role.py new file mode 100644 index 000000000..54e162122 --- /dev/null +++ b/pycti/entities/opencti_role.py @@ -0,0 +1,419 @@ +from typing import Dict, List + + +class Role: + """Representation of a role in OpenCTI + + Roles can have capabilities. Groups have roles, and the combined + capabilities of those roles determine what a group of users can do on the + platform. + + The dictionary representation of a role might look as below:: + + { + "id": "UUID", + "name": "Name of the role", + "description": "Description of the role", + "created_at": "YYYY-MM-DDThh:mm:ss.000Z", + "updated_at": "YYYY-MM-DDThh:mm:ss.000Z", + "can_manage_sensitive_config": false, + "capabilities": [{ + "id": "UUID", + "name": "Name of the capability", + "description": "Description of the capability" + }] + }. + + Check the properties attribute of the class to understand what default + properties are fetched. + """ + + def __init__(self, opencti): + self.opencti = opencti + self.properties = """ + id + standard_id + entity_type + parent_types + + name + description + + created_at + updated_at + + capabilities { + id + name + description + } + can_manage_sensitive_config + """ + + def list(self, **kwargs) -> List[Dict]: + """Search or list the roles on the server. + + :param search: + Defaults to None. + :type search: str, optional + :param first: Defaults to 500 Return the first x results from ID or + beginning if $after is not specified. + :type first: int, optional + :param after: Return all results after the given ID, useful for + pagination. Ignored if returning all results, defaults to None. + :type after: str, optional + :param orderBy: Field to order by. Must be one of "name", + "created_at", "updated_at", or "_score". Defaults + to "name", defaults to "name". + :type orderBy: str, optional + :param orderMode: Direction to order in, either "asc" or "desc", + defaults to "asc". + :type orderMode: str, optional + :param customAttributes: Defaults to None. Custom attributes to return + from query. If None, defaults are used. + :type customAttributes: str, optional + :param getAll: Defaults to False. Retrieve all results. If true then + the "first" param is ignored. + :type getAll: bool, optional + :param withPagination: Defaults to False Whether to include pagination + pageInfo properties in result. + :type withPagination: bool, optional + + :return: List of Python dictionaries with the properties of the role. + :rtype: List[Dict] + """ + search = kwargs.get("search", None) + first = kwargs.get("first", 500) + after = kwargs.get("after", None) + orderBy = kwargs.get("orderBy", None) + orderMode = kwargs.get("orderMode", None) + customAttributes = kwargs.get("customAttributes", None) + getAll = kwargs.get("getAll", False) + withPagination = kwargs.get("withPagination", False) + + self.opencti.admin_logger.info( + "Searching roles matching search term", {"search": search} + ) + + if getAll: + first = 100 + + query = ( + """ + query Roles($first: Int, $after: ID, $orderBy: RolesOrdering, $orderMode: OrderingMode, $search: String) { + roles(first: $first, after: $after, orderBy: $orderBy, orderMode: $orderMode, search: $search) { + edges { + node { + """ + + (self.properties if customAttributes is None + else customAttributes) + + """ + } + } + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + globalCount + } + } + } + """ + ) + result = self.opencti.query( + query, + { + "first": first, + "after": after, + "orderBy": orderBy, + "orderMode": orderMode, + "search": search + } + ) + if getAll: + final_data = [] + data = self.opencti.process_multiple(result["data"]["roles"]) + final_data = final_data + data + while result["data"]["roles"]["pageInfo"]["hasNextPage"]: + after = result["data"]["roles"]["pageInfo"]["endCursor"] + result = self.opencti.query( + query, + { + "first": first, + "after": after, + "orderBy": orderBy, + "orderMode": orderMode, + "search": search + } + ) + data = self.opencti.process_multiple(result["data"]["roles"]) + final_data = final_data + data + return final_data + else: + return self.opencti.process_multiple(result["data"]["roles"], + withPagination) + + def read(self, **kwargs) -> Dict: + """Get a role given its ID or a search term + + One of id or search must be provided. + + :param id: ID of the role on the platform + :type id: str, optional + :param search: Search term for a role, e.g. its name + :type search: str, optional + :param customAttributes: Custom attributes on the role to return + :type customAttributes: str, optional + + :return: Representation of the role + :rtype: Dict + """ + id = kwargs.get("id", None) + search = kwargs.get("search", None) + customAttributes = kwargs.get("customAttributes", None) + + if id is not None: + self.opencti.admin_logger.info("Reading role", {"id": id}) + query = ( + """ + query Role($id: String!) { + role(id: $id) { + """ + + (self.properties if customAttributes is None + else customAttributes) + + """ + } + } + """ + ) + result = self.opencti.query(query, {"id": id}) + return self.opencti.process_multiple_fields(result["data"]["role"]) + elif search is not None: + result = self.list(search=search) + return result[0] if len(result) > 0 else None + else: + self.opencti.admin_logger.error( + "[opencti_role] Missing parameters: id or search" + ) + return None + + def delete(self, **kwargs): + """Delete a role given its ID + + :param role: ID for the role on the platform. + :type role: str + """ + id = kwargs.get("id", None) + + if id is None: + self.opencti.admin_logger.error( + "[opencti_role] Missing parameter: id" + ) + return None + + self.opencti.admin_logger.info("Deleting role", {"id": id}) + query = """ + mutation RoleDelete($id: ID!) { + roleEdit(id: $id) { + delete + } + } + """ + self.opencti.query(query, {"id": id}) + + def create(self, **kwargs) -> Dict: + """Add a new role to OpenCTI. + + :param name: Name to assign to the role. + :type name: str + :param description: Optional. Description of the role, defaults to + None. + :type description: str, optional + :param customAttributes: Custom attributes to return on role + :type customAttributes: str, optional + :return: Representation of the role. + :rtype: Dict + """ + name = kwargs.get("name", None) + description = kwargs.get("description", None) + customAttributes = kwargs.get("customAttributes", None) + + if name is None: + self.opencti.admin_logger.error( + "[opencti_role] Missing parameter: name" + ) + return None + + self.opencti.admin_logger.info("Creating new role", { + "name": name, "description": description + }) + query = ( + """ + mutation RoleAdd($input: RoleAddInput!) { + roleAdd(input: $input) { + """ + + (self.properties if customAttributes is None + else customAttributes) + + """ + } + } + """ + ) + result = self.opencti.query(query, { + "input": { + "name": name, + "description": description + } + }) + return self.opencti.process_multiple_fields(result["data"]["roleAdd"]) + + def update_field(self, **kwargs) -> Dict: + """Updates a given role with the given inputs + + Example of input:: + + [ + { + "key": "name", + "value": "NewCustomRole" + }, + { + "key": "can_manage_sensitive_config", + "value": False + } + ] + + :param id: ID for the role on the platform + :type id: str + :param input: List of EditInput objects + :type input: List[Dict] + :param customAttributes: Custom attributes to return on the role + :type customAttributes: str, optional + + :return: Representation of the role + :rtype: Dict + """ + id = kwargs.get("id", None) + input = kwargs.get("input", None) + customAttributes = kwargs.get("customAttributes", None) + + if id is None or input is None: + self.opencti.admin_logger.error( + "[opencti_role] Missing parameters: id and input") + return None + + self.opencti.admin_logger.info("Editing role with input", { + "id": id, "input": input + }) + query = ( + """ + mutation RoleFieldPatch($id: ID!, $input: [EditInput]!) { + roleEdit(id: $id) { + fieldPatch(input: $input) { + """ + + (self.properties if customAttributes is None + else customAttributes) + + """ + } + } + } + """ + ) + result = self.opencti.query(query, {"id": id, "input": input}) + return self.opencti.process_multiple_fields( + result["data"]["roleEdit"]["fieldPatch"]) + + def add_capability(self, **kwargs) -> Dict: + """Adds a capability to a role + + :param id: ID of the role. + :type id: str + :param capability_id: ID of the capability to add. + :type capability_id: str + :return: Representation of the relationship, including the role and + capability + :rtype: Dict + """ + id = kwargs.get("id", None) + capability_id = kwargs.get("capability_id", None) + + if id is None or capability_id is None: + self.opencti.admin_logger( + "[opencti_role] Missing parameters: id and capability_id") + return None + + self.opencti.admin_logger.info("Adding capability to role", { + "roleId": id, "capabilityId": capability_id + }) + query = ( + """ + mutation RoleAddCapability($id: ID!, input: InternalRelationshipAddInput!) { + roleEdit(id: $id) { + relationAdd(input: $input) { + id + standard_id + entity_type + parent_types + created_at + updated_at + + from { + """ + + self.properties + + """ + } + + to { + id, name, description + } + } + } + } + """ + ) + result = self.opencti.query(query, {"id": id, "input": { + "relationship_type": "has-capability", + "toId": capability_id + }}) + return self.opencti.process_multiple_fields( + result["data"]["roleEdit"]["relationAdd"]) + + def delete_capability(self, **kwargs) -> Dict: + """Removes a capability from a role + + :param id: ID of the role + :type id: str + :param capability_id: ID of the capability to remove + :type capability_id: str + :return: Representation of the role after removing the capability + :rtype: Dict + """ + id = kwargs.get("id", None) + capability_id = kwargs.get("capability_id", None) + + if id is None or capability_id is None: + self.opencti.admin_logger.error( + "[opencti_role] Missing parameters: id and capability_id") + return None + + self.opencti.admin_logger.info("Removing capability from role", { + "roleId": id, "capabilityId": capability_id + }) + query = ( + """ + mutation RoleDelCapability($id: ID!, toId: ID!) { + roleEdit(id: $id) { + relationDelete(fromId: $id, toId: $toId, relationship_type: "has-capability") { + """ + + self.properties + + """ + } + } + } + """ + ) + result = self.opencti.query(query, {"id": id, "toId": capability_id}) + return self.opencti.process_multiple_fields( + result["data"]["roleEdit"]["relationDelete"] + ) diff --git a/tests/02-integration/entities/test_entity_crud.py b/tests/02-integration/entities/test_entity_crud.py index 716942b38..3482c53ab 100644 --- a/tests/02-integration/entities/test_entity_crud.py +++ b/tests/02-integration/entities/test_entity_crud.py @@ -44,7 +44,8 @@ def test_update(entity_class): assert "id" in test_indicator, "No ID on object" if len(entity_class.update_data()) > 0: - function_present = getattr(entity_class.own_class(), "update_field", None) + function_present = getattr( + entity_class.own_class(), "update_field", None) if function_present: for update_field, update_value in entity_class.update_data().items(): class_data[update_field] = update_value @@ -60,7 +61,8 @@ def test_update(entity_class): result = entity_class.own_class().read(id=result["id"]) assert result["id"] == test_indicator["id"], "Updated SDO does not match old ID" - compare_values(class_data, result, entity_class.get_compare_exception_keys()) + compare_values(class_data, result, + entity_class.get_compare_exception_keys()) else: result = test_indicator @@ -96,6 +98,24 @@ def test_filter(entity_class): entity_class.base_class().delete(id=test_indicator["id"]) +def test_search(entity_class): + if not entity_class.get_search(): + return + + class_data = entity_class.data() + test_indicator = entity_class.own_class().create(**class_data) + assert test_indicator is not None, "Response is NoneType" + assert "id" in test_indicator, "No ID on object" + test_indicator = entity_class.own_class().read(search=entity_class.get_search()) + compare_values( + class_data, + test_indicator, + entity_class.get_compare_exception_keys(), + ) + + entity_class.base_class().delete(id=test_indicator["id"]) + + def test_relation(entity_class): if not entity_class.relation_test(): return diff --git a/tests/cases/entities.py b/tests/cases/entities.py index a45e9c7b6..67fff869a 100644 --- a/tests/cases/entities.py +++ b/tests/cases/entities.py @@ -171,6 +171,10 @@ def case_case_rft(api_client): def task(api_client): return TaskTest(api_client) + @staticmethod + def case_role(api_client): + return RoleTest(api_client) + class EntityTest: def __init__(self, api_client): @@ -212,6 +216,9 @@ def get_filter(self) -> Dict[str, str]: "filterGroups": [], } + def get_search(self) -> str: + return None + def stix_class(self): pass @@ -1151,3 +1158,26 @@ def data(self) -> Dict: def own_class(self): return self.api_client.threat_actor_individual + + +class RoleTest(EntityTest): + def data(self) -> Dict: + return { + "name": "TestRole", + "description": "This is a role for testing", + } + + def own_class(self): + return self.api_client.role + + def base_class(self): + return self.own_class() + + def update_data(self) -> Dict[str, bool]: + return {"can_manage_sensitive_config": True} + + def get_filter(self) -> Dict[str, str]: + return {} + + def get_search(self) -> str: + return "TestRole" From 2ba0aa432a990241dde13046d1ce939531b456a6 Mon Sep 17 00:00:00 2001 From: peritz Date: Mon, 20 Jan 2025 15:49:07 +0000 Subject: [PATCH 02/27] Add Group entity to the library --- pycti/api/opencti_api_client.py | 5 +- pycti/entities/opencti_group.py | 703 ++++++++++++++++++ .../entities/test_entity_crud.py | 2 +- tests/02-integration/entities/test_group.py | 79 ++ tests/cases/entities.py | 37 + 5 files changed, 824 insertions(+), 2 deletions(-) create mode 100644 pycti/entities/opencti_group.py create mode 100644 tests/02-integration/entities/test_group.py diff --git a/pycti/api/opencti_api_client.py b/pycti/api/opencti_api_client.py index 814cc9033..6de578be7 100644 --- a/pycti/api/opencti_api_client.py +++ b/pycti/api/opencti_api_client.py @@ -63,6 +63,7 @@ from pycti.entities.opencti_vulnerability import Vulnerability from pycti.entities.opencti_capability import Capability from pycti.entities.opencti_role import Role +from pycti.entities.opencti_group import Group from pycti.utils.opencti_logger import logger from pycti.utils.opencti_stix2 import OpenCTIStix2 from pycti.utils.opencti_stix2_utils import OpenCTIStix2Utils @@ -204,6 +205,7 @@ def __init__( # Admin functionality self.capability = Capability(self) self.role = Role(self) + self.group = Group(self) # Check if openCTI is available if perform_health_check and not self.health_check(): @@ -722,7 +724,8 @@ def create_draft(self, **kwargs): ) return queryResult["data"]["draftWorkspaceAdd"]["id"] else: - self.app_logger.error("[create_draft] Missing parameter: draft_name") + self.app_logger.error( + "[create_draft] Missing parameter: draft_name") return None def upload_pending_file(self, **kwargs): diff --git a/pycti/entities/opencti_group.py b/pycti/entities/opencti_group.py new file mode 100644 index 000000000..c3d15d768 --- /dev/null +++ b/pycti/entities/opencti_group.py @@ -0,0 +1,703 @@ +from typing import List, Dict + + +class Group: + """Representation of a Group in OpenCTI + + Groups have members and also have assigned roles. Roles attached to a group + determine what members of the group have permissions to do according to + the capabilities the role has. + + Additionally, groups have a confidence level which informs the effective + confidence of members of the group. + + Groups also have permissions on Marking Definitions. Assigned marking + definitions allow users to apply their capabilities on objects with those + definitions. Additionally, there are default markings added to all objects + created by members of a group, and max shareable definitions which + determine which objects users can export from the platform to share. + + Representation of a group in Python looks like:: + + { + "id": "UUID", + "name": "Group name", + "description": "Group description", + "created_at": "ISO 8901 datetime", + "updated_at": "ISO 8901 datetime", + "default_assignation": False, + "no_creators": True, + "restrict_delete": True, + "default_hidden_types": ["STIX type"], + "auto_new_marking": False, + "allowed_marking": [{ + "id": "UUID", + "standard_id": "marking-definition--UUID", + "definition_type": "TLP", + "definition": "TLP:GREEN" + }], + "default_marking": [{ + "entity_type": "STIX type", + "values": [{ + "id": "UUID", + "standard_id": "marking-definition--UUID", + "definition_type": "TLP", + "deinition": "TLP:GREEN" + }] + }], + not_shareable_marking_types: [ + "PAP" + ], + max_shareable_marking: [{ + "id": "UUID", + "standard_id": "marking-definition--UUID", + "definition_type": "TLP", + "definition": "TLP:GREEN" + }], + group_confidence_level: { + "max_confidence": 90, + "overrides": [{ + "entity_type": "STIX type", + "max_confidence": 80 + }] + }, + "roles": { + "edges": [{ + "node": { + "id": "UUID", + "name": "Role name", + "capabilities": [{ + "id": "UUID", + "name": "Capability name" + }] + } + }] + }, + "members": { + "edges": [{ + "node": { + "id": "UUID", + "individual_id": "UUID", + "user_email": "email address", + "name": "Username" + } + }] + } + }. + """ + + def __init__(self, opencti): + self.opencti = opencti + self.properties = """ + id + standard_id + name + description + + created_at + updated_at + + default_assignation + no_creators + restrict_delete + default_hidden_types + + auto_new_marking + + allowed_marking { + id, standard_id, definition_type, definition + } + + default_marking { + entity_type + values { + id, standard_id, definition_type, definition + } + } + + not_shareable_marking_types + max_shareable_marking { + id, standard_id, definition_type, definition + } + + group_confidence_level { + max_confidence + overrides { + entity_type + max_confidence + } + } + + roles { + edges { + node { + id, name + capabilities { + id, name + } + } + } + } + + members { + edges { + node { + id, individual_id, user_email, name + } + } + } + """ + + def list(self, + first: int = 500, + after: str = None, + orderBy: str = None, + orderMode: str = None, + search: str = None, + filters: dict = None, + customAttributes: str = None, + getAll: bool = False, + withPagination: bool = False) -> List[Dict]: + """Lists groups based on a number of filters. + + :param first: Retrieve this number of results. If 0 + then fetches all results, defaults to 0. + :type first: int, optional + :param after: ID of the group to fetch results + after in the list of all results, defaults to None. + :type after: str, optional + :param orderBy: Field by which to order results. + Must be one of name, default_assignation, no_creators, + restrict_delete, auto_new_marking, created_at, updated_at, + group_confidence_level, and _score, defaults to "name". + :type orderBy: str, optional + :param orderMode: Direction of ordering. Must be + one of "asc" or "desc", defaults to "asc". + :type orderMode: str, optional + :param search: String to search groups for, defaults to None. + :type search: str, optional + :param filters: OpenCTI API FilterGroup object. + This is an advanced parameter. To learn more please search for + the FilterGroup object in the OpenCTI GraphQL Playground, defaults + to {}. + :type filters: dict, optional + :param customAttributes: Custom attributes to fetch from the GraphQL + query + :type customAttributes: str, optional + :param getAll: Defaults to False. Whether or not to get all results + from the search. If True then param first is ignored. + :type getAll: bool, optional + :param withPagination: Defaults to False. Whether to return pagination + info with results. + :type withPagination: bool, optional + :return: List of groups in dictionary representation. + :rtype: list[dict] + """ + if getAll: + first = 100 + + self.opencti.admin_logger.info("Fetching groups with filters", + {"filters": filters}) + query = ( + """ + query Groups($first: Int, $after: ID, $orderBy: GroupsOrdering, $orderMode: OrderingMode, $search: String, $filters: FilterGroup) { + groups(first: $first, after: $after, orderBy: $orderBy, orderMode: $orderMode, search: $search, filters: $filters) { + edges { + node { + """ + + (self.properties if customAttributes is None + else customAttributes) + + """ + } + } + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + globalCount + } + } + } + """ + ) + result = self.opencti.query(query, { + "first": first, + "after": after, + "orderBy": orderBy, + "orderMode": orderMode, + "search": search, + "filters": filters + }) + + if getAll: + final_data = [] + data = self.opencti.process_multiple(result["data"]["groups"]) + final_data = final_data + data + while result["data"]["groups"]["pageInfo"]["hasNextPage"]: + after = result["data"]["groups"]["pageInfo"]["endCursor"] + result = self.opencti.query(query, { + "first": first, + "after": after, + "orderBy": orderBy, + "orderMode": orderMode, + "search": search, + "filters": filters + }) + data = self.opencti.process_multiple(result["data"]["groups"]) + final_data = final_data + data + return final_data + else: + return self.opencti.process_multiple(result["data"]["groups"], + withPagination) + + def read(self, + id: str = None, + filters: dict = None, + search: str = None, + customAttributes: str = None) -> dict: + """Fetch a given group from OpenCTI + + :param id: ID of the group to fetch + :type id: str, optional + :param filters: Filters to apply to find single group + :type filters: dict, optional + :param customAttributes: Custom attributes to fetch for the group + :type customAttributes: str + :return: Representation of a group. + :rtype: dict + """ + if id is not None: + self.opencti.admin_logger.info( + "Fetching group with ID", {"id": id}) + query = ( + """ + query Group($id: String!) { + group(id: $id) { + """ + + (self.properties if customAttributes is None + else customAttributes) + + """ + } + } + """ + ) + result = self.opencti.query(query, {"id": id}) + return self.opencti.process_multiple_fields( + result["data"]["group"]) + elif filters is not None or search is not None: + results = self.list(filters=filters, search=search) + return results[0] if results else None + else: + self.opencti.admin_logger.error( + "[opencti_group] Missing parameters: id or filters") + return None + + def create(self, + name: str, + group_confidence_level: dict, + description: str = None, + default_assignation: bool = False, + no_creators: bool = False, + restrict_delete: bool = False, + auto_new_marking: bool = False, + customAttributes: str = None) -> dict: + """Create a group with required details + + Groups can be configured after creation using other functions. + + :param name: Name of the group to create. + :type name: str + :param id_confidence_level: Confidence-level dictionary, with a + max_confidence member between 0 and 100 (incl) and an overrides + list with max_confidence and the entity_type it applies to. + :type id_confidence_level: dict + :param description: Description of the group + :type description: str, optional + :param default_assignation: Defaults to False. Whether or not to assign + this group by default to all new users. + :type default_assignation: bool, optional + :param no_creators: Defaults to False. Whether or not to create + authors for members of this group. + :type no_creators: bool, optional + :param restrict_delete: Defaults to False. Whether or not to restrict + members deleting entities that are not their own. + :type restrict_delete: bool, optional + :param auto_new_marking: Defaults to False. Whether or not to allow + members access to new markings automatically. + :type auto_new_marking: bool, optional + :param customAttributes: Attributes to retrieve from the new group + :type customAttributes: str, optional + :return: Representation of the group. + :rtype: dict + """ + self.opencti.admin_logger.info( + "Creating new group with parameters", { + "name": name, + "group_confidence_level": group_confidence_level, + "description": description, + "default_assignation": default_assignation, + "no_creators": no_creators, + "restrict_delete": restrict_delete, + "auto_new_marking": auto_new_marking + } + ) + query = ( + """ + mutation GroupAdd($input: GroupAddInput!) { + groupAdd(input: $input) { + """ + + (self.properties if customAttributes is None + else customAttributes) + + """ + } + } + """ + ) + result = self.opencti.query(query, {"input": { + "name": name, + "description": description, + "default_assignation": default_assignation, + "no_creators": no_creators, + "restrict_delete": restrict_delete, + "auto_new_marking": auto_new_marking, + "group_confidence_level": group_confidence_level + }}) + return self.opencti.process_multiple_fields( + result["data"]["groupAdd"]) + + def delete(self, id: str): + """Delete a given group from OpenCTI + + :param id: ID of the group to delete. + :type id: str + """ + self.opencti.admin_logger.info("Deleting group", {"id": id}) + query = """ + mutation GroupDelete($id: ID!) { + groupEdit(id: $id) { + delete + } + } + """ + self.opencti.query(query, {"id": id}) + + def update_field(self, + id: str, + input: List[Dict], + customAttributes: str = None) -> Dict: + """Update a group using fieldPatch + + :param id: ID of the group to update + :type id: str + :param input: FieldPatchInput object to edit group + :type input: List[Dict] + :param customAttributes: Custom attributes to retrieve from group + :type customAttribues: str, optional + :return: Representation of a group + :rtype: dict + """ + self.opencti.admin_logger.info("Editing group with input", { + "input": input}) + query = ( + """ + mutation GroupEdit($id: ID!, $input:[EditInput]!) { + groupEdit(id: $id) { + fieldPatch(input: $input) { + """ + + (self.properties if customAttributes is None + else customAttributes) + + """ + } + } + } + """ + ) + result = self.opencti.query(query, {"id": id, "input": input}) + return self.opencti.process_multiple_fields( + result["data"]["groupEdit"]["fieldPatch"]) + + def add_member(self, + id: str, + user_id: str) -> dict: + """Add a member to a given group. + + :param id: ID of the group to add a member to + :type id: str + :param user_id: ID to add to the group + :type user_id: str + :return: Representation of the relationship + :rtype: dict + """ + self.opencti.admin_logger.info( + "Adding member to group", {"groupId": id, "userId": user_id} + ) + query = ( + """ + mutation MemberAdd($groupId: ID!, $userId: ID!) { + groupEdit(id: $groupId) { + relationAdd(input: { + fromId: $userId, + relationship_type: "member-of" + }) { + id, standard_id, entity_type, created_at, updated_at + from { + id, entity_type + } + + to { + id, entity_type + } + } + } + } + """ + ) + result = self.opencti.query(query, {"groupId": id, "userId": user_id}) + return self.opencti.process_multiple_fields( + result["data"]["groupEdit"]["relationAdd"]) + + def delete_member(self, + id: str, + user_id: str) -> dict: + """Remove a given user from a group + + :param id: ID to remove a user from + :type id: str + :param user: ID to remove from the group + :type user: str + :return: Representation of the group after the member has been removed + :rtype: dict + """ + self.opencti.admin_logger.info( + "Removing member from group", {"groupId": id, "userId": user_id} + ) + query = ( + """ + mutation MemberDelete ($groupId: ID!, $userId: StixRef!) { + groupEdit(id: $groupId) { + relationDelete(fromId: $userId, relationship_type: "member-of") { + """ + + self.properties + + """ + } + } + } + """ + ) + result = self.opencti.query(query, {"groupId": id, "userId": user_id}) + return self.opencti.process_multiple_fields( + result["data"]["groupEdit"]["relationDelete"]) + + def add_role(self, + id: str, + role_id: str) -> dict: + """Add a role to a given group + + :param id: ID to add a role to + :type id: str + :param role_id: Role ID to add to the group + :type role: str + :return: Representation of the group after a role has been added + :rtype: dict + """ + self.opencti.admin_logger.info( + "Adding role to group", {"groupId": id, "roleId": role_id}) + query = ( + """ + mutation RoleAdd($groupId: ID!, $roleId: ID!) { + groupEdit(id: $groupId) { + relationAdd(input: { + toId: $roleId, relationship_type: "has-role" + }) { + id, standard_id, entity_type, created_at, updated_at + from { + id, entity_type + } + + to { + id, entity_type + } + } + } + } + """ + ) + result = self.opencti.query(query, {"groupId": id, "roleId": role_id}) + return self.opencti.process_multiple_fields( + result["data"]["groupEdit"]["relationAdd"]) + + def delete_role(self, + id: str, + role_id: str) -> dict: + """Removes a role from a given group + + :param id: ID to remove role from + :type id: str + :param role_id: Role ID to remove from the group + :type role_id: str + :return: Representation of the group after role is removed + :rtype: dict + """ + self.opencti.admin_logger.info( + "Removing role from group", {"groupId": id, "roleId": role_id}) + query = ( + """ + mutation RoleDelete($groupId: ID!, $roleId: StixRef!) { + groupEdit(id: $groupId) { + relationDelete(toId: $roleId, relationship_type: "has-role") { + """ + + self.properties + + """ + } + } + } + """ + ) + result = self.opencti.query(query, {"groupId": id, "roleId": role_id}) + return self.opencti.process_multiple_fields( + result["data"]["groupEdit"]["relationDelete"]) + + def edit_default_marking(self, + id: str, + marking_ids: List[str], + entity_type: str = "GLOBAL") -> dict: + """Adds a default marking to the group. + + :param id: ID of the group. + :type id: str + :param marking_ids: IDs of the markings to add, or an empty list to + remove all default markings + :type marking_ids: List[str] + :param entity: STIX entity type to add default + marking for. If set to "GLOBAL" applies to all entity types, + defaults to "GLOBAL". + :type entity: str, optional + :return: Group after adding the default marking. + :rtype: dict + """ + self.opencti.admin_logger.info( + "Setting default markings for entity on group", { + "markings": marking_ids, + "entity_type": entity_type, + "groupId": id + } + ) + query = ( + """ + mutation EditDefaultMarking($id: ID!, $entity_type: String!, $values: [String!]) { + groupEdit(id: $id) { + editDefaultMarking(input: { + entity_type: $entity_type, + values: $values + }) { + """ + + self.properties + + """ + } + } + } + """ + ) + result = self.opencti.query(query, { + "id": id, + "entity_type": entity_type, + "values": marking_ids + }) + return self.opencti.process_multiple_fields( + result["data"]["groupEdit"]["editDefaultMarking"]) + + def add_allowed_marking(self, + id: str, + marking_id: str) -> dict: + """Allow a group to access a marking + + :param id: ID of group to authorise + :type id: str + :param marking: ID of marking to authorise + :type marking: str + :return: Relationship from the group to the marking definition + :rtype: dict + """ + self.opencti.admin_logger.info( + "Granting group access to marking definition", { + "groupId": id, "markingId": marking_id + }) + query = """ + mutation GroupEditionMarkingsMarkingDefinitionsRelationAddMutation( + $id: ID! + $input: InternalRelationshipAddInput! + ) { + groupEdit(id: $id) { + relationAdd(input: $input) { + from { + __typename + ...GroupEditionMarkings_group + id + } + id + } + } + } + + fragment GroupEditionMarkings_group on Group { + id + default_assignation + allowed_marking { + id + } + not_shareable_marking_types + max_shareable_marking { + id + definition + definition_type + x_opencti_order + } + default_marking { + entity_type + values { + id + } + } + } + """ + result = self.opencti.query(query, {"id": id, "input": { + "relationship_type": "accesses-to", + "toId": marking_id + }}) + return self.opencti.process_multiple_fields( + result["data"]["groupEdit"]["relationAdd"]) + + def delete_allowed_marking(self, + id: str, + marking_id: str) -> dict: + """Removes access to a marking for a group + + :param id: ID of group to forbid + :type id: str + :param marking: ID of marking to deny + :type marking: str + :return: Group after denying access to marking definition + :rtype: dict + """ + self.opencti.admin_logger.info( + "Forbidding group access to marking definition", { + "groupId": id, "markingId": marking_id + }) + query = ( + """ + mutation MarkingForbid($groupId: ID!, $markingId: StixRef!) { + groupEdit(id: $groupId) { + relationDelete(toId: $markingId, relationship_type: "accesses-to") { + """ + + self.properties + + """ + } + } + } + """ + ) + result = self.opencti.query( + query, {"groupId": id, "markingId": marking_id}) + return self.opencti.process_multiple_fields( + result["data"]["groupEdit"]["relationDelete"]) diff --git a/tests/02-integration/entities/test_entity_crud.py b/tests/02-integration/entities/test_entity_crud.py index 3482c53ab..66efb03d9 100644 --- a/tests/02-integration/entities/test_entity_crud.py +++ b/tests/02-integration/entities/test_entity_crud.py @@ -49,7 +49,7 @@ def test_update(entity_class): if function_present: for update_field, update_value in entity_class.update_data().items(): class_data[update_field] = update_value - input = [{"key": update_field, "value": str(update_value)}] + input = [{"key": update_field, "value": update_value}] result = entity_class.own_class().update_field( id=test_indicator["id"], input=input ) diff --git a/tests/02-integration/entities/test_group.py b/tests/02-integration/entities/test_group.py new file mode 100644 index 000000000..47cbfe5d4 --- /dev/null +++ b/tests/02-integration/entities/test_group.py @@ -0,0 +1,79 @@ +from tests.cases.entities import GroupTest, RoleTest, MarkingDefinitionTest + + +def test_group_roles(api_client): + group_data = GroupTest(api_client).data() + role_data = RoleTest(api_client).data() + test_group = api_client.group.create(**group_data) + test_role = api_client.role.create(**role_data) + assert test_group is not None, "Create group response is NoneType" + assert test_role is not None, "Create role response is NoneType" + + api_client.group.add_role(id=test_group["id"], role_id=test_role["id"]) + result = api_client.group.read(id=test_group["id"]) + assert result["rolesIds"][0] == test_role["id"] + + api_client.group.delete_role(id=test_group["id"], role_id=test_role["id"]) + result = api_client.group.read(id=test_group["id"]) + assert len(result["rolesIds"]) == 0 + + api_client.group.delete(id=test_group["id"]) + api_client.role.delete(id=test_role["id"]) + + +def test_group_default_markings(api_client): + group_data = GroupTest(api_client).data() + marking_test = MarkingDefinitionTest(api_client) + marking_data = marking_test.data() + test_group = api_client.group.create(**group_data) + test_marking = marking_test.own_class().create(**marking_data) + assert test_group is not None, "Create group response is NoneType" + assert test_marking is not None, "Create marking response is NoneType" + + api_client.group.edit_default_marking( + id=test_group["id"], + marking_ids=[test_marking["id"]] + ) + result = api_client.group.read(id=test_group["id"]) + assert result["default_marking"][0]["values"][0]["id"] == test_marking["id"] + + api_client.group.edit_default_marking( + id=test_group["id"], + marking_ids=[] + ) + result = api_client.group.read(id=test_group["id"]) + assert len(result["default_marking"][0]["values"]) == 0 + + api_client.group.delete(id=test_group["id"]) + marking_test.base_class().delete(id=test_marking["id"]) + + +def test_group_membership(api_client): + pass + + +def test_group_allowed_markings(api_client): + group_data = GroupTest(api_client).data() + marking_test = MarkingDefinitionTest(api_client) + marking_data = marking_test.data() + test_group = api_client.group.create(**group_data) + test_marking = marking_test.own_class().create(**marking_data) + assert test_group is not None, "Create group response is NoneType" + assert test_marking is not None, "Create marking response is NoneType" + + api_client.group.add_allowed_marking( + id=test_group["id"], + marking_id=test_marking["id"] + ) + result = api_client.group.read(id=test_group["id"]) + assert result["allowed_marking"][0]["id"] == test_marking["id"] + + api_client.group.delete_allowed_marking( + id=test_group["id"], + marking_id=test_marking["id"] + ) + result = api_client.group.read(id=test_group["id"]) + assert len(result["allowed_marking"]) == 0 + + api_client.group.delete(id=test_group["id"]) + marking_test.base_class().delete(id=test_marking["id"]) diff --git a/tests/cases/entities.py b/tests/cases/entities.py index 67fff869a..691bae27a 100644 --- a/tests/cases/entities.py +++ b/tests/cases/entities.py @@ -175,6 +175,10 @@ def task(api_client): def case_role(api_client): return RoleTest(api_client) + @staticmethod + def case_group(api_client): + return GroupTest(api_client) + class EntityTest: def __init__(self, api_client): @@ -1181,3 +1185,36 @@ def get_filter(self) -> Dict[str, str]: def get_search(self) -> str: return "TestRole" + + +class GroupTest(EntityTest): + def data(self) -> Dict: + return { + "name": "TestGroup", + "group_confidence_level": { + "max_confidence": 80, + "overrides": [] + } + } + + def own_class(self): + return self.api_client.group + + def base_class(self): + return self.own_class() + + def update_data(self) -> Dict: + return { + "description": "This is a test group", + "no_creators": True, + "group_confidence_level": { + "max_confidence": 90, + "overrides": [{ + "entity_type": "Indicator", + "max_confidence": 80 + }] + } + } + + def get_search(self) -> str: + return "TestGroup" From 4533f798d05c32fc63ea2c2864106652a35f513c Mon Sep 17 00:00:00 2001 From: peritz Date: Wed, 22 Jan 2025 09:34:51 +0000 Subject: [PATCH 03/27] Add capability test to Role object --- pycti/entities/opencti_capability.py | 2 +- pycti/entities/opencti_role.py | 15 +++++++++------ tests/02-integration/entities/test_role.py | 22 ++++++++++++++++++++++ 3 files changed, 32 insertions(+), 7 deletions(-) create mode 100644 tests/02-integration/entities/test_role.py diff --git a/pycti/entities/opencti_capability.py b/pycti/entities/opencti_capability.py index 8d027a655..04c0713d4 100644 --- a/pycti/entities/opencti_capability.py +++ b/pycti/entities/opencti_capability.py @@ -24,7 +24,7 @@ def __init__(self, opencti): """ def list(self) -> List[Dict]: - self.opencti.logger.info("Listing capabilities") + self.opencti.admin_logger.info("Listing capabilities") query = ( """ query Capabilities { diff --git a/pycti/entities/opencti_role.py b/pycti/entities/opencti_role.py index 54e162122..7ec12a400 100644 --- a/pycti/entities/opencti_role.py +++ b/pycti/entities/opencti_role.py @@ -348,24 +348,27 @@ def add_capability(self, **kwargs) -> Dict: }) query = ( """ - mutation RoleAddCapability($id: ID!, input: InternalRelationshipAddInput!) { + mutation RoleAddCapability($id: ID!, $input: InternalRelationshipAddInput!) { roleEdit(id: $id) { relationAdd(input: $input) { id - standard_id entity_type parent_types created_at updated_at from { - """ + ... on Role { + """ + self.properties + """ + } } to { - id, name, description + ... on Capability { + id, name, description + } } } } @@ -402,9 +405,9 @@ def delete_capability(self, **kwargs) -> Dict: }) query = ( """ - mutation RoleDelCapability($id: ID!, toId: ID!) { + mutation RoleDelCapability($id: ID!, $toId: StixRef!) { roleEdit(id: $id) { - relationDelete(fromId: $id, toId: $toId, relationship_type: "has-capability") { + relationDelete(toId: $toId, relationship_type: "has-capability") { """ + self.properties + """ diff --git a/tests/02-integration/entities/test_role.py b/tests/02-integration/entities/test_role.py new file mode 100644 index 000000000..5f2d0b9cd --- /dev/null +++ b/tests/02-integration/entities/test_role.py @@ -0,0 +1,22 @@ +from tests.cases.entities import RoleTest, MarkingDefinitionTest + + +def test_role_capabilities(api_client): + role_test = RoleTest(api_client) + role_data = role_test.data() + test_role = role_test.own_class().create(**role_data) + assert test_role is not None, "Create role response is NoneType" + + capability_id = api_client.capability.list()[0]["id"] + + role_test.own_class().add_capability( + id=test_role["id"], capability_id=capability_id) + result = role_test.own_class().read(id=test_role["id"]) + assert capability_id in result["capabilitiesIds"] + + role_test.own_class().delete_capability( + id=test_role["id"], capability_id=capability_id) + result = role_test.own_class().read(id=test_role["id"]) + assert capability_id not in result["capabilitiesIds"] + + role_test.base_class().delete(id=test_role["id"]) From c0281c6ed50236ac405a289825b7b3b87ad337f5 Mon Sep 17 00:00:00 2001 From: peritz Date: Wed, 22 Jan 2025 16:27:03 +0000 Subject: [PATCH 04/27] Add User entity --- pycti/api/opencti_api_client.py | 4 +- pycti/entities/opencti_user.py | 821 +++++++++++++++++++++ tests/02-integration/entities/test_user.py | 74 ++ tests/cases/entities.py | 37 + 4 files changed, 935 insertions(+), 1 deletion(-) create mode 100644 pycti/entities/opencti_user.py create mode 100644 tests/02-integration/entities/test_user.py diff --git a/pycti/api/opencti_api_client.py b/pycti/api/opencti_api_client.py index 6de578be7..66b6f4078 100644 --- a/pycti/api/opencti_api_client.py +++ b/pycti/api/opencti_api_client.py @@ -64,6 +64,7 @@ from pycti.entities.opencti_capability import Capability from pycti.entities.opencti_role import Role from pycti.entities.opencti_group import Group +from pycti.entities.opencti_user import User from pycti.utils.opencti_logger import logger from pycti.utils.opencti_stix2 import OpenCTIStix2 from pycti.utils.opencti_stix2_utils import OpenCTIStix2Utils @@ -206,6 +207,7 @@ def __init__( self.capability = Capability(self) self.role = Role(self) self.group = Group(self) + self.user = User(self) # Check if openCTI is available if perform_health_check and not self.health_check(): @@ -635,7 +637,7 @@ def process_multiple_fields(self, data): # Administrative data if "groups" in data: - data["groups"] = self.process_multiple(data["group"]) + data["groups"] = self.process_multiple(data["groups"]) data["groupsIds"] = self.process_multiple_ids(data["groups"]) if "objectOrganization" in data: data["objectOrganization"] = self.process_multiple( diff --git a/pycti/entities/opencti_user.py b/pycti/entities/opencti_user.py new file mode 100644 index 000000000..260571f8d --- /dev/null +++ b/pycti/entities/opencti_user.py @@ -0,0 +1,821 @@ +import secrets + +from typing import List, Dict + + +class User: + """Representation of a user on the OpenCTI platform + + Users can be member of multiple groups, from which its permissions + (capabilities) are derived. Additionally, users are part of organisations, + and sometimes administrating them (Enterprise edition). + + They have configured confidence, and an effective confidence (which might + be set by the group). + + Representation of a User:: + + { + "id": "UUID", + "individual_id": "UUID of Creator object for user", + "user_email": "Email address assigned to user", + "firstname": "First name", + "lastname": "Last name", + "name": "Name", + "description": "This is a user", + "language": "auto", + "theme": "light|dark|auto", + "unit_system": "auto|Metric|Imperial", + "external": true, + "restrict_delete": true, + "account_status": "active", + "account_lock_after_date": "ISO 8601 date", + "created_at": "ISO 8601 date", + "updated_at": "ISO 8601 date", + "roles": [{ + "id": "UUID", + "name": "Role name", + "description": "Role description", + "capabilities": [{ + "id": "UUID", + "name": "KNUPDATE" + }] + }], + "capabilities": [{ + "id": "UUID", + "name": "KNUPDATE" + }], + "groups": { + "edges": [{ + "node": { + "id": "UUID", + "name": "group name", + "description": "Group description" + } + }] + }, + "objectOrganization": { + "edges": [{ + "node": { + "id": "UUID", + "is_inferred": false, + "name": "Org name", + "description": "Organisation description" + } + }] + }, + "administrated_organizations": [{ + "id": "UUID", + "name": "Org name", + "description": "Organisation description" + }], + "user_confidence_level": { + "max_confidence": 90, + "overrides": [{ + "entity_type": "type", + "max_confidence": 100 + }] + }, + "effective_confidence_level": { + "max_confidence": 90, + "source": { + "type": "User|Group|Bypass", + "object": { # Only on type == Group + "id": "UUID", + "name": "group name" + } + }, + "overrides": { + "entity_type": "type", + "max_confidence": 90, + "source": { + "type": "User|Group|Bypass", + "object": { # Only on type == Group + "id": "UUID", + "name": "group name" + } + } + } + }, + "api_token": "API_KEY", # only available if requested + "sessions": [{ + "id": "UUID", + "created": "ISO 8901 datetime", + "ttl": 100, + "originalMaxAge": 100 + }], # only available if requested + "submenu_show_icons": true, # only for MeUser + "submenu_auto_collaps": true # only for MeUser + } + + You can view the properties, token_properties, session_properties, and + me_properties attributes of a User object to view what attributes will be + present in a User or MeUser object. + """ + + def __init__(self, opencti): + self.opencti = opencti + self.properties = """ + id + standard_id + individual_id + user_email + firstname + lastname + name + description + + language + theme + unit_system + + external + restrict_delete + + account_status + account_lock_after_date + + created_at + updated_at + + unit_system + submenu_show_icons + submenu_auto_collapse + monochrome_labels + + roles { + id, name, description + capabilities { + id, name + } + } + + groups { + edges { + node { + id, name, description + } + } + } + + objectOrganization { + edges { + node { + id, is_inferred, name, description + } + } + } + + administrated_organizations { + id, name, description + } + + user_confidence_level { + max_confidence + overrides { + entity_type, max_confidence + } + } + effective_confidence_level { + max_confidence + source { + type + object { + ... on Group { + id, name + } + } + } + overrides { + entity_type, max_confidence + source { + type + object { + ... on Group { + id, name + } + } + } + } + } + """ + + self.token_properties = """ + api_token + """ + + self.session_properties = """ + sessions { + id, created, ttl, originalMaxAge + } + """ + + self.me_properties = """ + id + individual_id + user_email + firstname + lastname + name + description + + theme + language + unit_system + submenu_show_icons + submenu_auto_collapse + + objectOrganization { + edges { + node { + id, name + } + } + } + + administrated_organizations { + id, name + } + + capabilities { + id, name, description + } + + groups { + edges { + node { + id, name, description + } + } + } + + effective_confidence_level { + max_confidence + source { + type + object { + ... on Group { + id, name + } + } + } + overrides { + entity_type, max_confidence + source { + type + object { + ... on Group { + id, name + } + } + } + } + } + """ + + def list(self, + first: int = 500, + after: str = None, + orderBy: str = "name", + orderMode: str = "asc", + filters: dict = {}, + search: str = None, + include_sessions: bool = False, + customAttributes: str = None, + getAll: bool = False, + withPagination: bool = False) -> List[Dict]: + """Search/list users on the platform + + Searches users given some conditions. Defaults to listing all users. + + :param first: Defaults to 500. Retrieve this number of results. + :type first: int, optional + :param after: Retrieves all results after the user with this ID. + Ignored if None, empty, or if fetching all results, defaults to + None. + :type after: str, optional + :param orderBy: Orders results by this field. + Can be one of user, user_email, firstname, lastname, language, + external, created_at, updated_at, or _score, defaults to "name". + :type orderBy: str, optional + :param orderMode: Ordering direction. Must be one + of "asc" or "desc", defaults to "asc". + :type orderMode: str, optional + :param filters: OpenCTI API FilterGroup object. + This is an advanced parameter. To learn more please search for + the FilterGroup object in the OpenCTI GraphQL Playground, defaults + to {}. + :type filters: dict, optional + :param search: String to search for when listing + users, defaults to None. + :type search: str, optional + :param include_sessions: Whether or not to + include a list of sessions with results, defaults to False. + :type include_sessions: bool, optional + :param customAttributes: Custom attributes to fetch from the GraphQL + query + :type customAttributes: str, optional + :param getAll: Defaults to False. Whether or not to get all results + from the search. If True then param first is ignored. + :type getAll: bool, optional + :param withPagination: Defaults to False. Whether to return pagination + info with results. + :type withPagination: bool, optional + :return: Returns a list of users, sorted as specified. + :rtype: list[dict] + """ + if getAll: + first = 100 + + self.opencti.admin_logger.info("Fetching users with filters", + {"filters": filters}) + query = ( + """ + query ListUsers($first: Int, $after: ID, $orderBy: UsersOrdering, $orderMode: OrderingMode, $filters: FilterGroup, $search: String) { + users(first: $first, after: $after, orderBy: $orderBy, orderMode: $orderMode, filters: $filters, search: $search) { + edges { + node { + """ + + (self.properties if customAttributes is None + else customAttributes) + + (self.session_properties if include_sessions else "") + + """ + } + } + + pageInfo { + startCursor, endCursor, hasNextPage, hasPreviousPage + globalCount + } + } + } + """ + ) + result = self.opencti.query(query, { + "first": first, + "after": after, + "orderBy": orderBy, + "orderMode": orderMode, + "filters": filters, + "search": search + }) + + if getAll: + final_data = [] + data = self.opencti.process_multiple(result["data"]["users"]) + final_data = final_data + data + while result["data"]["users"]["pageInfo"]["hasNextPage"]: + after = result["data"]["users"]["pageInfo"]["endCursor"] + result = self.opencti.query(query, { + "first": first, + "after": after, + "orderBy": orderBy, + "orderMode": orderMode, + "filters": filters, + "search": search + }) + data = self.opencti.process_multiple(result["data"]["users"]) + final_data = final_data + data + return final_data + else: + return self.opencti.process_multiple(result["data"]["users"], + withPagination) + + def read(self, + id: str = None, + include_sessions: bool = False, + include_token: bool = False, + customAttributes: str = None, + filters: dict = None, + search: str = None) -> dict: + """Reads user details from the platform. + + :param id: ID of the user to fetch + :type id: str, optional + :param include_sessions: Whether or not to + include a list of sessions for the given user, defaults to False. + :type include_sessions: bool, optional + :param include_token: Whether or not to include + the user's API token, defaults to False. + :type include_token: bool, optional + :param customAttributes: Custom attributes to include instead of the + defaults + :type customAttribues: str, optional + :param filters: Filters to apply to find a single user + :type filters: dict, optional + :param search: Search term to use to find a single user + :type search: str, optional + :return: Representation of the user as a Python dictionary. + :rtype: dict + """ + if id is not None: + self.opencti.admin_logger.info( + "Fetching user with ID", {"id": id} + ) + query = ( + """ + query User($id: String!) { + user(id: $id) { + """ + + (self.properties if customAttributes is None + else customAttributes) + + (self.token_properties if include_token else "") + + (self.session_properties if include_sessions else "") + + """ + } + } + """ + ) + result = self.opencti.query(query, {"id": id}) + return self.opencti.process_multiple_fields(result["data"]["user"]) + elif filters is not None or search is not None: + results = self.list( + filters=filters, + search=search, + include_sessions=include_sessions, + customAttributes=customAttributes) + user = results[0] if results else None + if not include_token or user is None: + return user + else: + return self.read( + id=user["id"], + include_sessions=include_sessions, + include_token=include_token, + customAttributes=customAttributes + ) + else: + self.opencti.admin_logger.error( + "[opencti_user] Missing paramters: id, search, or filters") + return None + + def create(self, + name: str, + user_email: str, + password: str = None, + firstname: str = None, + lastname: str = None, + description: str = None, + language: str = None, + theme: str = None, + objectOrganization: List[str] = None, + account_status: str = None, + account_lock_after_date: str = None, + unit_system: str = None, + submenu_show_icons: bool = False, + submenu_auto_collapse: bool = False, + monochrome_labels: bool = False, + groups: List[str] = None, + user_confidence_level: Dict = None, + customAttributes: str = None, + include_token: bool = False) -> dict: + """Creates a new user with basic details + + Note that when SSO is connected users generally do not need to be + manually created. + + Additionally note that if there is no password passed to this function + then a random password will be created and will not be returned. This + is useful for creating service accounts and connector accounts. + + :param name: Name to assign to the user. + :type name: str + :param user_email: Email address for the user. + :type user_email: str + :param password: Password that should be assigned + to the user. If one is not provided then a random one will be + generated, defaults to None. + :type password: str, optional + :param firstname: First name of the user + :type firstname: str, optional + :param lastname: Last name of the user + :type lastname: str, optional + :param description: Description for the user + :type description: str, optional + :param language: Language the user should use + :type language: str, optional + :param theme: Theme to set for the user, either light or dark + :type theme: str, optional + :param objectOrganization: List of organization IDs to add the user to + :type objectOgranization: List[str], optional + :param account_status: The status of the account: Active, Expired, + Inactive, or Locked + :type account_status: str, optional + :param account_lock_after_date: ISO 8901 of when account should be + locked + :type account_lock_after_date: str, optional + :param unit_system: Unit system for the user, metric or imperial + :type unit_system: str, optional + :param submenu_show_icons: Defaults to False. Whether or not to show + icons in submenus on the left hand menu bar in the UI + :type submenu_show_icons: bool, optional + :param submenu_auto_collaps: Defaults to False. Whether to auto- + collapse the left hand menu bar in the UI + :type submenu_auto_collapse: bool, optional + :param monochrome_labels: Defaults to False. Whether to ignore colours + and just show entity labels in monochrome. + :type monochrome_labels: bool, optional + :param groups: List of group IDs to add the user to + :type groups: List[str], optional + :param user_confidence_level: Confidence level object to assign to the + user. This may not impact effective confidence depending on group + membership. + :type user_confidence_level: Dict + :param customAttributes: Custom attributes to return for the user + :type customAttributes: str, optional + :param include_token: Defaults to False. Whether to include the API + token for the new user in the response. + :type include_token: bool, optional + :return: Representation of the user without sessions or API token. + :rtype: dict + """ + self.opencti.admin_logger.info( + "Creating a new user", {"name": name, "email": user_email}) + if password is None: + self.opencti.admin_logger.info( + "Generating random password for user", { + "name": name, "user_email": user_email + }) + password = secrets.token_urlsafe(64) + query = ( + """ + mutation UserAdd($input: UserAddInput!) { + userAdd(input: $input) { + """ + + (self.properties if customAttributes is None + else customAttributes) + + (self.token_properties if include_token else "") + + """ + } + } + """ + ) + result = self.opencti.query(query, {"input": { + "user_email": user_email, + "name": name, + "password": password, + "firstname": firstname, + "lastname": lastname, + "description": description, + "language": language, + "theme": theme, + "objectOrganization": objectOrganization, + "account_status": account_status, + "account_lock_after_date": account_lock_after_date, + "unit_system": unit_system, + "submenu_show_icons": submenu_show_icons, + "submenu_auto_collapse": submenu_auto_collapse, + "monochrome_labels": monochrome_labels, + "groups": groups, + "user_confidence_level": user_confidence_level + }}) + return self.opencti.process_multiple_fields( + result["data"]["userAdd"]) + + def delete(self, id: str): + """Deletes the given user from the platform. + + :param id: ID of the user to delete. + :type id: str + """ + self.opencti.admin_logger.info("Deleting user", {"id": id}) + query = """ + mutation DeleteUser($id: ID!) { + userEdit(id: $id) { + delete + } + } + """ + self.opencti.query(query, {"id": id}) + + def me(self, + include_token: bool = False, + customAttributes: str = None) -> dict: + """Reads the currently authenticated user. + + :param include_token: Whether to inclued the API + token of the currently authenticated user, defaults to False. + :type include_token: bool, optional + :param customAttributes: Custom attributes to return on the User + :type customAttributes: str, optional + :return: Representation of the user. + :rtype: dict + """ + self.opencti.admin_logger.info("Reading MeUser") + query = ( + """ + query Me { + me { + """ + + (self.me_properties if customAttributes is None + else customAttributes) + + (self.token_properties if include_token else "") + + """ + } + } + """ + ) + result = self.opencti.query(query) + return self.opencti.process_multiple_fields(result["data"]["me"]) + + def update_field(self, + id: str, + input: List[Dict], + customAttributes: str = None) -> Dict: + """Update a given user using fieldPatch + + :param id: ID of the user to update. + :type id: str + :param input: FieldPatchInput objects to edit user + :type input: List[Dict] + :param customAttributes: Custom attributes to return from the mutation + :type customAttributes: str, optional + :return: Representation of the user without sessions or API token. + :rtype: dict + """ + self.opencti.admin_logger.info( + "Editing user with input (not shown to hide password and API token" + " changes)", {"id": id}) + query = ( + """ + mutation UserEdit($id: ID!, $input: [EditInput]!) { + userEdit(id: $id) { + fieldPatch(input: $input) { + """ + + (self.properties if customAttributes is None + else customAttributes) + + """ + } + } + } + """ + ) + result = self.opencti.query(query, {"id": id, "input": input}) + return self.opencti.process_multiple_fields( + result["data"]["userEdit"]["fieldPatch"]) + + def add_membership(self, + id: str, + group_id: str) -> dict: + """Adds the user to a given group. + + :param id: User ID to add to the group. + :type id: str + :param group_id: Group ID to add the user to. + :type group_id: str + :return: Representation of the InternalRelationship + :rtype: dict + """ + self.opencti.admin_logger.info("Adding user to group", { + "id": id, "group_id": group_id + }) + query = """ + mutation UserAddMembership($id: ID!, $group_id: ID!) { + userEdit(id: $id) { + relationAdd(input: { + relationship_type: "member-of", + toId: $group_id + }) { + id + from { + ... on User { + id, name, user_email + } + } + to { + ... on Group { + id, name, description + } + } + } + } + } + """ + result = self.opencti.query(query, {"id": id, "group_id": group_id}) + return self.opencti.process_multiple_fields( + result["data"]["userEdit"]["relationAdd"]) + + def delete_membership(self, + id: str, + group_id: str) -> dict: + """Removes the user from the given group. + + :param id: User ID to remove from the group. + :type id: str + :param group_id: Group ID to remove the user from. + :type group_id: str + :return: Representation of the user without sessions or API token + :rtype: dict + """ + self.opencti.admin_logger.info("Removing used from group", { + "id": id, "group_id": group_id}) + query = ( + """ + mutation UserDeleteMembership($id: ID!, $group_id: StixRef!) { + userEdit(id: $id) { + relationDelete(toId: $group_id, relationship_type: "member-of") { + """ + + self.properties + + """ + } + } + } + """ + ) + result = self.opencti.query(query, {"id": id, "group_id": group_id}) + return self.opencti.process_multiple_fields( + result["data"]["userEdit"]["relationDelete"]) + + def add_organization(self, id: str, organization_id: str) -> Dict: + """Adds a user to an organization + + :param id: User ID to add to organization + :type id: str + :param organization_id: ID of organization to add to + :type organization_id: str + :return: Representation of user without sessions or API key + :rtype: Dict + """ + self.opencti.admin_logger.info("Adding user to organization", { + "id": id, "organization_id": organization_id + }) + query = ( + """ + mutation UserAddOrganization($id: ID!, $organization_id: ID!) { + userEdit(id: $id) { + organizationAdd(organizationId: $organization_id) { + """ + + self.properties + + """ + } + } + } + """ + ) + result = self.opencti.query(query, { + "id": id, "organization_id": organization_id}) + return self.opencti.process_multiple_fields( + result["data"]["userEdit"]["organizationAdd"]) + + def delete_organization(self, id: str, organization_id: str) -> Dict: + """Delete a user from an organization + + :param id: User ID to remove from organization + :type id: str + :param organization_id: ID of organization to remove from + :type organization_id: str + :return: Representation of user without sessions or API key + :rtype: Dict + """ + self.opencti.admin_logger.info("Removing user from organization", { + "id": id, "organization_id": organization_id + }) + query = ( + """ + mutation UserDeleteOrganization($id: ID!, $organization_id: ID!) { + userEdit(id: $id) { + organizationDelete(organizationId: $organization_id) { + """ + + self.properties + + """ + } + } + } + """ + ) + result = self.opencti.query(query, { + "id": id, "organization_id": organization_id}) + return self.opencti.process_multiple_fields( + result["data"]["userEdit"]["organizationDelete"]) + + def token_renew(self, + id: str, + include_token: bool = False) -> str: + """Rotates the API token for the given user + + :param user: User ID to rotate API token for. + :type user: str + :param include_token: Whether to include new API + token in response from server, defaults to False. + :type include_token: bool, optional + :return: API token if include_token else None. + :rtype: str + """ + self.opencti.admin_logger.info("Rotating API key for user", {"id": id}) + query = ( + """ + mutation UserRotateToken($id: ID!) { + userEdit(id: $id) { + tokenRenew { + """ + + self.properties + + (self.token_properties if include_token else "") + + """ + } + } + } + """ + ) + result = self.opencti.query(query, {"id": id}) + return self.opencti.process_multiple_fields( + result["data"]["userEdit"]["tokenRenew"]) diff --git a/tests/02-integration/entities/test_user.py b/tests/02-integration/entities/test_user.py new file mode 100644 index 000000000..265bb6c29 --- /dev/null +++ b/tests/02-integration/entities/test_user.py @@ -0,0 +1,74 @@ +from tests.cases.entities import GroupTest, UserTest, IdentityOrganizationTest + + +def test_user_membership(api_client): + user_test = UserTest(api_client) + group_test = GroupTest(api_client) + + test_user = user_test.own_class().create(**user_test.data()) + test_group = group_test.own_class().create(**group_test.data()) + + try: + assert test_user is not None, "User create response returned NoneType" + assert test_group is not None, "Group create response returned NoneType" + + user_test.own_class().add_membership( + id=test_user["id"], + group_id=test_group["id"] + ) + result = user_test.own_class().read(id=test_user["id"]) + assert test_group["id"] in result["groupsIds"] + + user_test.own_class().delete_membership( + id=test_user["id"], + group_id=test_group["id"] + ) + result = user_test.own_class().read(id=test_user["id"]) + assert test_group["id"] not in result["groupsIds"] + finally: + user_test.base_class().delete(id=test_user["id"]) + group_test.base_class().delete(id=test_group["id"]) + + +def test_user_organization(api_client): + user_test = UserTest(api_client) + org_test = IdentityOrganizationTest(api_client) + + test_user = user_test.own_class().create(**user_test.data()) + test_org = org_test.own_class().create(**org_test.data()) + + try: + assert test_user is not None, "User create response returned NoneType" + assert test_org is not None, "Organization create response returned NoneType" + + user_test.own_class().add_organization( + id=test_user["id"], + organization_id=test_org["id"] + ) + result = user_test.own_class().read(id=test_user["id"]) + assert test_org["id"] in result["objectOrganizationIds"] + + user_test.own_class().delete_organization( + id=test_user["id"], + organization_id=test_org["id"] + ) + result = user_test.own_class().read(id=test_user["id"]) + assert test_org["id"] not in result["objectOrganizationIds"] + finally: + user_test.base_class().delete(id=test_user["id"]) + org_test.base_class().delete(id=test_org["id"]) + + +def test_user_token_renew(api_client): + user_test = UserTest(api_client) + test_user = user_test.own_class().create(**user_test.data(), + include_token=True) + try: + assert test_user is not None, "User create response returned NoneType" + + old_token = test_user["api_token"] + result = user_test.own_class().token_renew(id=test_user["id"], + include_token=True) + assert old_token != result["api_token"] + finally: + user_test.own_class().delete(id=test_user["id"]) diff --git a/tests/cases/entities.py b/tests/cases/entities.py index 691bae27a..1f3b41abd 100644 --- a/tests/cases/entities.py +++ b/tests/cases/entities.py @@ -179,6 +179,10 @@ def case_role(api_client): def case_group(api_client): return GroupTest(api_client) + @staticmethod + def case_user(api_client): + return UserTest(api_client) + class EntityTest: def __init__(self, api_client): @@ -1218,3 +1222,36 @@ def update_data(self) -> Dict: def get_search(self) -> str: return "TestGroup" + + +class UserTest(EntityTest): + def data(self) -> Dict: + return { + "name": "Test User", + "user_email": "test@localhost.local", + } + + def own_class(self): + return self.api_client.user + + def base_class(self): + return self.own_class() + + def update_data(self) -> Dict: + return { + "description": "This is a test user", + "firstname": "Test", + "lastname": "User", + "user_confidence_level": { + "max_confidence": 70, + "overrides": [{ + "entity_type": "Indicator", + "max_confidence": 80 + }] + }, + "account_status": "Locked", + "submenu_show_icons": True + } + + def get_search(self): + return '"Test User"' From 62200afb7aedbd59d265add0d6ef431103b4479d Mon Sep 17 00:00:00 2001 From: peritz Date: Wed, 22 Jan 2025 16:27:25 +0000 Subject: [PATCH 05/27] Add membership tests for Group entity --- pycti/api/opencti_api_client.py | 4 +++ tests/02-integration/entities/test_group.py | 27 +++++++++++++++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/pycti/api/opencti_api_client.py b/pycti/api/opencti_api_client.py index 66b6f4078..2732e11da 100644 --- a/pycti/api/opencti_api_client.py +++ b/pycti/api/opencti_api_client.py @@ -654,6 +654,10 @@ def process_multiple_fields(self, data): data["capabilitiesIds"] = self.process_multiple_ids( data["capabilities"] ) + if "members" in data: + data["members"] = self.process_multiple(data["members"]) + data["membersIds"] = self.process_multiple_ids(data["members"]) + # See aliases of GraphQL query in stix_core_object method if "name_alt" in data: diff --git a/tests/02-integration/entities/test_group.py b/tests/02-integration/entities/test_group.py index 47cbfe5d4..974288a72 100644 --- a/tests/02-integration/entities/test_group.py +++ b/tests/02-integration/entities/test_group.py @@ -1,4 +1,5 @@ -from tests.cases.entities import GroupTest, RoleTest, MarkingDefinitionTest +from tests.cases.entities import ( + GroupTest, RoleTest, MarkingDefinitionTest, UserTest) def test_group_roles(api_client): @@ -49,7 +50,29 @@ def test_group_default_markings(api_client): def test_group_membership(api_client): - pass + group_test = GroupTest(api_client) + user_test = UserTest(api_client) + + test_group = group_test.own_class().create(**group_test.data()) + test_user = user_test.own_class().create(**user_test.data()) + + try: + assert test_group is not None, "Create group response is NoneType" + assert test_user is not None, "Create user response is NoneType" + + group_test.own_class().add_member( + id=test_group["id"], user_id=test_user["id"]) + result = group_test.own_class().read(id=test_group["id"]) + assert result["membersIds"][0] == test_user["id"] + + group_test.own_class().delete_member( + id=test_group["id"], + user_id=test_user["id"]) + result = group_test.own_class().read(id=test_group["id"]) + assert len(result["membersIds"]) == 0 + finally: + group_test.base_class().delete(id=test_group["id"]) + user_test.base_class().delete(id=test_user["id"]) def test_group_allowed_markings(api_client): From 3f8868b9a5eb54cd28b5cba6ea2a31e0c2214163 Mon Sep 17 00:00:00 2001 From: peritz Date: Wed, 22 Jan 2025 17:16:18 +0000 Subject: [PATCH 06/27] Add capabilities test for Role entity --- tests/02-integration/entities/test_role.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/02-integration/entities/test_role.py b/tests/02-integration/entities/test_role.py index 5f2d0b9cd..f33d156fd 100644 --- a/tests/02-integration/entities/test_role.py +++ b/tests/02-integration/entities/test_role.py @@ -1,4 +1,4 @@ -from tests.cases.entities import RoleTest, MarkingDefinitionTest +from tests.cases.entities import RoleTest def test_role_capabilities(api_client): @@ -18,5 +18,5 @@ def test_role_capabilities(api_client): id=test_role["id"], capability_id=capability_id) result = role_test.own_class().read(id=test_role["id"]) assert capability_id not in result["capabilitiesIds"] - + role_test.base_class().delete(id=test_role["id"]) From 0c7fa48e265c64d05aa6ad3f453a0bec097f41bc Mon Sep 17 00:00:00 2001 From: peritz Date: Wed, 22 Jan 2025 17:16:47 +0000 Subject: [PATCH 07/27] Add tests for Settings entity --- pycti/api/opencti_api_client.py | 2 ++ tests/cases/entities.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/pycti/api/opencti_api_client.py b/pycti/api/opencti_api_client.py index 2732e11da..f81c32517 100644 --- a/pycti/api/opencti_api_client.py +++ b/pycti/api/opencti_api_client.py @@ -65,6 +65,7 @@ from pycti.entities.opencti_role import Role from pycti.entities.opencti_group import Group from pycti.entities.opencti_user import User +from pycti.entities.opencti_settings import Settings from pycti.utils.opencti_logger import logger from pycti.utils.opencti_stix2 import OpenCTIStix2 from pycti.utils.opencti_stix2_utils import OpenCTIStix2Utils @@ -208,6 +209,7 @@ def __init__( self.role = Role(self) self.group = Group(self) self.user = User(self) + self.settings = Settings(self) # Check if openCTI is available if perform_health_check and not self.health_check(): diff --git a/tests/cases/entities.py b/tests/cases/entities.py index 1f3b41abd..5133ecfd5 100644 --- a/tests/cases/entities.py +++ b/tests/cases/entities.py @@ -183,6 +183,10 @@ def case_group(api_client): def case_user(api_client): return UserTest(api_client) + @staticmethod + def case_settings(api_client): + return SettingsTest(api_client) + class EntityTest: def __init__(self, api_client): @@ -1255,3 +1259,31 @@ def update_data(self) -> Dict: def get_search(self): return '"Test User"' + + +class SettingsTest(EntityTest): + def setup(self): + # Save current platform information + return + + def teardown(self): + # Restore platform information + return + + def data(self) -> Dict: + return {} + + def own_class(self): + return self.api_client.settings + + def base_class(self): + return self.own_class() + + def update_data(self): + return { + "platform_title": "This is a test platform", + "platform_theme": "dark" + } + + def get_filter(self): + return None From 3defc3143c5c8d264f183a8301c0690250a4b01b Mon Sep 17 00:00:00 2001 From: peritz Date: Wed, 22 Jan 2025 17:17:09 +0000 Subject: [PATCH 08/27] Add Settings entity stub to fail tests --- pycti/entities/opencti_settings.py | 187 +++++++++++++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 pycti/entities/opencti_settings.py diff --git a/pycti/entities/opencti_settings.py b/pycti/entities/opencti_settings.py new file mode 100644 index 000000000..741b2389c --- /dev/null +++ b/pycti/entities/opencti_settings.py @@ -0,0 +1,187 @@ +from typing import Dict + + +class Settings: + def __init__(self, opencti): + self.opencti = opencti + self.properties = """ + id + standard_id + entity_type + parent_types + + platform_organization { + id, name, description + } + + platform_title + platform_favicon + platform_email + platform_url + platform_language + + platform_cluster { + instances_number + } + + platform_modules { + id, enable, running, Warning + } + + platform_providers { + name, type, strategy, provider + } + + platform_user_statuses { + status, message + } + + platform_theme + platform_theme_dark_background + platform_theme_dark_paper + platform_theme_dark_nav + platform_theme_dark_primary + platform_theme_dark_secondary + platform_theme_dark_accent + platform_theme_dark_logo + platform_theme_dark_logo_collapsed + platform_theme_dark_logo_login + platform_theme_light_background + platform_theme_light_paper + platform_theme_light_nav + platform_theme_light_primary + platform_theme_light_secondary + platform_theme_light_accent + platform_theme_light_logo + platform_theme_light_logo_collapsed + platform_theme_light_logo_login + platform_map_tile_server_dark + platform_map_tile_server_light + + platform_openbas_url + platform_openbas_disable_display + + platform_openerm_url + platform_openmtd_url + + platform_ai_enabled + platform_ai_type + platform_ai_model + platform_ai_has_token + + platform_login_message + platform_consent_message + platform_consent_confirm_text + + platform_banner_text + platform_banner_level + + platform_session_idle_timeout + platform_session_timeout + + platform_whitemark + platform_demo + platform_reference_attachment + + platform_feature_flags { + id, enable, running, warning + } + + platform_critical_alerts { + message, type + details { + groups { + id, name, description + } + } + } + + platform_trash_enabled + + platform_protected_sensitive_config { + enabled + markings { + enabled, protected_ids + } + groups { + enabled, protected_ids + } + roles { + enabled, protected_ids + } + rules { + enabled, protected_ids + } + ce_ee_toggle { + enabled, protected_ids + } + file_indexing { + enabled, protected_ids + } + platform_organization { + enabled, protected_ids + } + } + + created_at + updated_at + enterprise_edition + analytics_google_analytics_v4 + + activity_listeners { + id, name, entity_type + } + """ + self.messages_properties = """ + platform_messages { + id, message, activated, dismissible, update_at, color + recipients { + id, name, entity_type + } + } + messages_administration { + id, message, activated, dismissible, update_at, color + recipients { + id, name, entity_type + } + } + """ + self.password_policy_properties = """ + otp_mandatory + password_policy_min_length + password_policy_max_length + password_policy_min_symbols + password_policy_min_numbers + password_policy_min_words + password_policy_min_lowercase + password_policy_min_uppercase + """ + + def create(self, **kwargs) -> Dict: + """Stub function for tests + + :return: Settings as defined by self.read() + :rtype: Dict + """ + self.opencti.admin_logger.info( + "Settings.create called with arguments", kwargs) + raise Exception + return self.read() + + def delete(self, **kwargs): + """Stub function for tests""" + self.opencti.admin_logger.info( + "Settings.delete called with arguments", kwargs) + raise Exception + + def read(self, **kwargs) -> Dict: + pass + + def update_field(self, **kwargs) -> Dict: + pass + + def edit_message(self, **kwargs) -> Dict: + pass + + def delete_message(self, **kwargs) -> Dict: + pass From f88ebd99651d4a67efdc7341b68a2d432c2d42da Mon Sep 17 00:00:00 2001 From: peritz Date: Fri, 24 Jan 2025 13:44:55 +0000 Subject: [PATCH 09/27] Add Settings entity --- pycti/api/opencti_api_client.py | 16 +- pycti/entities/opencti_settings.py | 220 +++++++++++++++++- .../02-integration/entities/test_settings.py | 49 ++++ tests/cases/entities.py | 17 +- 4 files changed, 290 insertions(+), 12 deletions(-) create mode 100644 tests/02-integration/entities/test_settings.py diff --git a/pycti/api/opencti_api_client.py b/pycti/api/opencti_api_client.py index f81c32517..06b7f3c5a 100644 --- a/pycti/api/opencti_api_client.py +++ b/pycti/api/opencti_api_client.py @@ -659,7 +659,21 @@ def process_multiple_fields(self, data): if "members" in data: data["members"] = self.process_multiple(data["members"]) data["membersIds"] = self.process_multiple_ids(data["members"]) - + if "platform_messages" in data: + data["platform_messages"] = self.process_multiple( + data["platform_messages"]) + data["platform_messages_ids"] = self.process_multiple_ids( + data["platform_messages"]) + if "messages_administration" in data: + data["messages_administration"] = self.process_multiple( + data["messages_administration"]) + data["messages_administration_ids"] = self.process_multiple_ids( + data["messages_administration"]) + if "recipients" in data: + data["recipients"] = self.process_multiple( + data["recipients"]) + data["recipientsIds"] = self.process_multiple_ids( + data["recipients"]) # See aliases of GraphQL query in stix_core_object method if "name_alt" in data: diff --git a/pycti/entities/opencti_settings.py b/pycti/entities/opencti_settings.py index 741b2389c..86a567a89 100644 --- a/pycti/entities/opencti_settings.py +++ b/pycti/entities/opencti_settings.py @@ -25,7 +25,7 @@ def __init__(self, opencti): } platform_modules { - id, enable, running, Warning + id, enable, warning } platform_providers { @@ -84,7 +84,7 @@ def __init__(self, opencti): platform_reference_attachment platform_feature_flags { - id, enable, running, warning + id, enable, warning } platform_critical_alerts { @@ -134,13 +134,13 @@ def __init__(self, opencti): """ self.messages_properties = """ platform_messages { - id, message, activated, dismissible, update_at, color + id, message, activated, dismissible, updated_at, color recipients { id, name, entity_type } } messages_administration { - id, message, activated, dismissible, update_at, color + id, message, activated, dismissible, updated_at, color recipients { id, name, entity_type } @@ -157,6 +157,50 @@ def __init__(self, opencti): password_policy_min_uppercase """ + self.editable_properties = """ + id + + platform_organization { + id + } + + platform_title + platform_favicon + platform_email + platform_language + + platform_theme + platform_theme_dark_background + platform_theme_dark_paper + platform_theme_dark_nav + platform_theme_dark_primary + platform_theme_dark_secondary + platform_theme_dark_accent + platform_theme_dark_logo + platform_theme_dark_logo_collapsed + platform_theme_dark_logo_login + platform_theme_light_background + platform_theme_light_paper + platform_theme_light_nav + platform_theme_light_primary + platform_theme_light_secondary + platform_theme_light_accent + platform_theme_light_logo + platform_theme_light_logo_collapsed + platform_theme_light_logo_login + + platform_login_message + platform_consent_message + platform_consent_confirm_text + platform_banner_text + platform_banner_level + + platform_whitemark + analytics_google_analytics_v4 + enterprise_edition + """ + self.password_policy_properties + self._deleted = False + def create(self, **kwargs) -> Dict: """Stub function for tests @@ -165,23 +209,179 @@ def create(self, **kwargs) -> Dict: """ self.opencti.admin_logger.info( "Settings.create called with arguments", kwargs) - raise Exception + self._deleted = False return self.read() def delete(self, **kwargs): """Stub function for tests""" self.opencti.admin_logger.info( "Settings.delete called with arguments", kwargs) - raise Exception + self._deleted = True def read(self, **kwargs) -> Dict: - pass + """Reads settings from the platform + + :param customAttributes: Custom attribues to return from query + :type customAttributes: str, optional + :param include_password_policy: Defaults to False. Whether to include + password policy properties in response. + :type include_password_policy: bool, optional + :param include_messages: Defaults to False. Whether to include messages + in query response. + :type include_messages: bool, optional + :return: Representation of the platform settings + :rtype: Dict + """ + if self._deleted: + return None + + custom_attributes = kwargs.get("customAttributes", None) + include_password_policy = kwargs.get("include_password_policy", False) + include_messages = kwargs.get("include_messages", False) + + self.opencti.admin_logger.info("Reading platform settings") + query = ( + """ + query PlatformSettings { + settings { + """ + + (self.properties if custom_attributes is None + else custom_attributes) + + (self.password_policy_properties if include_password_policy + else "") + + (self.messages_properties if include_messages else "") + + """ + } + } + """ + ) + result = self.opencti.query(query) + return self.opencti.process_multiple_fields(result["data"]["settings"]) def update_field(self, **kwargs) -> Dict: - pass + """Update settings using input to fieldPatch + + :param id: ID of the settings object to update + :type id: str + :param input: List of EditInput objects + :type input: List[Dict] + :param customAttributes: Custom attribues to return from query + :type customAttributes: str, optional + :param include_password_policy: Defaults to False. Whether to include + password policy properties in response. + :type include_password_policy: bool, optional + :param include_messages: Defaults to False. Whether to include messages + in query response. + :type include_messages: bool, optional + :return: Representation of the platform settings + :rtype: Dict + """ + id = kwargs.get("id", None) + input = kwargs.get("input", None) + custom_attributes = kwargs.get("customAttributes", None) + include_password_policy = kwargs.get("include_password_policy", False) + include_messages = kwargs.get("include_messages", False) + + if id is None or input is None: + self.opencti.admin_logger.error( + "[opencti_settings] Missing parameters: id and input" + ) + return None + + self.opencti.admin_logger.info("Updating settings with input", { + "id": id, + "input": input + }) + query = ( + """ + mutation SettingsUpdateField($id: ID!, $input: [EditInput]!) { + settingsEdit(id: $id) { + fieldPatch(input: $input) { + """ + + (self.properties if custom_attributes is None + else custom_attributes) + + (self.password_policy_properties if include_password_policy + else "") + + (self.messages_properties if include_messages else "") + + """ + } + } + } + """ + ) + result = self.opencti.query(query, {"id": id, "input": input}) + return self.opencti.process_multiple_fields( + result["data"]["settingsEdit"]["fieldPatch"]) def edit_message(self, **kwargs) -> Dict: - pass + """Edit or add a message to the platform + + To add a message, don't include an ID in the input object. To edit a + message an ID must be provided. + + :param id: ID of the settings object on the platform + :type id: str + :param input: SettingsMessageInput object + :type input: Dict + :return: Settings ID and message objects + :rtype: Dict + """ + id = kwargs.get("id", None) + input = kwargs.get("input", None) + if id is None or input is None: + self.opencti.admin_logger.error( + "[opencti_settings] Missing parameters: id and input") + return None + self.opencti.admin_logger.info( + "Editing message", {"id": id, "input": input}) + + query = ( + """ + mutation SettingsEditMessage($id: ID!, $input: SettingsMessageInput!) { + settingsEdit(id: $id) { + editMessage(input: $input) { + id + """ + + self.messages_properties + """ + } + } + } + """ + ) + result = self.opencti.query(query, {"id": id, "input": input}) + return self.opencti.process_multiple_fields( + result["data"]["settingsEdit"]["editMessage"]) def delete_message(self, **kwargs) -> Dict: - pass + """Delete a message from the platform + + :param id: ID of the settings object on the platform + :type id: str + :param input: ID of the message to delete + :type input: str + :return: Settings ID and message objects + :rtype: Dict + """ + id = kwargs.get("id", None) + input = kwargs.get("input", None) + if id is None: + self.opencti.admin_logger.info( + "[opencti_settings] Missing parameters: id") + return None + + query = ( + """ + mutation SettingsDeleteMessage($id: ID!, $input: String!) { + settingsEdit(id: $id) { + deleteMessage(input: $input) { + id + """ + + self.messages_properties + """ + } + } + } + """ + ) + result = self.opencti.query(query, {"id": id, "input": input}) + return self.opencti.process_multiple_fields( + result["data"]["settingsEdit"]["deleteMessage"]) diff --git a/tests/02-integration/entities/test_settings.py b/tests/02-integration/entities/test_settings.py new file mode 100644 index 000000000..52d4d5526 --- /dev/null +++ b/tests/02-integration/entities/test_settings.py @@ -0,0 +1,49 @@ +from tests.cases.entities import SettingsTest + + +def test_settings_messages(api_client): + settings_test = SettingsTest(api_client) + settings_test.setup() + try: + result = settings_test.own_class().read(include_messages=True) + id = result["id"] + num_messages = len(result["platform_messages"]) + + # Add message + test_message_data = { + "message": "This is a test message", + "activated": True, + "dismissible": True + } + result = settings_test.own_class().edit_message( + id=id, + input=test_message_data) + assert len(result["platform_messages"]) == num_messages + 1 + + test_message = result["platform_messages"][-1] + assert test_message["message"] == test_message_data["message"] + + # Update message + result = settings_test.own_class().edit_message( + id=id, + input={ + "id": test_message["id"], + "message": "This is an updated test message", + "activated": True, + "dismissible": False + } + ) + assert len(result["platform_messages"]) == num_messages + 1 + + updated_message = result["platform_messages"][-1] + assert updated_message["id"] == test_message["id"] + assert updated_message["message"] == "This is an updated test message" + + # Delete message + result = settings_test.own_class().delete_message( + id=id, input=test_message["id"] + ) + assert len(result["platform_messages"]) == num_messages + assert test_message["id"] not in result["platform_messages_ids"] + finally: + settings_test.teardown() diff --git a/tests/cases/entities.py b/tests/cases/entities.py index 5133ecfd5..d56f7a92e 100644 --- a/tests/cases/entities.py +++ b/tests/cases/entities.py @@ -1,3 +1,5 @@ +import json + from typing import Dict, List, Union from stix2 import TLP_GREEN, TLP_WHITE, AttackPattern @@ -1264,10 +1266,23 @@ def get_search(self): class SettingsTest(EntityTest): def setup(self): # Save current platform information + custom_attributes = self.own_class().editable_properties + self.own_class().create() + self._saved_settings = self.own_class().read( + customAttributes=custom_attributes) + if self._saved_settings["platform_organization"] is not None: + self._saved_settings[ + "platform_organization"] = self._saved_settings[ + "platform_organization"]["id"] return def teardown(self): # Restore platform information + id = self._saved_settings.pop("id") + input = [{"key": key, "value": value} + for key, value in self._saved_settings.items() + if value is not None] + self.own_class().update_field(id=id, input=input) return def data(self) -> Dict: @@ -1282,7 +1297,7 @@ def base_class(self): def update_data(self): return { "platform_title": "This is a test platform", - "platform_theme": "dark" + "platform_theme": "light" } def get_filter(self): From ed445cade448aa9adfdde906b45206c55805750e Mon Sep 17 00:00:00 2001 From: peritz Date: Fri, 24 Jan 2025 14:38:37 +0000 Subject: [PATCH 10/27] Standardise admin entities to use kwargs approach --- pycti/entities/opencti_capability.py | 2 +- pycti/entities/opencti_group.py | 164 ++++++++++++++++++--------- pycti/entities/opencti_role.py | 10 +- pycti/entities/opencti_user.py | 153 +++++++++++++++---------- 4 files changed, 210 insertions(+), 119 deletions(-) diff --git a/pycti/entities/opencti_capability.py b/pycti/entities/opencti_capability.py index 04c0713d4..b1ab2b770 100644 --- a/pycti/entities/opencti_capability.py +++ b/pycti/entities/opencti_capability.py @@ -27,7 +27,7 @@ def list(self) -> List[Dict]: self.opencti.admin_logger.info("Listing capabilities") query = ( """ - query Capabilities { + query CapabilityList { capabilities(first: 500) { edges { node { diff --git a/pycti/entities/opencti_group.py b/pycti/entities/opencti_group.py index c3d15d768..ea7ce9fdd 100644 --- a/pycti/entities/opencti_group.py +++ b/pycti/entities/opencti_group.py @@ -148,16 +148,7 @@ def __init__(self, opencti): } """ - def list(self, - first: int = 500, - after: str = None, - orderBy: str = None, - orderMode: str = None, - search: str = None, - filters: dict = None, - customAttributes: str = None, - getAll: bool = False, - withPagination: bool = False) -> List[Dict]: + def list(self, **kwargs) -> List[Dict]: """Lists groups based on a number of filters. :param first: Retrieve this number of results. If 0 @@ -193,6 +184,16 @@ def list(self, :return: List of groups in dictionary representation. :rtype: list[dict] """ + first = kwargs.get("first", 500) + after = kwargs.get("after", None) + orderBy = kwargs.get("orderBy", None) + orderMode = kwargs.get("orderMode", None) + search = kwargs.get("search", None) + filters = kwargs.get("filters", None) + customAttributes = kwargs.get("customAttributes", None) + getAll = kwargs.get("getAll", False) + withPagination = kwargs.get("withPagination", False) + if getAll: first = 100 @@ -251,11 +252,7 @@ def list(self, return self.opencti.process_multiple(result["data"]["groups"], withPagination) - def read(self, - id: str = None, - filters: dict = None, - search: str = None, - customAttributes: str = None) -> dict: + def read(self, **kwargs) -> Dict: """Fetch a given group from OpenCTI :param id: ID of the group to fetch @@ -267,6 +264,10 @@ def read(self, :return: Representation of a group. :rtype: dict """ + id = kwargs.get("id", None) + filters = kwargs.get("filters", None) + search = kwargs.get("search", None) + customAttributes = kwargs.get("customAttributes", None) if id is not None: self.opencti.admin_logger.info( "Fetching group with ID", {"id": id}) @@ -293,15 +294,7 @@ def read(self, "[opencti_group] Missing parameters: id or filters") return None - def create(self, - name: str, - group_confidence_level: dict, - description: str = None, - default_assignation: bool = False, - no_creators: bool = False, - restrict_delete: bool = False, - auto_new_marking: bool = False, - customAttributes: str = None) -> dict: + def create(self, **kwargs) -> dict: """Create a group with required details Groups can be configured after creation using other functions. @@ -331,6 +324,21 @@ def create(self, :return: Representation of the group. :rtype: dict """ + name = kwargs.get("name", None) + group_confidence_level = kwargs.get("group_confidence_level", None) + description = kwargs.get("description", None) + default_assignation = kwargs.get("default_assignation", False) + no_creators = kwargs.get("no_creators", False) + restrict_delete = kwargs.get("restrict_delete", False) + auto_new_marking = kwargs.get("auto_new_marking", False) + customAttributes = kwargs.get("customAttributes", None) + + if name is None or group_confidence_level is None: + self.opencti.admin_logger.error( + "[opencti_group] Missing parameters: name and " + "group_confidence_level") + return None + self.opencti.admin_logger.info( "Creating new group with parameters", { "name": name, @@ -382,10 +390,7 @@ def delete(self, id: str): """ self.opencti.query(query, {"id": id}) - def update_field(self, - id: str, - input: List[Dict], - customAttributes: str = None) -> Dict: + def update_field(self, **kwargs) -> Dict: """Update a group using fieldPatch :param id: ID of the group to update @@ -397,6 +402,15 @@ def update_field(self, :return: Representation of a group :rtype: dict """ + id = kwargs.get("id", None) + input = kwargs.get("input", None) + customAttributes = kwargs.get("customAttributes", None) + + if id is None or input is None: + self.opencti.admin_logger.error( + "[opencti_group] Missing parameters: id and input") + return None + self.opencti.admin_logger.info("Editing group with input", { "input": input}) query = ( @@ -417,9 +431,7 @@ def update_field(self, return self.opencti.process_multiple_fields( result["data"]["groupEdit"]["fieldPatch"]) - def add_member(self, - id: str, - user_id: str) -> dict: + def add_member(self, **kwargs) -> dict: """Add a member to a given group. :param id: ID of the group to add a member to @@ -429,6 +441,14 @@ def add_member(self, :return: Representation of the relationship :rtype: dict """ + id = kwargs.get("id", None) + user_id = kwargs.get("user_id", None) + + if id is None or user_id is None: + self.opencti.admin_logger.error( + "[opencti_group] Missing parameters: id and user_id") + return None + self.opencti.admin_logger.info( "Adding member to group", {"groupId": id, "userId": user_id} ) @@ -457,9 +477,7 @@ def add_member(self, return self.opencti.process_multiple_fields( result["data"]["groupEdit"]["relationAdd"]) - def delete_member(self, - id: str, - user_id: str) -> dict: + def delete_member(self, **kwargs) -> dict: """Remove a given user from a group :param id: ID to remove a user from @@ -469,6 +487,14 @@ def delete_member(self, :return: Representation of the group after the member has been removed :rtype: dict """ + id = kwargs.get("id", None) + user_id = kwargs.get("user_id", None) + + if id is None or user_id is None: + self.opencti.admin_logger.error( + "[opencti_group] Missing parameters: id and user_id") + return None + self.opencti.admin_logger.info( "Removing member from group", {"groupId": id, "userId": user_id} ) @@ -489,9 +515,7 @@ def delete_member(self, return self.opencti.process_multiple_fields( result["data"]["groupEdit"]["relationDelete"]) - def add_role(self, - id: str, - role_id: str) -> dict: + def add_role(self, **kwargs) -> Dict: """Add a role to a given group :param id: ID to add a role to @@ -501,6 +525,14 @@ def add_role(self, :return: Representation of the group after a role has been added :rtype: dict """ + id = kwargs.get("id", None) + role_id = kwargs.get("role_id", None) + + if id is None or role_id is None: + self.opencti.admin_logger.error( + "[opencti_group] Missing parameters: id and role_id") + return None + self.opencti.admin_logger.info( "Adding role to group", {"groupId": id, "roleId": role_id}) query = ( @@ -527,9 +559,7 @@ def add_role(self, return self.opencti.process_multiple_fields( result["data"]["groupEdit"]["relationAdd"]) - def delete_role(self, - id: str, - role_id: str) -> dict: + def delete_role(self, **kwargs) -> Dict: """Removes a role from a given group :param id: ID to remove role from @@ -539,6 +569,14 @@ def delete_role(self, :return: Representation of the group after role is removed :rtype: dict """ + id = kwargs.get("id", None) + role_id = kwargs.get("role_id", None) + + if id is None or role_id is None: + self.opencti.admin_logger.error( + "[opencti_group] Missing parameters: id and role_id") + return None + self.opencti.admin_logger.info( "Removing role from group", {"groupId": id, "roleId": role_id}) query = ( @@ -558,10 +596,7 @@ def delete_role(self, return self.opencti.process_multiple_fields( result["data"]["groupEdit"]["relationDelete"]) - def edit_default_marking(self, - id: str, - marking_ids: List[str], - entity_type: str = "GLOBAL") -> dict: + def edit_default_marking(self, **kwargs) -> Dict: """Adds a default marking to the group. :param id: ID of the group. @@ -576,6 +611,15 @@ def edit_default_marking(self, :return: Group after adding the default marking. :rtype: dict """ + id = kwargs.get("id", None) + marking_ids = kwargs.get("marking_ids", None) + entity_type = kwargs.get("entity_type", "GLOBAL") + + if id is None or marking_ids is None: + self.opencti.admin_logger.error( + "[opencti_group] Missing parameters: id and marking_ids") + return None + self.opencti.admin_logger.info( "Setting default markings for entity on group", { "markings": marking_ids, @@ -607,18 +651,24 @@ def edit_default_marking(self, return self.opencti.process_multiple_fields( result["data"]["groupEdit"]["editDefaultMarking"]) - def add_allowed_marking(self, - id: str, - marking_id: str) -> dict: + def add_allowed_marking(self, **kwargs) -> Dict: """Allow a group to access a marking :param id: ID of group to authorise :type id: str - :param marking: ID of marking to authorise - :type marking: str + :param marking_id: ID of marking to authorise + :type marking_id: str :return: Relationship from the group to the marking definition :rtype: dict """ + id = kwargs.get("id", None) + marking_id = kwargs.get("marking_id", None) + + if id is None or marking_id is None: + self.opencti.admin_logger.error( + "[opencti_group] Missing parameters: id and marking_id") + return None + self.opencti.admin_logger.info( "Granting group access to marking definition", { "groupId": id, "markingId": marking_id @@ -668,18 +718,24 @@ def add_allowed_marking(self, return self.opencti.process_multiple_fields( result["data"]["groupEdit"]["relationAdd"]) - def delete_allowed_marking(self, - id: str, - marking_id: str) -> dict: + def delete_allowed_marking(self, **kwargs) -> Dict: """Removes access to a marking for a group :param id: ID of group to forbid :type id: str - :param marking: ID of marking to deny - :type marking: str + :param marking_id: ID of marking to deny + :type marking_id: str :return: Group after denying access to marking definition :rtype: dict """ + id = kwargs.get("id", None) + marking_id = kwargs.get("marking_id", None) + + if id is None or marking_id is None: + self.opencti.admin_logger.error( + "[opencti_group] Missing parameters: id and marking_id") + return None + self.opencti.admin_logger.info( "Forbidding group access to marking definition", { "groupId": id, "markingId": marking_id diff --git a/pycti/entities/opencti_role.py b/pycti/entities/opencti_role.py index 7ec12a400..be597f309 100644 --- a/pycti/entities/opencti_role.py +++ b/pycti/entities/opencti_role.py @@ -100,7 +100,7 @@ def list(self, **kwargs) -> List[Dict]: query = ( """ - query Roles($first: Int, $after: ID, $orderBy: RolesOrdering, $orderMode: OrderingMode, $search: String) { + query RoleList($first: Int, $after: ID, $orderBy: RolesOrdering, $orderMode: OrderingMode, $search: String) { roles(first: $first, after: $after, orderBy: $orderBy, orderMode: $orderMode, search: $search) { edges { node { @@ -177,7 +177,7 @@ def read(self, **kwargs) -> Dict: self.opencti.admin_logger.info("Reading role", {"id": id}) query = ( """ - query Role($id: String!) { + query RoleRead($id: String!) { role(id: $id) { """ + (self.properties if customAttributes is None @@ -250,7 +250,7 @@ def create(self, **kwargs) -> Dict: }) query = ( """ - mutation RoleAdd($input: RoleAddInput!) { + mutation RoleCreate($input: RoleAddInput!) { roleAdd(input: $input) { """ + (self.properties if customAttributes is None @@ -308,7 +308,7 @@ def update_field(self, **kwargs) -> Dict: }) query = ( """ - mutation RoleFieldPatch($id: ID!, $input: [EditInput]!) { + mutation RoleUpdate($id: ID!, $input: [EditInput]!) { roleEdit(id: $id) { fieldPatch(input: $input) { """ @@ -405,7 +405,7 @@ def delete_capability(self, **kwargs) -> Dict: }) query = ( """ - mutation RoleDelCapability($id: ID!, $toId: StixRef!) { + mutation RoleDeleteCapability($id: ID!, $toId: StixRef!) { roleEdit(id: $id) { relationDelete(toId: $toId, relationship_type: "has-capability") { """ diff --git a/pycti/entities/opencti_user.py b/pycti/entities/opencti_user.py index 260571f8d..4ac1bcc29 100644 --- a/pycti/entities/opencti_user.py +++ b/pycti/entities/opencti_user.py @@ -273,17 +273,7 @@ def __init__(self, opencti): } """ - def list(self, - first: int = 500, - after: str = None, - orderBy: str = "name", - orderMode: str = "asc", - filters: dict = {}, - search: str = None, - include_sessions: bool = False, - customAttributes: str = None, - getAll: bool = False, - withPagination: bool = False) -> List[Dict]: + def list(self, **kwargs) -> List[Dict]: """Search/list users on the platform Searches users given some conditions. Defaults to listing all users. @@ -324,6 +314,17 @@ def list(self, :return: Returns a list of users, sorted as specified. :rtype: list[dict] """ + first = kwargs.get("first", 500) + after = kwargs.get("after", None) + orderBy = kwargs.get("orderBy", "name") + orderMode = kwargs.get("orderMode", "asc") + filters = kwargs.get("filters", None) + search = kwargs.get("search", None) + include_sessions = kwargs.get("include_sessions", False) + customAttributes = kwargs.get("customAttributes", None) + getAll = kwargs.get("getAll", False) + withPagination = kwargs.get("withPagination", False) + if getAll: first = 100 @@ -331,7 +332,7 @@ def list(self, {"filters": filters}) query = ( """ - query ListUsers($first: Int, $after: ID, $orderBy: UsersOrdering, $orderMode: OrderingMode, $filters: FilterGroup, $search: String) { + query UserList($first: Int, $after: ID, $orderBy: UsersOrdering, $orderMode: OrderingMode, $filters: FilterGroup, $search: String) { users(first: $first, after: $after, orderBy: $orderBy, orderMode: $orderMode, filters: $filters, search: $search) { edges { node { @@ -381,13 +382,7 @@ def list(self, return self.opencti.process_multiple(result["data"]["users"], withPagination) - def read(self, - id: str = None, - include_sessions: bool = False, - include_token: bool = False, - customAttributes: str = None, - filters: dict = None, - search: str = None) -> dict: + def read(self, **kwargs) -> Dict: """Reads user details from the platform. :param id: ID of the user to fetch @@ -408,13 +403,19 @@ def read(self, :return: Representation of the user as a Python dictionary. :rtype: dict """ + id = kwargs.get("id", None) + include_sessions = kwargs.get("include_sessions", False) + include_token = kwargs.get("include_token", False) + customAttributes = kwargs.get("customAttributes", None) + filters = kwargs.get("filters", None) + search = kwargs.get("search", None) if id is not None: self.opencti.admin_logger.info( "Fetching user with ID", {"id": id} ) query = ( """ - query User($id: String!) { + query UserRead($id: String!) { user(id: $id) { """ + (self.properties if customAttributes is None @@ -449,26 +450,7 @@ def read(self, "[opencti_user] Missing paramters: id, search, or filters") return None - def create(self, - name: str, - user_email: str, - password: str = None, - firstname: str = None, - lastname: str = None, - description: str = None, - language: str = None, - theme: str = None, - objectOrganization: List[str] = None, - account_status: str = None, - account_lock_after_date: str = None, - unit_system: str = None, - submenu_show_icons: bool = False, - submenu_auto_collapse: bool = False, - monochrome_labels: bool = False, - groups: List[str] = None, - user_confidence_level: Dict = None, - customAttributes: str = None, - include_token: bool = False) -> dict: + def create(self, **kwargs) -> Dict: """Creates a new user with basic details Note that when SSO is connected users generally do not need to be @@ -529,6 +511,32 @@ def create(self, :return: Representation of the user without sessions or API token. :rtype: dict """ + name = kwargs.get("name", None) + user_email = kwargs.get("user_email", None) + password = kwargs.get("password", None) + firstname = kwargs.get("firstname", None) + lastname = kwargs.get("lastname", None) + description = kwargs.get("description", None) + language = kwargs.get("language", None) + theme = kwargs.get("theme", None) + objectOrganization = kwargs.get("objectOrganization", None) + account_status = kwargs.get("account_status", None) + account_lock_after_date = kwargs.get( + "account_lock_after_date", None) + unit_system = kwargs.get("unit_system", None) + submenu_show_icons = kwargs.get("submenu_show_icons", False) + submenu_auto_collapse = kwargs.get("submenu_auto_collapse", False) + monochrome_labels = kwargs.get("monochrome_labels", False) + groups = kwargs.get("groups", None) + user_confidence_level = kwargs.get("user_confidence_level", None) + customAttributes = kwargs.get("customAttributes", None) + include_token = kwargs.get("include_token", False) + + if name is None or user_email is None: + self.opencti.admin_logger.error( + "[opencti_user] Missing parameters: name and user_email") + return None + self.opencti.admin_logger.info( "Creating a new user", {"name": name, "email": user_email}) if password is None: @@ -572,12 +580,18 @@ def create(self, return self.opencti.process_multiple_fields( result["data"]["userAdd"]) - def delete(self, id: str): + def delete(self, **kwargs): """Deletes the given user from the platform. :param id: ID of the user to delete. :type id: str """ + id = kwargs.get("id", None) + if id is None: + self.opencti.admin_logger.error( + "[opencti_user] Missing parameter: id") + return None + self.opencti.admin_logger.info("Deleting user", {"id": id}) query = """ mutation DeleteUser($id: ID!) { @@ -588,9 +602,7 @@ def delete(self, id: str): """ self.opencti.query(query, {"id": id}) - def me(self, - include_token: bool = False, - customAttributes: str = None) -> dict: + def me(self, **kwargs) -> Dict: """Reads the currently authenticated user. :param include_token: Whether to inclued the API @@ -601,6 +613,9 @@ def me(self, :return: Representation of the user. :rtype: dict """ + include_token = kwargs.get("include_token", False) + customAttributes = kwargs.get("customAttributes", None) + self.opencti.admin_logger.info("Reading MeUser") query = ( """ @@ -618,10 +633,7 @@ def me(self, result = self.opencti.query(query) return self.opencti.process_multiple_fields(result["data"]["me"]) - def update_field(self, - id: str, - input: List[Dict], - customAttributes: str = None) -> Dict: + def update_field(self, **kwargs) -> Dict: """Update a given user using fieldPatch :param id: ID of the user to update. @@ -633,6 +645,14 @@ def update_field(self, :return: Representation of the user without sessions or API token. :rtype: dict """ + id = kwargs.get("id", None) + input = kwargs.get("input", None) + customAttributes = kwargs.get("customAttributes", None) + if id is None or input is None: + self.opencti.admin_logger.error( + "[opencti_user] Missing parameters: id and input") + return None + self.opencti.admin_logger.info( "Editing user with input (not shown to hide password and API token" " changes)", {"id": id}) @@ -654,9 +674,7 @@ def update_field(self, return self.opencti.process_multiple_fields( result["data"]["userEdit"]["fieldPatch"]) - def add_membership(self, - id: str, - group_id: str) -> dict: + def add_membership(self, **kwargs) -> Dict: """Adds the user to a given group. :param id: User ID to add to the group. @@ -666,6 +684,13 @@ def add_membership(self, :return: Representation of the InternalRelationship :rtype: dict """ + id = kwargs.get("id", None) + group_id = kwargs.get("group_id", None) + if id is None or group_id is None: + self.opencti.admin_logger.error( + "[opencti_user] Missing parameters: id and group_id") + return None + self.opencti.admin_logger.info("Adding user to group", { "id": id, "group_id": group_id }) @@ -695,9 +720,7 @@ def add_membership(self, return self.opencti.process_multiple_fields( result["data"]["userEdit"]["relationAdd"]) - def delete_membership(self, - id: str, - group_id: str) -> dict: + def delete_membership(self, **kwargs) -> Dict: """Removes the user from the given group. :param id: User ID to remove from the group. @@ -707,6 +730,13 @@ def delete_membership(self, :return: Representation of the user without sessions or API token :rtype: dict """ + id = kwargs.get("id", None) + group_id = kwargs.get("group_id", None) + if id is None or group_id is None: + self.opencti.admin_logger.error( + "[opencti_user] Missing parameters: id and group_id") + return None + self.opencti.admin_logger.info("Removing used from group", { "id": id, "group_id": group_id}) query = ( @@ -788,9 +818,7 @@ def delete_organization(self, id: str, organization_id: str) -> Dict: return self.opencti.process_multiple_fields( result["data"]["userEdit"]["organizationDelete"]) - def token_renew(self, - id: str, - include_token: bool = False) -> str: + def token_renew(self, **kwargs) -> Dict: """Rotates the API token for the given user :param user: User ID to rotate API token for. @@ -798,9 +826,16 @@ def token_renew(self, :param include_token: Whether to include new API token in response from server, defaults to False. :type include_token: bool, optional - :return: API token if include_token else None. - :rtype: str + :return: Representation of user + :rtype: Dict """ + id = kwargs.get("id", None) + include_token = kwargs.get("include_token", False) + if id is None: + self.opencti.admin_logger.error( + "[opencti_user] Missing parameter: id") + return None + self.opencti.admin_logger.info("Rotating API key for user", {"id": id}) query = ( """ From ebc010238d661ad7d2ff88a92a2625b4b53654ec Mon Sep 17 00:00:00 2001 From: peritz Date: Fri, 24 Jan 2025 14:39:06 +0000 Subject: [PATCH 11/27] Remove unused import from entities test cases --- tests/cases/entities.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/cases/entities.py b/tests/cases/entities.py index d56f7a92e..8f4109246 100644 --- a/tests/cases/entities.py +++ b/tests/cases/entities.py @@ -1,5 +1,3 @@ -import json - from typing import Dict, List, Union from stix2 import TLP_GREEN, TLP_WHITE, AttackPattern From a2b0d1bdf48121d20f02687cdd02ce356c523a9f Mon Sep 17 00:00:00 2001 From: peritz Date: Fri, 24 Jan 2025 14:39:22 +0000 Subject: [PATCH 12/27] Add cleanup code to CRUD tests --- .../entities/test_entity_crud.py | 175 ++++++++++-------- 1 file changed, 94 insertions(+), 81 deletions(-) diff --git a/tests/02-integration/entities/test_entity_crud.py b/tests/02-integration/entities/test_entity_crud.py index 66efb03d9..f44d3e822 100644 --- a/tests/02-integration/entities/test_entity_crud.py +++ b/tests/02-integration/entities/test_entity_crud.py @@ -15,69 +15,77 @@ def entity_class(entity): def test_entity_create(entity_class): class_data = entity_class.data() test_indicator = entity_class.own_class().create(**class_data) - assert test_indicator is not None, "Response is NoneType" - assert "id" in test_indicator, "No ID on object" - - entity_class.base_class().delete(id=test_indicator["id"]) + try: + assert test_indicator is not None, "Response is NoneType" + assert "id" in test_indicator, "No ID on object" + finally: + entity_class.base_class().delete(id=test_indicator["id"]) def test_read(entity_class): class_data = entity_class.data() test_indicator = entity_class.own_class().create(**class_data) - assert test_indicator is not None, "Response is NoneType" - assert "id" in test_indicator, "No ID on object" - assert "standard_id" in test_indicator, "No standard_id (STIX ID) on object" - test_indicator = entity_class.own_class().read(id=test_indicator["id"]) - compare_values( - class_data, - test_indicator, - entity_class.get_compare_exception_keys(), - ) + try: + assert test_indicator is not None, "Response is NoneType" + assert "id" in test_indicator, "No ID on object" + assert "standard_id" in test_indicator, "No standard_id (STIX ID) on object" + test_indicator = entity_class.own_class().read(id=test_indicator["id"]) + compare_values( + class_data, + test_indicator, + entity_class.get_compare_exception_keys(), + ) - entity_class.base_class().delete(id=test_indicator["id"]) + finally: + entity_class.base_class().delete(id=test_indicator["id"]) def test_update(entity_class): class_data = entity_class.data() test_indicator = entity_class.own_class().create(**class_data) - assert test_indicator is not None, "Response is NoneType" - assert "id" in test_indicator, "No ID on object" - - if len(entity_class.update_data()) > 0: - function_present = getattr( - entity_class.own_class(), "update_field", None) - if function_present: - for update_field, update_value in entity_class.update_data().items(): - class_data[update_field] = update_value - input = [{"key": update_field, "value": update_value}] - result = entity_class.own_class().update_field( - id=test_indicator["id"], input=input - ) + try: + assert test_indicator is not None, "Response is NoneType" + assert "id" in test_indicator, "No ID on object" + + if len(entity_class.update_data()) > 0: + function_present = getattr( + entity_class.own_class(), "update_field", None) + if function_present: + for update_field, update_value in entity_class.update_data().items(): + class_data[update_field] = update_value + input = [{"key": update_field, "value": update_value}] + result = entity_class.own_class().update_field( + id=test_indicator["id"], input=input + ) + else: + for update_field, update_value in entity_class.update_data().items(): + class_data[update_field] = update_value + class_data["update"] = True + result = entity_class.own_class().create(**class_data) + + result = entity_class.own_class().read(id=result["id"]) + assert result["id"] == test_indicator["id"], "Updated SDO does not match old ID" + compare_values(class_data, result, + entity_class.get_compare_exception_keys()) else: - for update_field, update_value in entity_class.update_data().items(): - class_data[update_field] = update_value - class_data["update"] = True - result = entity_class.own_class().create(**class_data) - - result = entity_class.own_class().read(id=result["id"]) - assert result["id"] == test_indicator["id"], "Updated SDO does not match old ID" - compare_values(class_data, result, - entity_class.get_compare_exception_keys()) - else: - result = test_indicator + result = test_indicator - entity_class.base_class().delete(id=result["id"]) + finally: + entity_class.base_class().delete(id=result["id"]) def test_delete(entity_class): class_data = entity_class.data() test_indicator = entity_class.own_class().create(**class_data) - assert test_indicator is not None, "Response is NoneType" - assert "id" in test_indicator, "No ID on object" - result = entity_class.base_class().delete(id=test_indicator["id"]) - assert result is None, f"Delete returned value '{result}'" - result = entity_class.own_class().read(id=test_indicator["id"]) - assert result is None, f"Read returned value '{result}' after delete" + try: + assert test_indicator is not None, "Response is NoneType" + assert "id" in test_indicator, "No ID on object" + result = entity_class.base_class().delete(id=test_indicator["id"]) + assert result is None, f"Delete returned value '{result}'" + result = entity_class.own_class().read(id=test_indicator["id"]) + assert result is None, f"Read returned value '{result}' after delete" + except AssertionError: + entity_class.base_class().delete(id=test_indicator["id"]) def test_filter(entity_class): @@ -86,16 +94,18 @@ def test_filter(entity_class): class_data = entity_class.data() test_indicator = entity_class.own_class().create(**class_data) - assert test_indicator is not None, "Response is NoneType" - assert "id" in test_indicator, "No ID on object" - test_indicator = entity_class.own_class().read(filters=entity_class.get_filter()) - compare_values( - class_data, - test_indicator, - entity_class.get_compare_exception_keys(), - ) - - entity_class.base_class().delete(id=test_indicator["id"]) + try: + assert test_indicator is not None, "Response is NoneType" + assert "id" in test_indicator, "No ID on object" + test_indicator = entity_class.own_class().read( + filters=entity_class.get_filter()) + compare_values( + class_data, + test_indicator, + entity_class.get_compare_exception_keys(), + ) + finally: + entity_class.base_class().delete(id=test_indicator["id"]) def test_search(entity_class): @@ -104,16 +114,17 @@ def test_search(entity_class): class_data = entity_class.data() test_indicator = entity_class.own_class().create(**class_data) - assert test_indicator is not None, "Response is NoneType" - assert "id" in test_indicator, "No ID on object" - test_indicator = entity_class.own_class().read(search=entity_class.get_search()) - compare_values( - class_data, - test_indicator, - entity_class.get_compare_exception_keys(), - ) - - entity_class.base_class().delete(id=test_indicator["id"]) + try: + assert test_indicator is not None, "Response is NoneType" + assert "id" in test_indicator, "No ID on object" + test_indicator = entity_class.own_class().read(search=entity_class.get_search()) + compare_values( + class_data, + test_indicator, + entity_class.get_compare_exception_keys(), + ) + finally: + entity_class.base_class().delete(id=test_indicator["id"]) def test_relation(entity_class): @@ -123,19 +134,21 @@ def test_relation(entity_class): class_data2 = entity_class.relation_test() test_indicator = entity_class.own_class().create(**class_data) test_indicator2 = entity_class.own_class().create(**class_data2) - assert test_indicator is not None, "Response is NoneType" - assert "id" in test_indicator, "No ID on object" - entity_class.own_class().add_stix_object_or_stix_relationship( - id=test_indicator["id"], - stixObjectOrStixRelationshipId=test_indicator2["id"], - ) - result = entity_class.own_class().read(id=test_indicator["id"]) - assert result["objectsIds"][0] == test_indicator2["id"] - entity_class.own_class().remove_stix_object_or_stix_relationship( - id=test_indicator["id"], - stixObjectOrStixRelationshipId=test_indicator2["id"], - ) - result = entity_class.own_class().read(id=test_indicator["id"]) - assert len(result["objectsIds"]) == 0 - entity_class.base_class().delete(id=test_indicator["id"]) - entity_class.base_class().delete(id=test_indicator2["id"]) + try: + assert test_indicator is not None, "Response is NoneType" + assert "id" in test_indicator, "No ID on object" + entity_class.own_class().add_stix_object_or_stix_relationship( + id=test_indicator["id"], + stixObjectOrStixRelationshipId=test_indicator2["id"], + ) + result = entity_class.own_class().read(id=test_indicator["id"]) + assert result["objectsIds"][0] == test_indicator2["id"] + entity_class.own_class().remove_stix_object_or_stix_relationship( + id=test_indicator["id"], + stixObjectOrStixRelationshipId=test_indicator2["id"], + ) + result = entity_class.own_class().read(id=test_indicator["id"]) + assert len(result["objectsIds"]) == 0 + finally: + entity_class.base_class().delete(id=test_indicator["id"]) + entity_class.base_class().delete(id=test_indicator2["id"]) From 917459fd6651cb703f310248b63ee8385dad5b61 Mon Sep 17 00:00:00 2001 From: peritz Date: Fri, 24 Jan 2025 15:16:59 +0000 Subject: [PATCH 13/27] Update documentation for new administrative entities --- docs/pycti/pycti.api.opencti_api_client.rst | 1 + .../pycti/pycti.api.opencti_api_connector.rst | 1 + docs/pycti/pycti.api.opencti_api_job.rst | 1 + docs/pycti/pycti.api.rst | 1 + .../pycti.connector.opencti_connector.rst | 1 + ...cti.connector.opencti_connector_helper.rst | 1 + docs/pycti/pycti.connector.rst | 1 + .../pycti.entities.opencti_attack_pattern.rst | 1 + .../pycti/pycti.entities.opencti_campaign.rst | 1 + .../pycti.entities.opencti_capability.rst | 11 ++ ...ycti.entities.opencti_course_of_action.rst | 1 + ...ti.entities.opencti_external_reference.rst | 1 + docs/pycti/pycti.entities.opencti_group.rst | 11 ++ .../pycti/pycti.entities.opencti_identity.rst | 1 + .../pycti/pycti.entities.opencti_incident.rst | 1 + .../pycti.entities.opencti_indicator.rst | 1 + .../pycti.entities.opencti_intrusion_set.rst | 1 + ...ycti.entities.opencti_kill_chain_phase.rst | 1 + docs/pycti/pycti.entities.opencti_malware.rst | 1 + ...ti.entities.opencti_marking_definition.rst | 1 + docs/pycti/pycti.entities.opencti_note.rst | 1 + docs/pycti/pycti.entities.opencti_opinion.rst | 1 + docs/pycti/pycti.entities.opencti_report.rst | 1 + docs/pycti/pycti.entities.opencti_role.rst | 11 ++ .../pycti/pycti.entities.opencti_settings.rst | 11 ++ ...ti.entities.opencti_stix_domain_entity.rst | 1 + .../pycti.entities.opencti_stix_entity.rst | 1 + ...pycti.entities.opencti_stix_observable.rst | 1 + ...ities.opencti_stix_observable_relation.rst | 1 + .../pycti.entities.opencti_stix_relation.rst | 1 + .../pycti.entities.opencti_stix_sighting.rst | 1 + docs/pycti/pycti.entities.opencti_tag.rst | 1 + .../pycti.entities.opencti_threat_actor.rst | 1 + docs/pycti/pycti.entities.opencti_tool.rst | 1 + docs/pycti/pycti.entities.opencti_user.rst | 11 ++ .../pycti.entities.opencti_vulnerability.rst | 1 + docs/pycti/pycti.entities.rst | 6 + docs/pycti/pycti.rst | 140 +++++++++++++++++- docs/pycti/pycti.utils.constants.rst | 1 + docs/pycti/pycti.utils.opencti_stix2.rst | 1 + docs/pycti/pycti.utils.rst | 1 + pycti/__init__.py | 13 ++ pycti/entities/opencti_capability.py | 10 +- pycti/entities/opencti_group.py | 69 +-------- pycti/entities/opencti_role.py | 16 -- pycti/entities/opencti_settings.py | 10 ++ pycti/entities/opencti_user.py | 95 ------------ 47 files changed, 258 insertions(+), 190 deletions(-) create mode 100644 docs/pycti/pycti.entities.opencti_capability.rst create mode 100644 docs/pycti/pycti.entities.opencti_group.rst create mode 100644 docs/pycti/pycti.entities.opencti_role.rst create mode 100644 docs/pycti/pycti.entities.opencti_settings.rst create mode 100644 docs/pycti/pycti.entities.opencti_user.rst diff --git a/docs/pycti/pycti.api.opencti_api_client.rst b/docs/pycti/pycti.api.opencti_api_client.rst index 654641f65..5c6f0ef1f 100644 --- a/docs/pycti/pycti.api.opencti_api_client.rst +++ b/docs/pycti/pycti.api.opencti_api_client.rst @@ -3,6 +3,7 @@ ================================ .. automodule:: pycti.api.opencti_api_client + :members: .. contents:: :local: diff --git a/docs/pycti/pycti.api.opencti_api_connector.rst b/docs/pycti/pycti.api.opencti_api_connector.rst index 0cf27329b..166ef3826 100644 --- a/docs/pycti/pycti.api.opencti_api_connector.rst +++ b/docs/pycti/pycti.api.opencti_api_connector.rst @@ -3,6 +3,7 @@ =================================== .. automodule:: pycti.api.opencti_api_connector + :members: .. contents:: :local: diff --git a/docs/pycti/pycti.api.opencti_api_job.rst b/docs/pycti/pycti.api.opencti_api_job.rst index 9f0703a6b..dc1d99cbb 100644 --- a/docs/pycti/pycti.api.opencti_api_job.rst +++ b/docs/pycti/pycti.api.opencti_api_job.rst @@ -3,6 +3,7 @@ ============================= .. automodule:: pycti.api.opencti_api_job + :members: .. contents:: :local: diff --git a/docs/pycti/pycti.api.rst b/docs/pycti/pycti.api.rst index 5c71211bc..2f21aad56 100644 --- a/docs/pycti/pycti.api.rst +++ b/docs/pycti/pycti.api.rst @@ -3,6 +3,7 @@ ============= .. automodule:: pycti.api + :members: .. contents:: :local: diff --git a/docs/pycti/pycti.connector.opencti_connector.rst b/docs/pycti/pycti.connector.opencti_connector.rst index 39ef7df12..367ce2ba6 100644 --- a/docs/pycti/pycti.connector.opencti_connector.rst +++ b/docs/pycti/pycti.connector.opencti_connector.rst @@ -3,6 +3,7 @@ ===================================== .. automodule:: pycti.connector.opencti_connector + :members: .. contents:: :local: diff --git a/docs/pycti/pycti.connector.opencti_connector_helper.rst b/docs/pycti/pycti.connector.opencti_connector_helper.rst index 4522c42db..69d779cfe 100644 --- a/docs/pycti/pycti.connector.opencti_connector_helper.rst +++ b/docs/pycti/pycti.connector.opencti_connector_helper.rst @@ -3,6 +3,7 @@ ============================================ .. automodule:: pycti.connector.opencti_connector_helper + :members: .. contents:: :local: diff --git a/docs/pycti/pycti.connector.rst b/docs/pycti/pycti.connector.rst index c7abab79f..1d191c299 100644 --- a/docs/pycti/pycti.connector.rst +++ b/docs/pycti/pycti.connector.rst @@ -3,6 +3,7 @@ =================== .. automodule:: pycti.connector + :members: .. contents:: :local: diff --git a/docs/pycti/pycti.entities.opencti_attack_pattern.rst b/docs/pycti/pycti.entities.opencti_attack_pattern.rst index 58f1ed0af..ea70f85fc 100644 --- a/docs/pycti/pycti.entities.opencti_attack_pattern.rst +++ b/docs/pycti/pycti.entities.opencti_attack_pattern.rst @@ -3,6 +3,7 @@ ========================================= .. automodule:: pycti.entities.opencti_attack_pattern + :members: .. contents:: :local: diff --git a/docs/pycti/pycti.entities.opencti_campaign.rst b/docs/pycti/pycti.entities.opencti_campaign.rst index bfd832e6b..406f55294 100644 --- a/docs/pycti/pycti.entities.opencti_campaign.rst +++ b/docs/pycti/pycti.entities.opencti_campaign.rst @@ -3,6 +3,7 @@ =================================== .. automodule:: pycti.entities.opencti_campaign + :members: .. contents:: :local: diff --git a/docs/pycti/pycti.entities.opencti_capability.rst b/docs/pycti/pycti.entities.opencti_capability.rst new file mode 100644 index 000000000..5abde4003 --- /dev/null +++ b/docs/pycti/pycti.entities.opencti_capability.rst @@ -0,0 +1,11 @@ +==================================== +``pycti.entities.opencti_capability`` +==================================== + +.. automodule:: pycti.entities.opencti_capability + :members: + + .. contents:: + :local: + +.. currentmodule:: pycti.entities.opencti_capability diff --git a/docs/pycti/pycti.entities.opencti_course_of_action.rst b/docs/pycti/pycti.entities.opencti_course_of_action.rst index a969db351..826867702 100644 --- a/docs/pycti/pycti.entities.opencti_course_of_action.rst +++ b/docs/pycti/pycti.entities.opencti_course_of_action.rst @@ -3,6 +3,7 @@ =========================================== .. automodule:: pycti.entities.opencti_course_of_action + :members: .. contents:: :local: diff --git a/docs/pycti/pycti.entities.opencti_external_reference.rst b/docs/pycti/pycti.entities.opencti_external_reference.rst index 5a49d4904..06e1e3716 100644 --- a/docs/pycti/pycti.entities.opencti_external_reference.rst +++ b/docs/pycti/pycti.entities.opencti_external_reference.rst @@ -3,6 +3,7 @@ ============================================= .. automodule:: pycti.entities.opencti_external_reference + :members: .. contents:: :local: diff --git a/docs/pycti/pycti.entities.opencti_group.rst b/docs/pycti/pycti.entities.opencti_group.rst new file mode 100644 index 000000000..58ec5dd2a --- /dev/null +++ b/docs/pycti/pycti.entities.opencti_group.rst @@ -0,0 +1,11 @@ +==================================== +``pycti.entities.opencti_group`` +==================================== + +.. automodule:: pycti.entities.opencti_group + :members: + + .. contents:: + :local: + +.. currentmodule:: pycti.entities.opencti_group diff --git a/docs/pycti/pycti.entities.opencti_identity.rst b/docs/pycti/pycti.entities.opencti_identity.rst index d75199094..0a9b281b7 100644 --- a/docs/pycti/pycti.entities.opencti_identity.rst +++ b/docs/pycti/pycti.entities.opencti_identity.rst @@ -3,6 +3,7 @@ =================================== .. automodule:: pycti.entities.opencti_identity + :members: .. contents:: :local: diff --git a/docs/pycti/pycti.entities.opencti_incident.rst b/docs/pycti/pycti.entities.opencti_incident.rst index 28398afe4..f4e34d67c 100644 --- a/docs/pycti/pycti.entities.opencti_incident.rst +++ b/docs/pycti/pycti.entities.opencti_incident.rst @@ -3,6 +3,7 @@ =================================== .. automodule:: pycti.entities.opencti_incident + :members: .. contents:: :local: diff --git a/docs/pycti/pycti.entities.opencti_indicator.rst b/docs/pycti/pycti.entities.opencti_indicator.rst index 491bed684..ac8feb75a 100644 --- a/docs/pycti/pycti.entities.opencti_indicator.rst +++ b/docs/pycti/pycti.entities.opencti_indicator.rst @@ -3,6 +3,7 @@ ==================================== .. automodule:: pycti.entities.opencti_indicator + :members: .. contents:: :local: diff --git a/docs/pycti/pycti.entities.opencti_intrusion_set.rst b/docs/pycti/pycti.entities.opencti_intrusion_set.rst index 30f952862..2a731f7e9 100644 --- a/docs/pycti/pycti.entities.opencti_intrusion_set.rst +++ b/docs/pycti/pycti.entities.opencti_intrusion_set.rst @@ -3,6 +3,7 @@ ======================================== .. automodule:: pycti.entities.opencti_intrusion_set + :members: .. contents:: :local: diff --git a/docs/pycti/pycti.entities.opencti_kill_chain_phase.rst b/docs/pycti/pycti.entities.opencti_kill_chain_phase.rst index 3702804d7..97b264cdd 100644 --- a/docs/pycti/pycti.entities.opencti_kill_chain_phase.rst +++ b/docs/pycti/pycti.entities.opencti_kill_chain_phase.rst @@ -3,6 +3,7 @@ =========================================== .. automodule:: pycti.entities.opencti_kill_chain_phase + :members: .. contents:: :local: diff --git a/docs/pycti/pycti.entities.opencti_malware.rst b/docs/pycti/pycti.entities.opencti_malware.rst index a3c237845..fe9f9abca 100644 --- a/docs/pycti/pycti.entities.opencti_malware.rst +++ b/docs/pycti/pycti.entities.opencti_malware.rst @@ -3,6 +3,7 @@ ================================== .. automodule:: pycti.entities.opencti_malware + :members: .. contents:: :local: diff --git a/docs/pycti/pycti.entities.opencti_marking_definition.rst b/docs/pycti/pycti.entities.opencti_marking_definition.rst index 84407f6fa..1a49845c7 100644 --- a/docs/pycti/pycti.entities.opencti_marking_definition.rst +++ b/docs/pycti/pycti.entities.opencti_marking_definition.rst @@ -3,6 +3,7 @@ ============================================= .. automodule:: pycti.entities.opencti_marking_definition + :members: .. contents:: :local: diff --git a/docs/pycti/pycti.entities.opencti_note.rst b/docs/pycti/pycti.entities.opencti_note.rst index 407e257ad..4729defdf 100644 --- a/docs/pycti/pycti.entities.opencti_note.rst +++ b/docs/pycti/pycti.entities.opencti_note.rst @@ -3,6 +3,7 @@ =============================== .. automodule:: pycti.entities.opencti_note + :members: .. contents:: :local: diff --git a/docs/pycti/pycti.entities.opencti_opinion.rst b/docs/pycti/pycti.entities.opencti_opinion.rst index 9d682098e..7ad46df43 100644 --- a/docs/pycti/pycti.entities.opencti_opinion.rst +++ b/docs/pycti/pycti.entities.opencti_opinion.rst @@ -3,6 +3,7 @@ ================================== .. automodule:: pycti.entities.opencti_opinion + :members: .. contents:: :local: diff --git a/docs/pycti/pycti.entities.opencti_report.rst b/docs/pycti/pycti.entities.opencti_report.rst index 85041ed80..63ea51c1e 100644 --- a/docs/pycti/pycti.entities.opencti_report.rst +++ b/docs/pycti/pycti.entities.opencti_report.rst @@ -3,6 +3,7 @@ ================================= .. automodule:: pycti.entities.opencti_report + :members: .. contents:: :local: diff --git a/docs/pycti/pycti.entities.opencti_role.rst b/docs/pycti/pycti.entities.opencti_role.rst new file mode 100644 index 000000000..6e34c766c --- /dev/null +++ b/docs/pycti/pycti.entities.opencti_role.rst @@ -0,0 +1,11 @@ +==================================== +``pycti.entities.opencti_role`` +==================================== + +.. automodule:: pycti.entities.opencti_role + :members: + + .. contents:: + :local: + +.. currentmodule:: pycti.entities.opencti_role diff --git a/docs/pycti/pycti.entities.opencti_settings.rst b/docs/pycti/pycti.entities.opencti_settings.rst new file mode 100644 index 000000000..7eda44768 --- /dev/null +++ b/docs/pycti/pycti.entities.opencti_settings.rst @@ -0,0 +1,11 @@ +==================================== +``pycti.entities.opencti_settings`` +==================================== + +.. automodule:: pycti.entities.opencti_settings + :members: + + .. contents:: + :local: + +.. currentmodule:: pycti.entities.opencti_settings diff --git a/docs/pycti/pycti.entities.opencti_stix_domain_entity.rst b/docs/pycti/pycti.entities.opencti_stix_domain_entity.rst index a2f9848f6..2b970281a 100644 --- a/docs/pycti/pycti.entities.opencti_stix_domain_entity.rst +++ b/docs/pycti/pycti.entities.opencti_stix_domain_entity.rst @@ -3,6 +3,7 @@ ============================================= .. automodule:: pycti.entities.opencti_stix_domain_object + :members: .. contents:: :local: diff --git a/docs/pycti/pycti.entities.opencti_stix_entity.rst b/docs/pycti/pycti.entities.opencti_stix_entity.rst index e901dcc2a..38be5bf92 100644 --- a/docs/pycti/pycti.entities.opencti_stix_entity.rst +++ b/docs/pycti/pycti.entities.opencti_stix_entity.rst @@ -3,6 +3,7 @@ ====================================== .. automodule:: pycti.entities.opencti_stix_entity + :members: .. contents:: :local: diff --git a/docs/pycti/pycti.entities.opencti_stix_observable.rst b/docs/pycti/pycti.entities.opencti_stix_observable.rst index 37cb95451..4b486732d 100644 --- a/docs/pycti/pycti.entities.opencti_stix_observable.rst +++ b/docs/pycti/pycti.entities.opencti_stix_observable.rst @@ -3,6 +3,7 @@ ========================================== .. automodule:: pycti.entities.opencti_stix_observable + :members: .. contents:: :local: diff --git a/docs/pycti/pycti.entities.opencti_stix_observable_relation.rst b/docs/pycti/pycti.entities.opencti_stix_observable_relation.rst index eac81d4ed..c32e4faf8 100644 --- a/docs/pycti/pycti.entities.opencti_stix_observable_relation.rst +++ b/docs/pycti/pycti.entities.opencti_stix_observable_relation.rst @@ -3,6 +3,7 @@ =================================================== .. automodule:: pycti.entities.opencti_stix_observable_relationship + :members: .. contents:: :local: diff --git a/docs/pycti/pycti.entities.opencti_stix_relation.rst b/docs/pycti/pycti.entities.opencti_stix_relation.rst index 2b148b37f..925160789 100644 --- a/docs/pycti/pycti.entities.opencti_stix_relation.rst +++ b/docs/pycti/pycti.entities.opencti_stix_relation.rst @@ -3,6 +3,7 @@ ======================================== .. automodule:: pycti.entities.opencti_stix_relation + :members: .. contents:: :local: diff --git a/docs/pycti/pycti.entities.opencti_stix_sighting.rst b/docs/pycti/pycti.entities.opencti_stix_sighting.rst index 42a077f5d..fc018cee3 100644 --- a/docs/pycti/pycti.entities.opencti_stix_sighting.rst +++ b/docs/pycti/pycti.entities.opencti_stix_sighting.rst @@ -3,6 +3,7 @@ ======================================== .. automodule:: pycti.entities.opencti_stix_sighting + :members: .. contents:: :local: diff --git a/docs/pycti/pycti.entities.opencti_tag.rst b/docs/pycti/pycti.entities.opencti_tag.rst index d9dd33116..de5f3c51a 100644 --- a/docs/pycti/pycti.entities.opencti_tag.rst +++ b/docs/pycti/pycti.entities.opencti_tag.rst @@ -3,6 +3,7 @@ ============================== .. automodule:: pycti.entities.opencti_tag + :members: .. contents:: :local: diff --git a/docs/pycti/pycti.entities.opencti_threat_actor.rst b/docs/pycti/pycti.entities.opencti_threat_actor.rst index d2bb80186..fdc9c92af 100644 --- a/docs/pycti/pycti.entities.opencti_threat_actor.rst +++ b/docs/pycti/pycti.entities.opencti_threat_actor.rst @@ -3,6 +3,7 @@ ======================================= .. automodule:: pycti.entities.opencti_threat_actor + :members: .. contents:: :local: diff --git a/docs/pycti/pycti.entities.opencti_tool.rst b/docs/pycti/pycti.entities.opencti_tool.rst index e546b5393..01d0c4ee8 100644 --- a/docs/pycti/pycti.entities.opencti_tool.rst +++ b/docs/pycti/pycti.entities.opencti_tool.rst @@ -3,6 +3,7 @@ =============================== .. automodule:: pycti.entities.opencti_tool + :members: .. contents:: :local: diff --git a/docs/pycti/pycti.entities.opencti_user.rst b/docs/pycti/pycti.entities.opencti_user.rst new file mode 100644 index 000000000..0aa82efb6 --- /dev/null +++ b/docs/pycti/pycti.entities.opencti_user.rst @@ -0,0 +1,11 @@ +==================================== +``pycti.entities.opencti_user`` +==================================== + +.. automodule:: pycti.entities.opencti_user + :members: + + .. contents:: + :local: + +.. currentmodule:: pycti.entities.opencti_user diff --git a/docs/pycti/pycti.entities.opencti_vulnerability.rst b/docs/pycti/pycti.entities.opencti_vulnerability.rst index cd1bc402e..02391de05 100644 --- a/docs/pycti/pycti.entities.opencti_vulnerability.rst +++ b/docs/pycti/pycti.entities.opencti_vulnerability.rst @@ -3,6 +3,7 @@ ======================================== .. automodule:: pycti.entities.opencti_vulnerability + :members: .. contents:: :local: diff --git a/docs/pycti/pycti.entities.rst b/docs/pycti/pycti.entities.rst index d229fdab5..c76ede085 100644 --- a/docs/pycti/pycti.entities.rst +++ b/docs/pycti/pycti.entities.rst @@ -3,6 +3,7 @@ ================== .. automodule:: pycti.entities + :members: .. contents:: :local: @@ -37,5 +38,10 @@ Submodules pycti.entities.opencti_threat_actor pycti.entities.opencti_tool pycti.entities.opencti_vulnerability + pycti.entities.opencti_capability + pycti.entities.opencti_role + pycti.entities.opencti_group + pycti.entities.opencti_user + pycti.entities.opencti_settings .. currentmodule:: pycti.entities diff --git a/docs/pycti/pycti.rst b/docs/pycti/pycti.rst index b5829609e..0a7a9ea49 100644 --- a/docs/pycti/pycti.rst +++ b/docs/pycti/pycti.rst @@ -38,11 +38,14 @@ Classes - :py:class:`CaseRft`: Undocumented. +- :py:class:`Channel`: + Undocumented. + - :py:class:`Task`: Undocumented. - :py:class:`ConnectorType`: - Create a collection of name/value pairs. + An enumeration. - :py:class:`CourseOfAction`: Undocumented. @@ -141,22 +144,22 @@ Classes Undocumented. - :py:class:`StixCyberObservable`: - Undocumented. + deprecated [>=6.2 & <6.5]` - :py:class:`StixNestedRefRelationship`: Undocumented. - :py:class:`StixCyberObservableTypes`: - Create a collection of name/value pairs. + An enumeration. - :py:class:`StixDomainObject`: Undocumented. - :py:class:`StixMetaTypes`: - Create a collection of name/value pairs. + An enumeration. - :py:class:`MultipleRefRelationship`: - Create a collection of name/value pairs. + An enumeration. - :py:class:`StixObjectOrStixRelationship`: Undocumented. @@ -185,18 +188,54 @@ Classes - :py:class:`CustomObjectTask`: Task object. +- :py:class:`CustomObjectChannel`: + Channel object. + +- :py:class:`CustomObservableCredential`: + Credential observable. + - :py:class:`CustomObservableHostname`: Hostname observable. - :py:class:`CustomObservableUserAgent`: User-Agent observable. +- :py:class:`CustomObservableBankAccount`: + Bank Account observable. + - :py:class:`CustomObservableCryptocurrencyWallet`: Cryptocurrency wallet observable. +- :py:class:`CustomObservablePaymentCard`: + Payment card observable. + +- :py:class:`CustomObservablePhoneNumber`: + Phone number observable. + +- :py:class:`CustomObservableTrackingNumber`: + Tracking number observable. + - :py:class:`CustomObservableText`: Text observable. +- :py:class:`CustomObservableMediaContent`: + Media-Content observable. + +- :py:class:`Capability`: + Represents a role capability on the OpenCTI platform + +- :py:class:`Role`: + Representation of a role in OpenCTI + +- :py:class:`Group`: + Representation of a Group in OpenCTI + +- :py:class:`User`: + Representation of a user on the OpenCTI platform + +- :py:class:`Settings`: + Represents the Settings object in OpenCTI + .. autoclass:: AttackPattern :members: @@ -233,6 +272,13 @@ Classes .. inheritance-diagram:: CaseRft :parts: 1 +.. autoclass:: Channel + :members: + + .. rubric:: Inheritance + .. inheritance-diagram:: Channel + :parts: 1 + .. autoclass:: Task :members: @@ -576,6 +622,20 @@ Classes .. inheritance-diagram:: CustomObjectTask :parts: 1 +.. autoclass:: CustomObjectChannel + :members: + + .. rubric:: Inheritance + .. inheritance-diagram:: CustomObjectChannel + :parts: 1 + +.. autoclass:: CustomObservableCredential + :members: + + .. rubric:: Inheritance + .. inheritance-diagram:: CustomObservableCredential + :parts: 1 + .. autoclass:: CustomObservableHostname :members: @@ -590,6 +650,13 @@ Classes .. inheritance-diagram:: CustomObservableUserAgent :parts: 1 +.. autoclass:: CustomObservableBankAccount + :members: + + .. rubric:: Inheritance + .. inheritance-diagram:: CustomObservableBankAccount + :parts: 1 + .. autoclass:: CustomObservableCryptocurrencyWallet :members: @@ -597,6 +664,27 @@ Classes .. inheritance-diagram:: CustomObservableCryptocurrencyWallet :parts: 1 +.. autoclass:: CustomObservablePaymentCard + :members: + + .. rubric:: Inheritance + .. inheritance-diagram:: CustomObservablePaymentCard + :parts: 1 + +.. autoclass:: CustomObservablePhoneNumber + :members: + + .. rubric:: Inheritance + .. inheritance-diagram:: CustomObservablePhoneNumber + :parts: 1 + +.. autoclass:: CustomObservableTrackingNumber + :members: + + .. rubric:: Inheritance + .. inheritance-diagram:: CustomObservableTrackingNumber + :parts: 1 + .. autoclass:: CustomObservableText :members: @@ -604,6 +692,48 @@ Classes .. inheritance-diagram:: CustomObservableText :parts: 1 +.. autoclass:: CustomObservableMediaContent + :members: + + .. rubric:: Inheritance + .. inheritance-diagram:: CustomObservableMediaContent + :parts: 1 + +.. autoclass:: Capability + :members: + + .. rubric:: Inheritance + .. inheritance-diagram:: Capability + :parts: 1 + +.. autoclass:: Role + :members: + + .. rubric:: Inheritance + .. inheritance-diagram:: Role + :parts: 1 + +.. autoclass:: Group + :members: + + .. rubric:: Inheritance + .. inheritance-diagram:: Group + :parts: 1 + +.. autoclass:: User + :members: + + .. rubric:: Inheritance + .. inheritance-diagram:: User + :parts: 1 + +.. autoclass:: Settings + :members: + + .. rubric:: Inheritance + .. inheritance-diagram:: Settings + :parts: 1 + Variables ========= diff --git a/docs/pycti/pycti.utils.constants.rst b/docs/pycti/pycti.utils.constants.rst index 130003518..015922ad8 100644 --- a/docs/pycti/pycti.utils.constants.rst +++ b/docs/pycti/pycti.utils.constants.rst @@ -3,6 +3,7 @@ ========================= .. automodule:: pycti.utils.constants + :members: .. contents:: :local: diff --git a/docs/pycti/pycti.utils.opencti_stix2.rst b/docs/pycti/pycti.utils.opencti_stix2.rst index 172580950..5830291fa 100644 --- a/docs/pycti/pycti.utils.opencti_stix2.rst +++ b/docs/pycti/pycti.utils.opencti_stix2.rst @@ -3,6 +3,7 @@ ============================= .. automodule:: pycti.utils.opencti_stix2 + :members: .. contents:: :local: diff --git a/docs/pycti/pycti.utils.rst b/docs/pycti/pycti.utils.rst index bfbea7224..d0e4a2209 100644 --- a/docs/pycti/pycti.utils.rst +++ b/docs/pycti/pycti.utils.rst @@ -3,6 +3,7 @@ =============== .. automodule:: pycti.utils + :members: .. contents:: :local: diff --git a/pycti/__init__.py b/pycti/__init__.py index c9c82e48c..1fd44d59e 100644 --- a/pycti/__init__.py +++ b/pycti/__init__.py @@ -51,6 +51,14 @@ from .entities.opencti_threat_actor_individual import ThreatActorIndividual from .entities.opencti_tool import Tool from .entities.opencti_vulnerability import Vulnerability + +# Administrative entities +from .entities.opencti_capability import Capability +from .entities.opencti_role import Role +from .entities.opencti_group import Group +from .entities.opencti_user import User +from .entities.opencti_settings import Settings + from .utils.constants import ( CustomObjectCaseIncident, CustomObjectChannel, @@ -151,4 +159,9 @@ "STIX_EXT_MITRE", "STIX_EXT_OCTI_SCO", "STIX_EXT_OCTI", + "Capability", + "Role", + "Group", + "User", + "Settings", ] diff --git a/pycti/entities/opencti_capability.py b/pycti/entities/opencti_capability.py index b1ab2b770..47a10ea98 100644 --- a/pycti/entities/opencti_capability.py +++ b/pycti/entities/opencti_capability.py @@ -4,13 +4,8 @@ class Capability: """Represents a role capability on the OpenCTI platform - The dictionary representation of a Capability has the following form:: - - { - "id": "UUID", - "name": "Name of the capability, e.g. KNUPDATE", - "description": "Create/Update knowledge" - }. + See the properties attribute to understand which properties are fetched by + default from the graphql queries. """ def __init__(self, opencti): @@ -24,6 +19,7 @@ def __init__(self, opencti): """ def list(self) -> List[Dict]: + """Lists all capabilities available on the platform""" self.opencti.admin_logger.info("Listing capabilities") query = ( """ diff --git a/pycti/entities/opencti_group.py b/pycti/entities/opencti_group.py index ea7ce9fdd..13497841d 100644 --- a/pycti/entities/opencti_group.py +++ b/pycti/entities/opencti_group.py @@ -17,73 +17,8 @@ class Group: created by members of a group, and max shareable definitions which determine which objects users can export from the platform to share. - Representation of a group in Python looks like:: - - { - "id": "UUID", - "name": "Group name", - "description": "Group description", - "created_at": "ISO 8901 datetime", - "updated_at": "ISO 8901 datetime", - "default_assignation": False, - "no_creators": True, - "restrict_delete": True, - "default_hidden_types": ["STIX type"], - "auto_new_marking": False, - "allowed_marking": [{ - "id": "UUID", - "standard_id": "marking-definition--UUID", - "definition_type": "TLP", - "definition": "TLP:GREEN" - }], - "default_marking": [{ - "entity_type": "STIX type", - "values": [{ - "id": "UUID", - "standard_id": "marking-definition--UUID", - "definition_type": "TLP", - "deinition": "TLP:GREEN" - }] - }], - not_shareable_marking_types: [ - "PAP" - ], - max_shareable_marking: [{ - "id": "UUID", - "standard_id": "marking-definition--UUID", - "definition_type": "TLP", - "definition": "TLP:GREEN" - }], - group_confidence_level: { - "max_confidence": 90, - "overrides": [{ - "entity_type": "STIX type", - "max_confidence": 80 - }] - }, - "roles": { - "edges": [{ - "node": { - "id": "UUID", - "name": "Role name", - "capabilities": [{ - "id": "UUID", - "name": "Capability name" - }] - } - }] - }, - "members": { - "edges": [{ - "node": { - "id": "UUID", - "individual_id": "UUID", - "user_email": "email address", - "name": "Username" - } - }] - } - }. + See the properties attribute to understand what properties are fetched by + default from GraphQL queries. """ def __init__(self, opencti): diff --git a/pycti/entities/opencti_role.py b/pycti/entities/opencti_role.py index be597f309..249f5a16d 100644 --- a/pycti/entities/opencti_role.py +++ b/pycti/entities/opencti_role.py @@ -8,22 +8,6 @@ class Role: capabilities of those roles determine what a group of users can do on the platform. - The dictionary representation of a role might look as below:: - - { - "id": "UUID", - "name": "Name of the role", - "description": "Description of the role", - "created_at": "YYYY-MM-DDThh:mm:ss.000Z", - "updated_at": "YYYY-MM-DDThh:mm:ss.000Z", - "can_manage_sensitive_config": false, - "capabilities": [{ - "id": "UUID", - "name": "Name of the capability", - "description": "Description of the capability" - }] - }. - Check the properties attribute of the class to understand what default properties are fetched. """ diff --git a/pycti/entities/opencti_settings.py b/pycti/entities/opencti_settings.py index 86a567a89..493580b76 100644 --- a/pycti/entities/opencti_settings.py +++ b/pycti/entities/opencti_settings.py @@ -2,6 +2,16 @@ class Settings: + """Represents the Settings object in OpenCTI + + These are the properties which are viewable in the customization and + security policies views on OpenCTI platform. This also includes all + messages on the platform. + + See the properties attribute to understand which properties are fetched by + default on graphql queries. + """ + def __init__(self, opencti): self.opencti = opencti self.properties = """ diff --git a/pycti/entities/opencti_user.py b/pycti/entities/opencti_user.py index 4ac1bcc29..d1780f977 100644 --- a/pycti/entities/opencti_user.py +++ b/pycti/entities/opencti_user.py @@ -13,101 +13,6 @@ class User: They have configured confidence, and an effective confidence (which might be set by the group). - Representation of a User:: - - { - "id": "UUID", - "individual_id": "UUID of Creator object for user", - "user_email": "Email address assigned to user", - "firstname": "First name", - "lastname": "Last name", - "name": "Name", - "description": "This is a user", - "language": "auto", - "theme": "light|dark|auto", - "unit_system": "auto|Metric|Imperial", - "external": true, - "restrict_delete": true, - "account_status": "active", - "account_lock_after_date": "ISO 8601 date", - "created_at": "ISO 8601 date", - "updated_at": "ISO 8601 date", - "roles": [{ - "id": "UUID", - "name": "Role name", - "description": "Role description", - "capabilities": [{ - "id": "UUID", - "name": "KNUPDATE" - }] - }], - "capabilities": [{ - "id": "UUID", - "name": "KNUPDATE" - }], - "groups": { - "edges": [{ - "node": { - "id": "UUID", - "name": "group name", - "description": "Group description" - } - }] - }, - "objectOrganization": { - "edges": [{ - "node": { - "id": "UUID", - "is_inferred": false, - "name": "Org name", - "description": "Organisation description" - } - }] - }, - "administrated_organizations": [{ - "id": "UUID", - "name": "Org name", - "description": "Organisation description" - }], - "user_confidence_level": { - "max_confidence": 90, - "overrides": [{ - "entity_type": "type", - "max_confidence": 100 - }] - }, - "effective_confidence_level": { - "max_confidence": 90, - "source": { - "type": "User|Group|Bypass", - "object": { # Only on type == Group - "id": "UUID", - "name": "group name" - } - }, - "overrides": { - "entity_type": "type", - "max_confidence": 90, - "source": { - "type": "User|Group|Bypass", - "object": { # Only on type == Group - "id": "UUID", - "name": "group name" - } - } - } - }, - "api_token": "API_KEY", # only available if requested - "sessions": [{ - "id": "UUID", - "created": "ISO 8901 datetime", - "ttl": 100, - "originalMaxAge": 100 - }], # only available if requested - "submenu_show_icons": true, # only for MeUser - "submenu_auto_collaps": true # only for MeUser - } - You can view the properties, token_properties, session_properties, and me_properties attributes of a User object to view what attributes will be present in a User or MeUser object. From d5f67b1ff23ae629c3e7d952c3aa4020af90beeb Mon Sep 17 00:00:00 2001 From: Jorge Candedo <62056484+jorgeacjitx@users.noreply.github.com> Date: Fri, 17 Jan 2025 09:28:19 +0100 Subject: [PATCH 14/27] Remove harcoded first value when getting all indicators (#457) --- pycti/entities/opencti_indicator.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pycti/entities/opencti_indicator.py b/pycti/entities/opencti_indicator.py index 6d209099f..f67162035 100644 --- a/pycti/entities/opencti_indicator.py +++ b/pycti/entities/opencti_indicator.py @@ -63,8 +63,6 @@ def list(self, **kwargs): get_all = kwargs.get("getAll", False) with_pagination = kwargs.get("withPagination", False) with_files = kwargs.get("withFiles", False) - if get_all: - first = 100 self.opencti.app_logger.info( "Listing Indicators with filters", {"filters": json.dumps(filters)} From 11370094bf2c5b85b97e39a3ad9f7a641b3dcd1e Mon Sep 17 00:00:00 2001 From: Samuel Hassine Date: Fri, 17 Jan 2025 12:03:45 +0100 Subject: [PATCH 15/27] [client] Reverse the marking definition ID generation arguments --- pycti/entities/opencti_marking_definition.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pycti/entities/opencti_marking_definition.py b/pycti/entities/opencti_marking_definition.py index cf01d5cac..af9da8b11 100644 --- a/pycti/entities/opencti_marking_definition.py +++ b/pycti/entities/opencti_marking_definition.py @@ -25,8 +25,8 @@ def __init__(self, opencti): """ @staticmethod - def generate_id(definition, definition_type): - data = {"definition": definition, "definition_type": definition_type} + def generate_id(definition_type, definition): + data = {"definition_type": definition_type, "definition": definition} data = canonicalize(data, utf8=False) id = str(uuid.uuid5(uuid.UUID("00abedb4-aa42-466c-9c01-fed23315a9b7"), data)) return "marking-definition--" + id @@ -34,7 +34,7 @@ def generate_id(definition, definition_type): @staticmethod def generate_id_from_data(data): return MarkingDefinition.generate_id( - data["definition"], data["definition_type"] + data["definition_type"], data["definition"] ) """ From 3682d607ec0301fa486e33f7dbd020e1ff664838 Mon Sep 17 00:00:00 2001 From: Filigran Automation Date: Fri, 17 Jan 2025 11:26:00 +0000 Subject: [PATCH 16/27] [client] Release 6.4.8 --- pycti/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pycti/__init__.py b/pycti/__init__.py index 1fd44d59e..28e151faa 100644 --- a/pycti/__init__.py +++ b/pycti/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -__version__ = "6.4.7" +__version__ = "6.4.8" from .api.opencti_api_client import OpenCTIApiClient from .api.opencti_api_connector import OpenCTIApiConnector From d7c79c34ff7c5c0a5de7b0f3a8aad51aa5ece8fb Mon Sep 17 00:00:00 2001 From: Samuel Hassine Date: Sat, 18 Jan 2025 07:04:27 +0100 Subject: [PATCH 17/27] [client] Fix marking definition ID generation --- pycti/entities/opencti_marking_definition.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pycti/entities/opencti_marking_definition.py b/pycti/entities/opencti_marking_definition.py index af9da8b11..2ae75af15 100644 --- a/pycti/entities/opencti_marking_definition.py +++ b/pycti/entities/opencti_marking_definition.py @@ -26,6 +26,19 @@ def __init__(self, opencti): @staticmethod def generate_id(definition_type, definition): + # Handle static IDs from OpenCTI + if definition_type == "TLP": + if definition == "TLP:CLEAR" or definition == "TLP:WHITE": + return "marking-definition--613f2e26-407d-48c7-9eca-b8e91df99dc9" + if definition == "TLP:GREEN": + return "marking-definition--34098fce-860f-48ae-8e50-ebd3cc5e41da" + if definition == "TLP:AMBER": + return "marking-definition--f88d31f6-486f-44da-b317-01333bde0b82" + if definition == "TLP:AMBER+STRICT": + return "marking-definition--826578e1-40ad-459f-bc73-ede076f81f37" + if definition == "TLP:RED": + return "marking-definition--5e57c739-391a-4eb3-b6be-7d15ca92d5ed" + # Generate IDs data = {"definition_type": definition_type, "definition": definition} data = canonicalize(data, utf8=False) id = str(uuid.uuid5(uuid.UUID("00abedb4-aa42-466c-9c01-fed23315a9b7"), data)) From 92167ffb57ea6046e7cfcdaed10df6dd1a218008 Mon Sep 17 00:00:00 2001 From: Jeremy Cloarec <159018898+JeremyCloarec@users.noreply.github.com> Date: Mon, 27 Jan 2025 10:05:13 +0100 Subject: [PATCH 18/27] [client] remove default confidence value of 15 for sightings (opencti #6835) --- pycti/utils/opencti_stix2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pycti/utils/opencti_stix2.py b/pycti/utils/opencti_stix2.py index 668cf0c4f..ef8cca884 100644 --- a/pycti/utils/opencti_stix2.py +++ b/pycti/utils/opencti_stix2.py @@ -1378,7 +1378,7 @@ def import_sighting( created=stix_sighting["created"] if "created" in stix_sighting else None, modified=stix_sighting["modified"] if "modified" in stix_sighting else None, confidence=( - stix_sighting["confidence"] if "confidence" in stix_sighting else 15 + stix_sighting["confidence"] if "confidence" in stix_sighting else None ), createdBy=extras["created_by_id"] if "created_by_id" in extras else None, objectMarking=( From 0c2c4661fecfa8924c6808c03595b0f6567038c4 Mon Sep 17 00:00:00 2001 From: Souad Hadjiat Date: Mon, 27 Jan 2025 11:28:18 +0100 Subject: [PATCH 19/27] [client] Hide Authorization header (opencti/9693) --- pycti/api/opencti_api_client.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pycti/api/opencti_api_client.py b/pycti/api/opencti_api_client.py index 06b7f3c5a..cf1d695cd 100644 --- a/pycti/api/opencti_api_client.py +++ b/pycti/api/opencti_api_client.py @@ -237,8 +237,11 @@ def set_synchronized_upsert_header(self, synchronized): def set_previous_standard_header(self, previous_standard): self.request_headers["previous-standard"] = previous_standard - def get_request_headers(self): - return self.request_headers + def get_request_headers(self, hide_token=True): + request_headers_copy = self.request_headers.copy() + if hide_token and "Authorization" in request_headers_copy: + request_headers_copy["Authorization"] = "*****" + return request_headers_copy def set_retry_number(self, retry_number): self.request_headers["opencti-retry-number"] = ( From 5b472db16677619d057f1b1783f6958b0c95e077 Mon Sep 17 00:00:00 2001 From: peritz Date: Mon, 27 Jan 2025 14:53:56 +0000 Subject: [PATCH 20/27] Allow customAttributes for listing capabilities --- pycti/entities/opencti_capability.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/pycti/entities/opencti_capability.py b/pycti/entities/opencti_capability.py index 47a10ea98..e3b8c4841 100644 --- a/pycti/entities/opencti_capability.py +++ b/pycti/entities/opencti_capability.py @@ -18,8 +18,16 @@ def __init__(self, opencti): attribute_order """ - def list(self) -> List[Dict]: - """Lists all capabilities available on the platform""" + def list(self, **kwargs) -> List[Dict]: + """Lists all capabilities available on the platform + + :param customAttributes: Custom attributes to retrieve from the GraphQL + query. + :type customAttributes: str, optional + :return: List of capabilities + :rtype: List[Dict] + """ + customAttributes = kwargs.get("customAttributes") self.opencti.admin_logger.info("Listing capabilities") query = ( """ @@ -28,7 +36,8 @@ def list(self) -> List[Dict]: edges { node { """ - + self.properties + + (self.properties if customAttributes is None + else customAttributes) + """ } } From 901bd7bde5312a6504140f6d3ea25bcb9c9bae35 Mon Sep 17 00:00:00 2001 From: peritz Date: Mon, 27 Jan 2025 14:54:11 +0000 Subject: [PATCH 21/27] Remove ambiguous first parameter to capabilities query --- pycti/entities/opencti_capability.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pycti/entities/opencti_capability.py b/pycti/entities/opencti_capability.py index e3b8c4841..90f1e4eeb 100644 --- a/pycti/entities/opencti_capability.py +++ b/pycti/entities/opencti_capability.py @@ -32,7 +32,7 @@ def list(self, **kwargs) -> List[Dict]: query = ( """ query CapabilityList { - capabilities(first: 500) { + capabilities { edges { node { """ From fa1d099e15e94ae5632a8e209df0d9a2d5c2065e Mon Sep 17 00:00:00 2001 From: peritz Date: Mon, 27 Jan 2025 15:32:10 +0000 Subject: [PATCH 22/27] Move test stubs for Settings to entities test file --- pycti/entities/opencti_settings.py | 21 ------------------- tests/cases/entities.py | 33 +++++++++++++++++++++++++++++- 2 files changed, 32 insertions(+), 22 deletions(-) diff --git a/pycti/entities/opencti_settings.py b/pycti/entities/opencti_settings.py index 493580b76..75e06d66a 100644 --- a/pycti/entities/opencti_settings.py +++ b/pycti/entities/opencti_settings.py @@ -209,24 +209,6 @@ def __init__(self, opencti): analytics_google_analytics_v4 enterprise_edition """ + self.password_policy_properties - self._deleted = False - - def create(self, **kwargs) -> Dict: - """Stub function for tests - - :return: Settings as defined by self.read() - :rtype: Dict - """ - self.opencti.admin_logger.info( - "Settings.create called with arguments", kwargs) - self._deleted = False - return self.read() - - def delete(self, **kwargs): - """Stub function for tests""" - self.opencti.admin_logger.info( - "Settings.delete called with arguments", kwargs) - self._deleted = True def read(self, **kwargs) -> Dict: """Reads settings from the platform @@ -242,9 +224,6 @@ def read(self, **kwargs) -> Dict: :return: Representation of the platform settings :rtype: Dict """ - if self._deleted: - return None - custom_attributes = kwargs.get("customAttributes", None) include_password_policy = kwargs.get("include_password_policy", False) include_messages = kwargs.get("include_messages", False) diff --git a/tests/cases/entities.py b/tests/cases/entities.py index 8f4109246..89f67817d 100644 --- a/tests/cases/entities.py +++ b/tests/cases/entities.py @@ -3,6 +3,7 @@ from stix2 import TLP_GREEN, TLP_WHITE, AttackPattern from pycti.utils.constants import ContainerTypes, IdentityTypes, LocationTypes +from pycti.entities.opencti_settings import Settings from tests.utils import get_incident_end_date, get_incident_start_date @@ -1262,7 +1263,37 @@ def get_search(self): class SettingsTest(EntityTest): + + class SettingsWrapper(Settings): + def __init__(self, opencti): + self._deleted = False + super().__init__(opencti) + + def create(self, **kwargs) -> Dict: + """Stub function for tests + + :return: Settings as defined by self.read() + :rtype: Dict + """ + self.opencti.admin_logger.info( + "Settings.create called with arguments", kwargs) + self._deleted = False + return self.read() + + def delete(self, **kwargs): + """Stub function for tests""" + self.opencti.admin_logger.info( + "Settings.delete called with arguments", kwargs) + self._deleted = True + + def read(self, **kwargs): + """Stub function for tests""" + if self._deleted: + return None + return super().read(**kwargs) + def setup(self): + self._ownclass = self.SettingsWrapper(self.api_client) # Save current platform information custom_attributes = self.own_class().editable_properties self.own_class().create() @@ -1287,7 +1318,7 @@ def data(self) -> Dict: return {} def own_class(self): - return self.api_client.settings + return self._ownclass def base_class(self): return self.own_class() From 7bf2aca38748a74c663a06a8c9f3121be2bdc2b0 Mon Sep 17 00:00:00 2001 From: peritz Date: Mon, 27 Jan 2025 15:40:34 +0000 Subject: [PATCH 23/27] Correct minor issues around typing and args convention --- pycti/entities/opencti_group.py | 47 ++++++++++++++++-------------- pycti/entities/opencti_role.py | 26 ++++++++--------- pycti/entities/opencti_settings.py | 14 ++++----- pycti/entities/opencti_user.py | 46 ++++++++++++++++++----------- 4 files changed, 74 insertions(+), 59 deletions(-) diff --git a/pycti/entities/opencti_group.py b/pycti/entities/opencti_group.py index 13497841d..d0d145808 100644 --- a/pycti/entities/opencti_group.py +++ b/pycti/entities/opencti_group.py @@ -1,4 +1,4 @@ -from typing import List, Dict +from typing import List, Dict, Optional class Group: @@ -187,9 +187,11 @@ def list(self, **kwargs) -> List[Dict]: return self.opencti.process_multiple(result["data"]["groups"], withPagination) - def read(self, **kwargs) -> Dict: + def read(self, **kwargs) -> Optional[Dict]: """Fetch a given group from OpenCTI + One of id or filters is required. + :param id: ID of the group to fetch :type id: str, optional :param filters: Filters to apply to find single group @@ -197,7 +199,7 @@ def read(self, **kwargs) -> Dict: :param customAttributes: Custom attributes to fetch for the group :type customAttributes: str :return: Representation of a group. - :rtype: dict + :rtype: Optional[Dict] """ id = kwargs.get("id", None) filters = kwargs.get("filters", None) @@ -222,14 +224,15 @@ def read(self, **kwargs) -> Dict: return self.opencti.process_multiple_fields( result["data"]["group"]) elif filters is not None or search is not None: - results = self.list(filters=filters, search=search) + results = self.list(filters=filters, search=search, + customAttributes=customAttributes) return results[0] if results else None else: self.opencti.admin_logger.error( "[opencti_group] Missing parameters: id or filters") return None - def create(self, **kwargs) -> dict: + def create(self, **kwargs) -> Optional[Dict]: """Create a group with required details Groups can be configured after creation using other functions. @@ -257,7 +260,7 @@ def create(self, **kwargs) -> dict: :param customAttributes: Attributes to retrieve from the new group :type customAttributes: str, optional :return: Representation of the group. - :rtype: dict + :rtype: Optional[Dict] """ name = kwargs.get("name", None) group_confidence_level = kwargs.get("group_confidence_level", None) @@ -325,7 +328,7 @@ def delete(self, id: str): """ self.opencti.query(query, {"id": id}) - def update_field(self, **kwargs) -> Dict: + def update_field(self, **kwargs) -> Optional[Dict]: """Update a group using fieldPatch :param id: ID of the group to update @@ -335,7 +338,7 @@ def update_field(self, **kwargs) -> Dict: :param customAttributes: Custom attributes to retrieve from group :type customAttribues: str, optional :return: Representation of a group - :rtype: dict + :rtype: Optional[Dict] """ id = kwargs.get("id", None) input = kwargs.get("input", None) @@ -366,7 +369,7 @@ def update_field(self, **kwargs) -> Dict: return self.opencti.process_multiple_fields( result["data"]["groupEdit"]["fieldPatch"]) - def add_member(self, **kwargs) -> dict: + def add_member(self, **kwargs) -> Optional[Dict]: """Add a member to a given group. :param id: ID of the group to add a member to @@ -374,7 +377,7 @@ def add_member(self, **kwargs) -> dict: :param user_id: ID to add to the group :type user_id: str :return: Representation of the relationship - :rtype: dict + :rtype: Optional[Dict] """ id = kwargs.get("id", None) user_id = kwargs.get("user_id", None) @@ -412,7 +415,7 @@ def add_member(self, **kwargs) -> dict: return self.opencti.process_multiple_fields( result["data"]["groupEdit"]["relationAdd"]) - def delete_member(self, **kwargs) -> dict: + def delete_member(self, **kwargs) -> Optional[Dict]: """Remove a given user from a group :param id: ID to remove a user from @@ -420,7 +423,7 @@ def delete_member(self, **kwargs) -> dict: :param user: ID to remove from the group :type user: str :return: Representation of the group after the member has been removed - :rtype: dict + :rtype: Optional[Dict] """ id = kwargs.get("id", None) user_id = kwargs.get("user_id", None) @@ -450,7 +453,7 @@ def delete_member(self, **kwargs) -> dict: return self.opencti.process_multiple_fields( result["data"]["groupEdit"]["relationDelete"]) - def add_role(self, **kwargs) -> Dict: + def add_role(self, **kwargs) -> Optional[Dict]: """Add a role to a given group :param id: ID to add a role to @@ -458,7 +461,7 @@ def add_role(self, **kwargs) -> Dict: :param role_id: Role ID to add to the group :type role: str :return: Representation of the group after a role has been added - :rtype: dict + :rtype: Optional[Dict] """ id = kwargs.get("id", None) role_id = kwargs.get("role_id", None) @@ -494,7 +497,7 @@ def add_role(self, **kwargs) -> Dict: return self.opencti.process_multiple_fields( result["data"]["groupEdit"]["relationAdd"]) - def delete_role(self, **kwargs) -> Dict: + def delete_role(self, **kwargs) -> Optional[Dict]: """Removes a role from a given group :param id: ID to remove role from @@ -502,7 +505,7 @@ def delete_role(self, **kwargs) -> Dict: :param role_id: Role ID to remove from the group :type role_id: str :return: Representation of the group after role is removed - :rtype: dict + :rtype: Optional[Dict] """ id = kwargs.get("id", None) role_id = kwargs.get("role_id", None) @@ -531,7 +534,7 @@ def delete_role(self, **kwargs) -> Dict: return self.opencti.process_multiple_fields( result["data"]["groupEdit"]["relationDelete"]) - def edit_default_marking(self, **kwargs) -> Dict: + def edit_default_marking(self, **kwargs) -> Optional[Dict]: """Adds a default marking to the group. :param id: ID of the group. @@ -544,7 +547,7 @@ def edit_default_marking(self, **kwargs) -> Dict: defaults to "GLOBAL". :type entity: str, optional :return: Group after adding the default marking. - :rtype: dict + :rtype: Optional[Dict] """ id = kwargs.get("id", None) marking_ids = kwargs.get("marking_ids", None) @@ -586,7 +589,7 @@ def edit_default_marking(self, **kwargs) -> Dict: return self.opencti.process_multiple_fields( result["data"]["groupEdit"]["editDefaultMarking"]) - def add_allowed_marking(self, **kwargs) -> Dict: + def add_allowed_marking(self, **kwargs) -> Optional[Dict]: """Allow a group to access a marking :param id: ID of group to authorise @@ -594,7 +597,7 @@ def add_allowed_marking(self, **kwargs) -> Dict: :param marking_id: ID of marking to authorise :type marking_id: str :return: Relationship from the group to the marking definition - :rtype: dict + :rtype: Optional[Dict] """ id = kwargs.get("id", None) marking_id = kwargs.get("marking_id", None) @@ -653,7 +656,7 @@ def add_allowed_marking(self, **kwargs) -> Dict: return self.opencti.process_multiple_fields( result["data"]["groupEdit"]["relationAdd"]) - def delete_allowed_marking(self, **kwargs) -> Dict: + def delete_allowed_marking(self, **kwargs) -> Optional[Dict]: """Removes access to a marking for a group :param id: ID of group to forbid @@ -661,7 +664,7 @@ def delete_allowed_marking(self, **kwargs) -> Dict: :param marking_id: ID of marking to deny :type marking_id: str :return: Group after denying access to marking definition - :rtype: dict + :rtype: Optional[Dict] """ id = kwargs.get("id", None) marking_id = kwargs.get("marking_id", None) diff --git a/pycti/entities/opencti_role.py b/pycti/entities/opencti_role.py index 249f5a16d..799fff01c 100644 --- a/pycti/entities/opencti_role.py +++ b/pycti/entities/opencti_role.py @@ -1,4 +1,4 @@ -from typing import Dict, List +from typing import Dict, List, Optional class Role: @@ -138,7 +138,7 @@ def list(self, **kwargs) -> List[Dict]: return self.opencti.process_multiple(result["data"]["roles"], withPagination) - def read(self, **kwargs) -> Dict: + def read(self, **kwargs) -> Optional[Dict]: """Get a role given its ID or a search term One of id or search must be provided. @@ -151,7 +151,7 @@ def read(self, **kwargs) -> Dict: :type customAttributes: str, optional :return: Representation of the role - :rtype: Dict + :rtype: Optional[Dict] """ id = kwargs.get("id", None) search = kwargs.get("search", None) @@ -185,8 +185,8 @@ def read(self, **kwargs) -> Dict: def delete(self, **kwargs): """Delete a role given its ID - :param role: ID for the role on the platform. - :type role: str + :param id: ID for the role on the platform. + :type id: str """ id = kwargs.get("id", None) @@ -206,7 +206,7 @@ def delete(self, **kwargs): """ self.opencti.query(query, {"id": id}) - def create(self, **kwargs) -> Dict: + def create(self, **kwargs) -> Optional[Dict]: """Add a new role to OpenCTI. :param name: Name to assign to the role. @@ -217,7 +217,7 @@ def create(self, **kwargs) -> Dict: :param customAttributes: Custom attributes to return on role :type customAttributes: str, optional :return: Representation of the role. - :rtype: Dict + :rtype: Optional[Dict] """ name = kwargs.get("name", None) description = kwargs.get("description", None) @@ -252,7 +252,7 @@ def create(self, **kwargs) -> Dict: }) return self.opencti.process_multiple_fields(result["data"]["roleAdd"]) - def update_field(self, **kwargs) -> Dict: + def update_field(self, **kwargs) -> Optional[Dict]: """Updates a given role with the given inputs Example of input:: @@ -276,7 +276,7 @@ def update_field(self, **kwargs) -> Dict: :type customAttributes: str, optional :return: Representation of the role - :rtype: Dict + :rtype: Optional[Dict] """ id = kwargs.get("id", None) input = kwargs.get("input", None) @@ -308,7 +308,7 @@ def update_field(self, **kwargs) -> Dict: return self.opencti.process_multiple_fields( result["data"]["roleEdit"]["fieldPatch"]) - def add_capability(self, **kwargs) -> Dict: + def add_capability(self, **kwargs) -> Optional[Dict]: """Adds a capability to a role :param id: ID of the role. @@ -317,7 +317,7 @@ def add_capability(self, **kwargs) -> Dict: :type capability_id: str :return: Representation of the relationship, including the role and capability - :rtype: Dict + :rtype: Optional[Dict] """ id = kwargs.get("id", None) capability_id = kwargs.get("capability_id", None) @@ -366,7 +366,7 @@ def add_capability(self, **kwargs) -> Dict: return self.opencti.process_multiple_fields( result["data"]["roleEdit"]["relationAdd"]) - def delete_capability(self, **kwargs) -> Dict: + def delete_capability(self, **kwargs) -> Optional[Dict]: """Removes a capability from a role :param id: ID of the role @@ -374,7 +374,7 @@ def delete_capability(self, **kwargs) -> Dict: :param capability_id: ID of the capability to remove :type capability_id: str :return: Representation of the role after removing the capability - :rtype: Dict + :rtype: Optional[Dict] """ id = kwargs.get("id", None) capability_id = kwargs.get("capability_id", None) diff --git a/pycti/entities/opencti_settings.py b/pycti/entities/opencti_settings.py index 75e06d66a..2802e3008 100644 --- a/pycti/entities/opencti_settings.py +++ b/pycti/entities/opencti_settings.py @@ -1,4 +1,4 @@ -from typing import Dict +from typing import Dict, Optional class Settings: @@ -247,7 +247,7 @@ def read(self, **kwargs) -> Dict: result = self.opencti.query(query) return self.opencti.process_multiple_fields(result["data"]["settings"]) - def update_field(self, **kwargs) -> Dict: + def update_field(self, **kwargs) -> Optional[Dict]: """Update settings using input to fieldPatch :param id: ID of the settings object to update @@ -263,7 +263,7 @@ def update_field(self, **kwargs) -> Dict: in query response. :type include_messages: bool, optional :return: Representation of the platform settings - :rtype: Dict + :rtype: Optional[Dict] """ id = kwargs.get("id", None) input = kwargs.get("input", None) @@ -302,7 +302,7 @@ def update_field(self, **kwargs) -> Dict: return self.opencti.process_multiple_fields( result["data"]["settingsEdit"]["fieldPatch"]) - def edit_message(self, **kwargs) -> Dict: + def edit_message(self, **kwargs) -> Optional[Dict]: """Edit or add a message to the platform To add a message, don't include an ID in the input object. To edit a @@ -313,7 +313,7 @@ def edit_message(self, **kwargs) -> Dict: :param input: SettingsMessageInput object :type input: Dict :return: Settings ID and message objects - :rtype: Dict + :rtype: Optional[Dict] """ id = kwargs.get("id", None) input = kwargs.get("input", None) @@ -341,7 +341,7 @@ def edit_message(self, **kwargs) -> Dict: return self.opencti.process_multiple_fields( result["data"]["settingsEdit"]["editMessage"]) - def delete_message(self, **kwargs) -> Dict: + def delete_message(self, **kwargs) -> Optional[Dict]: """Delete a message from the platform :param id: ID of the settings object on the platform @@ -349,7 +349,7 @@ def delete_message(self, **kwargs) -> Dict: :param input: ID of the message to delete :type input: str :return: Settings ID and message objects - :rtype: Dict + :rtype: Optional[Dict] """ id = kwargs.get("id", None) input = kwargs.get("input", None) diff --git a/pycti/entities/opencti_user.py b/pycti/entities/opencti_user.py index d1780f977..f2cf90514 100644 --- a/pycti/entities/opencti_user.py +++ b/pycti/entities/opencti_user.py @@ -1,6 +1,6 @@ import secrets -from typing import List, Dict +from typing import List, Dict, Optional class User: @@ -287,7 +287,7 @@ def list(self, **kwargs) -> List[Dict]: return self.opencti.process_multiple(result["data"]["users"], withPagination) - def read(self, **kwargs) -> Dict: + def read(self, **kwargs) -> Optional[Dict]: """Reads user details from the platform. :param id: ID of the user to fetch @@ -306,7 +306,7 @@ def read(self, **kwargs) -> Dict: :param search: Search term to use to find a single user :type search: str, optional :return: Representation of the user as a Python dictionary. - :rtype: dict + :rtype: Optional[Dict] """ id = kwargs.get("id", None) include_sessions = kwargs.get("include_sessions", False) @@ -355,7 +355,7 @@ def read(self, **kwargs) -> Dict: "[opencti_user] Missing paramters: id, search, or filters") return None - def create(self, **kwargs) -> Dict: + def create(self, **kwargs) -> Optional[Dict]: """Creates a new user with basic details Note that when SSO is connected users generally do not need to be @@ -414,7 +414,7 @@ def create(self, **kwargs) -> Dict: token for the new user in the response. :type include_token: bool, optional :return: Representation of the user without sessions or API token. - :rtype: dict + :rtype: Optional[Dict] """ name = kwargs.get("name", None) user_email = kwargs.get("user_email", None) @@ -538,7 +538,7 @@ def me(self, **kwargs) -> Dict: result = self.opencti.query(query) return self.opencti.process_multiple_fields(result["data"]["me"]) - def update_field(self, **kwargs) -> Dict: + def update_field(self, **kwargs) -> Optional[Dict]: """Update a given user using fieldPatch :param id: ID of the user to update. @@ -548,7 +548,7 @@ def update_field(self, **kwargs) -> Dict: :param customAttributes: Custom attributes to return from the mutation :type customAttributes: str, optional :return: Representation of the user without sessions or API token. - :rtype: dict + :rtype: Optional[Dict] """ id = kwargs.get("id", None) input = kwargs.get("input", None) @@ -579,7 +579,7 @@ def update_field(self, **kwargs) -> Dict: return self.opencti.process_multiple_fields( result["data"]["userEdit"]["fieldPatch"]) - def add_membership(self, **kwargs) -> Dict: + def add_membership(self, **kwargs) -> Optional[Dict]: """Adds the user to a given group. :param id: User ID to add to the group. @@ -587,7 +587,7 @@ def add_membership(self, **kwargs) -> Dict: :param group_id: Group ID to add the user to. :type group_id: str :return: Representation of the InternalRelationship - :rtype: dict + :rtype: Optional[Dict] """ id = kwargs.get("id", None) group_id = kwargs.get("group_id", None) @@ -625,7 +625,7 @@ def add_membership(self, **kwargs) -> Dict: return self.opencti.process_multiple_fields( result["data"]["userEdit"]["relationAdd"]) - def delete_membership(self, **kwargs) -> Dict: + def delete_membership(self, **kwargs) -> Optional[Dict]: """Removes the user from the given group. :param id: User ID to remove from the group. @@ -633,7 +633,7 @@ def delete_membership(self, **kwargs) -> Dict: :param group_id: Group ID to remove the user from. :type group_id: str :return: Representation of the user without sessions or API token - :rtype: dict + :rtype: Optional[Dict] """ id = kwargs.get("id", None) group_id = kwargs.get("group_id", None) @@ -661,7 +661,7 @@ def delete_membership(self, **kwargs) -> Dict: return self.opencti.process_multiple_fields( result["data"]["userEdit"]["relationDelete"]) - def add_organization(self, id: str, organization_id: str) -> Dict: + def add_organization(self, **kwargs) -> Optional[Dict]: """Adds a user to an organization :param id: User ID to add to organization @@ -669,8 +669,14 @@ def add_organization(self, id: str, organization_id: str) -> Dict: :param organization_id: ID of organization to add to :type organization_id: str :return: Representation of user without sessions or API key - :rtype: Dict + :rtype: Optional[Dict] """ + id = kwargs.get("id", None) + organization_id = kwargs.get("organization_id", None) + if id is None or organization_id is None: + self.opencti.admin_logger.error( + "[opencti_user] Missing parameters: id and organization_id") + self.opencti.admin_logger.info("Adding user to organization", { "id": id, "organization_id": organization_id }) @@ -692,7 +698,7 @@ def add_organization(self, id: str, organization_id: str) -> Dict: return self.opencti.process_multiple_fields( result["data"]["userEdit"]["organizationAdd"]) - def delete_organization(self, id: str, organization_id: str) -> Dict: + def delete_organization(self, **kwargs) -> Optional[Dict]: """Delete a user from an organization :param id: User ID to remove from organization @@ -700,8 +706,14 @@ def delete_organization(self, id: str, organization_id: str) -> Dict: :param organization_id: ID of organization to remove from :type organization_id: str :return: Representation of user without sessions or API key - :rtype: Dict + :rtype: Optional[Dict] """ + id = kwargs.get("id", None) + organization_id = kwargs.get("organization_id", None) + if id is None or organization_id is None: + self.opencti.admin_logger.error( + "[opencti_user] Missing parameters: id and organization_id") + self.opencti.admin_logger.info("Removing user from organization", { "id": id, "organization_id": organization_id }) @@ -723,7 +735,7 @@ def delete_organization(self, id: str, organization_id: str) -> Dict: return self.opencti.process_multiple_fields( result["data"]["userEdit"]["organizationDelete"]) - def token_renew(self, **kwargs) -> Dict: + def token_renew(self, **kwargs) -> Optional[Dict]: """Rotates the API token for the given user :param user: User ID to rotate API token for. @@ -732,7 +744,7 @@ def token_renew(self, **kwargs) -> Dict: token in response from server, defaults to False. :type include_token: bool, optional :return: Representation of user - :rtype: Dict + :rtype: Optional[Dict] """ id = kwargs.get("id", None) include_token = kwargs.get("include_token", False) From 1b90773a42c5cb9e1566fc43851e2e98e2f85eb0 Mon Sep 17 00:00:00 2001 From: peritz Date: Mon, 27 Jan 2025 16:51:13 +0000 Subject: [PATCH 24/27] Sort imports using isort --- pycti/__init__.py | 15 +++++++-------- pycti/api/opencti_api_client.py | 10 +++++----- pycti/entities/opencti_capability.py | 2 +- pycti/entities/opencti_group.py | 2 +- pycti/entities/opencti_user.py | 3 +-- tests/02-integration/entities/test_group.py | 3 +-- tests/02-integration/entities/test_user.py | 2 +- tests/cases/entities.py | 2 +- 8 files changed, 18 insertions(+), 21 deletions(-) diff --git a/pycti/__init__.py b/pycti/__init__.py index 28e151faa..7a7c9b374 100644 --- a/pycti/__init__.py +++ b/pycti/__init__.py @@ -12,6 +12,9 @@ from .connector.opencti_metric_handler import OpenCTIMetricHandler from .entities.opencti_attack_pattern import AttackPattern from .entities.opencti_campaign import Campaign + +# Administrative entities +from .entities.opencti_capability import Capability from .entities.opencti_case_incident import CaseIncident from .entities.opencti_case_rfi import CaseRfi from .entities.opencti_case_rft import CaseRft @@ -21,6 +24,7 @@ from .entities.opencti_data_source import DataSource from .entities.opencti_external_reference import ExternalReference from .entities.opencti_feedback import Feedback +from .entities.opencti_group import Group from .entities.opencti_grouping import Grouping from .entities.opencti_identity import Identity from .entities.opencti_incident import Incident @@ -37,6 +41,8 @@ from .entities.opencti_observed_data import ObservedData from .entities.opencti_opinion import Opinion from .entities.opencti_report import Report +from .entities.opencti_role import Role +from .entities.opencti_settings import Settings from .entities.opencti_stix_core_relationship import StixCoreRelationship from .entities.opencti_stix_cyber_observable import StixCyberObservable from .entities.opencti_stix_domain_object import StixDomainObject @@ -50,15 +56,8 @@ from .entities.opencti_threat_actor_group import ThreatActorGroup from .entities.opencti_threat_actor_individual import ThreatActorIndividual from .entities.opencti_tool import Tool -from .entities.opencti_vulnerability import Vulnerability - -# Administrative entities -from .entities.opencti_capability import Capability -from .entities.opencti_role import Role -from .entities.opencti_group import Group from .entities.opencti_user import User -from .entities.opencti_settings import Settings - +from .entities.opencti_vulnerability import Vulnerability from .utils.constants import ( CustomObjectCaseIncident, CustomObjectChannel, diff --git a/pycti/api/opencti_api_client.py b/pycti/api/opencti_api_client.py index cf1d695cd..5dcfbea67 100644 --- a/pycti/api/opencti_api_client.py +++ b/pycti/api/opencti_api_client.py @@ -14,6 +14,7 @@ from pycti.api.opencti_api_work import OpenCTIApiWork from pycti.entities.opencti_attack_pattern import AttackPattern from pycti.entities.opencti_campaign import Campaign +from pycti.entities.opencti_capability import Capability from pycti.entities.opencti_case_incident import CaseIncident from pycti.entities.opencti_case_rfi import CaseRfi from pycti.entities.opencti_case_rft import CaseRft @@ -24,6 +25,7 @@ from pycti.entities.opencti_event import Event from pycti.entities.opencti_external_reference import ExternalReference from pycti.entities.opencti_feedback import Feedback +from pycti.entities.opencti_group import Group from pycti.entities.opencti_grouping import Grouping from pycti.entities.opencti_identity import Identity from pycti.entities.opencti_incident import Incident @@ -42,6 +44,8 @@ from pycti.entities.opencti_observed_data import ObservedData from pycti.entities.opencti_opinion import Opinion from pycti.entities.opencti_report import Report +from pycti.entities.opencti_role import Role +from pycti.entities.opencti_settings import Settings from pycti.entities.opencti_stix import Stix from pycti.entities.opencti_stix_core_object import StixCoreObject from pycti.entities.opencti_stix_core_relationship import StixCoreRelationship @@ -59,13 +63,9 @@ from pycti.entities.opencti_threat_actor_group import ThreatActorGroup from pycti.entities.opencti_threat_actor_individual import ThreatActorIndividual from pycti.entities.opencti_tool import Tool +from pycti.entities.opencti_user import User from pycti.entities.opencti_vocabulary import Vocabulary from pycti.entities.opencti_vulnerability import Vulnerability -from pycti.entities.opencti_capability import Capability -from pycti.entities.opencti_role import Role -from pycti.entities.opencti_group import Group -from pycti.entities.opencti_user import User -from pycti.entities.opencti_settings import Settings from pycti.utils.opencti_logger import logger from pycti.utils.opencti_stix2 import OpenCTIStix2 from pycti.utils.opencti_stix2_utils import OpenCTIStix2Utils diff --git a/pycti/entities/opencti_capability.py b/pycti/entities/opencti_capability.py index 90f1e4eeb..7cf316608 100644 --- a/pycti/entities/opencti_capability.py +++ b/pycti/entities/opencti_capability.py @@ -1,4 +1,4 @@ -from typing import List, Dict +from typing import Dict, List class Capability: diff --git a/pycti/entities/opencti_group.py b/pycti/entities/opencti_group.py index d0d145808..0cc16e691 100644 --- a/pycti/entities/opencti_group.py +++ b/pycti/entities/opencti_group.py @@ -1,4 +1,4 @@ -from typing import List, Dict, Optional +from typing import Dict, List, Optional class Group: diff --git a/pycti/entities/opencti_user.py b/pycti/entities/opencti_user.py index f2cf90514..918dda90b 100644 --- a/pycti/entities/opencti_user.py +++ b/pycti/entities/opencti_user.py @@ -1,6 +1,5 @@ import secrets - -from typing import List, Dict, Optional +from typing import Dict, List, Optional class User: diff --git a/tests/02-integration/entities/test_group.py b/tests/02-integration/entities/test_group.py index 974288a72..687242cc8 100644 --- a/tests/02-integration/entities/test_group.py +++ b/tests/02-integration/entities/test_group.py @@ -1,5 +1,4 @@ -from tests.cases.entities import ( - GroupTest, RoleTest, MarkingDefinitionTest, UserTest) +from tests.cases.entities import GroupTest, MarkingDefinitionTest, RoleTest, UserTest def test_group_roles(api_client): diff --git a/tests/02-integration/entities/test_user.py b/tests/02-integration/entities/test_user.py index 265bb6c29..5129cf5c6 100644 --- a/tests/02-integration/entities/test_user.py +++ b/tests/02-integration/entities/test_user.py @@ -1,4 +1,4 @@ -from tests.cases.entities import GroupTest, UserTest, IdentityOrganizationTest +from tests.cases.entities import GroupTest, IdentityOrganizationTest, UserTest def test_user_membership(api_client): diff --git a/tests/cases/entities.py b/tests/cases/entities.py index 89f67817d..ab7eb44d8 100644 --- a/tests/cases/entities.py +++ b/tests/cases/entities.py @@ -2,8 +2,8 @@ from stix2 import TLP_GREEN, TLP_WHITE, AttackPattern -from pycti.utils.constants import ContainerTypes, IdentityTypes, LocationTypes from pycti.entities.opencti_settings import Settings +from pycti.utils.constants import ContainerTypes, IdentityTypes, LocationTypes from tests.utils import get_incident_end_date, get_incident_start_date From 901905ef499864dc2000bce0b068138fb1f4408d Mon Sep 17 00:00:00 2001 From: peritz Date: Mon, 27 Jan 2025 17:02:13 +0000 Subject: [PATCH 25/27] Format files with black --- pycti/api/opencti_api_client.py | 25 +- pycti/entities/opencti_capability.py | 3 +- pycti/entities/opencti_group.py | 217 ++++++++++-------- pycti/entities/opencti_role.py | 93 ++++---- pycti/entities/opencti_settings.py | 50 ++-- pycti/entities/opencti_user.py | 214 +++++++++-------- .../entities/test_entity_crud.py | 15 +- tests/02-integration/entities/test_group.py | 21 +- tests/02-integration/entities/test_role.py | 6 +- .../02-integration/entities/test_settings.py | 10 +- tests/02-integration/entities/test_user.py | 20 +- tests/cases/entities.py | 47 ++-- 12 files changed, 372 insertions(+), 349 deletions(-) diff --git a/pycti/api/opencti_api_client.py b/pycti/api/opencti_api_client.py index 5dcfbea67..ae57b2dfe 100644 --- a/pycti/api/opencti_api_client.py +++ b/pycti/api/opencti_api_client.py @@ -656,27 +656,25 @@ def process_multiple_fields(self, data): data["rolesIds"] = self.process_multiple_ids(data["roles"]) if "capabilities" in data: data["capabilities"] = self.process_multiple(data["capabilities"]) - data["capabilitiesIds"] = self.process_multiple_ids( - data["capabilities"] - ) + data["capabilitiesIds"] = self.process_multiple_ids(data["capabilities"]) if "members" in data: data["members"] = self.process_multiple(data["members"]) data["membersIds"] = self.process_multiple_ids(data["members"]) if "platform_messages" in data: - data["platform_messages"] = self.process_multiple( - data["platform_messages"]) + data["platform_messages"] = self.process_multiple(data["platform_messages"]) data["platform_messages_ids"] = self.process_multiple_ids( - data["platform_messages"]) + data["platform_messages"] + ) if "messages_administration" in data: data["messages_administration"] = self.process_multiple( - data["messages_administration"]) + data["messages_administration"] + ) data["messages_administration_ids"] = self.process_multiple_ids( - data["messages_administration"]) + data["messages_administration"] + ) if "recipients" in data: - data["recipients"] = self.process_multiple( - data["recipients"]) - data["recipientsIds"] = self.process_multiple_ids( - data["recipients"]) + data["recipients"] = self.process_multiple(data["recipients"]) + data["recipientsIds"] = self.process_multiple_ids(data["recipients"]) # See aliases of GraphQL query in stix_core_object method if "name_alt" in data: @@ -749,8 +747,7 @@ def create_draft(self, **kwargs): ) return queryResult["data"]["draftWorkspaceAdd"]["id"] else: - self.app_logger.error( - "[create_draft] Missing parameter: draft_name") + self.app_logger.error("[create_draft] Missing parameter: draft_name") return None def upload_pending_file(self, **kwargs): diff --git a/pycti/entities/opencti_capability.py b/pycti/entities/opencti_capability.py index 7cf316608..7f54dcd80 100644 --- a/pycti/entities/opencti_capability.py +++ b/pycti/entities/opencti_capability.py @@ -36,8 +36,7 @@ def list(self, **kwargs) -> List[Dict]: edges { node { """ - + (self.properties if customAttributes is None - else customAttributes) + + (self.properties if customAttributes is None else customAttributes) + """ } } diff --git a/pycti/entities/opencti_group.py b/pycti/entities/opencti_group.py index 0cc16e691..81d5d7a94 100644 --- a/pycti/entities/opencti_group.py +++ b/pycti/entities/opencti_group.py @@ -132,8 +132,9 @@ def list(self, **kwargs) -> List[Dict]: if getAll: first = 100 - self.opencti.admin_logger.info("Fetching groups with filters", - {"filters": filters}) + self.opencti.admin_logger.info( + "Fetching groups with filters", {"filters": filters} + ) query = ( """ query Groups($first: Int, $after: ID, $orderBy: GroupsOrdering, $orderMode: OrderingMode, $search: String, $filters: FilterGroup) { @@ -141,8 +142,7 @@ def list(self, **kwargs) -> List[Dict]: edges { node { """ - + (self.properties if customAttributes is None - else customAttributes) + + (self.properties if customAttributes is None else customAttributes) + """ } } @@ -157,14 +157,17 @@ def list(self, **kwargs) -> List[Dict]: } """ ) - result = self.opencti.query(query, { - "first": first, - "after": after, - "orderBy": orderBy, - "orderMode": orderMode, - "search": search, - "filters": filters - }) + result = self.opencti.query( + query, + { + "first": first, + "after": after, + "orderBy": orderBy, + "orderMode": orderMode, + "search": search, + "filters": filters, + }, + ) if getAll: final_data = [] @@ -172,20 +175,24 @@ def list(self, **kwargs) -> List[Dict]: final_data = final_data + data while result["data"]["groups"]["pageInfo"]["hasNextPage"]: after = result["data"]["groups"]["pageInfo"]["endCursor"] - result = self.opencti.query(query, { - "first": first, - "after": after, - "orderBy": orderBy, - "orderMode": orderMode, - "search": search, - "filters": filters - }) + result = self.opencti.query( + query, + { + "first": first, + "after": after, + "orderBy": orderBy, + "orderMode": orderMode, + "search": search, + "filters": filters, + }, + ) data = self.opencti.process_multiple(result["data"]["groups"]) final_data = final_data + data return final_data else: - return self.opencti.process_multiple(result["data"]["groups"], - withPagination) + return self.opencti.process_multiple( + result["data"]["groups"], withPagination + ) def read(self, **kwargs) -> Optional[Dict]: """Fetch a given group from OpenCTI @@ -206,30 +213,29 @@ def read(self, **kwargs) -> Optional[Dict]: search = kwargs.get("search", None) customAttributes = kwargs.get("customAttributes", None) if id is not None: - self.opencti.admin_logger.info( - "Fetching group with ID", {"id": id}) + self.opencti.admin_logger.info("Fetching group with ID", {"id": id}) query = ( """ query Group($id: String!) { group(id: $id) { """ - + (self.properties if customAttributes is None - else customAttributes) + + (self.properties if customAttributes is None else customAttributes) + """ } } """ ) result = self.opencti.query(query, {"id": id}) - return self.opencti.process_multiple_fields( - result["data"]["group"]) + return self.opencti.process_multiple_fields(result["data"]["group"]) elif filters is not None or search is not None: - results = self.list(filters=filters, search=search, - customAttributes=customAttributes) + results = self.list( + filters=filters, search=search, customAttributes=customAttributes + ) return results[0] if results else None else: self.opencti.admin_logger.error( - "[opencti_group] Missing parameters: id or filters") + "[opencti_group] Missing parameters: id or filters" + ) return None def create(self, **kwargs) -> Optional[Dict]: @@ -273,44 +279,48 @@ def create(self, **kwargs) -> Optional[Dict]: if name is None or group_confidence_level is None: self.opencti.admin_logger.error( - "[opencti_group] Missing parameters: name and " - "group_confidence_level") + "[opencti_group] Missing parameters: name and " "group_confidence_level" + ) return None self.opencti.admin_logger.info( - "Creating new group with parameters", { + "Creating new group with parameters", + { "name": name, "group_confidence_level": group_confidence_level, "description": description, "default_assignation": default_assignation, "no_creators": no_creators, "restrict_delete": restrict_delete, - "auto_new_marking": auto_new_marking - } + "auto_new_marking": auto_new_marking, + }, ) query = ( """ mutation GroupAdd($input: GroupAddInput!) { groupAdd(input: $input) { """ - + (self.properties if customAttributes is None - else customAttributes) + + (self.properties if customAttributes is None else customAttributes) + """ } } """ ) - result = self.opencti.query(query, {"input": { - "name": name, - "description": description, - "default_assignation": default_assignation, - "no_creators": no_creators, - "restrict_delete": restrict_delete, - "auto_new_marking": auto_new_marking, - "group_confidence_level": group_confidence_level - }}) - return self.opencti.process_multiple_fields( - result["data"]["groupAdd"]) + result = self.opencti.query( + query, + { + "input": { + "name": name, + "description": description, + "default_assignation": default_assignation, + "no_creators": no_creators, + "restrict_delete": restrict_delete, + "auto_new_marking": auto_new_marking, + "group_confidence_level": group_confidence_level, + } + }, + ) + return self.opencti.process_multiple_fields(result["data"]["groupAdd"]) def delete(self, id: str): """Delete a given group from OpenCTI @@ -346,19 +356,18 @@ def update_field(self, **kwargs) -> Optional[Dict]: if id is None or input is None: self.opencti.admin_logger.error( - "[opencti_group] Missing parameters: id and input") + "[opencti_group] Missing parameters: id and input" + ) return None - self.opencti.admin_logger.info("Editing group with input", { - "input": input}) + self.opencti.admin_logger.info("Editing group with input", {"input": input}) query = ( """ mutation GroupEdit($id: ID!, $input:[EditInput]!) { groupEdit(id: $id) { fieldPatch(input: $input) { """ - + (self.properties if customAttributes is None - else customAttributes) + + (self.properties if customAttributes is None else customAttributes) + """ } } @@ -367,7 +376,8 @@ def update_field(self, **kwargs) -> Optional[Dict]: ) result = self.opencti.query(query, {"id": id, "input": input}) return self.opencti.process_multiple_fields( - result["data"]["groupEdit"]["fieldPatch"]) + result["data"]["groupEdit"]["fieldPatch"] + ) def add_member(self, **kwargs) -> Optional[Dict]: """Add a member to a given group. @@ -384,14 +394,14 @@ def add_member(self, **kwargs) -> Optional[Dict]: if id is None or user_id is None: self.opencti.admin_logger.error( - "[opencti_group] Missing parameters: id and user_id") + "[opencti_group] Missing parameters: id and user_id" + ) return None self.opencti.admin_logger.info( "Adding member to group", {"groupId": id, "userId": user_id} ) - query = ( - """ + query = """ mutation MemberAdd($groupId: ID!, $userId: ID!) { groupEdit(id: $groupId) { relationAdd(input: { @@ -410,10 +420,10 @@ def add_member(self, **kwargs) -> Optional[Dict]: } } """ - ) result = self.opencti.query(query, {"groupId": id, "userId": user_id}) return self.opencti.process_multiple_fields( - result["data"]["groupEdit"]["relationAdd"]) + result["data"]["groupEdit"]["relationAdd"] + ) def delete_member(self, **kwargs) -> Optional[Dict]: """Remove a given user from a group @@ -430,7 +440,8 @@ def delete_member(self, **kwargs) -> Optional[Dict]: if id is None or user_id is None: self.opencti.admin_logger.error( - "[opencti_group] Missing parameters: id and user_id") + "[opencti_group] Missing parameters: id and user_id" + ) return None self.opencti.admin_logger.info( @@ -451,7 +462,8 @@ def delete_member(self, **kwargs) -> Optional[Dict]: ) result = self.opencti.query(query, {"groupId": id, "userId": user_id}) return self.opencti.process_multiple_fields( - result["data"]["groupEdit"]["relationDelete"]) + result["data"]["groupEdit"]["relationDelete"] + ) def add_role(self, **kwargs) -> Optional[Dict]: """Add a role to a given group @@ -468,13 +480,14 @@ def add_role(self, **kwargs) -> Optional[Dict]: if id is None or role_id is None: self.opencti.admin_logger.error( - "[opencti_group] Missing parameters: id and role_id") + "[opencti_group] Missing parameters: id and role_id" + ) return None self.opencti.admin_logger.info( - "Adding role to group", {"groupId": id, "roleId": role_id}) - query = ( - """ + "Adding role to group", {"groupId": id, "roleId": role_id} + ) + query = """ mutation RoleAdd($groupId: ID!, $roleId: ID!) { groupEdit(id: $groupId) { relationAdd(input: { @@ -492,10 +505,10 @@ def add_role(self, **kwargs) -> Optional[Dict]: } } """ - ) result = self.opencti.query(query, {"groupId": id, "roleId": role_id}) return self.opencti.process_multiple_fields( - result["data"]["groupEdit"]["relationAdd"]) + result["data"]["groupEdit"]["relationAdd"] + ) def delete_role(self, **kwargs) -> Optional[Dict]: """Removes a role from a given group @@ -512,11 +525,13 @@ def delete_role(self, **kwargs) -> Optional[Dict]: if id is None or role_id is None: self.opencti.admin_logger.error( - "[opencti_group] Missing parameters: id and role_id") + "[opencti_group] Missing parameters: id and role_id" + ) return None self.opencti.admin_logger.info( - "Removing role from group", {"groupId": id, "roleId": role_id}) + "Removing role from group", {"groupId": id, "roleId": role_id} + ) query = ( """ mutation RoleDelete($groupId: ID!, $roleId: StixRef!) { @@ -532,7 +547,8 @@ def delete_role(self, **kwargs) -> Optional[Dict]: ) result = self.opencti.query(query, {"groupId": id, "roleId": role_id}) return self.opencti.process_multiple_fields( - result["data"]["groupEdit"]["relationDelete"]) + result["data"]["groupEdit"]["relationDelete"] + ) def edit_default_marking(self, **kwargs) -> Optional[Dict]: """Adds a default marking to the group. @@ -555,15 +571,13 @@ def edit_default_marking(self, **kwargs) -> Optional[Dict]: if id is None or marking_ids is None: self.opencti.admin_logger.error( - "[opencti_group] Missing parameters: id and marking_ids") + "[opencti_group] Missing parameters: id and marking_ids" + ) return None self.opencti.admin_logger.info( - "Setting default markings for entity on group", { - "markings": marking_ids, - "entity_type": entity_type, - "groupId": id - } + "Setting default markings for entity on group", + {"markings": marking_ids, "entity_type": entity_type, "groupId": id}, ) query = ( """ @@ -581,13 +595,12 @@ def edit_default_marking(self, **kwargs) -> Optional[Dict]: } """ ) - result = self.opencti.query(query, { - "id": id, - "entity_type": entity_type, - "values": marking_ids - }) + result = self.opencti.query( + query, {"id": id, "entity_type": entity_type, "values": marking_ids} + ) return self.opencti.process_multiple_fields( - result["data"]["groupEdit"]["editDefaultMarking"]) + result["data"]["groupEdit"]["editDefaultMarking"] + ) def add_allowed_marking(self, **kwargs) -> Optional[Dict]: """Allow a group to access a marking @@ -604,13 +617,14 @@ def add_allowed_marking(self, **kwargs) -> Optional[Dict]: if id is None or marking_id is None: self.opencti.admin_logger.error( - "[opencti_group] Missing parameters: id and marking_id") + "[opencti_group] Missing parameters: id and marking_id" + ) return None self.opencti.admin_logger.info( - "Granting group access to marking definition", { - "groupId": id, "markingId": marking_id - }) + "Granting group access to marking definition", + {"groupId": id, "markingId": marking_id}, + ) query = """ mutation GroupEditionMarkingsMarkingDefinitionsRelationAddMutation( $id: ID! @@ -649,12 +663,16 @@ def add_allowed_marking(self, **kwargs) -> Optional[Dict]: } } """ - result = self.opencti.query(query, {"id": id, "input": { - "relationship_type": "accesses-to", - "toId": marking_id - }}) + result = self.opencti.query( + query, + { + "id": id, + "input": {"relationship_type": "accesses-to", "toId": marking_id}, + }, + ) return self.opencti.process_multiple_fields( - result["data"]["groupEdit"]["relationAdd"]) + result["data"]["groupEdit"]["relationAdd"] + ) def delete_allowed_marking(self, **kwargs) -> Optional[Dict]: """Removes access to a marking for a group @@ -671,13 +689,14 @@ def delete_allowed_marking(self, **kwargs) -> Optional[Dict]: if id is None or marking_id is None: self.opencti.admin_logger.error( - "[opencti_group] Missing parameters: id and marking_id") + "[opencti_group] Missing parameters: id and marking_id" + ) return None self.opencti.admin_logger.info( - "Forbidding group access to marking definition", { - "groupId": id, "markingId": marking_id - }) + "Forbidding group access to marking definition", + {"groupId": id, "markingId": marking_id}, + ) query = ( """ mutation MarkingForbid($groupId: ID!, $markingId: StixRef!) { @@ -691,7 +710,7 @@ def delete_allowed_marking(self, **kwargs) -> Optional[Dict]: } """ ) - result = self.opencti.query( - query, {"groupId": id, "markingId": marking_id}) + result = self.opencti.query(query, {"groupId": id, "markingId": marking_id}) return self.opencti.process_multiple_fields( - result["data"]["groupEdit"]["relationDelete"]) + result["data"]["groupEdit"]["relationDelete"] + ) diff --git a/pycti/entities/opencti_role.py b/pycti/entities/opencti_role.py index 799fff01c..37a815078 100644 --- a/pycti/entities/opencti_role.py +++ b/pycti/entities/opencti_role.py @@ -89,8 +89,7 @@ def list(self, **kwargs) -> List[Dict]: edges { node { """ - + (self.properties if customAttributes is None - else customAttributes) + + (self.properties if customAttributes is None else customAttributes) + """ } } @@ -112,8 +111,8 @@ def list(self, **kwargs) -> List[Dict]: "after": after, "orderBy": orderBy, "orderMode": orderMode, - "search": search - } + "search": search, + }, ) if getAll: final_data = [] @@ -128,15 +127,16 @@ def list(self, **kwargs) -> List[Dict]: "after": after, "orderBy": orderBy, "orderMode": orderMode, - "search": search - } + "search": search, + }, ) data = self.opencti.process_multiple(result["data"]["roles"]) final_data = final_data + data return final_data else: - return self.opencti.process_multiple(result["data"]["roles"], - withPagination) + return self.opencti.process_multiple( + result["data"]["roles"], withPagination + ) def read(self, **kwargs) -> Optional[Dict]: """Get a role given its ID or a search term @@ -164,8 +164,7 @@ def read(self, **kwargs) -> Optional[Dict]: query RoleRead($id: String!) { role(id: $id) { """ - + (self.properties if customAttributes is None - else customAttributes) + + (self.properties if customAttributes is None else customAttributes) + """ } } @@ -191,9 +190,7 @@ def delete(self, **kwargs): id = kwargs.get("id", None) if id is None: - self.opencti.admin_logger.error( - "[opencti_role] Missing parameter: id" - ) + self.opencti.admin_logger.error("[opencti_role] Missing parameter: id") return None self.opencti.admin_logger.info("Deleting role", {"id": id}) @@ -224,32 +221,26 @@ def create(self, **kwargs) -> Optional[Dict]: customAttributes = kwargs.get("customAttributes", None) if name is None: - self.opencti.admin_logger.error( - "[opencti_role] Missing parameter: name" - ) + self.opencti.admin_logger.error("[opencti_role] Missing parameter: name") return None - self.opencti.admin_logger.info("Creating new role", { - "name": name, "description": description - }) + self.opencti.admin_logger.info( + "Creating new role", {"name": name, "description": description} + ) query = ( """ mutation RoleCreate($input: RoleAddInput!) { roleAdd(input: $input) { """ - + (self.properties if customAttributes is None - else customAttributes) + + (self.properties if customAttributes is None else customAttributes) + """ } } """ ) - result = self.opencti.query(query, { - "input": { - "name": name, - "description": description - } - }) + result = self.opencti.query( + query, {"input": {"name": name, "description": description}} + ) return self.opencti.process_multiple_fields(result["data"]["roleAdd"]) def update_field(self, **kwargs) -> Optional[Dict]: @@ -284,20 +275,20 @@ def update_field(self, **kwargs) -> Optional[Dict]: if id is None or input is None: self.opencti.admin_logger.error( - "[opencti_role] Missing parameters: id and input") + "[opencti_role] Missing parameters: id and input" + ) return None - self.opencti.admin_logger.info("Editing role with input", { - "id": id, "input": input - }) + self.opencti.admin_logger.info( + "Editing role with input", {"id": id, "input": input} + ) query = ( """ mutation RoleUpdate($id: ID!, $input: [EditInput]!) { roleEdit(id: $id) { fieldPatch(input: $input) { """ - + (self.properties if customAttributes is None - else customAttributes) + + (self.properties if customAttributes is None else customAttributes) + """ } } @@ -306,7 +297,8 @@ def update_field(self, **kwargs) -> Optional[Dict]: ) result = self.opencti.query(query, {"id": id, "input": input}) return self.opencti.process_multiple_fields( - result["data"]["roleEdit"]["fieldPatch"]) + result["data"]["roleEdit"]["fieldPatch"] + ) def add_capability(self, **kwargs) -> Optional[Dict]: """Adds a capability to a role @@ -324,12 +316,13 @@ def add_capability(self, **kwargs) -> Optional[Dict]: if id is None or capability_id is None: self.opencti.admin_logger( - "[opencti_role] Missing parameters: id and capability_id") + "[opencti_role] Missing parameters: id and capability_id" + ) return None - self.opencti.admin_logger.info("Adding capability to role", { - "roleId": id, "capabilityId": capability_id - }) + self.opencti.admin_logger.info( + "Adding capability to role", {"roleId": id, "capabilityId": capability_id} + ) query = ( """ mutation RoleAddCapability($id: ID!, $input: InternalRelationshipAddInput!) { @@ -359,12 +352,16 @@ def add_capability(self, **kwargs) -> Optional[Dict]: } """ ) - result = self.opencti.query(query, {"id": id, "input": { - "relationship_type": "has-capability", - "toId": capability_id - }}) + result = self.opencti.query( + query, + { + "id": id, + "input": {"relationship_type": "has-capability", "toId": capability_id}, + }, + ) return self.opencti.process_multiple_fields( - result["data"]["roleEdit"]["relationAdd"]) + result["data"]["roleEdit"]["relationAdd"] + ) def delete_capability(self, **kwargs) -> Optional[Dict]: """Removes a capability from a role @@ -381,12 +378,14 @@ def delete_capability(self, **kwargs) -> Optional[Dict]: if id is None or capability_id is None: self.opencti.admin_logger.error( - "[opencti_role] Missing parameters: id and capability_id") + "[opencti_role] Missing parameters: id and capability_id" + ) return None - self.opencti.admin_logger.info("Removing capability from role", { - "roleId": id, "capabilityId": capability_id - }) + self.opencti.admin_logger.info( + "Removing capability from role", + {"roleId": id, "capabilityId": capability_id}, + ) query = ( """ mutation RoleDeleteCapability($id: ID!, $toId: StixRef!) { diff --git a/pycti/entities/opencti_settings.py b/pycti/entities/opencti_settings.py index 2802e3008..8ffac5629 100644 --- a/pycti/entities/opencti_settings.py +++ b/pycti/entities/opencti_settings.py @@ -167,7 +167,8 @@ def __init__(self, opencti): password_policy_min_uppercase """ - self.editable_properties = """ + self.editable_properties = ( + """ id platform_organization { @@ -208,7 +209,9 @@ def __init__(self, opencti): platform_whitemark analytics_google_analytics_v4 enterprise_edition - """ + self.password_policy_properties + """ + + self.password_policy_properties + ) def read(self, **kwargs) -> Dict: """Reads settings from the platform @@ -234,10 +237,8 @@ def read(self, **kwargs) -> Dict: query PlatformSettings { settings { """ - + (self.properties if custom_attributes is None - else custom_attributes) - + (self.password_policy_properties if include_password_policy - else "") + + (self.properties if custom_attributes is None else custom_attributes) + + (self.password_policy_properties if include_password_policy else "") + (self.messages_properties if include_messages else "") + """ } @@ -277,20 +278,17 @@ def update_field(self, **kwargs) -> Optional[Dict]: ) return None - self.opencti.admin_logger.info("Updating settings with input", { - "id": id, - "input": input - }) + self.opencti.admin_logger.info( + "Updating settings with input", {"id": id, "input": input} + ) query = ( """ mutation SettingsUpdateField($id: ID!, $input: [EditInput]!) { settingsEdit(id: $id) { fieldPatch(input: $input) { """ - + (self.properties if custom_attributes is None - else custom_attributes) - + (self.password_policy_properties if include_password_policy - else "") + + (self.properties if custom_attributes is None else custom_attributes) + + (self.password_policy_properties if include_password_policy else "") + (self.messages_properties if include_messages else "") + """ } @@ -300,7 +298,8 @@ def update_field(self, **kwargs) -> Optional[Dict]: ) result = self.opencti.query(query, {"id": id, "input": input}) return self.opencti.process_multiple_fields( - result["data"]["settingsEdit"]["fieldPatch"]) + result["data"]["settingsEdit"]["fieldPatch"] + ) def edit_message(self, **kwargs) -> Optional[Dict]: """Edit or add a message to the platform @@ -319,10 +318,10 @@ def edit_message(self, **kwargs) -> Optional[Dict]: input = kwargs.get("input", None) if id is None or input is None: self.opencti.admin_logger.error( - "[opencti_settings] Missing parameters: id and input") + "[opencti_settings] Missing parameters: id and input" + ) return None - self.opencti.admin_logger.info( - "Editing message", {"id": id, "input": input}) + self.opencti.admin_logger.info("Editing message", {"id": id, "input": input}) query = ( """ @@ -331,7 +330,8 @@ def edit_message(self, **kwargs) -> Optional[Dict]: editMessage(input: $input) { id """ - + self.messages_properties + """ + + self.messages_properties + + """ } } } @@ -339,7 +339,8 @@ def edit_message(self, **kwargs) -> Optional[Dict]: ) result = self.opencti.query(query, {"id": id, "input": input}) return self.opencti.process_multiple_fields( - result["data"]["settingsEdit"]["editMessage"]) + result["data"]["settingsEdit"]["editMessage"] + ) def delete_message(self, **kwargs) -> Optional[Dict]: """Delete a message from the platform @@ -354,8 +355,7 @@ def delete_message(self, **kwargs) -> Optional[Dict]: id = kwargs.get("id", None) input = kwargs.get("input", None) if id is None: - self.opencti.admin_logger.info( - "[opencti_settings] Missing parameters: id") + self.opencti.admin_logger.info("[opencti_settings] Missing parameters: id") return None query = ( @@ -365,7 +365,8 @@ def delete_message(self, **kwargs) -> Optional[Dict]: deleteMessage(input: $input) { id """ - + self.messages_properties + """ + + self.messages_properties + + """ } } } @@ -373,4 +374,5 @@ def delete_message(self, **kwargs) -> Optional[Dict]: ) result = self.opencti.query(query, {"id": id, "input": input}) return self.opencti.process_multiple_fields( - result["data"]["settingsEdit"]["deleteMessage"]) + result["data"]["settingsEdit"]["deleteMessage"] + ) diff --git a/pycti/entities/opencti_user.py b/pycti/entities/opencti_user.py index 918dda90b..98e1afbe4 100644 --- a/pycti/entities/opencti_user.py +++ b/pycti/entities/opencti_user.py @@ -232,8 +232,9 @@ def list(self, **kwargs) -> List[Dict]: if getAll: first = 100 - self.opencti.admin_logger.info("Fetching users with filters", - {"filters": filters}) + self.opencti.admin_logger.info( + "Fetching users with filters", {"filters": filters} + ) query = ( """ query UserList($first: Int, $after: ID, $orderBy: UsersOrdering, $orderMode: OrderingMode, $filters: FilterGroup, $search: String) { @@ -241,8 +242,7 @@ def list(self, **kwargs) -> List[Dict]: edges { node { """ - + (self.properties if customAttributes is None - else customAttributes) + + (self.properties if customAttributes is None else customAttributes) + (self.session_properties if include_sessions else "") + """ } @@ -256,14 +256,17 @@ def list(self, **kwargs) -> List[Dict]: } """ ) - result = self.opencti.query(query, { - "first": first, - "after": after, - "orderBy": orderBy, - "orderMode": orderMode, - "filters": filters, - "search": search - }) + result = self.opencti.query( + query, + { + "first": first, + "after": after, + "orderBy": orderBy, + "orderMode": orderMode, + "filters": filters, + "search": search, + }, + ) if getAll: final_data = [] @@ -271,20 +274,24 @@ def list(self, **kwargs) -> List[Dict]: final_data = final_data + data while result["data"]["users"]["pageInfo"]["hasNextPage"]: after = result["data"]["users"]["pageInfo"]["endCursor"] - result = self.opencti.query(query, { - "first": first, - "after": after, - "orderBy": orderBy, - "orderMode": orderMode, - "filters": filters, - "search": search - }) + result = self.opencti.query( + query, + { + "first": first, + "after": after, + "orderBy": orderBy, + "orderMode": orderMode, + "filters": filters, + "search": search, + }, + ) data = self.opencti.process_multiple(result["data"]["users"]) final_data = final_data + data return final_data else: - return self.opencti.process_multiple(result["data"]["users"], - withPagination) + return self.opencti.process_multiple( + result["data"]["users"], withPagination + ) def read(self, **kwargs) -> Optional[Dict]: """Reads user details from the platform. @@ -314,16 +321,13 @@ def read(self, **kwargs) -> Optional[Dict]: filters = kwargs.get("filters", None) search = kwargs.get("search", None) if id is not None: - self.opencti.admin_logger.info( - "Fetching user with ID", {"id": id} - ) + self.opencti.admin_logger.info("Fetching user with ID", {"id": id}) query = ( """ query UserRead($id: String!) { user(id: $id) { """ - + (self.properties if customAttributes is None - else customAttributes) + + (self.properties if customAttributes is None else customAttributes) + (self.token_properties if include_token else "") + (self.session_properties if include_sessions else "") + """ @@ -338,7 +342,8 @@ def read(self, **kwargs) -> Optional[Dict]: filters=filters, search=search, include_sessions=include_sessions, - customAttributes=customAttributes) + customAttributes=customAttributes, + ) user = results[0] if results else None if not include_token or user is None: return user @@ -347,11 +352,12 @@ def read(self, **kwargs) -> Optional[Dict]: id=user["id"], include_sessions=include_sessions, include_token=include_token, - customAttributes=customAttributes + customAttributes=customAttributes, ) else: self.opencti.admin_logger.error( - "[opencti_user] Missing paramters: id, search, or filters") + "[opencti_user] Missing paramters: id, search, or filters" + ) return None def create(self, **kwargs) -> Optional[Dict]: @@ -425,8 +431,7 @@ def create(self, **kwargs) -> Optional[Dict]: theme = kwargs.get("theme", None) objectOrganization = kwargs.get("objectOrganization", None) account_status = kwargs.get("account_status", None) - account_lock_after_date = kwargs.get( - "account_lock_after_date", None) + account_lock_after_date = kwargs.get("account_lock_after_date", None) unit_system = kwargs.get("unit_system", None) submenu_show_icons = kwargs.get("submenu_show_icons", False) submenu_auto_collapse = kwargs.get("submenu_auto_collapse", False) @@ -438,51 +443,56 @@ def create(self, **kwargs) -> Optional[Dict]: if name is None or user_email is None: self.opencti.admin_logger.error( - "[opencti_user] Missing parameters: name and user_email") + "[opencti_user] Missing parameters: name and user_email" + ) return None self.opencti.admin_logger.info( - "Creating a new user", {"name": name, "email": user_email}) + "Creating a new user", {"name": name, "email": user_email} + ) if password is None: self.opencti.admin_logger.info( - "Generating random password for user", { - "name": name, "user_email": user_email - }) + "Generating random password for user", + {"name": name, "user_email": user_email}, + ) password = secrets.token_urlsafe(64) query = ( """ mutation UserAdd($input: UserAddInput!) { userAdd(input: $input) { """ - + (self.properties if customAttributes is None - else customAttributes) + + (self.properties if customAttributes is None else customAttributes) + (self.token_properties if include_token else "") + """ } } """ ) - result = self.opencti.query(query, {"input": { - "user_email": user_email, - "name": name, - "password": password, - "firstname": firstname, - "lastname": lastname, - "description": description, - "language": language, - "theme": theme, - "objectOrganization": objectOrganization, - "account_status": account_status, - "account_lock_after_date": account_lock_after_date, - "unit_system": unit_system, - "submenu_show_icons": submenu_show_icons, - "submenu_auto_collapse": submenu_auto_collapse, - "monochrome_labels": monochrome_labels, - "groups": groups, - "user_confidence_level": user_confidence_level - }}) - return self.opencti.process_multiple_fields( - result["data"]["userAdd"]) + result = self.opencti.query( + query, + { + "input": { + "user_email": user_email, + "name": name, + "password": password, + "firstname": firstname, + "lastname": lastname, + "description": description, + "language": language, + "theme": theme, + "objectOrganization": objectOrganization, + "account_status": account_status, + "account_lock_after_date": account_lock_after_date, + "unit_system": unit_system, + "submenu_show_icons": submenu_show_icons, + "submenu_auto_collapse": submenu_auto_collapse, + "monochrome_labels": monochrome_labels, + "groups": groups, + "user_confidence_level": user_confidence_level, + } + }, + ) + return self.opencti.process_multiple_fields(result["data"]["userAdd"]) def delete(self, **kwargs): """Deletes the given user from the platform. @@ -492,8 +502,7 @@ def delete(self, **kwargs): """ id = kwargs.get("id", None) if id is None: - self.opencti.admin_logger.error( - "[opencti_user] Missing parameter: id") + self.opencti.admin_logger.error("[opencti_user] Missing parameter: id") return None self.opencti.admin_logger.info("Deleting user", {"id": id}) @@ -526,8 +535,7 @@ def me(self, **kwargs) -> Dict: query Me { me { """ - + (self.me_properties if customAttributes is None - else customAttributes) + + (self.me_properties if customAttributes is None else customAttributes) + (self.token_properties if include_token else "") + """ } @@ -554,20 +562,22 @@ def update_field(self, **kwargs) -> Optional[Dict]: customAttributes = kwargs.get("customAttributes", None) if id is None or input is None: self.opencti.admin_logger.error( - "[opencti_user] Missing parameters: id and input") + "[opencti_user] Missing parameters: id and input" + ) return None self.opencti.admin_logger.info( "Editing user with input (not shown to hide password and API token" - " changes)", {"id": id}) + " changes)", + {"id": id}, + ) query = ( """ mutation UserEdit($id: ID!, $input: [EditInput]!) { userEdit(id: $id) { fieldPatch(input: $input) { """ - + (self.properties if customAttributes is None - else customAttributes) + + (self.properties if customAttributes is None else customAttributes) + """ } } @@ -576,7 +586,8 @@ def update_field(self, **kwargs) -> Optional[Dict]: ) result = self.opencti.query(query, {"id": id, "input": input}) return self.opencti.process_multiple_fields( - result["data"]["userEdit"]["fieldPatch"]) + result["data"]["userEdit"]["fieldPatch"] + ) def add_membership(self, **kwargs) -> Optional[Dict]: """Adds the user to a given group. @@ -592,12 +603,13 @@ def add_membership(self, **kwargs) -> Optional[Dict]: group_id = kwargs.get("group_id", None) if id is None or group_id is None: self.opencti.admin_logger.error( - "[opencti_user] Missing parameters: id and group_id") + "[opencti_user] Missing parameters: id and group_id" + ) return None - self.opencti.admin_logger.info("Adding user to group", { - "id": id, "group_id": group_id - }) + self.opencti.admin_logger.info( + "Adding user to group", {"id": id, "group_id": group_id} + ) query = """ mutation UserAddMembership($id: ID!, $group_id: ID!) { userEdit(id: $id) { @@ -622,7 +634,8 @@ def add_membership(self, **kwargs) -> Optional[Dict]: """ result = self.opencti.query(query, {"id": id, "group_id": group_id}) return self.opencti.process_multiple_fields( - result["data"]["userEdit"]["relationAdd"]) + result["data"]["userEdit"]["relationAdd"] + ) def delete_membership(self, **kwargs) -> Optional[Dict]: """Removes the user from the given group. @@ -638,11 +651,13 @@ def delete_membership(self, **kwargs) -> Optional[Dict]: group_id = kwargs.get("group_id", None) if id is None or group_id is None: self.opencti.admin_logger.error( - "[opencti_user] Missing parameters: id and group_id") + "[opencti_user] Missing parameters: id and group_id" + ) return None - self.opencti.admin_logger.info("Removing used from group", { - "id": id, "group_id": group_id}) + self.opencti.admin_logger.info( + "Removing used from group", {"id": id, "group_id": group_id} + ) query = ( """ mutation UserDeleteMembership($id: ID!, $group_id: StixRef!) { @@ -658,7 +673,8 @@ def delete_membership(self, **kwargs) -> Optional[Dict]: ) result = self.opencti.query(query, {"id": id, "group_id": group_id}) return self.opencti.process_multiple_fields( - result["data"]["userEdit"]["relationDelete"]) + result["data"]["userEdit"]["relationDelete"] + ) def add_organization(self, **kwargs) -> Optional[Dict]: """Adds a user to an organization @@ -674,11 +690,13 @@ def add_organization(self, **kwargs) -> Optional[Dict]: organization_id = kwargs.get("organization_id", None) if id is None or organization_id is None: self.opencti.admin_logger.error( - "[opencti_user] Missing parameters: id and organization_id") + "[opencti_user] Missing parameters: id and organization_id" + ) - self.opencti.admin_logger.info("Adding user to organization", { - "id": id, "organization_id": organization_id - }) + self.opencti.admin_logger.info( + "Adding user to organization", + {"id": id, "organization_id": organization_id}, + ) query = ( """ mutation UserAddOrganization($id: ID!, $organization_id: ID!) { @@ -692,10 +710,12 @@ def add_organization(self, **kwargs) -> Optional[Dict]: } """ ) - result = self.opencti.query(query, { - "id": id, "organization_id": organization_id}) + result = self.opencti.query( + query, {"id": id, "organization_id": organization_id} + ) return self.opencti.process_multiple_fields( - result["data"]["userEdit"]["organizationAdd"]) + result["data"]["userEdit"]["organizationAdd"] + ) def delete_organization(self, **kwargs) -> Optional[Dict]: """Delete a user from an organization @@ -711,11 +731,13 @@ def delete_organization(self, **kwargs) -> Optional[Dict]: organization_id = kwargs.get("organization_id", None) if id is None or organization_id is None: self.opencti.admin_logger.error( - "[opencti_user] Missing parameters: id and organization_id") + "[opencti_user] Missing parameters: id and organization_id" + ) - self.opencti.admin_logger.info("Removing user from organization", { - "id": id, "organization_id": organization_id - }) + self.opencti.admin_logger.info( + "Removing user from organization", + {"id": id, "organization_id": organization_id}, + ) query = ( """ mutation UserDeleteOrganization($id: ID!, $organization_id: ID!) { @@ -729,10 +751,12 @@ def delete_organization(self, **kwargs) -> Optional[Dict]: } """ ) - result = self.opencti.query(query, { - "id": id, "organization_id": organization_id}) + result = self.opencti.query( + query, {"id": id, "organization_id": organization_id} + ) return self.opencti.process_multiple_fields( - result["data"]["userEdit"]["organizationDelete"]) + result["data"]["userEdit"]["organizationDelete"] + ) def token_renew(self, **kwargs) -> Optional[Dict]: """Rotates the API token for the given user @@ -748,8 +772,7 @@ def token_renew(self, **kwargs) -> Optional[Dict]: id = kwargs.get("id", None) include_token = kwargs.get("include_token", False) if id is None: - self.opencti.admin_logger.error( - "[opencti_user] Missing parameter: id") + self.opencti.admin_logger.error("[opencti_user] Missing parameter: id") return None self.opencti.admin_logger.info("Rotating API key for user", {"id": id}) @@ -769,4 +792,5 @@ def token_renew(self, **kwargs) -> Optional[Dict]: ) result = self.opencti.query(query, {"id": id}) return self.opencti.process_multiple_fields( - result["data"]["userEdit"]["tokenRenew"]) + result["data"]["userEdit"]["tokenRenew"] + ) diff --git a/tests/02-integration/entities/test_entity_crud.py b/tests/02-integration/entities/test_entity_crud.py index f44d3e822..b49eed7cf 100644 --- a/tests/02-integration/entities/test_entity_crud.py +++ b/tests/02-integration/entities/test_entity_crud.py @@ -48,8 +48,7 @@ def test_update(entity_class): assert "id" in test_indicator, "No ID on object" if len(entity_class.update_data()) > 0: - function_present = getattr( - entity_class.own_class(), "update_field", None) + function_present = getattr(entity_class.own_class(), "update_field", None) if function_present: for update_field, update_value in entity_class.update_data().items(): class_data[update_field] = update_value @@ -64,9 +63,12 @@ def test_update(entity_class): result = entity_class.own_class().create(**class_data) result = entity_class.own_class().read(id=result["id"]) - assert result["id"] == test_indicator["id"], "Updated SDO does not match old ID" - compare_values(class_data, result, - entity_class.get_compare_exception_keys()) + assert ( + result["id"] == test_indicator["id"] + ), "Updated SDO does not match old ID" + compare_values( + class_data, result, entity_class.get_compare_exception_keys() + ) else: result = test_indicator @@ -98,7 +100,8 @@ def test_filter(entity_class): assert test_indicator is not None, "Response is NoneType" assert "id" in test_indicator, "No ID on object" test_indicator = entity_class.own_class().read( - filters=entity_class.get_filter()) + filters=entity_class.get_filter() + ) compare_values( class_data, test_indicator, diff --git a/tests/02-integration/entities/test_group.py b/tests/02-integration/entities/test_group.py index 687242cc8..8018d3853 100644 --- a/tests/02-integration/entities/test_group.py +++ b/tests/02-integration/entities/test_group.py @@ -31,16 +31,12 @@ def test_group_default_markings(api_client): assert test_marking is not None, "Create marking response is NoneType" api_client.group.edit_default_marking( - id=test_group["id"], - marking_ids=[test_marking["id"]] + id=test_group["id"], marking_ids=[test_marking["id"]] ) result = api_client.group.read(id=test_group["id"]) assert result["default_marking"][0]["values"][0]["id"] == test_marking["id"] - api_client.group.edit_default_marking( - id=test_group["id"], - marking_ids=[] - ) + api_client.group.edit_default_marking(id=test_group["id"], marking_ids=[]) result = api_client.group.read(id=test_group["id"]) assert len(result["default_marking"][0]["values"]) == 0 @@ -59,14 +55,13 @@ def test_group_membership(api_client): assert test_group is not None, "Create group response is NoneType" assert test_user is not None, "Create user response is NoneType" - group_test.own_class().add_member( - id=test_group["id"], user_id=test_user["id"]) + group_test.own_class().add_member(id=test_group["id"], user_id=test_user["id"]) result = group_test.own_class().read(id=test_group["id"]) assert result["membersIds"][0] == test_user["id"] group_test.own_class().delete_member( - id=test_group["id"], - user_id=test_user["id"]) + id=test_group["id"], user_id=test_user["id"] + ) result = group_test.own_class().read(id=test_group["id"]) assert len(result["membersIds"]) == 0 finally: @@ -84,15 +79,13 @@ def test_group_allowed_markings(api_client): assert test_marking is not None, "Create marking response is NoneType" api_client.group.add_allowed_marking( - id=test_group["id"], - marking_id=test_marking["id"] + id=test_group["id"], marking_id=test_marking["id"] ) result = api_client.group.read(id=test_group["id"]) assert result["allowed_marking"][0]["id"] == test_marking["id"] api_client.group.delete_allowed_marking( - id=test_group["id"], - marking_id=test_marking["id"] + id=test_group["id"], marking_id=test_marking["id"] ) result = api_client.group.read(id=test_group["id"]) assert len(result["allowed_marking"]) == 0 diff --git a/tests/02-integration/entities/test_role.py b/tests/02-integration/entities/test_role.py index f33d156fd..e8f873aaf 100644 --- a/tests/02-integration/entities/test_role.py +++ b/tests/02-integration/entities/test_role.py @@ -10,12 +10,14 @@ def test_role_capabilities(api_client): capability_id = api_client.capability.list()[0]["id"] role_test.own_class().add_capability( - id=test_role["id"], capability_id=capability_id) + id=test_role["id"], capability_id=capability_id + ) result = role_test.own_class().read(id=test_role["id"]) assert capability_id in result["capabilitiesIds"] role_test.own_class().delete_capability( - id=test_role["id"], capability_id=capability_id) + id=test_role["id"], capability_id=capability_id + ) result = role_test.own_class().read(id=test_role["id"]) assert capability_id not in result["capabilitiesIds"] diff --git a/tests/02-integration/entities/test_settings.py b/tests/02-integration/entities/test_settings.py index 52d4d5526..96f4a5341 100644 --- a/tests/02-integration/entities/test_settings.py +++ b/tests/02-integration/entities/test_settings.py @@ -13,11 +13,9 @@ def test_settings_messages(api_client): test_message_data = { "message": "This is a test message", "activated": True, - "dismissible": True + "dismissible": True, } - result = settings_test.own_class().edit_message( - id=id, - input=test_message_data) + result = settings_test.own_class().edit_message(id=id, input=test_message_data) assert len(result["platform_messages"]) == num_messages + 1 test_message = result["platform_messages"][-1] @@ -30,8 +28,8 @@ def test_settings_messages(api_client): "id": test_message["id"], "message": "This is an updated test message", "activated": True, - "dismissible": False - } + "dismissible": False, + }, ) assert len(result["platform_messages"]) == num_messages + 1 diff --git a/tests/02-integration/entities/test_user.py b/tests/02-integration/entities/test_user.py index 5129cf5c6..6148f18e7 100644 --- a/tests/02-integration/entities/test_user.py +++ b/tests/02-integration/entities/test_user.py @@ -13,15 +13,13 @@ def test_user_membership(api_client): assert test_group is not None, "Group create response returned NoneType" user_test.own_class().add_membership( - id=test_user["id"], - group_id=test_group["id"] + id=test_user["id"], group_id=test_group["id"] ) result = user_test.own_class().read(id=test_user["id"]) assert test_group["id"] in result["groupsIds"] user_test.own_class().delete_membership( - id=test_user["id"], - group_id=test_group["id"] + id=test_user["id"], group_id=test_group["id"] ) result = user_test.own_class().read(id=test_user["id"]) assert test_group["id"] not in result["groupsIds"] @@ -42,15 +40,13 @@ def test_user_organization(api_client): assert test_org is not None, "Organization create response returned NoneType" user_test.own_class().add_organization( - id=test_user["id"], - organization_id=test_org["id"] + id=test_user["id"], organization_id=test_org["id"] ) result = user_test.own_class().read(id=test_user["id"]) assert test_org["id"] in result["objectOrganizationIds"] user_test.own_class().delete_organization( - id=test_user["id"], - organization_id=test_org["id"] + id=test_user["id"], organization_id=test_org["id"] ) result = user_test.own_class().read(id=test_user["id"]) assert test_org["id"] not in result["objectOrganizationIds"] @@ -61,14 +57,14 @@ def test_user_organization(api_client): def test_user_token_renew(api_client): user_test = UserTest(api_client) - test_user = user_test.own_class().create(**user_test.data(), - include_token=True) + test_user = user_test.own_class().create(**user_test.data(), include_token=True) try: assert test_user is not None, "User create response returned NoneType" old_token = test_user["api_token"] - result = user_test.own_class().token_renew(id=test_user["id"], - include_token=True) + result = user_test.own_class().token_renew( + id=test_user["id"], include_token=True + ) assert old_token != result["api_token"] finally: user_test.own_class().delete(id=test_user["id"]) diff --git a/tests/cases/entities.py b/tests/cases/entities.py index ab7eb44d8..7d6451c9f 100644 --- a/tests/cases/entities.py +++ b/tests/cases/entities.py @@ -1200,10 +1200,7 @@ class GroupTest(EntityTest): def data(self) -> Dict: return { "name": "TestGroup", - "group_confidence_level": { - "max_confidence": 80, - "overrides": [] - } + "group_confidence_level": {"max_confidence": 80, "overrides": []}, } def own_class(self): @@ -1218,11 +1215,8 @@ def update_data(self) -> Dict: "no_creators": True, "group_confidence_level": { "max_confidence": 90, - "overrides": [{ - "entity_type": "Indicator", - "max_confidence": 80 - }] - } + "overrides": [{"entity_type": "Indicator", "max_confidence": 80}], + }, } def get_search(self) -> str: @@ -1249,13 +1243,10 @@ def update_data(self) -> Dict: "lastname": "User", "user_confidence_level": { "max_confidence": 70, - "overrides": [{ - "entity_type": "Indicator", - "max_confidence": 80 - }] + "overrides": [{"entity_type": "Indicator", "max_confidence": 80}], }, "account_status": "Locked", - "submenu_show_icons": True + "submenu_show_icons": True, } def get_search(self): @@ -1276,14 +1267,16 @@ def create(self, **kwargs) -> Dict: :rtype: Dict """ self.opencti.admin_logger.info( - "Settings.create called with arguments", kwargs) + "Settings.create called with arguments", kwargs + ) self._deleted = False return self.read() def delete(self, **kwargs): """Stub function for tests""" self.opencti.admin_logger.info( - "Settings.delete called with arguments", kwargs) + "Settings.delete called with arguments", kwargs + ) self._deleted = True def read(self, **kwargs): @@ -1297,20 +1290,21 @@ def setup(self): # Save current platform information custom_attributes = self.own_class().editable_properties self.own_class().create() - self._saved_settings = self.own_class().read( - customAttributes=custom_attributes) + self._saved_settings = self.own_class().read(customAttributes=custom_attributes) if self._saved_settings["platform_organization"] is not None: - self._saved_settings[ - "platform_organization"] = self._saved_settings[ - "platform_organization"]["id"] + self._saved_settings["platform_organization"] = self._saved_settings[ + "platform_organization" + ]["id"] return def teardown(self): # Restore platform information id = self._saved_settings.pop("id") - input = [{"key": key, "value": value} - for key, value in self._saved_settings.items() - if value is not None] + input = [ + {"key": key, "value": value} + for key, value in self._saved_settings.items() + if value is not None + ] self.own_class().update_field(id=id, input=input) return @@ -1324,10 +1318,7 @@ def base_class(self): return self.own_class() def update_data(self): - return { - "platform_title": "This is a test platform", - "platform_theme": "light" - } + return {"platform_title": "This is a test platform", "platform_theme": "light"} def get_filter(self): return None From 0a77c8a812bfb077416216b6732bceb7134c069e Mon Sep 17 00:00:00 2001 From: peritz Date: Fri, 31 Jan 2025 11:02:38 +0000 Subject: [PATCH 26/27] Update variable names for admin entities --- pycti/entities/opencti_capability.py | 8 +++-- pycti/entities/opencti_group.py | 34 ++++++++++--------- pycti/entities/opencti_role.py | 24 +++++++------- pycti/entities/opencti_settings.py | 2 +- pycti/entities/opencti_user.py | 49 ++++++++++++++++------------ 5 files changed, 65 insertions(+), 52 deletions(-) diff --git a/pycti/entities/opencti_capability.py b/pycti/entities/opencti_capability.py index 7f54dcd80..8d41ee266 100644 --- a/pycti/entities/opencti_capability.py +++ b/pycti/entities/opencti_capability.py @@ -13,9 +13,13 @@ def __init__(self, opencti): self.properties = """ id entity_type + parent_types name description attribute_order + + created_at + updated_at """ def list(self, **kwargs) -> List[Dict]: @@ -27,7 +31,7 @@ def list(self, **kwargs) -> List[Dict]: :return: List of capabilities :rtype: List[Dict] """ - customAttributes = kwargs.get("customAttributes") + custom_attributes = kwargs.get("customAttributes") self.opencti.admin_logger.info("Listing capabilities") query = ( """ @@ -36,7 +40,7 @@ def list(self, **kwargs) -> List[Dict]: edges { node { """ - + (self.properties if customAttributes is None else customAttributes) + + (self.properties if custom_attributes is None else custom_attributes) + """ } } diff --git a/pycti/entities/opencti_group.py b/pycti/entities/opencti_group.py index 81d5d7a94..a4b9ee596 100644 --- a/pycti/entities/opencti_group.py +++ b/pycti/entities/opencti_group.py @@ -29,6 +29,8 @@ def __init__(self, opencti): name description + entity_type + parent_types created_at updated_at @@ -125,7 +127,7 @@ def list(self, **kwargs) -> List[Dict]: orderMode = kwargs.get("orderMode", None) search = kwargs.get("search", None) filters = kwargs.get("filters", None) - customAttributes = kwargs.get("customAttributes", None) + custom_attributes = kwargs.get("customAttributes", None) getAll = kwargs.get("getAll", False) withPagination = kwargs.get("withPagination", False) @@ -142,7 +144,7 @@ def list(self, **kwargs) -> List[Dict]: edges { node { """ - + (self.properties if customAttributes is None else customAttributes) + + (self.properties if custom_attributes is None else custom_attributes) + """ } } @@ -211,7 +213,7 @@ def read(self, **kwargs) -> Optional[Dict]: id = kwargs.get("id", None) filters = kwargs.get("filters", None) search = kwargs.get("search", None) - customAttributes = kwargs.get("customAttributes", None) + custom_attributes = kwargs.get("customAttributes", None) if id is not None: self.opencti.admin_logger.info("Fetching group with ID", {"id": id}) query = ( @@ -219,7 +221,7 @@ def read(self, **kwargs) -> Optional[Dict]: query Group($id: String!) { group(id: $id) { """ - + (self.properties if customAttributes is None else customAttributes) + + (self.properties if custom_attributes is None else custom_attributes) + """ } } @@ -229,7 +231,7 @@ def read(self, **kwargs) -> Optional[Dict]: return self.opencti.process_multiple_fields(result["data"]["group"]) elif filters is not None or search is not None: results = self.list( - filters=filters, search=search, customAttributes=customAttributes + filters=filters, search=search, customAttributes=custom_attributes ) return results[0] if results else None else: @@ -275,11 +277,11 @@ def create(self, **kwargs) -> Optional[Dict]: no_creators = kwargs.get("no_creators", False) restrict_delete = kwargs.get("restrict_delete", False) auto_new_marking = kwargs.get("auto_new_marking", False) - customAttributes = kwargs.get("customAttributes", None) + custom_attributes = kwargs.get("customAttributes", None) if name is None or group_confidence_level is None: self.opencti.admin_logger.error( - "[opencti_group] Missing parameters: name and " "group_confidence_level" + "[opencti_group] Missing parameters: name and group_confidence_level" ) return None @@ -300,7 +302,7 @@ def create(self, **kwargs) -> Optional[Dict]: mutation GroupAdd($input: GroupAddInput!) { groupAdd(input: $input) { """ - + (self.properties if customAttributes is None else customAttributes) + + (self.properties if custom_attributes is None else custom_attributes) + """ } } @@ -352,7 +354,7 @@ def update_field(self, **kwargs) -> Optional[Dict]: """ id = kwargs.get("id", None) input = kwargs.get("input", None) - customAttributes = kwargs.get("customAttributes", None) + custom_attributes = kwargs.get("customAttributes", None) if id is None or input is None: self.opencti.admin_logger.error( @@ -367,7 +369,7 @@ def update_field(self, **kwargs) -> Optional[Dict]: groupEdit(id: $id) { fieldPatch(input: $input) { """ - + (self.properties if customAttributes is None else customAttributes) + + (self.properties if custom_attributes is None else custom_attributes) + """ } } @@ -402,7 +404,7 @@ def add_member(self, **kwargs) -> Optional[Dict]: "Adding member to group", {"groupId": id, "userId": user_id} ) query = """ - mutation MemberAdd($groupId: ID!, $userId: ID!) { + mutation GroupEditMemberAdd($groupId: ID!, $userId: ID!) { groupEdit(id: $groupId) { relationAdd(input: { fromId: $userId, @@ -449,7 +451,7 @@ def delete_member(self, **kwargs) -> Optional[Dict]: ) query = ( """ - mutation MemberDelete ($groupId: ID!, $userId: StixRef!) { + mutation GroupEditMemberDelete ($groupId: ID!, $userId: StixRef!) { groupEdit(id: $groupId) { relationDelete(fromId: $userId, relationship_type: "member-of") { """ @@ -488,7 +490,7 @@ def add_role(self, **kwargs) -> Optional[Dict]: "Adding role to group", {"groupId": id, "roleId": role_id} ) query = """ - mutation RoleAdd($groupId: ID!, $roleId: ID!) { + mutation GroupEditRoleAdd($groupId: ID!, $roleId: ID!) { groupEdit(id: $groupId) { relationAdd(input: { toId: $roleId, relationship_type: "has-role" @@ -534,7 +536,7 @@ def delete_role(self, **kwargs) -> Optional[Dict]: ) query = ( """ - mutation RoleDelete($groupId: ID!, $roleId: StixRef!) { + mutation GroupEditRoleDelete($groupId: ID!, $roleId: StixRef!) { groupEdit(id: $groupId) { relationDelete(toId: $roleId, relationship_type: "has-role") { """ @@ -581,7 +583,7 @@ def edit_default_marking(self, **kwargs) -> Optional[Dict]: ) query = ( """ - mutation EditDefaultMarking($id: ID!, $entity_type: String!, $values: [String!]) { + mutation GroupEditEditDefaultMarking($id: ID!, $entity_type: String!, $values: [String!]) { groupEdit(id: $id) { editDefaultMarking(input: { entity_type: $entity_type, @@ -699,7 +701,7 @@ def delete_allowed_marking(self, **kwargs) -> Optional[Dict]: ) query = ( """ - mutation MarkingForbid($groupId: ID!, $markingId: StixRef!) { + mutation GroupEditMarkingRemove($groupId: ID!, $markingId: StixRef!) { groupEdit(id: $groupId) { relationDelete(toId: $markingId, relationship_type: "accesses-to") { """ diff --git a/pycti/entities/opencti_role.py b/pycti/entities/opencti_role.py index 37a815078..d2fec4a9d 100644 --- a/pycti/entities/opencti_role.py +++ b/pycti/entities/opencti_role.py @@ -71,9 +71,9 @@ def list(self, **kwargs) -> List[Dict]: after = kwargs.get("after", None) orderBy = kwargs.get("orderBy", None) orderMode = kwargs.get("orderMode", None) - customAttributes = kwargs.get("customAttributes", None) + custom_attributes = kwargs.get("customAttributes", None) getAll = kwargs.get("getAll", False) - withPagination = kwargs.get("withPagination", False) + with_pagination = kwargs.get("withPagination", False) self.opencti.admin_logger.info( "Searching roles matching search term", {"search": search} @@ -89,7 +89,7 @@ def list(self, **kwargs) -> List[Dict]: edges { node { """ - + (self.properties if customAttributes is None else customAttributes) + + (self.properties if custom_attributes is None else custom_attributes) + """ } } @@ -135,7 +135,7 @@ def list(self, **kwargs) -> List[Dict]: return final_data else: return self.opencti.process_multiple( - result["data"]["roles"], withPagination + result["data"]["roles"], with_pagination ) def read(self, **kwargs) -> Optional[Dict]: @@ -155,7 +155,7 @@ def read(self, **kwargs) -> Optional[Dict]: """ id = kwargs.get("id", None) search = kwargs.get("search", None) - customAttributes = kwargs.get("customAttributes", None) + custom_attributes = kwargs.get("customAttributes", None) if id is not None: self.opencti.admin_logger.info("Reading role", {"id": id}) @@ -164,7 +164,7 @@ def read(self, **kwargs) -> Optional[Dict]: query RoleRead($id: String!) { role(id: $id) { """ - + (self.properties if customAttributes is None else customAttributes) + + (self.properties if custom_attributes is None else custom_attributes) + """ } } @@ -218,7 +218,7 @@ def create(self, **kwargs) -> Optional[Dict]: """ name = kwargs.get("name", None) description = kwargs.get("description", None) - customAttributes = kwargs.get("customAttributes", None) + custom_attributes = kwargs.get("customAttributes", None) if name is None: self.opencti.admin_logger.error("[opencti_role] Missing parameter: name") @@ -232,7 +232,7 @@ def create(self, **kwargs) -> Optional[Dict]: mutation RoleCreate($input: RoleAddInput!) { roleAdd(input: $input) { """ - + (self.properties if customAttributes is None else customAttributes) + + (self.properties if custom_attributes is None else custom_attributes) + """ } } @@ -271,7 +271,7 @@ def update_field(self, **kwargs) -> Optional[Dict]: """ id = kwargs.get("id", None) input = kwargs.get("input", None) - customAttributes = kwargs.get("customAttributes", None) + custom_attributes = kwargs.get("customAttributes", None) if id is None or input is None: self.opencti.admin_logger.error( @@ -288,7 +288,7 @@ def update_field(self, **kwargs) -> Optional[Dict]: roleEdit(id: $id) { fieldPatch(input: $input) { """ - + (self.properties if customAttributes is None else customAttributes) + + (self.properties if custom_attributes is None else custom_attributes) + """ } } @@ -325,7 +325,7 @@ def add_capability(self, **kwargs) -> Optional[Dict]: ) query = ( """ - mutation RoleAddCapability($id: ID!, $input: InternalRelationshipAddInput!) { + mutation RoleEditAddCapability($id: ID!, $input: InternalRelationshipAddInput!) { roleEdit(id: $id) { relationAdd(input: $input) { id @@ -388,7 +388,7 @@ def delete_capability(self, **kwargs) -> Optional[Dict]: ) query = ( """ - mutation RoleDeleteCapability($id: ID!, $toId: StixRef!) { + mutation RoleEditDeleteCapability($id: ID!, $toId: StixRef!) { roleEdit(id: $id) { relationDelete(toId: $toId, relationship_type: "has-capability") { """ diff --git a/pycti/entities/opencti_settings.py b/pycti/entities/opencti_settings.py index 8ffac5629..6fb42ade7 100644 --- a/pycti/entities/opencti_settings.py +++ b/pycti/entities/opencti_settings.py @@ -360,7 +360,7 @@ def delete_message(self, **kwargs) -> Optional[Dict]: query = ( """ - mutation SettingsDeleteMessage($id: ID!, $input: String!) { + mutation SettingsEditDeleteMessage($id: ID!, $input: String!) { settingsEdit(id: $id) { deleteMessage(input: $input) { id diff --git a/pycti/entities/opencti_user.py b/pycti/entities/opencti_user.py index 98e1afbe4..ecec35c50 100644 --- a/pycti/entities/opencti_user.py +++ b/pycti/entities/opencti_user.py @@ -39,6 +39,8 @@ def __init__(self, opencti): account_status account_lock_after_date + entity_type + parent_types created_at updated_at @@ -129,6 +131,11 @@ def __init__(self, opencti): submenu_show_icons submenu_auto_collapse + entity_type + parent_types + created_at + updated_at + objectOrganization { edges { node { @@ -225,9 +232,9 @@ def list(self, **kwargs) -> List[Dict]: filters = kwargs.get("filters", None) search = kwargs.get("search", None) include_sessions = kwargs.get("include_sessions", False) - customAttributes = kwargs.get("customAttributes", None) + custom_attributes = kwargs.get("customAttributes", None) getAll = kwargs.get("getAll", False) - withPagination = kwargs.get("withPagination", False) + with_pagination = kwargs.get("withPagination", False) if getAll: first = 100 @@ -242,7 +249,7 @@ def list(self, **kwargs) -> List[Dict]: edges { node { """ - + (self.properties if customAttributes is None else customAttributes) + + (self.properties if custom_attributes is None else custom_attributes) + (self.session_properties if include_sessions else "") + """ } @@ -290,7 +297,7 @@ def list(self, **kwargs) -> List[Dict]: return final_data else: return self.opencti.process_multiple( - result["data"]["users"], withPagination + result["data"]["users"], with_pagination ) def read(self, **kwargs) -> Optional[Dict]: @@ -317,7 +324,7 @@ def read(self, **kwargs) -> Optional[Dict]: id = kwargs.get("id", None) include_sessions = kwargs.get("include_sessions", False) include_token = kwargs.get("include_token", False) - customAttributes = kwargs.get("customAttributes", None) + custom_attributes = kwargs.get("customAttributes", None) filters = kwargs.get("filters", None) search = kwargs.get("search", None) if id is not None: @@ -327,7 +334,7 @@ def read(self, **kwargs) -> Optional[Dict]: query UserRead($id: String!) { user(id: $id) { """ - + (self.properties if customAttributes is None else customAttributes) + + (self.properties if custom_attributes is None else custom_attributes) + (self.token_properties if include_token else "") + (self.session_properties if include_sessions else "") + """ @@ -342,7 +349,7 @@ def read(self, **kwargs) -> Optional[Dict]: filters=filters, search=search, include_sessions=include_sessions, - customAttributes=customAttributes, + customAttributes=custom_attributes, ) user = results[0] if results else None if not include_token or user is None: @@ -352,7 +359,7 @@ def read(self, **kwargs) -> Optional[Dict]: id=user["id"], include_sessions=include_sessions, include_token=include_token, - customAttributes=customAttributes, + customAttributes=custom_attributes, ) else: self.opencti.admin_logger.error( @@ -429,7 +436,7 @@ def create(self, **kwargs) -> Optional[Dict]: description = kwargs.get("description", None) language = kwargs.get("language", None) theme = kwargs.get("theme", None) - objectOrganization = kwargs.get("objectOrganization", None) + object_organization = kwargs.get("objectOrganization", None) account_status = kwargs.get("account_status", None) account_lock_after_date = kwargs.get("account_lock_after_date", None) unit_system = kwargs.get("unit_system", None) @@ -438,7 +445,7 @@ def create(self, **kwargs) -> Optional[Dict]: monochrome_labels = kwargs.get("monochrome_labels", False) groups = kwargs.get("groups", None) user_confidence_level = kwargs.get("user_confidence_level", None) - customAttributes = kwargs.get("customAttributes", None) + custom_attributes = kwargs.get("customAttributes", None) include_token = kwargs.get("include_token", False) if name is None or user_email is None: @@ -461,7 +468,7 @@ def create(self, **kwargs) -> Optional[Dict]: mutation UserAdd($input: UserAddInput!) { userAdd(input: $input) { """ - + (self.properties if customAttributes is None else customAttributes) + + (self.properties if custom_attributes is None else custom_attributes) + (self.token_properties if include_token else "") + """ } @@ -480,7 +487,7 @@ def create(self, **kwargs) -> Optional[Dict]: "description": description, "language": language, "theme": theme, - "objectOrganization": objectOrganization, + "objectOrganization": object_organization, "account_status": account_status, "account_lock_after_date": account_lock_after_date, "unit_system": unit_system, @@ -527,7 +534,7 @@ def me(self, **kwargs) -> Dict: :rtype: dict """ include_token = kwargs.get("include_token", False) - customAttributes = kwargs.get("customAttributes", None) + custom_attributes = kwargs.get("customAttributes", None) self.opencti.admin_logger.info("Reading MeUser") query = ( @@ -535,7 +542,7 @@ def me(self, **kwargs) -> Dict: query Me { me { """ - + (self.me_properties if customAttributes is None else customAttributes) + + (self.me_properties if custom_attributes is None else custom_attributes) + (self.token_properties if include_token else "") + """ } @@ -559,7 +566,7 @@ def update_field(self, **kwargs) -> Optional[Dict]: """ id = kwargs.get("id", None) input = kwargs.get("input", None) - customAttributes = kwargs.get("customAttributes", None) + custom_attributes = kwargs.get("customAttributes", None) if id is None or input is None: self.opencti.admin_logger.error( "[opencti_user] Missing parameters: id and input" @@ -577,7 +584,7 @@ def update_field(self, **kwargs) -> Optional[Dict]: userEdit(id: $id) { fieldPatch(input: $input) { """ - + (self.properties if customAttributes is None else customAttributes) + + (self.properties if custom_attributes is None else custom_attributes) + """ } } @@ -611,7 +618,7 @@ def add_membership(self, **kwargs) -> Optional[Dict]: "Adding user to group", {"id": id, "group_id": group_id} ) query = """ - mutation UserAddMembership($id: ID!, $group_id: ID!) { + mutation UserEditAddMembership($id: ID!, $group_id: ID!) { userEdit(id: $id) { relationAdd(input: { relationship_type: "member-of", @@ -660,7 +667,7 @@ def delete_membership(self, **kwargs) -> Optional[Dict]: ) query = ( """ - mutation UserDeleteMembership($id: ID!, $group_id: StixRef!) { + mutation UserEditDeleteMembership($id: ID!, $group_id: StixRef!) { userEdit(id: $id) { relationDelete(toId: $group_id, relationship_type: "member-of") { """ @@ -699,7 +706,7 @@ def add_organization(self, **kwargs) -> Optional[Dict]: ) query = ( """ - mutation UserAddOrganization($id: ID!, $organization_id: ID!) { + mutation UserEditAddOrganization($id: ID!, $organization_id: ID!) { userEdit(id: $id) { organizationAdd(organizationId: $organization_id) { """ @@ -740,7 +747,7 @@ def delete_organization(self, **kwargs) -> Optional[Dict]: ) query = ( """ - mutation UserDeleteOrganization($id: ID!, $organization_id: ID!) { + mutation UserEditDeleteOrganization($id: ID!, $organization_id: ID!) { userEdit(id: $id) { organizationDelete(organizationId: $organization_id) { """ @@ -778,7 +785,7 @@ def token_renew(self, **kwargs) -> Optional[Dict]: self.opencti.admin_logger.info("Rotating API key for user", {"id": id}) query = ( """ - mutation UserRotateToken($id: ID!) { + mutation UserEditRotateToken($id: ID!) { userEdit(id: $id) { tokenRenew { """ From 3ed3a1e0067f29c893ddbf22fa034997dddd686e Mon Sep 17 00:00:00 2001 From: peritz Date: Fri, 31 Jan 2025 11:03:09 +0000 Subject: [PATCH 27/27] Cleanup test entities only if they exist --- .../entities/test_entity_crud.py | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/tests/02-integration/entities/test_entity_crud.py b/tests/02-integration/entities/test_entity_crud.py index b49eed7cf..245488355 100644 --- a/tests/02-integration/entities/test_entity_crud.py +++ b/tests/02-integration/entities/test_entity_crud.py @@ -19,7 +19,8 @@ def test_entity_create(entity_class): assert test_indicator is not None, "Response is NoneType" assert "id" in test_indicator, "No ID on object" finally: - entity_class.base_class().delete(id=test_indicator["id"]) + if test_indicator and "id" in test_indicator: + entity_class.base_class().delete(id=test_indicator["id"]) def test_read(entity_class): @@ -37,7 +38,8 @@ def test_read(entity_class): ) finally: - entity_class.base_class().delete(id=test_indicator["id"]) + if test_indicator and "id" in test_indicator: + entity_class.base_class().delete(id=test_indicator["id"]) def test_update(entity_class): @@ -73,7 +75,8 @@ def test_update(entity_class): result = test_indicator finally: - entity_class.base_class().delete(id=result["id"]) + if test_indicator and "id" in test_indicator: + entity_class.base_class().delete(id=result["id"]) def test_delete(entity_class): @@ -87,7 +90,8 @@ def test_delete(entity_class): result = entity_class.own_class().read(id=test_indicator["id"]) assert result is None, f"Read returned value '{result}' after delete" except AssertionError: - entity_class.base_class().delete(id=test_indicator["id"]) + if test_indicator and "id" in test_indicator: + entity_class.base_class().delete(id=test_indicator["id"]) def test_filter(entity_class): @@ -108,7 +112,8 @@ def test_filter(entity_class): entity_class.get_compare_exception_keys(), ) finally: - entity_class.base_class().delete(id=test_indicator["id"]) + if test_indicator and "id" in test_indicator: + entity_class.base_class().delete(id=test_indicator["id"]) def test_search(entity_class): @@ -127,7 +132,8 @@ def test_search(entity_class): entity_class.get_compare_exception_keys(), ) finally: - entity_class.base_class().delete(id=test_indicator["id"]) + if test_indicator and "id" in test_indicator: + entity_class.base_class().delete(id=test_indicator["id"]) def test_relation(entity_class): @@ -153,5 +159,7 @@ def test_relation(entity_class): result = entity_class.own_class().read(id=test_indicator["id"]) assert len(result["objectsIds"]) == 0 finally: - entity_class.base_class().delete(id=test_indicator["id"]) - entity_class.base_class().delete(id=test_indicator2["id"]) + if test_indicator and "id" in test_indicator: + entity_class.base_class().delete(id=test_indicator["id"]) + if test_indicator2 and "id" in test_indicator2: + entity_class.base_class().delete(id=test_indicator2["id"])