From 558b99ead9ffee66fd7d1010053e2930e7681379 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Tue, 30 Jan 2024 10:39:47 -0700 Subject: [PATCH 01/95] update date for release --- README.md | 2 +- docs/changelog.rst | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c663aaf9..b2c6c2d0 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ **Latest Version:** 1.5.1
-**Release Date:** TBD +**Release Date:** January 30, 2024 [![Coverage Status](https://coveralls.io/repos/github/carbonblack/carbon-black-cloud-sdk-python/badge.svg?t=Id6Baf)](https://coveralls.io/github/carbonblack/carbon-black-cloud-sdk-python) [![Codeship Status for carbonblack/carbon-black-cloud-sdk-python](https://app.codeship.com/projects/9e55a370-a772-0138-aae4-129773225755/status?branch=develop)](https://app.codeship.com/projects/402767) diff --git a/docs/changelog.rst b/docs/changelog.rst index 7cfca911..ae9707f4 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,6 @@ Changelog ================================ -CBC SDK 1.5.1 - Released TBD +CBC SDK 1.5.1 - Released January 30, 2024 ----------------------------------------- New Features: @@ -40,10 +40,10 @@ Bug Fixes: from that point, but *never* raised beyond it. This eliminates a problem of "hung" searches. Documentation: -* ReadTheDocs generation has been improved to show the inherited methods. There are some helper functions on -SearchQuery classes such as add_criteria() inherited from CriteriaBuilderSupportMixin and first() inherited from -IterableQueryMixin. +* ReadTheDocs generation has been improved to show the inherited methods. There are some helper functions on + ``SearchQuery`` classes such as ``add_criteria()`` inherited from ``CriteriaBuilderSupportMixin`` and ``first()`` + inherited from ``IterableQueryMixin``. CBC SDK 1.5.0 - Released October 24, 2023 ----------------------------------------- From bf324ab082f6bf893416e6cc8eed8c12bd1c08c2 Mon Sep 17 00:00:00 2001 From: Alex Van Brunt Date: Tue, 13 Feb 2024 16:09:28 -0700 Subject: [PATCH 02/95] Add bypass rule config --- .../platform/models/policy_ruleconfig.yaml | 3 + src/cbc_sdk/platform/policies.py | 28 +- src/cbc_sdk/platform/policy_ruleconfigs.py | 112 +++++++- .../unit/fixtures/platform/mock_policies.py | 265 +++++++++++++++++- .../platform/mock_policy_ruleconfigs.py | 198 +++++++++++++ .../unit/platform/test_policy_ruleconfigs.py | 78 +++++- 6 files changed, 662 insertions(+), 22 deletions(-) diff --git a/src/cbc_sdk/platform/models/policy_ruleconfig.yaml b/src/cbc_sdk/platform/models/policy_ruleconfig.yaml index 50b72db6..ea7a1405 100644 --- a/src/cbc_sdk/platform/models/policy_ruleconfig.yaml +++ b/src/cbc_sdk/platform/models/policy_ruleconfig.yaml @@ -18,3 +18,6 @@ properties: parameters: type: object description: The parameters associated with this rule config + exclusions: + type: object + description: The exclusions associated with this rule config diff --git a/src/cbc_sdk/platform/policies.py b/src/cbc_sdk/platform/policies.py index d7a67e11..2f3f9291 100644 --- a/src/cbc_sdk/platform/policies.py +++ b/src/cbc_sdk/platform/policies.py @@ -18,7 +18,8 @@ from cbc_sdk.base import MutableBaseModel, BaseQuery, IterableQueryMixin, AsyncQueryMixin from cbc_sdk.platform.devices import Device from cbc_sdk.platform.policy_ruleconfigs import (PolicyRuleConfig, CorePreventionRuleConfig, - HostBasedFirewallRuleConfig, DataCollectionRuleConfig) + HostBasedFirewallRuleConfig, DataCollectionRuleConfig, + BypassRuleConfig) from cbc_sdk.platform.previewer import DevicePolicyChangePreview from cbc_sdk.errors import ApiError, ServerError, InvalidObjectError @@ -26,7 +27,8 @@ SPECIFIC_RULECONFIGS = MappingProxyType({ "core_prevention": CorePreventionRuleConfig, "host_based_firewall": HostBasedFirewallRuleConfig, - "data_collection": DataCollectionRuleConfig + "data_collection": DataCollectionRuleConfig, + "bypass": BypassRuleConfig }) @@ -696,6 +698,28 @@ def object_rule_configs_list(self): """ return [rconf for rconf in self.object_rule_configs.values()] + @property + def bypass_rule_configs(self): + """ + Returns a dictionary of bypass rule configuration IDs and objects for this Policy. + + Returns: + dict: A dictionary with bypass rule configuration IDs as keys and BypassRuleConfig objects + as values. + """ + return {key: rconf for (key, rconf) in self.object_rule_configs.items() + if isinstance(rconf, BypassRuleConfig)} + + @property + def bypass_rule_configs_list(self): + """ + Returns a list of bypass rule configuration objects for this Policy. + + Returns: + list: A list of BypassRuleConfig objects. + """ + return [rconf for rconf in self.object_rule_configs.values() if isinstance(rconf, BypassRuleConfig)] + @property def core_prevention_rule_configs(self): """ diff --git a/src/cbc_sdk/platform/policy_ruleconfigs.py b/src/cbc_sdk/platform/policy_ruleconfigs.py index 6a17bf74..14692637 100644 --- a/src/cbc_sdk/platform/policy_ruleconfigs.py +++ b/src/cbc_sdk/platform/policy_ruleconfigs.py @@ -263,8 +263,10 @@ def _refresh(self): def _update_ruleconfig(self): """Perform the internal update of the rule configuration object.""" url = self.urlobject_single.format(self._cb.credentials.org_key, self._parent._model_unique_id) - body = [{"id": self.id, "parameters": self.parameters}] - self._cb.put_object(url, body) + body = {"id": self.id, "parameters": self.parameters} + if "exclusions" in self._info: + body["exclusions"] = self.exclusions + self._cb.put_object(url, [body]) def _delete_ruleconfig(self): """Perform the internal delete of the rule configuration object.""" @@ -292,6 +294,15 @@ def set_assignment_mode(self, mode): raise ApiError(f"invalid assignment mode: {mode}") self.set_parameter("WindowsAssignmentMode", mode) + def replace_exclusions(self, exclusions): + """ + Replaces all the exclusions for a bypasss rule configuration + + Args: + exclusions(dict): The entire exclusion set to be replaced + """ + self._info['exclusions'] = exclusions + class HostBasedFirewallRuleConfig(PolicyRuleConfig): """Represents a host-based firewall rule configuration in the policy.""" @@ -301,7 +312,7 @@ class HostBasedFirewallRuleConfig(PolicyRuleConfig): def __init__(self, cb, parent, model_unique_id=None, initial_data=None, force_init=False, full_doc=False): """ - Initialize the CorePreventionRuleConfig object. + Initialize the HostBasedFirewallRuleConfig object. Args: cb (BaseAPI): Reference to API object used to communicate with the server. @@ -643,7 +654,7 @@ class DataCollectionRuleConfig(PolicyRuleConfig): To update a DataCollectionRuleConfig, change the values of its property fields, then call its save() method. This requires the org.policies(UPDATE) permission. - To delete an existing CorePreventionRuleConfig, call its delete() method. This requires the org.policies(DELETE) + To delete an existing DataCollectionRuleConfig, call its delete() method. This requires the org.policies(DELETE) permission. """ urlobject_single = "/policyservice/v1/orgs/{0}/policies/{1}/rule_configs/data_collection" @@ -651,7 +662,7 @@ class DataCollectionRuleConfig(PolicyRuleConfig): def __init__(self, cb, parent, model_unique_id=None, initial_data=None, force_init=False, full_doc=False): """ - Initialize the CorePreventionRuleConfig object. + Initialize the DataCollectionRuleConfig object. Args: cb (BaseAPI): Reference to API object used to communicate with the server. @@ -696,3 +707,94 @@ def _delete_ruleconfig(self): """Perform the internal delete of the rule configuration object.""" url = self.urlobject_single.format(self._cb.credentials.org_key, self._parent._model_unique_id) + f"/{self.id}" self._cb.delete_object(url) + + +class BypassRuleConfig(PolicyRuleConfig): + """ + Represents a bypass rule configuration in the policy. + + Create one of these objects, associating it with a Policy, and set its properties, then call its save() method to + add the rule configuration to the policy. This requires the org.policies(UPDATE) permission. + + To update a BypassRuleConfig, change the values of its property fields, then call its save() method. This + requires the org.policies(UPDATE) permission. + + To delete an existing BypassRuleConfig, call its delete() method. This requires the org.policies(DELETE) + permission. + """ + urlobject_single = "/policyservice/v1/orgs/{0}/policies/{1}/rule_configs/bypass" + swagger_meta_file = "platform/models/policy_ruleconfig.yaml" + + def __init__(self, cb, parent, model_unique_id=None, initial_data=None, force_init=False, full_doc=False): + """ + Initialize the BypassRuleConfig object. + + Args: + cb (BaseAPI): Reference to API object used to communicate with the server. + parent (Policy): The "parent" policy of this rule configuration. + model_unique_id (str): ID of the rule configuration. + initial_data (dict): Initial data used to populate the rule configuration. + force_init (bool): If True, forces the object to be refreshed after constructing. Default False. + full_doc (bool): If True, object is considered "fully" initialized. Default False. + """ + super(BypassRuleConfig, self).__init__(cb, parent, model_unique_id, initial_data, force_init, full_doc) + + def _refresh(self): + """ + Refreshes the rule configuration object from the server. + + Required Permissions: + org.policies (READ) + + Returns: + bool: True if the refresh was successful. + + Raises: + InvalidObjectError: If the object is unparented or its ID is invalid. + """ + url = self.urlobject_single.format(self._cb.credentials.org_key, self._parent._model_unique_id) + return_data = self._cb.get_object(url) + ruleconfig_data = [d for d in return_data.get("results", []) if d.get("id", "") == self._model_unique_id] + if ruleconfig_data: + self._info = ruleconfig_data[0] + self._mark_changed(False) + else: + raise InvalidObjectError(f"invalid data collection ID: {self._model_unique_id}") + return True + + def _update_ruleconfig(self): + """Perform the internal update of the rule configuration object.""" + url = self.urlobject_single.format(self._cb.credentials.org_key, self._parent._model_unique_id) + body = {"id": self.id} + if "exclusions" in self._info: + body["exclusions"] = self.exclusions + + self._cb.put_object(url, body) + + def _delete_ruleconfig(self): + """Perform the internal delete of the rule configuration object.""" + url = self.urlobject_single.format(self._cb.credentials.org_key, self._parent._model_unique_id) + self._cb.delete_object(url) + + def replace_exclusions(self, exclusions): + """ + Replaces all the exclusions for a bypasss rule configuration + + Args: + exclusions(dict): The entire exclusion set to be replaced + """ + self._mark_changed(True) + self._info['exclusions'] = exclusions + + @property + def parameter_names(self): + """Not Supported""" + raise Exception("Not Suppported") + + def get_parameter(self, name, default_value=None): + """Not Supported""" + raise Exception("Not Suppported") + + def set_parameter(self, name, value): + """Not Supported""" + raise Exception("Not Suppported") diff --git a/src/tests/unit/fixtures/platform/mock_policies.py b/src/tests/unit/fixtures/platform/mock_policies.py index ed6b7029..cd7ee2d3 100644 --- a/src/tests/unit/fixtures/platform/mock_policies.py +++ b/src/tests/unit/fixtures/platform/mock_policies.py @@ -315,6 +315,64 @@ }, "enable_host_based_firewall": False } + }, + { + "id": "1664f2e6-645f-4d6e-98ec-0c80485cbe0f", + "name": "Event Reporting Exclusions", + "description": "Allows customers to exclude specific processes from reporting events to CBC", + "inherited_from": "psc:region", + "category": "bypass", + "parameters": {} + }, + { + "id": "1c03d653-eca4-4adc-81a1-04b17b6cbffc", + "name": "Event Reporting and Sensor Operation Exclusions", + "description": "Allows customers to exclude specific processes and process events from reporting to CBC", + "inherited_from": "psc:region", + "category": "bypass", + "parameters": {}, + "exclusions": { + "windows": [ + { + "id": 8090, + "criteria": [ + { + "id": 13426, + "type": "initiator_process", + "attributes": [ + { + "id": 93774, + "name": "process_name", + "values": [ + "**\\explorer.exe" + ] + } + ] + }, + { + "id": 13427, + "type": "operation", + "attributes": [ + { + "id": 93775, + "name": "operation_type", + "values": [ + "ALL" + ] + } + ] + } + ], + "comments": "", + "type": "ENDPOINT_STANDARD_PROCESS_BYPASS", + "apply_to_descendent_processes": True, + "created_by": "ABCD1234", + "created_at": "2024-01-27T13:29:44.839Z", + "modified_by": "ABCD1234", + "modified_at": "2024-01-27T13:29:44.839Z" + } + ] + } } ] } @@ -1738,22 +1796,107 @@ POLICY_CONFIG_PRESENTATION = { "configs": [ + { + "id": "cc075469-8d1e-4056-84b6-0e6f437c4010", + "name": "XDR", + "description": "Turns on XDR network data collection at the sensor", + "presentation": { + "category": "data_collection" + }, + "parameters": [] + }, { "id": "91c919da-fb90-4e63-9eac-506255b0a0d0", "name": "Authentication Events", - "description": "Authentication Events", + "description": "Turns on Windows authentication events at the sensor", "presentation": { "category": "data_collection" }, "parameters": [] }, + { + "id": "1c03d653-eca4-4adc-81a1-04b17b6cbffc", + "name": "Event Reporting and Sensor Operation Exclusions", + "description": "Allows customers to exclude specific processes and process events from reporting to CBC", + "presentation": { + "name": "process_exclusion.name", + "category": "bypass", + "description": [ + "process_exclusion.description" + ], + "platforms": [ + { + "platform": "WINDOWS", + "exclusions": { + "criteria": [ + "initiator_process", + "operations" + ], + "additional_attributes": [ + "type", + "inheritence" + ] + } + } + ] + }, + "parameters": [] + }, + { + "id": "0aa2b31a-f938-4cf9-acee-7cf7b810eb79", + "name": "Background Scan", + "description": "This rapid config handles DRE rules and sensor settings associated with Background Scan", + "presentation": { + "category": "sensor_settings" + }, + "parameters": [] + }, + { + "id": "1664f2e6-645f-4d6e-98ec-0c80485cbe0f", + "name": "Event Reporting Exclusions", + "description": "Allows customers to exclude specific processes from reporting events to CBC", + "presentation": { + "name": "event_reporting_exclusion.name", + "category": "bypass", + "description": [ + "event_reporting_exclusion.description" + ], + "platforms": [ + { + "platform": "WINDOWS", + "exclusions": { + "criteria": [ + "initiator_process", + "operations" + ], + "additional_attributes": [ + "type", + "inheritence" + ] + } + } + ] + }, + "parameters": [] + }, + { + "id": "df181779-f623-415d-879e-91c40246535d", + "name": "Host Based Firewall", + "description": "These are the Host based Firewall Rules which will be executed by the sensor." + " The Definition will be part of Main Policies.", + "presentation": { + "category": "hbfw" + }, + "parameters": [] + }, { "id": "1f8a5e4b-34f2-4d31-9f8f-87c56facaec8", "name": "Advanced Scripting Prevention", - "description": "Addresses malicious fileless and file-backed scripts that leverage native programs [...]", + "description": "Addresses malicious fileless and file-backed scripts that leverage native programs" + " and common scripting languages.", "presentation": { "name": "amsi.name", - "category": "core_prevention", + "category": "core-prevention", "description": [ "amsi.description" ], @@ -1794,10 +1937,11 @@ { "id": "ac67fa14-f6be-4df9-93f2-6de0dbd96061", "name": "Credential Theft", - "description": "Addresses threat actors obtaining credentials and relies on detecting the malicious [...]", + "description": "Addresses threat actors obtaining credentials and relies on detecting the malicious use of" + " TTPs/behaviors that indicate such activity.", "presentation": { "name": "cred_theft.name", - "category": "core_prevention", + "category": "core-prevention", "description": [ "cred_theft.description" ], @@ -1836,21 +1980,23 @@ ] }, { - "id": "df181779-f623-415d-879e-91c40246535d", - "name": "Host Based Firewall", - "description": "These are the Host based Firewall Rules which will be executed by the sensor. [...].", + "id": "491dd777-5a76-4f58-88bf-d29926d12778", + "name": "Prevalent Module Exclusions", + "description": "Collects events created when a process loads a common library. Enabling this will increase" + " the number of events reported for expected process behavior.", "presentation": { - "category": "hbfw" + "category": "data_collection" }, "parameters": [] }, { "id": "c4ed61b3-d5aa-41a9-814f-0f277451532b", "name": "Carbon Black Threat Intel", - "description": "Addresses common and pervasive TTPs used for malicious activity as well as [...]", + "description": "Addresses common and pervasive TTPs used for malicious activity as well as living off the" + " land TTPs/behaviors detected by Carbon Black’s Threat Analysis Unit.", "presentation": { "name": "cbti.name", - "category": "core_prevention", + "category": "core-prevention", "description": [ "cbti.description" ], @@ -1891,10 +2037,12 @@ { "id": "88b19232-7ebb-48ef-a198-2a75a282de5d", "name": "Privilege Escalation", - "description": "Addresses behaviors that indicate a threat actor has gained elevated access via [...]", + "description": "Addresses behaviors that indicate a threat actor has gained elevated access via a bug or" + " misconfiguration within an operating system, and leverages the detection of TTPs/behaviors to prevent" + " such activity.", "presentation": { "name": "privesc.name", - "category": "core_prevention", + "category": "core-prevention", "description": [ "privesc.description" ], @@ -1931,6 +2079,97 @@ ] } ] + }, + { + "id": "97a03cc2-5796-4864-b16d-790d06bea20d", + "name": "Defense Evasion", + "description": "Addresses common TTPs/behaviors that threat actors use to avoid detection such as" + " uninstalling or disabling security software, obfuscating or encrypting data/scripts and abusing" + " trusted processes to hide and disguise their malicious activity.", + "presentation": { + "name": "defense_evasion.name", + "category": "core-prevention", + "description": [ + "defense_evasion.description" + ], + "platforms": [ + { + "platform": "WINDOWS", + "header": "defense_evasion.windows.heading", + "subHeader": [ + "defense_evasion.windows.sub_heading" + ], + "actions": [ + { + "component": "assignment-mode-selector", + "parameter": "WindowsAssignmentMode" + } + ] + } + ] + }, + "parameters": [ + { + "default": "BLOCK", + "name": "WindowsAssignmentMode", + "description": "Used to change assignment mode to PREVENT or BLOCK", + "recommended": "BLOCK", + "validations": [ + { + "type": "enum", + "values": [ + "REPORT", + "BLOCK" + ] + } + ] + } + ] + }, + { + "id": "8a16234c-9848-473a-a803-f0f0ffaf5f29", + "name": "Persistence", + "description": "Addresses common TTPs/behaviors that threat actors use to retain access to systems across" + " restarts, changed credentials, and other interruptions that could cut off their access.", + "presentation": { + "name": "persistence.name", + "category": "core-prevention", + "description": [ + "persistence.description" + ], + "platforms": [ + { + "platform": "WINDOWS", + "header": "persistence.windows.heading", + "subHeader": [ + "persistence.windows.sub_heading" + ], + "actions": [ + { + "component": "assignment-mode-selector", + "parameter": "WindowsAssignmentMode" + } + ] + } + ] + }, + "parameters": [ + { + "default": "BLOCK", + "name": "WindowsAssignmentMode", + "description": "Used to change assignment mode to PREVENT or BLOCK", + "recommended": "BLOCK", + "validations": [ + { + "type": "enum", + "values": [ + "REPORT", + "BLOCK" + ] + } + ] + } + ] } ] } diff --git a/src/tests/unit/fixtures/platform/mock_policy_ruleconfigs.py b/src/tests/unit/fixtures/platform/mock_policy_ruleconfigs.py index 2c8066eb..d56398ee 100644 --- a/src/tests/unit/fixtures/platform/mock_policy_ruleconfigs.py +++ b/src/tests/unit/fixtures/platform/mock_policy_ruleconfigs.py @@ -1223,3 +1223,201 @@ ], "failed": [] } + +BYPASS_RULE_CONFIGS = { + "results": [ + { + "id": "1664f2e6-645f-4d6e-98ec-0c80485cbe0f", + "name": "Event Reporting Exclusions", + "description": "Allows customers to exclude specific processes from reporting events to CBC", + "inherited_from": "psc:region", + "category": "bypass", + "parameters": {} + }, + { + "id": "1c03d653-eca4-4adc-81a1-04b17b6cbffc", + "name": "Event Reporting and Sensor Operation Exclusions", + "description": "Allows customers to exclude specific processes and process events from reporting to CBC", + "inherited_from": "psc:region", + "category": "bypass", + "parameters": {}, + "exclusions": { + "windows": [ + { + "id": 8090, + "criteria": [ + { + "id": 13426, + "type": "initiator_process", + "attributes": [ + { + "id": 93774, + "name": "process_name", + "values": [ + "**\\explorer.exe" + ] + } + ] + }, + { + "id": 13427, + "type": "operation", + "attributes": [ + { + "id": 93775, + "name": "operation_type", + "values": [ + "ALL" + ] + } + ] + } + ], + "comments": "", + "type": "ENDPOINT_STANDARD_PROCESS_BYPASS", + "apply_to_descendent_processes": True, + "created_by": "ABCD1234", + "created_at": "2024-01-27T13:29:44.839Z", + "modified_by": "ABCD1234", + "modified_at": "2024-01-27T13:29:44.839Z" + } + ] + } + } + ] +} + +BYPASS_RULE_CONFIGS_UPDATE = { + "id": "1c03d653-eca4-4adc-81a1-04b17b6cbffc", + "name": "Event Reporting and Sensor Operation Exclusions", + "description": "Allows customers to exclude specific processes and process events from reporting to CBC", + "inherited_from": "psc:region", + "category": "bypass", + "parameters": {}, + "exclusions": { + "windows": [ + { + "criteria": [ + { + "type": "initiator_process", + "attributes": [ + { + "name": "process_name", + "values": [ + "**\\explorer.exe" + ] + } + ] + }, + { + "type": "operation", + "attributes": [ + { + "name": "operation_type", + "values": [ + "ALL" + ] + } + ] + } + ], + "comments": "", + "apply_to_descendent_processes": True, + "type": "ENDPOINT_STANDARD_PROCESS_BYPASS" + } + ] + } +} + +BYPASS_RULE_CONFIGS_UPDATE_REQUEST = { + "id": "1c03d653-eca4-4adc-81a1-04b17b6cbffc", + "exclusions": { + "windows": [ + { + "criteria": [ + { + "type": "initiator_process", + "attributes": [ + { + "name": "process_name", + "values": [ + "**\\explorer.exe" + ] + } + ] + }, + { + "type": "operation", + "attributes": [ + { + "name": "operation_type", + "values": [ + "ALL" + ] + } + ] + } + ], + "comments": "", + "apply_to_descendent_processes": True, + "type": "ENDPOINT_STANDARD_PROCESS_BYPASS" + } + ] + } +} + +BYPASS_RULE_CONFIGS_UPDATE_RESPONSE = { + "successful": [ + { + "id": "1c03d653-eca4-4adc-81a1-04b17b6cbffc", + "name": "Event Reporting and Sensor Operation Exclusions", + "description": "Allows customers to exclude specific processes and process events from reporting to CBC", + "inherited_from": "psc:region", + "category": "bypass", + "parameters": {}, + "exclusions": { + "windows": [ + { + "id": 8090, + "criteria": [ + { + "id": 13426, + "type": "initiator_process", + "attributes": [ + { + "id": 93774, + "name": "process_name", + "values": [ + "**\\explorer.exe" + ] + } + ] + }, + { + "id": 13427, + "type": "operation", + "attributes": [ + { + "id": 93775, + "name": "operation_type", + "values": [ + "ALL" + ] + } + ] + } + ], + "comments": "", + "type": "ENDPOINT_STANDARD_PROCESS_BYPASS", + "apply_to_descendent_processes": True, + "created_by": "ABCDEFD", + "created_at": "2024-01-27T13:29:44.839Z", + "modified_by": "ABCDEFD", + "modified_at": "2024-01-27T13:29:44.839Z" + } + ] + } + } + ], + "failed": [] +} diff --git a/src/tests/unit/platform/test_policy_ruleconfigs.py b/src/tests/unit/platform/test_policy_ruleconfigs.py index 9639fe3f..bfefcb6f 100644 --- a/src/tests/unit/platform/test_policy_ruleconfigs.py +++ b/src/tests/unit/platform/test_policy_ruleconfigs.py @@ -18,7 +18,7 @@ from cbc_sdk.rest_api import CBCloudAPI from cbc_sdk.platform import Policy, PolicyRuleConfig from cbc_sdk.platform.policy_ruleconfigs import (CorePreventionRuleConfig, HostBasedFirewallRuleConfig, - DataCollectionRuleConfig) + DataCollectionRuleConfig, BypassRuleConfig) from cbc_sdk.errors import ApiError, InvalidObjectError, ServerError from tests.unit.fixtures.CBCSDKMock import CBCSDKMock from tests.unit.fixtures.platform.mock_policies import (FULL_POLICY_1, BASIC_CONFIG_TEMPLATE_RETURN, @@ -41,7 +41,11 @@ HBFW_EXPORT_RULE_CONFIGS_RESPONSE, HBFW_EXPORT_RULE_CONFIGS_RESPONSE_CSV, DATA_COLLECTION_RETURNS, DATA_COLLECTION_UPDATE_1, - DATA_COLLECTION_UPDATE_RETURNS_1) + DATA_COLLECTION_UPDATE_RETURNS_1, + BYPASS_RULE_CONFIGS, + BYPASS_RULE_CONFIGS_UPDATE, + BYPASS_RULE_CONFIGS_UPDATE_REQUEST, + BYPASS_RULE_CONFIGS_UPDATE_RESPONSE) logging.basicConfig(format='%(asctime)s %(levelname)s:%(message)s', level=logging.DEBUG, filename='log.txt') @@ -218,10 +222,13 @@ def test_rule_config_initialization_matches_categories(policy): assert isinstance(cfg, HostBasedFirewallRuleConfig) elif cfg.category == "data_collection": assert isinstance(cfg, DataCollectionRuleConfig) + elif cfg.category == "bypass": + assert isinstance(cfg, BypassRuleConfig) else: assert not isinstance(cfg, CorePreventionRuleConfig) assert not isinstance(cfg, HostBasedFirewallRuleConfig) assert not isinstance(cfg, DataCollectionRuleConfig) + assert not isinstance(cfg, BypassRuleConfig) def test_core_prevention_refresh(cbcsdk_mock, policy): @@ -752,3 +759,70 @@ def on_delete(url, body): assert rule_config.name == 'Authentication Events' rule_config.delete() assert delete_called + + +def test_bypass_refresh(cbcsdk_mock, policy): + """Tests the refresh operation for a BypassRuleConfig.""" + cbcsdk_mock.mock_request('GET', '/policyservice/v1/orgs/test/policies/65536/rule_configs/bypass', + BYPASS_RULE_CONFIGS) + for rule_config in policy.bypass_rule_configs_list: + rule_config.refresh() + + +def test_bypass_update_and_save(cbcsdk_mock, policy): + """Tests updating the bypass data and saving it.""" + put_called = False + + def on_put(url, body, **kwargs): + nonlocal put_called + assert body == BYPASS_RULE_CONFIGS_UPDATE_REQUEST + put_called = True + return copy.deepcopy(BYPASS_RULE_CONFIGS_UPDATE_RESPONSE) + + cbcsdk_mock.mock_request('GET', '/policyservice/v1/orgs/test/policies/65536/configs/presentation', + POLICY_CONFIG_PRESENTATION) + cbcsdk_mock.mock_request('PUT', '/policyservice/v1/orgs/test/policies/65536/rule_configs/bypass', on_put) + rule_config = policy.bypass_rule_configs['1c03d653-eca4-4adc-81a1-04b17b6cbffc'] + assert rule_config.name == 'Event Reporting and Sensor Operation Exclusions' + rule_config.replace_exclusions(BYPASS_RULE_CONFIGS_UPDATE_REQUEST["exclusions"]) + + rule_config.save() + assert put_called + + +def test_bypass_update_via_replace(cbcsdk_mock, policy): + """Tests updating the bypass data and saving it via replace_rule_config.""" + put_called = False + + def on_put(url, body, **kwargs): + nonlocal put_called + assert body == BYPASS_RULE_CONFIGS_UPDATE_REQUEST + put_called = True + return copy.deepcopy(BYPASS_RULE_CONFIGS_UPDATE_RESPONSE) + + cbcsdk_mock.mock_request('GET', '/policyservice/v1/orgs/test/policies/65536/configs/presentation', + POLICY_CONFIG_PRESENTATION) + cbcsdk_mock.mock_request('PUT', '/policyservice/v1/orgs/test/policies/65536/rule_configs/bypass', on_put) + rule_config = policy.bypass_rule_configs['1c03d653-eca4-4adc-81a1-04b17b6cbffc'] + assert rule_config.name == 'Event Reporting and Sensor Operation Exclusions' + + policy.replace_rule_config('1c03d653-eca4-4adc-81a1-04b17b6cbffc', BYPASS_RULE_CONFIGS_UPDATE) + assert put_called + + +def test_bypass_delete(cbcsdk_mock, policy): + """Tests delete of bypass rules.""" + delete_called = False + + def on_delete(url, body): + nonlocal delete_called + delete_called = True + return CBCSDKMock.StubResponse(None, scode=204) + + cbcsdk_mock.mock_request('GET', '/policyservice/v1/orgs/test/policies/65536/configs/presentation', + POLICY_CONFIG_PRESENTATION) + cbcsdk_mock.mock_request('DELETE', '/policyservice/v1/orgs/test/policies/65536/rule_configs/bypass', on_delete) + rule_config = policy.bypass_rule_configs['1664f2e6-645f-4d6e-98ec-0c80485cbe0f'] + assert rule_config.name == 'Event Reporting Exclusions' + rule_config.delete() + assert delete_called From 13848bf779588a1d4af294d0d17d4fc83f0b119c Mon Sep 17 00:00:00 2001 From: Alex Van Brunt Date: Tue, 13 Feb 2024 16:20:51 -0700 Subject: [PATCH 03/95] Add marked_changed --- src/cbc_sdk/platform/policy_ruleconfigs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/cbc_sdk/platform/policy_ruleconfigs.py b/src/cbc_sdk/platform/policy_ruleconfigs.py index 14692637..a0f8d1fa 100644 --- a/src/cbc_sdk/platform/policy_ruleconfigs.py +++ b/src/cbc_sdk/platform/policy_ruleconfigs.py @@ -301,6 +301,7 @@ def replace_exclusions(self, exclusions): Args: exclusions(dict): The entire exclusion set to be replaced """ + self._mark_changed(True) self._info['exclusions'] = exclusions From c74aea0240909d6125e3bee5646c7b69ab120563 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Tue, 13 Feb 2024 12:28:07 -0700 Subject: [PATCH 04/95] added BackoffHandler and the tests for it (100% coverage) --- src/cbc_sdk/utils.py | 141 +++++++++++++++++++++++++++++++++++ src/tests/unit/test_utils.py | 49 +++++++++++- 2 files changed, 188 insertions(+), 2 deletions(-) diff --git a/src/cbc_sdk/utils.py b/src/cbc_sdk/utils.py index e1ae7de5..9fe73802 100755 --- a/src/cbc_sdk/utils.py +++ b/src/cbc_sdk/utils.py @@ -15,6 +15,8 @@ from __future__ import absolute_import import dateutil.parser +import time +from cbc_sdk.errors import TimeoutError cb_datetime_format = "%Y-%m-%d %H:%M:%S.%f" @@ -47,3 +49,142 @@ def convert_to_cb(dt): str: The date and time as a string. """ return dt.strftime(cb_datetime_format) + + +class BackoffHandler: + """ + Logic for handling exponential backoff of multiple communications requests. + + The logic also handles timeouts of operations that go on too long. + + Example:: + + backoff = BackoffHandler(timeout=600000) # 10 minutes = 600 seconds + with backoff as b: + while operation_continues(): + b.pause() + do_operation() + """ + def __init__(self, cb, timeout=0, initial=0.1, multiplier=2.0, threshold=2.0): + """ + Initialize the ``BackoffHandler``. + + Args: + cb (BaseAPI): The API object for the operation. + timeout (int): The timeout for the operation, in milliseconds. If this is 0, the default timeout as + configured in the credentials will be used. The default is 0. + initial (float): The initial value for the exponential backoff pause, in seconds. The default is 0.1. + multiplier (float): The value by which the exponential backoff pause will be multiplied each time + a pause happens. The default is 2.0. + threshold (float): The maximum value for the exponential backoff pause, in seconds. The default is 2.0. + """ + self._cb = cb + self._timeout = cb.credentials.default_timeout if timeout == 0 else timeout + self._initial = initial + self._multiplier = multiplier + self._threshold = threshold + + class BackoffOperation: + """ + Handler for a single operation requiring exponential backoff between communication attempts. + + This is returned by ``BackoffHandler`` as part of the ``with`` operation, and is stored in the variable + referred to in its ``as`` clause. + """ + def __init__(self, timeout, initial, multiplier, threshold): + """ + Initialize the ``BackoffOperation``. + + Args: + timeout (int): The timeout for the operation, in milliseconds. + initial (float): The initial value for the exponential backoff pause, in seconds. + multiplier (float): The value by which the exponential backoff pause will be multiplied each time + a pause happens. + threshold (float): The maximum value for the exponential backoff pause, in seconds. + """ + self._initial = initial + self._multiplier = multiplier + self._threshold = threshold + self._timeout_time = time.time() * 1000 + timeout + self._pausetime = 0.0 + self._first = True + + def pause(self): + """ + Pauses operation for a determined amount of time. + + The method also checks for a timeout and raises ``TimeoutError`` if it happens, and computes the amount + of time to pause the next time this method is called. + + Raises: + TimeoutError: If the timeout value is reached. + """ + if time.time() * 1000 > self._timeout_time: + raise TimeoutError(message="Operation timed out") + if self._pausetime > 0.0: + time.sleep(self._pausetime) + if time.time() * 1000 > self._timeout_time: + raise TimeoutError(message="Operation timed out") + if self._first: + self._pausetime = self._initial + self._first = False + else: + self._pausetime = min(self._pausetime * self._multiplier, self._threshold) + + def reset(self, full=False): + """ + Resets the state of the operation so that the pause time is reset. + + Does not affect the timeout value. This should be used, for instance, after a successful operation to + minimize the pause before the next operation is started. + + Args: + full (bool): If this is ``True``, the next pause time will be reset to 0. If this is ``False``, the + next pause time will be reset to the initial pause time. + """ + if full: + self._pausetime = 0.0 + self._first = True + else: + self._pausetime = self._initial + + + def __enter__(self): + """ + Called at entry of the context specified by this object. + + Returns: + BackoffHandler.BackoffOperation: A new ``BackoffOperation`` object to manage the current operation. + """ + return BackoffHandler.BackoffOperation(self._timeout, self._initial, self._multiplier, self._threshold) + + def __exit__(self, exc_type, exc_val, exc_tb): + """ + Called at exit of the context specified by this object. + + This context does not suppress exceptions. + + Args: + exc_type (Any): Exception type (not used). + exc_val (Any): Exception value (not used). + exc_tb (Any): Exception traceback (not used). + + Returns: + bool: Always ``False``. + """ + return False + + @property + def timeout(self): + """Returns the current timeout associated with this handler, in milliseconds.""" + return self._timeout + + @timeout.setter + def timeout(self, val): + """ + Sets the the current timeout associated with this handler + + Args: + val (int): New timeout value to set, in milliseconds. + """ + self._timeout = self._cb.credentials.default_timeout if val == 0 else val diff --git a/src/tests/unit/test_utils.py b/src/tests/unit/test_utils.py index 3b0878ea..90f7969e 100755 --- a/src/tests/unit/test_utils.py +++ b/src/tests/unit/test_utils.py @@ -11,9 +11,12 @@ """Test code for the utility functions""" -# import pytest +import pytest +import time from datetime import datetime -from cbc_sdk.utils import convert_from_cb, convert_to_cb +from cbc_sdk.utils import convert_from_cb, convert_to_cb, BackoffHandler +from cbc_sdk.errors import TimeoutError +from cbc_sdk.connection import BaseAPI # ==================================== Unit TESTS BELOW ==================================== @@ -46,3 +49,45 @@ def test_convert_to_cb(): t = datetime(2020, 3, 11, 18, 34, 11, 123456) s = convert_to_cb(t) assert s == "2020-03-11 18:34:11.123456" + + +def test_backoff_handler_operation(): + """Test the operation of the BackoffHandler.""" + cb = BaseAPI(integration_name='test1', url='https://example.com', token='ABCDEFGHIJKLM', org_key='A1B2C3D4') + sut = BackoffHandler(cb, threshold=0.5) + assert sut.timeout == 300000 + assert sut._initial == 0.1 + assert sut._multiplier == 2.0 + assert sut._threshold == 0.5 + with sut as b: + assert b._pausetime == 0.0 + b.pause() + assert b._pausetime == 0.1 + b.pause() + assert b._pausetime == 0.2 + b.pause() + assert b._pausetime == 0.4 + b.pause() + assert b._pausetime == 0.5 + b.reset() + assert b._pausetime == 0.1 + b.pause() + assert b._pausetime == 0.2 + b.reset(True) + assert b._pausetime == 0.0 + + +def test_backoff_handler_timeouts(): + """Test the raising of TimeoutError by the BackoffHandler.""" + cb = BaseAPI(integration_name='test1', url='https://example.com', token='ABCDEFGHIJKLM', org_key='A1B2C3D4') + sut = BackoffHandler(cb, timeout=10) + with sut as b: + time.sleep(0.1) + with pytest.raises(TimeoutError): + b.pause() + sut.timeout = 250 + with sut as b: + b.pause() # no pause + b.pause() # pauses 0.1 sec + with pytest.raises(TimeoutError): + b.pause() From e77b1729f94e26ca09ab7b73019bc100988f7b43 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Tue, 13 Feb 2024 12:45:39 -0700 Subject: [PATCH 05/95] added BackoffHandler support to Job._await_completion --- src/cbc_sdk/platform/jobs.py | 47 +++++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/src/cbc_sdk/platform/jobs.py b/src/cbc_sdk/platform/jobs.py index 598fca73..0b242430 100644 --- a/src/cbc_sdk/platform/jobs.py +++ b/src/cbc_sdk/platform/jobs.py @@ -18,6 +18,7 @@ import time from cbc_sdk.base import NewBaseModel, BaseQuery, IterableQueryMixin, AsyncQueryMixin from cbc_sdk.errors import ObjectNotFoundError, ServerError +from cbc_sdk.utils import BackoffHandler log = logging.getLogger(__name__) @@ -85,33 +86,42 @@ def get_progress(self): self._info['progress'] = resp return resp['num_total'], resp['num_completed'], resp.get('message', None) - def _await_completion(self): + def _await_completion(self, timeout=0): """ Waits for this job to complete by examining the progress data. Required Permissions: jobs.status(READ) + Args: + timeout (int): The timeout for this wait in milliseconds. If this is 0, the default value will be used. + Returns: Job: This object. + + Raises: + TimeoutError: If the wait times out. """ - progress_data = (1, 0, '') - do_sleep = False - errorcount = 0 - while progress_data[1] < progress_data[0]: - if do_sleep: - time.sleep(0.5) - try: - progress_data = self.get_progress() - except (ServerError, ObjectNotFoundError): - errorcount += 1 - if errorcount == 3: - raise - progress_data = (1, 0, '') - do_sleep = True + backoff = BackoffHandler(self._cb, timeout=timeout) + with backoff as b: + progress_data = (1, 0, '') + errorcount = 0 + last_nc = 0 + while progress_data[1] < progress_data[0]: + b.pause() + try: + progress_data = self.get_progress() + if progress_data[1] > last_nc: + last_nc = progress_data[1] + b.reset() + except (ServerError, ObjectNotFoundError): + errorcount += 1 + if errorcount == 3: + raise + progress_data = (1, 0, '') return self - def await_completion(self): + def await_completion(self, timeout=0): """ Create a Python Future to check for job completion and return results when available. @@ -121,11 +131,14 @@ def await_completion(self): Required Permissions: jobs.status(READ) + Args: + timeout (int): The timeout for this wait in milliseconds. If this is 0, the default value will be used. + Returns: Future: A future which can be used to wait for this job's completion. When complete, the result of the Future will be this object. """ - return self._cb._async_submit(lambda arg, kwarg: arg[0]._await_completion(), self) + return self._cb._async_submit(lambda arg, kwarg: arg[0]._await_completion(timeout), self) def get_output_as_stream(self, output): """ From 9527fb89a0894cce1d1f14f5491bc3fefb6c1555 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Tue, 13 Feb 2024 14:51:44 -0700 Subject: [PATCH 06/95] deflake8'd --- src/cbc_sdk/utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/cbc_sdk/utils.py b/src/cbc_sdk/utils.py index 9fe73802..2dbfcffa 100755 --- a/src/cbc_sdk/utils.py +++ b/src/cbc_sdk/utils.py @@ -148,7 +148,6 @@ def reset(self, full=False): else: self._pausetime = self._initial - def __enter__(self): """ Called at entry of the context specified by this object. From 182c2c72cf3f4bf5633c80b4f408d6499aef282e Mon Sep 17 00:00:00 2001 From: Alex Van Brunt Date: Wed, 28 Feb 2024 15:24:43 -0700 Subject: [PATCH 07/95] Fix a few small process bugs --- src/cbc_sdk/base.py | 3 +++ src/cbc_sdk/platform/events.py | 2 +- .../unit/platform/test_platform_process.py | 27 +++++++++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/cbc_sdk/base.py b/src/cbc_sdk/base.py index 40c7f115..b5cd7705 100644 --- a/src/cbc_sdk/base.py +++ b/src/cbc_sdk/base.py @@ -790,6 +790,9 @@ def refresh(self): # pragma: no cover """Reload this object from the server.""" raise ApiError("refresh() called on an unrefreshable model") + def _refresh(self): + """Override protected refresh""" + pass class MutableBaseModel(NewBaseModel): """Base model for objects that can have properties changed and then saved back to the server.""" diff --git a/src/cbc_sdk/platform/events.py b/src/cbc_sdk/platform/events.py index c41ea9e7..65d44e5a 100644 --- a/src/cbc_sdk/platform/events.py +++ b/src/cbc_sdk/platform/events.py @@ -217,7 +217,7 @@ def _search(self, start=0, rows=0): self._total_segments = result.get("total_segments", 0) self._processed_segments = result.get("processed_segments", 0) self._count_valid = True - if self._processed_segments != self._total_segments: + if self._processed_segments != self._total_segments and len(result.get('results', [])) != self._total_results: retry_counter = 0 if self._processed_segments > last_processed_segments else retry_counter + 1 last_processed_segments = max(last_processed_segments, self._processed_segments) if retry_counter == MAX_EVENT_SEARCH_RETRIES: diff --git a/src/tests/unit/platform/test_platform_process.py b/src/tests/unit/platform/test_platform_process.py index e4d64807..405afe77 100644 --- a/src/tests/unit/platform/test_platform_process.py +++ b/src/tests/unit/platform/test_platform_process.py @@ -1448,3 +1448,30 @@ def on_validation_post(url, body, **kwargs): procTree = api.select(Process.Tree, "WNEXFKQ7-0002b226-000015bd-00000000-1d6225bbba74c00") assert procTree is not None + + +def test_process_missing_property(cbcsdk_mock): + """Testing Process missing property""" + def on_validation_post(url, body, **kwargs): + assert body == {"query": "process_guid:WNEXFKQ7\\-0002b226\\-000015bd\\-00000000\\-1d6225bbba74c00"} + return POST_PROCESS_VALIDATION_RESP + + # mock the search validation + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_validation", on_validation_post) + # mock the POST of a search + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_jobs", + POST_PROCESS_SEARCH_JOB_RESP) + # mock the GET to check search status + cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=0"), + GET_PROCESS_SEARCH_JOB_RESP) + # mock the GET to get search results + cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" + "2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=500"), + GET_PROCESS_SEARCH_JOB_RESULTS_RESP) + api = cbcsdk_mock.api + guid = 'WNEXFKQ7-0002b226-000015bd-00000000-1d6225bbba74c00' + process = api.select(Process, guid) + + with pytest.raises(AttributeError): + process.invalid_property \ No newline at end of file From d0001b48b29d42a6db456a3ba02accd294b16483 Mon Sep 17 00:00:00 2001 From: Alex Van Brunt Date: Wed, 28 Feb 2024 15:30:49 -0700 Subject: [PATCH 08/95] Fix flake8 --- src/cbc_sdk/base.py | 1 + src/cbc_sdk/platform/events.py | 3 ++- src/tests/unit/platform/test_platform_process.py | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/cbc_sdk/base.py b/src/cbc_sdk/base.py index b5cd7705..b9db9154 100644 --- a/src/cbc_sdk/base.py +++ b/src/cbc_sdk/base.py @@ -794,6 +794,7 @@ def _refresh(self): """Override protected refresh""" pass + class MutableBaseModel(NewBaseModel): """Base model for objects that can have properties changed and then saved back to the server.""" _new_object_http_method = "POST" diff --git a/src/cbc_sdk/platform/events.py b/src/cbc_sdk/platform/events.py index 65d44e5a..24e728df 100644 --- a/src/cbc_sdk/platform/events.py +++ b/src/cbc_sdk/platform/events.py @@ -217,7 +217,8 @@ def _search(self, start=0, rows=0): self._total_segments = result.get("total_segments", 0) self._processed_segments = result.get("processed_segments", 0) self._count_valid = True - if self._processed_segments != self._total_segments and len(result.get('results', [])) != self._total_results: + if self._processed_segments != self._total_segments \ + and len(result.get('results', [])) != self._total_results: retry_counter = 0 if self._processed_segments > last_processed_segments else retry_counter + 1 last_processed_segments = max(last_processed_segments, self._processed_segments) if retry_counter == MAX_EVENT_SEARCH_RETRIES: diff --git a/src/tests/unit/platform/test_platform_process.py b/src/tests/unit/platform/test_platform_process.py index 405afe77..ed197f80 100644 --- a/src/tests/unit/platform/test_platform_process.py +++ b/src/tests/unit/platform/test_platform_process.py @@ -1472,6 +1472,6 @@ def on_validation_post(url, body, **kwargs): api = cbcsdk_mock.api guid = 'WNEXFKQ7-0002b226-000015bd-00000000-1d6225bbba74c00' process = api.select(Process, guid) - + with pytest.raises(AttributeError): - process.invalid_property \ No newline at end of file + process.invalid_property From 7832fea00387a16ffdd9f48d403ae3d65ce6d717 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Thu, 7 Mar 2024 10:12:46 -0700 Subject: [PATCH 09/95] ported solution back into SDK and added unit tests --- src/cbc_sdk/platform/devices.py | 2 +- src/cbc_sdk/platform/jobs.py | 57 +++++++++++---- src/tests/unit/fixtures/platform/mock_jobs.py | 4 + src/tests/unit/platform/test_jobs.py | 73 ++++++++++++++++++- 4 files changed, 117 insertions(+), 19 deletions(-) diff --git a/src/cbc_sdk/platform/devices.py b/src/cbc_sdk/platform/devices.py index 30d62d82..dcd21e59 100644 --- a/src/cbc_sdk/platform/devices.py +++ b/src/cbc_sdk/platform/devices.py @@ -1173,7 +1173,7 @@ def export(self): url = self._build_url("/_export") resp = self._cb.post_object(url, body=request) result = resp.json() - return Job(self._cb, result["id"], result) + return Job(self._cb, result["id"], result, wait_status=True) def scroll(self, rows=10000): """ diff --git a/src/cbc_sdk/platform/jobs.py b/src/cbc_sdk/platform/jobs.py index 0b242430..c160ab46 100644 --- a/src/cbc_sdk/platform/jobs.py +++ b/src/cbc_sdk/platform/jobs.py @@ -17,7 +17,7 @@ import logging import time from cbc_sdk.base import NewBaseModel, BaseQuery, IterableQueryMixin, AsyncQueryMixin -from cbc_sdk.errors import ObjectNotFoundError, ServerError +from cbc_sdk.errors import ObjectNotFoundError, ServerError, ApiError from cbc_sdk.utils import BackoffHandler @@ -31,7 +31,7 @@ class Job(NewBaseModel): primary_key = "id" swagger_meta_file = "platform/models/job.yaml" - def __init__(self, cb, model_unique_id, initial_data=None): + def __init__(self, cb, model_unique_id, initial_data=None, **kwargs): """ Initialize the Job object. @@ -39,8 +39,16 @@ def __init__(self, cb, model_unique_id, initial_data=None): cb (BaseAPI): Reference to API object used to communicate with the server. model_unique_id (int): ID of the job. initial_data (dict): Initial data used to populate the job. + kwargs (dict): Additional keyword arguments. + + Keyword Args: + wait_status (bool): If ``True``, causes the job to wait on change in status instead of relying on the + progress API call (workaround for server issue). Default is ``False``. """ super(Job, self).__init__(cb, model_unique_id, initial_data) + self._wait_status = False + if kwargs.get("wait_status", None): + self._wait_status = True if model_unique_id is not None and initial_data is None: self._refresh() else: @@ -104,21 +112,38 @@ def _await_completion(self, timeout=0): """ backoff = BackoffHandler(self._cb, timeout=timeout) with backoff as b: - progress_data = (1, 0, '') errorcount = 0 - last_nc = 0 - while progress_data[1] < progress_data[0]: - b.pause() - try: - progress_data = self.get_progress() - if progress_data[1] > last_nc: - last_nc = progress_data[1] - b.reset() - except (ServerError, ObjectNotFoundError): - errorcount += 1 - if errorcount == 3: - raise - progress_data = (1, 0, '') + if self._wait_status: + status = "" + while status not in ("FAILED", "COMPLETED"): + b.pause() + try: + self._refresh() + if self.status != status: + status = self.status + b.reset() + except (ServerError, ObjectNotFoundError): + errorcount += 1 + if errorcount == 3: + raise + status = "" + if status == "FAILED": + raise ApiError(f"Job {self.id} reports failure") + else: + progress_data = (1, 0, '') + last_nc = 0 + while progress_data[1] < progress_data[0]: + b.pause() + try: + progress_data = self.get_progress() + if progress_data[1] > last_nc: + last_nc = progress_data[1] + b.reset() + except (ServerError, ObjectNotFoundError): + errorcount += 1 + if errorcount == 3: + raise + progress_data = (1, 0, '') return self def await_completion(self, timeout=0): diff --git a/src/tests/unit/fixtures/platform/mock_jobs.py b/src/tests/unit/fixtures/platform/mock_jobs.py index b5525bec..41104dd5 100644 --- a/src/tests/unit/fixtures/platform/mock_jobs.py +++ b/src/tests/unit/fixtures/platform/mock_jobs.py @@ -73,3 +73,7 @@ "message": "" }, ] + +AWAIT_COMPLETION_DETAILS_PROGRESS_1 = ["CREATED", "CREATED", "CREATED", "COMPLETED"] + +AWAIT_COMPLETION_DETAILS_PROGRESS_2 = ["CREATED", "CREATED", "CREATED", "FAILED"] \ No newline at end of file diff --git a/src/tests/unit/platform/test_jobs.py b/src/tests/unit/platform/test_jobs.py index 6d6eadb6..9eb0bb94 100644 --- a/src/tests/unit/platform/test_jobs.py +++ b/src/tests/unit/platform/test_jobs.py @@ -1,4 +1,5 @@ """Tests for the Job object of the CBC SDK""" +import copy import pytest import logging @@ -6,11 +7,13 @@ import os from tempfile import mkstemp from cbc_sdk.platform import Job -from cbc_sdk.errors import ServerError +from cbc_sdk.errors import ServerError, ApiError from cbc_sdk.rest_api import CBCloudAPI from tests.unit.fixtures.CBCSDKMock import CBCSDKMock from tests.unit.fixtures.platform.mock_jobs import (FIND_ALL_JOBS_RESP, JOB_DETAILS_1, JOB_DETAILS_2, PROGRESS_1, - PROGRESS_2, AWAIT_COMPLETION_PROGRESS) + PROGRESS_2, AWAIT_COMPLETION_PROGRESS, + AWAIT_COMPLETION_DETAILS_PROGRESS_1, + AWAIT_COMPLETION_DETAILS_PROGRESS_2) logging.basicConfig(format='%(asctime)s %(levelname)s:%(message)s', level=logging.DEBUG, filename='log.txt') @@ -137,6 +140,72 @@ def on_progress(url, query_params, default): future.result() +def test_job_await_completion_status(cbcsdk_mock): + """Test that await_completion() functions if _wait_status is set.""" + pr_index = 0 + + def on_details(url, query_params, default): + nonlocal pr_index + assert pr_index < len(AWAIT_COMPLETION_DETAILS_PROGRESS_1), "Too many status calls made" + return_value = copy.deepcopy(JOB_DETAILS_1) + return_value['status'] = AWAIT_COMPLETION_DETAILS_PROGRESS_1[pr_index] + pr_index += 1 + return return_value + + cbcsdk_mock.mock_request('GET', '/jobs/v1/orgs/test/jobs/12345', on_details) + api = cbcsdk_mock.api + job = api.select(Job, 12345) + job._wait_status = True + future = job.await_completion() + result = future.result() + assert result is job + assert pr_index == len(AWAIT_COMPLETION_DETAILS_PROGRESS_1) + + +def test_job_await_completion_status_error(cbcsdk_mock): + """Test that await_completion() throws a ServerError if it gets too many ServerErrors internally.""" + first_time = True + + def on_details(url, query_params, default): + nonlocal first_time + if first_time: + first_time = False + return_value = copy.deepcopy(JOB_DETAILS_1) + return_value['status'] = "CREATED" + return return_value + raise ServerError(400, "Ain't happening") + + cbcsdk_mock.mock_request('GET', '/jobs/v1/orgs/test/jobs/12345', on_details) + api = cbcsdk_mock.api + job = api.select(Job, 12345) + job._wait_status = True + future = job.await_completion() + with pytest.raises(ServerError): + future.result() + + +def test_job_await_completion_status_failure(cbcsdk_mock): + """Test that await_completion() throws an ApiError if it gets a FAILURE response.""" + pr_index = 0 + + def on_details(url, query_params, default): + nonlocal pr_index + assert pr_index < len(AWAIT_COMPLETION_DETAILS_PROGRESS_2), "Too many status calls made" + return_value = copy.deepcopy(JOB_DETAILS_1) + return_value['status'] = AWAIT_COMPLETION_DETAILS_PROGRESS_2[pr_index] + pr_index += 1 + return return_value + + cbcsdk_mock.mock_request('GET', '/jobs/v1/orgs/test/jobs/12345', on_details) + api = cbcsdk_mock.api + job = api.select(Job, 12345) + job._wait_status = True + future = job.await_completion() + with pytest.raises(ApiError): + future.result() + assert pr_index == len(AWAIT_COMPLETION_DETAILS_PROGRESS_2) + + def test_job_output_export_string(cbcsdk_mock): """Tests exporting the results of a job as a string.""" cbcsdk_mock.mock_request('GET', '/jobs/v1/orgs/test/jobs/12345', JOB_DETAILS_1) From 5c28003bb49386521f6c96020b283c0519a48b67 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Thu, 7 Mar 2024 10:26:38 -0700 Subject: [PATCH 10/95] added autodetect of wait status based on job type --- src/cbc_sdk/platform/devices.py | 2 +- src/cbc_sdk/platform/jobs.py | 9 +++++---- src/tests/unit/platform/test_devicev6_api.py | 3 +++ 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/cbc_sdk/platform/devices.py b/src/cbc_sdk/platform/devices.py index dcd21e59..30d62d82 100644 --- a/src/cbc_sdk/platform/devices.py +++ b/src/cbc_sdk/platform/devices.py @@ -1173,7 +1173,7 @@ def export(self): url = self._build_url("/_export") resp = self._cb.post_object(url, body=request) result = resp.json() - return Job(self._cb, result["id"], result, wait_status=True) + return Job(self._cb, result["id"], result) def scroll(self, rows=10000): """ diff --git a/src/cbc_sdk/platform/jobs.py b/src/cbc_sdk/platform/jobs.py index c160ab46..79cd133b 100644 --- a/src/cbc_sdk/platform/jobs.py +++ b/src/cbc_sdk/platform/jobs.py @@ -31,7 +31,9 @@ class Job(NewBaseModel): primary_key = "id" swagger_meta_file = "platform/models/job.yaml" - def __init__(self, cb, model_unique_id, initial_data=None, **kwargs): + JOB_TYPES_WAITING_ON_STATUS = ('ENDPOINTS', ) + + def __init__(self, cb, model_unique_id, initial_data=None): """ Initialize the Job object. @@ -39,7 +41,6 @@ def __init__(self, cb, model_unique_id, initial_data=None, **kwargs): cb (BaseAPI): Reference to API object used to communicate with the server. model_unique_id (int): ID of the job. initial_data (dict): Initial data used to populate the job. - kwargs (dict): Additional keyword arguments. Keyword Args: wait_status (bool): If ``True``, causes the job to wait on change in status instead of relying on the @@ -47,12 +48,12 @@ def __init__(self, cb, model_unique_id, initial_data=None, **kwargs): """ super(Job, self).__init__(cb, model_unique_id, initial_data) self._wait_status = False - if kwargs.get("wait_status", None): - self._wait_status = True if model_unique_id is not None and initial_data is None: self._refresh() else: self._full_init = True + if self._info['type'] in self.JOB_TYPES_WAITING_ON_STATUS: + self._wait_status = True @classmethod def _query_implementation(cls, cb, **kwargs): diff --git a/src/tests/unit/platform/test_devicev6_api.py b/src/tests/unit/platform/test_devicev6_api.py index eff6bf2f..ec08087e 100755 --- a/src/tests/unit/platform/test_devicev6_api.py +++ b/src/tests/unit/platform/test_devicev6_api.py @@ -433,3 +433,6 @@ def post_validate(url, body, **kwargs): assert job assert isinstance(job, Job) assert job.id == 11608915 + assert job._wait_status + + From 238baf15880bd7829411a4f1e11eb4fe5a52b2a0 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Thu, 7 Mar 2024 14:02:55 -0700 Subject: [PATCH 11/95] deflake8'd --- src/tests/unit/fixtures/platform/mock_jobs.py | 2 +- src/tests/unit/platform/test_devicev6_api.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/tests/unit/fixtures/platform/mock_jobs.py b/src/tests/unit/fixtures/platform/mock_jobs.py index 41104dd5..1a929893 100644 --- a/src/tests/unit/fixtures/platform/mock_jobs.py +++ b/src/tests/unit/fixtures/platform/mock_jobs.py @@ -76,4 +76,4 @@ AWAIT_COMPLETION_DETAILS_PROGRESS_1 = ["CREATED", "CREATED", "CREATED", "COMPLETED"] -AWAIT_COMPLETION_DETAILS_PROGRESS_2 = ["CREATED", "CREATED", "CREATED", "FAILED"] \ No newline at end of file +AWAIT_COMPLETION_DETAILS_PROGRESS_2 = ["CREATED", "CREATED", "CREATED", "FAILED"] diff --git a/src/tests/unit/platform/test_devicev6_api.py b/src/tests/unit/platform/test_devicev6_api.py index ec08087e..87b94fd9 100755 --- a/src/tests/unit/platform/test_devicev6_api.py +++ b/src/tests/unit/platform/test_devicev6_api.py @@ -434,5 +434,3 @@ def post_validate(url, body, **kwargs): assert isinstance(job, Job) assert job.id == 11608915 assert job._wait_status - - From 33394ae1cccc95937b53559f1ee52e6c100d92b2 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Fri, 8 Mar 2024 10:52:31 -0700 Subject: [PATCH 12/95] added initial encoding detection implementation --- .../file_credential_provider.py | 32 +++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/src/cbc_sdk/credential_providers/file_credential_provider.py b/src/cbc_sdk/credential_providers/file_credential_provider.py index 746e7070..d6a11562 100755 --- a/src/cbc_sdk/credential_providers/file_credential_provider.py +++ b/src/cbc_sdk/credential_providers/file_credential_provider.py @@ -11,6 +11,7 @@ """Credentials provider that reads the credentials from a file.""" +import codecs import configparser import logging import os @@ -112,6 +113,31 @@ def _security_check(self, path): log.warning("Security warning: " + failmsg) return True + @staticmethod + def _get_encoding(file): + """ + Detects which encoding a file is in. + + Args: + file (Path): The file to be tested. + + Returns: + str: The (possibly-guessed) encoding for the file. + """ + try: + with open(file, "rb") as f: + prefix = bytearray(f.read(5)) + if prefix.startswith(codecs.BOM_UTF8): + return "utf_8_sig" + if prefix.startswith(codecs.BOM_UTF16_LE): + return "utf_16_le" + if prefix.startswith(codecs.BOM_UTF16_BE): + return "utf_16_be" + return "utf_8" + except OSError: + log.warning(f"unable to read encoding of file {file}, assuming utf_8 encoding") + return "utf_8" + def get_credentials(self, section=None): """ Return a Credentials object containing the configured credentials. @@ -132,14 +158,14 @@ def get_credentials(self, section=None): cred_files = [p for p in self._search_path if self._security_check(p)] if not cred_files: raise CredentialError(f"Unable to locate credential file(s) from {self._search_path}") - raw_cred_files = [str(p) for p in cred_files] # needed to support 3.6.0 correctly & for error message try: parser = configparser.ConfigParser() - parser.read(raw_cred_files) + for file in cred_files: + parser.read(str(file), encoding=self._get_encoding(file)) for sect in parser.sections(): new_creds[sect] = Credentials({name: value for (name, value) in parser.items(sect)}) except configparser.Error as e: - raise CredentialError(f"Unable to read credential file(s) {raw_cred_files}") from e + raise CredentialError(f"Unable to read credential file(s) {cred_files}") from e self._cached_credentials = new_creds if section in self._cached_credentials: return self._cached_credentials[section] From f19372179c42a1e9e876b2282afb7773d9f80e77 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Fri, 8 Mar 2024 14:59:25 -0700 Subject: [PATCH 13/95] added test code - only partial solution, reading UTF-16 files still fails --- .../file_credential_provider.py | 3 ++- src/tests/unit/credential_providers/test_file.py | 10 ++++++++-- .../test_file_data/config_valid_utf16be.cbc | Bin 0 -> 524 bytes .../test_file_data/config_valid_utf16le.cbc | Bin 0 -> 524 bytes .../test_file_data/config_valid_utf8sig.cbc | 15 +++++++++++++++ 5 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 src/tests/unit/credential_providers/test_file_data/config_valid_utf16be.cbc create mode 100644 src/tests/unit/credential_providers/test_file_data/config_valid_utf16le.cbc create mode 100644 src/tests/unit/credential_providers/test_file_data/config_valid_utf8sig.cbc diff --git a/src/cbc_sdk/credential_providers/file_credential_provider.py b/src/cbc_sdk/credential_providers/file_credential_provider.py index d6a11562..b21cca37 100755 --- a/src/cbc_sdk/credential_providers/file_credential_provider.py +++ b/src/cbc_sdk/credential_providers/file_credential_provider.py @@ -65,7 +65,7 @@ def _file_stat(self, path): Returns: os.stat_result: The resulting status. """ - return path.stat() + return path.stat() # pragma: no cover def _security_check(self, path): """ @@ -161,6 +161,7 @@ def get_credentials(self, section=None): try: parser = configparser.ConfigParser() for file in cred_files: + # use string as filename parameter to maintain compatibility parser.read(str(file), encoding=self._get_encoding(file)) for sect in parser.sections(): new_creds[sect] = Credentials({name: value for (name, value) in parser.items(sect)}) diff --git a/src/tests/unit/credential_providers/test_file.py b/src/tests/unit/credential_providers/test_file.py index 519f84e1..8448cafa 100755 --- a/src/tests/unit/credential_providers/test_file.py +++ b/src/tests/unit/credential_providers/test_file.py @@ -127,9 +127,15 @@ def mock_stat(path): assert last_failmsg.endswith(suffix) -def test_read_single_file(): +@pytest.mark.parametrize("filename", [ + "config_valid.cbc", + "config_valid_utf8sig.cbc", + "config_valid_utf16be.cbc", + "config_valid_utf16le.cbc" +]) +def test_read_single_file(filename): """Test the basic reading of multiple credential sets from a single file.""" - sut = FileCredentialProvider(path_of("config_valid.cbc")) + sut = FileCredentialProvider(path_of(filename)) creds = sut.get_credentials("default") assert creds.url == "http://example.com" assert creds.token == "ABCDEFGH" diff --git a/src/tests/unit/credential_providers/test_file_data/config_valid_utf16be.cbc b/src/tests/unit/credential_providers/test_file_data/config_valid_utf16be.cbc new file mode 100644 index 0000000000000000000000000000000000000000..f06be7089ef26cac8e295a2d1ac44f99bfacdbb0 GIT binary patch literal 524 zcmbV}!A`?K3`FOgk10ywgp>nNO8=oktEg;4)FoSmZPorf@a#)^pqI)@VtdBpk@NGb zwLTQJRjY%Nir!U`jpU6!sf+e{)EyCZg7rn6$YfFf)5>Pq2%ZSHtG1s6q opxW~;Fgw&+vfld*_sLwOMY|%_Ot$X0=bLW$OaEVa>1uxEKM1&DBme*a literal 0 HcmV?d00001 diff --git a/src/tests/unit/credential_providers/test_file_data/config_valid_utf16le.cbc b/src/tests/unit/credential_providers/test_file_data/config_valid_utf16le.cbc new file mode 100644 index 0000000000000000000000000000000000000000..aa0f69908544bb8f24503cfbfb6ce7a8f9aa0406 GIT binary patch literal 524 zcmbV}%}&EW420*LhbcU_JjpU8qsEZC-=!S?o!P-+N@|m{ER27V@cRB+TOWo^1k9yKd&w2sP zXmtwCI6I?;Glz8x?+(TXkzjnHF{;Ol@yt;IiGMHx?lO2`ueplM40I;Ft?Lz(cc_|Y zQ_X~Vw_Sz#iEr5GNC2_jK{d~uxfQ6E+Zbp)m($t pfNIaX!0b?O$$IZM+$VF97VU~yGugW1o^SfcU;6#ZD_8ru@&#z$Vk7_n literal 0 HcmV?d00001 diff --git a/src/tests/unit/credential_providers/test_file_data/config_valid_utf8sig.cbc b/src/tests/unit/credential_providers/test_file_data/config_valid_utf8sig.cbc new file mode 100644 index 00000000..b289cac9 --- /dev/null +++ b/src/tests/unit/credential_providers/test_file_data/config_valid_utf8sig.cbc @@ -0,0 +1,15 @@ +[default] +url=http://example.com +token=ABCDEFGH +org_key=A1B2C3D4 +ssl_verify=false +ssl_verify_hostname=no +ssl_cert_file=foo.certs +ssl_force_tls_1_2=1 +proxy=proxy.example +ignore_system_proxy=on +integration=Covax + +[partial] +url=http://example.com +ssl_verify=False From 4e9a50c81a443aa8c58e966b1ea71b3d9f2c9b16 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Fri, 8 Mar 2024 15:16:38 -0700 Subject: [PATCH 14/95] got a working solution --- .../file_credential_provider.py | 14 +++++++++++--- src/tests/unit/credential_providers/test_file.py | 8 ++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/cbc_sdk/credential_providers/file_credential_provider.py b/src/cbc_sdk/credential_providers/file_credential_provider.py index b21cca37..ef97b4eb 100755 --- a/src/cbc_sdk/credential_providers/file_credential_provider.py +++ b/src/cbc_sdk/credential_providers/file_credential_provider.py @@ -134,7 +134,7 @@ def _get_encoding(file): if prefix.startswith(codecs.BOM_UTF16_BE): return "utf_16_be" return "utf_8" - except OSError: + except OSError: # pragma: no cover log.warning(f"unable to read encoding of file {file}, assuming utf_8 encoding") return "utf_8" @@ -161,8 +161,16 @@ def get_credentials(self, section=None): try: parser = configparser.ConfigParser() for file in cred_files: - # use string as filename parameter to maintain compatibility - parser.read(str(file), encoding=self._get_encoding(file)) + encoding = self._get_encoding(file) + if encoding.startswith("utf_16"): + with open(file, 'rt', encoding=encoding) as f: + # skip the BOM at the start of the file, because that seems to break ConfigParser + ch = f.read(1) + assert ch == "\ufeff" + parser.read_file(f, source=str(file)) + else: + # use string as filename parameter to maintain compatibility + parser.read(str(file), encoding=encoding) for sect in parser.sections(): new_creds[sect] = Credentials({name: value for (name, value) in parser.items(sect)}) except configparser.Error as e: diff --git a/src/tests/unit/credential_providers/test_file.py b/src/tests/unit/credential_providers/test_file.py index 8448cafa..c2f0dceb 100755 --- a/src/tests/unit/credential_providers/test_file.py +++ b/src/tests/unit/credential_providers/test_file.py @@ -198,3 +198,11 @@ def test_file_with_parsing_error(): sut = FileCredentialProvider(path_of("config_parseerror.cbc")) with pytest.raises(CredentialError): sut.get_credentials("default") + + +def test_no_search_path_error(): + """Tests that an error is thrown if no files can be found in the search path.""" + sut = FileCredentialProvider() + sut._search_path = [] + with pytest.raises(CredentialError): + sut.get_credentials() From 62c40fcacf5ca9f5e2e0e63b161d2facb918ca67 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Fri, 8 Mar 2024 15:27:26 -0700 Subject: [PATCH 15/95] added notes in the documentation that we can take UTF-16 files now --- docs/authentication.rst | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/docs/authentication.rst b/docs/authentication.rst index 5405884f..9ee343e9 100644 --- a/docs/authentication.rst +++ b/docs/authentication.rst @@ -3,7 +3,6 @@ Authentication ============== - Carbon Black Cloud APIs require authentication to secure your data. There are several methods for authentication listed below. Every method requires @@ -52,9 +51,6 @@ Store the credential with a profile name, and reference the profile when creatin For more examples on Live Response, check :doc:`live-response` - - - Authentication Methods ---------------------- @@ -117,8 +113,9 @@ Authentication Methods With a File ^^^^^^^^^^^ Credentials may be supplied in a file that resembles a Windows ``.INI`` file in structure, which allows for -multiple "profiles" or sets of credentials to be supplied in a single file. The file format is backwards compatible with -CBAPI, so older files can continue to be used. +multiple "profiles" or sets of credentials to be supplied in a single file. The file format is backwards compatible +with CBAPI, so older files can continue to be used. The file must be encoded as UTF-8, or as UTF-16 using either +big-endian or little-endian format. **Example of a credentials file containing two profiles** From 9a181ffc824e223f62b50cc7b29891fbd56fa046 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Thu, 14 Mar 2024 16:43:10 -0600 Subject: [PATCH 16/95] beginnings of the devices guide --- docs/devices.rst | 73 ++++++++++++++++++++++++++++++++++++++++++++++++ docs/guides.rst | 2 ++ 2 files changed, 75 insertions(+) create mode 100644 docs/devices.rst diff --git a/docs/devices.rst b/docs/devices.rst new file mode 100644 index 00000000..5b42a705 --- /dev/null +++ b/docs/devices.rst @@ -0,0 +1,73 @@ +Devices +======= + +*Devices*, also known as *endpoints*, are at the heart of Carbon Black Cloud's functionality. Each device has a +Carbon Black Cloud sensor installed on it, which communicates with Carbon Black analytics and the Carbon Black Cloud +console. + +Using the Carbon Black Cloud SDK, you can search for devices with a wide range of criteria, filtering on many different +fields. You can also perform actions on individual devices, such as setting quarantine status, setting bypass status, +or upgrading to a new sensor version. + +Searching for Devices +--------------------- + +Using a query of the ``Device`` object, you can list the devices configured for your organization:: + + >>> from cbc_sdk import CBCloudAPI + >>> api = CBCloudAPI(profile='sample') + >>> from cbc_sdk.platform import Device + >>> query = api.select(Device).where("os:WINDOWS") + >>> query.add_criteria('target_priority', ['LOW']).add_criteria('virtualization_provider', ['VirtualBox']) + >>> for d in query: + ... print(f"{d.name} - {d.last_internal_ip_address}") + DESKTOP-A19 - 10.0.2.15 + DESKTOP-Q210 - 10.10.25.210 + DESKTOP-Q211 - 10.10.25.211 + DESKTOP-Q211B - 10.10.25.210 + EVALUATION-1 - 10.0.2.15 + EVALUATION-2 - 10.0.2.15 + STAGING-1A - 192.168.1.100 + ZZIGNORE-1 - 10.0.3.15 + +The criteria supported in the ``where()`` and ``add_criteria()`` query methods are too numerous to enumerate here; +please see +`the Developer Network documentation `_ +for more details. + +The results of a search query can also be exported:: + + >>> from cbc_sdk import CBCloudAPI + >>> api = CBCloudAPI(profile='sample') + >>> from cbc_sdk.platform import Device + >>> query = api.select(Device).where("os:WINDOWS") + >>> query.add_criteria('target_priority', ['LOW']).add_criteria('virtualization_provider', ['VirtualBox']) + >>> job = query.export() + >>> csv_report = job.get_output_as_string() + >>> # can also get the output as a file or as enumerated lines of text + +Device Actions +-------------- + +Bypass Enable/Disable ++++++++++++++++++++++ + +Setting a device to *bypass* disables all enforcement on the device; its sensor stops sending data to the Carbon Black +Cloud. + +Quarantine +++++++++++ + +A device that has been *quarantined* has its outbound traffic limited, and all inbound traffic to it stopped. This +would be used on any device determined to be interacting badly. + +Background Scan ++++++++++++++++ + +Enabling *background scan* causes a one-time inventory scan on the device to identify any malware files already present +there. The background scan is of the type specified in the device's policy, if that policy has background scans +enabled, or a standard background scan if it does not. + +Disabling background scan causes any background scan currently running on the device to be temporarily suspended; it +will restart when background scan is enabled again, or when the endpoint restarts. + diff --git a/docs/guides.rst b/docs/guides.rst index dd19bcce..787b2ae8 100755 --- a/docs/guides.rst +++ b/docs/guides.rst @@ -30,6 +30,7 @@ Feature Guides asset-groups audit-log developing-credential-providers + devices device-control differential-analysis live-query @@ -48,6 +49,7 @@ Feature Guides * :doc:`asset-groups` - Create and modify Asset Groups, and preview the impact changes to policy ranking or asset group definition will have. * :doc:`alerts-migration` - Update from SDK 1.4.3 or earlier to SDK 1.5.0 or later to get the benefits of the Alerts v7 API. * :doc:`audit-log` - Retrieve audit log events indicating various "system" events. +* :doc:`devices` - Search for, get information about, and act on endpoints. * :doc:`device-control` - Control the blocking of USB devices on endpoints. * :doc:`differential-analysis` - Provides the ability to compare and understand the changes between two Live Query runs * :doc:`live-query` - Live Query allows operators to ask questions of endpoints From 10af90b3c295b30c79cc0cbe6682654265d4ae4c Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Mon, 18 Mar 2024 12:20:08 -0600 Subject: [PATCH 17/95] added the device actions samples --- docs/devices.rst | 90 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 82 insertions(+), 8 deletions(-) diff --git a/docs/devices.rst b/docs/devices.rst index 5b42a705..9df4e2d7 100644 --- a/docs/devices.rst +++ b/docs/devices.rst @@ -21,14 +21,14 @@ Using a query of the ``Device`` object, you can list the devices configured for >>> query.add_criteria('target_priority', ['LOW']).add_criteria('virtualization_provider', ['VirtualBox']) >>> for d in query: ... print(f"{d.name} - {d.last_internal_ip_address}") - DESKTOP-A19 - 10.0.2.15 - DESKTOP-Q210 - 10.10.25.210 - DESKTOP-Q211 - 10.10.25.211 - DESKTOP-Q211B - 10.10.25.210 - EVALUATION-1 - 10.0.2.15 - EVALUATION-2 - 10.0.2.15 - STAGING-1A - 192.168.1.100 - ZZIGNORE-1 - 10.0.3.15 + DESKTOP-A19 - 10.0.2.44 + DESKTOP-Q210 - 10.10.25.169 + DESKTOP-Q211 - 10.10.25.170 + DESKTOP-Q211B - 10.10.25.180 + EVALUATION-1 - 10.0.2.51 + EVALUATION-2 - 10.0.2.52 + STAGING-1A - 192.168.1.99 + ZZIGNORE-1 - 10.0.3.74 The criteria supported in the ``where()`` and ``add_criteria()`` query methods are too numerous to enumerate here; please see @@ -46,21 +46,77 @@ The results of a search query can also be exported:: >>> csv_report = job.get_output_as_string() >>> # can also get the output as a file or as enumerated lines of text +Search Scrolling +++++++++++++++++ + +A Device Search request can return no more than 10,000 items at a time. Some customers may have more endpoints than +that; to return *all* devices, you can use the ``scroll()`` method on the query to continue searching after all devices +that have been previously returned. This snippet illustrates the technique:: + + # assume "api" is your CBCloudAPI reference + query = api.select(Device) + # add search terms and/or criteria to the query (not shown here) + for d in query: + do_something_with_device(d) # whatever you need for each device + while query.num_remaining > 0: + query.scroll() # default is 10,000 devices at a time + for d in query: + do_something_with_device(d) + Device Actions -------------- +Most device actions in the Carbon Black Cloud can be performed on a single device through the ``Device`` object, +on multiple devices specified by ID, or on the results of a device query. + Bypass Enable/Disable +++++++++++++++++++++ Setting a device to *bypass* disables all enforcement on the device; its sensor stops sending data to the Carbon Black Cloud. +Setting bypass on a single device:: + + >>> # assume "api" is your CBCloudAPI reference + >>> d = api.select(Device, 12345) + >>> d.bypass(True) + +Setting bypass on multiple devices:: + + >>> # assume "api" is your CBCloudAPI reference + api.device_bypass([1001, 1002, 1003], True) + +Setting bypass on the results of a device search:: + + >>> # assume "api" is your CBCloudAPI reference + query = api.select(Device) + # add search terms and/or criteria to the query (not shown here) + query.bypass(True) + Quarantine ++++++++++ A device that has been *quarantined* has its outbound traffic limited, and all inbound traffic to it stopped. This would be used on any device determined to be interacting badly. +Setting quarantine on a single device:: + + >>> # assume "api" is your CBCloudAPI reference + >>> d = api.select(Device, 12345) + >>> d.quarantine(True) + +Setting quarantine on multiple devices:: + + >>> # assume "api" is your CBCloudAPI reference + api.device_quarantine([1001, 1002, 1003], True) + +Setting quarantine on the results of a device search:: + + >>> # assume "api" is your CBCloudAPI reference + query = api.select(Device) + # add search terms and/or criteria to the query (not shown here) + query.quarantine(True) + Background Scan +++++++++++++++ @@ -71,3 +127,21 @@ enabled, or a standard background scan if it does not. Disabling background scan causes any background scan currently running on the device to be temporarily suspended; it will restart when background scan is enabled again, or when the endpoint restarts. +Enabling background scan on a single device:: + + >>> # assume "api" is your CBCloudAPI reference + >>> d = api.select(Device, 12345) + >>> d.background_scan(True) + +Enabling background scan on multiple devices:: + + >>> # assume "api" is your CBCloudAPI reference + api.device_background_scan([1001, 1002, 1003], True) + +Enabling background scan on the results of a device search:: + + >>> # assume "api" is your CBCloudAPI reference + query = api.select(Device) + # add search terms and/or criteria to the query (not shown here) + query.background_scan(True) + From 067045565a9a6b58dab62fb1a92820a2c7344200 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Mon, 18 Mar 2024 14:40:52 -0600 Subject: [PATCH 18/95] editorial changes requested by Alex --- docs/devices.rst | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/docs/devices.rst b/docs/devices.rst index 9df4e2d7..a99d0daa 100644 --- a/docs/devices.rst +++ b/docs/devices.rst @@ -3,7 +3,7 @@ Devices *Devices*, also known as *endpoints*, are at the heart of Carbon Black Cloud's functionality. Each device has a Carbon Black Cloud sensor installed on it, which communicates with Carbon Black analytics and the Carbon Black Cloud -console. +back end. Using the Carbon Black Cloud SDK, you can search for devices with a wide range of criteria, filtering on many different fields. You can also perform actions on individual devices, such as setting quarantine status, setting bypass status, @@ -56,10 +56,11 @@ that have been previously returned. This snippet illustrates the technique:: # assume "api" is your CBCloudAPI reference query = api.select(Device) # add search terms and/or criteria to the query (not shown here) + query.scroll() # fetch the first batch of items - 10,000 is default for d in query: do_something_with_device(d) # whatever you need for each device while query.num_remaining > 0: - query.scroll() # default is 10,000 devices at a time + query.scroll() # fetch next batch for d in query: do_something_with_device(d) @@ -96,8 +97,9 @@ Setting bypass on the results of a device search:: Quarantine ++++++++++ -A device that has been *quarantined* has its outbound traffic limited, and all inbound traffic to it stopped. This -would be used on any device determined to be interacting badly. +A device that has been *quarantined* has its outbound traffic limited, and all inbound traffic to it stopped, except +for communication ith the Carbon Black Cloud back end. This would be used on any device determined to be interacting +badly. Setting quarantine on a single device:: @@ -121,11 +123,8 @@ Background Scan +++++++++++++++ Enabling *background scan* causes a one-time inventory scan on the device to identify any malware files already present -there. The background scan is of the type specified in the device's policy, if that policy has background scans -enabled, or a standard background scan if it does not. - -Disabling background scan causes any background scan currently running on the device to be temporarily suspended; it -will restart when background scan is enabled again, or when the endpoint restarts. +there. Disabling background scan causes any background scan currently running on the device to be temporarily +suspended; it will restart when background scan is enabled again, or when the endpoint restarts. Enabling background scan on a single device:: From c73725491b747c7552f05d97f97d1de236d3ca8d Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Mon, 18 Mar 2024 14:49:49 -0600 Subject: [PATCH 19/95] minor wording and typo fixes --- docs/devices.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/devices.rst b/docs/devices.rst index a99d0daa..16dddf0d 100644 --- a/docs/devices.rst +++ b/docs/devices.rst @@ -98,8 +98,8 @@ Quarantine ++++++++++ A device that has been *quarantined* has its outbound traffic limited, and all inbound traffic to it stopped, except -for communication ith the Carbon Black Cloud back end. This would be used on any device determined to be interacting -badly. +for communication with the Carbon Black Cloud back end. This would be used on any device determined to be interacting +maliciously. Setting quarantine on a single device:: From 44f2817921f14f30e2bb026472843618bbd9dfed Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Mon, 18 Mar 2024 16:13:00 -0600 Subject: [PATCH 20/95] fix the scroll example --- docs/devices.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/devices.rst b/docs/devices.rst index 16dddf0d..b89a981e 100644 --- a/docs/devices.rst +++ b/docs/devices.rst @@ -56,12 +56,12 @@ that have been previously returned. This snippet illustrates the technique:: # assume "api" is your CBCloudAPI reference query = api.select(Device) # add search terms and/or criteria to the query (not shown here) - query.scroll() # fetch the first batch of items - 10,000 is default - for d in query: + devicelist = query.scroll() # fetch the first batch of items - 10,000 is default + for d in devicelist: do_something_with_device(d) # whatever you need for each device while query.num_remaining > 0: - query.scroll() # fetch next batch - for d in query: + devicelist = query.scroll() # fetch next batch + for d in devicelist: do_something_with_device(d) Device Actions From 9457abe0cbc2770caca14ab58f6ac7def31befd6 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Mon, 18 Mar 2024 16:20:43 -0600 Subject: [PATCH 21/95] added Alex's suggestion for the scroll example --- docs/devices.rst | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/docs/devices.rst b/docs/devices.rst index b89a981e..656d1c83 100644 --- a/docs/devices.rst +++ b/docs/devices.rst @@ -56,13 +56,10 @@ that have been previously returned. This snippet illustrates the technique:: # assume "api" is your CBCloudAPI reference query = api.select(Device) # add search terms and/or criteria to the query (not shown here) - devicelist = query.scroll() # fetch the first batch of items - 10,000 is default - for d in devicelist: - do_something_with_device(d) # whatever you need for each device - while query.num_remaining > 0: - devicelist = query.scroll() # fetch next batch + while query.num_remaining is None or query.num_remaining > 0: + devicelist = query.scroll() # fetch the batch - 10,000 is default for d in devicelist: - do_something_with_device(d) + do_something_with_device(d) # whatever you need for each device Device Actions -------------- From ce5ece854403368600a0f3b132edf911f8f4f9e3 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Thu, 21 Mar 2024 11:21:19 -0600 Subject: [PATCH 22/95] added link to process fields from Process docstring --- src/cbc_sdk/platform/processes.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/cbc_sdk/platform/processes.py b/src/cbc_sdk/platform/processes.py index ec2030fc..df4e2400 100644 --- a/src/cbc_sdk/platform/processes.py +++ b/src/cbc_sdk/platform/processes.py @@ -54,6 +54,10 @@ class Process(UnrefreshableModel): Objects of this type are retrieved through queries to the Carbon Black Cloud server, such as via ``AsyncProcessQuery``. + Processes have many fields, too many to list here; for a complete list of available fields, visit + `the Search Fields page `_ + on the Carbon Black Developer Network, and filter on the ``PROCESS`` route. + Examples: >>> # use the Process GUID directly >>> process = api.select(Process, "WNEXFKQ7-00050603-0000066c-00000000-1d6c9acb43e29bb") From 6ba238c9cc16468c6108b3f8792f808c1b3ed53a Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Thu, 21 Mar 2024 12:11:34 -0600 Subject: [PATCH 23/95] deflake8'd --- src/cbc_sdk/platform/processes.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/cbc_sdk/platform/processes.py b/src/cbc_sdk/platform/processes.py index df4e2400..5cbe7169 100644 --- a/src/cbc_sdk/platform/processes.py +++ b/src/cbc_sdk/platform/processes.py @@ -55,7 +55,8 @@ class Process(UnrefreshableModel): ``AsyncProcessQuery``. Processes have many fields, too many to list here; for a complete list of available fields, visit - `the Search Fields page `_ + `the Search Fields page + `_ on the Carbon Black Developer Network, and filter on the ``PROCESS`` route. Examples: From 0a7c5ef3b5b3319d374871762729385d9cba6e8a Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Thu, 21 Mar 2024 10:20:42 -0600 Subject: [PATCH 24/95] added a faceting example --- docs/devices.rst | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/devices.rst b/docs/devices.rst index 656d1c83..b577b840 100644 --- a/docs/devices.rst +++ b/docs/devices.rst @@ -46,6 +46,27 @@ The results of a search query can also be exported:: >>> csv_report = job.get_output_as_string() >>> # can also get the output as a file or as enumerated lines of text +Faceting +++++++++ + +Facet search queries return statistical information indicating the relative weighting of the requested values as per +the specified criteria. Device queries support faceting:: + + >>> from cbc_sdk import CBCloudAPI + >>> api = CBCloudAPI(profile='sample') + >>> from cbc_sdk.platform import Device + >>> query = api.select(Device).where("os:WINDOWS") + >>> query.add_criteria('target_priority', ['LOW']).add_criteria('virtualization_provider', ['VirtualBox']) + >>> facets = query.facets(['policy_id']) + >>> for value in facets[0].values_: + ... print(f"Policy ID {value.id}: {value.total} device(s)") + Policy ID 8801: 4 device(s) + Policy ID 81664: 3 device(s) + Policy ID 82804: 1 device(s) + +Note that you can facet on multiple fields by passing more than one field name to the ``facets()`` call. It returns +one ``DeviceFacet`` object per field name, each of which may contain multiple ``DeviceFacetValue`` objects. + Search Scrolling ++++++++++++++++ From 3bdf8c16ebb08b23b8c3ff1d5262e86825edf2b8 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Tue, 9 Apr 2024 15:47:35 -0600 Subject: [PATCH 25/95] normalized the arguments to _perform_query, and fixed up IterableQueryMixin to do the right thing unsurprisingly --- src/cbc_sdk/audit_remediation/base.py | 49 ++++++++++--------- src/cbc_sdk/base.py | 36 ++++++++------ src/cbc_sdk/platform/alerts.py | 16 +++--- src/cbc_sdk/platform/devices.py | 11 ++--- src/cbc_sdk/platform/events.py | 9 +++- src/cbc_sdk/platform/processes.py | 17 ++++++- .../platform/vulnerability_assessment.py | 6 ++- .../unit/platform/test_platform_events.py | 6 +-- 8 files changed, 88 insertions(+), 62 deletions(-) diff --git a/src/cbc_sdk/audit_remediation/base.py b/src/cbc_sdk/audit_remediation/base.py index 9a676407..d98f97f2 100644 --- a/src/cbc_sdk/audit_remediation/base.py +++ b/src/cbc_sdk/audit_remediation/base.py @@ -1000,13 +1000,13 @@ def _count(self): return self._total_results - def _perform_query(self, start=0, rows=0): + def _perform_query(self, from_row=0, max_rows=0): """ Performs the query and returns the results of the query in an iterable fashion. Args: - start (int): The row to start the query at (default 0). - rows (int): The maximum number of rows to be returned (default 0, meaning "all"). + from_row (int): The row to start the query at (default 0). + max_rows (int): The maximum number of rows to be returned (default 0, meaning "all"). Returns: Iterable: The iterated query. @@ -1014,11 +1014,11 @@ def _perform_query(self, start=0, rows=0): url = self._doc_class.urlobject_history.format( self._cb.credentials.org_key ) - current = start + current = from_row numrows = 0 still_querying = True while still_querying: - request = self._build_request(start, rows) + request = self._build_request(from_row, max_rows) resp = self._cb.post_object(url, body=request) result = resp.json() @@ -1031,11 +1031,11 @@ def _perform_query(self, start=0, rows=0): current += 1 numrows += 1 - if rows and numrows == rows: + if max_rows and numrows == max_rows: still_querying = False break - start = current + from_row = current if current >= self._total_results: still_querying = False break @@ -1312,13 +1312,13 @@ def _count(self): return self._total_results - def _perform_query(self, start=0, rows=0): + def _perform_query(self, from_row=0, max_rows=0): """ Performs the query and returns the results of the query in an iterable fashion. Args: - start (int): The row to start the query at (default 0). - rows (int): The maximum number of rows to be returned (default 0, meaning "all"). + from_row (int): The row to start the query at (default 0). + max_rows (int): The maximum number of rows to be returned (default 0, meaning "all"). Returns: Iterable: The iterated query. @@ -1329,11 +1329,11 @@ def _perform_query(self, start=0, rows=0): url = self._doc_class.urlobject.format( self._cb.credentials.org_key, self._run_id ) - current = start + current = from_row numrows = 0 still_querying = True while still_querying: - request = self._build_request(start, rows) + request = self._build_request(from_row, max_rows) resp = self._cb.post_object(url, body=request) result = resp.json() @@ -1348,11 +1348,11 @@ def _perform_query(self, start=0, rows=0): current += 1 numrows += 1 - if rows and numrows == rows: + if max_rows and numrows == max_rows: still_querying = False break - start = current + from_row = current if current >= self._total_results: still_querying = False break @@ -1722,12 +1722,13 @@ def _build_request(self, rows): request["criteria"] = self._criteria return request - def _perform_query(self, rows=0): + def _perform_query(self, from_row=0, max_rows=0): """ Performs the query and returns the results of the query in an iterable fashion. Args: - rows (int): The maximum number of rows to be returned (default 0, meaning "all"). + from_row (int): Not used, inserted for compatibility. + max_rows (int): The maximum number of rows to be returned (default 0, meaning "all"). Returns: Iterable: The iterated query. @@ -1738,7 +1739,7 @@ def _perform_query(self, rows=0): url = self._doc_class.urlobject.format( self._cb.credentials.org_key, self._run_id ) - request = self._build_request(rows) + request = self._build_request(max_rows) resp = self._cb.post_object(url, body=request) result = resp.json() results = result.get("terms", []) @@ -1857,13 +1858,13 @@ def _count(self): return self._total_results - def _perform_query(self, start=0, rows=0): + def _perform_query(self, from_row=0, max_rows=0): """ Performs the query and returns the results of the query in an iterable fashion. Args: - start (int): The row to start the query at (default 0). - rows (int): The maximum number of rows to be returned (default 0, meaning "all"). + from_row (int): The row to start the query at (default 0). + max_rows (int): The maximum number of rows to be returned (default 0, meaning "all"). Returns: Iterable: The iterated query. @@ -1871,11 +1872,11 @@ def _perform_query(self, start=0, rows=0): url = self._doc_class.urlobject_history.format( self._cb.credentials.org_key ) - current = start + current = from_row numrows = 0 still_querying = True while still_querying: - request = self._build_request(start, rows) + request = self._build_request(from_row, max_rows) resp = self._cb.post_object(url, body=request) result = resp.json() @@ -1888,11 +1889,11 @@ def _perform_query(self, start=0, rows=0): current += 1 numrows += 1 - if rows and numrows == rows: + if max_rows and numrows == max_rows: still_querying = False break - start = current + from_row = current if current >= self._total_results: still_querying = False break diff --git a/src/cbc_sdk/base.py b/src/cbc_sdk/base.py index b9db9154..c26787dc 100644 --- a/src/cbc_sdk/base.py +++ b/src/cbc_sdk/base.py @@ -1026,7 +1026,7 @@ def __init__(self, query=None): def _clone(self): return self.__class__(self._query) - def _perform_query(self): + def _perform_query(self, from_row=0, max_rows=-1): # This has the effect of generating an empty iterator. yield from () @@ -1050,10 +1050,7 @@ def first(self): Returns: obj: First query item """ - res = self[:1] - if res is None or not len(res): - return None - return res[0] + return self.__getitem__(0) def one(self): """ @@ -1066,9 +1063,7 @@ def one(self): MoreThanOneResultError: If the query returns more than one item ObjectNotFoundError: If the query returns zero items """ - res = self[:2] - if res is None: - return None + res = list(self._perform_query(from_row=0, max_rows=2)) label = str(self._query) if self._query else "" if len(res) == 0: raise ObjectNotFoundError("query_uri", message="0 results for query {0:s}".format(label)) @@ -1115,7 +1110,7 @@ def __getitem__(self, item): return [results[ii] for ii in range(*item.indices(len(results)))] elif isinstance(item, int): results = list(self._perform_query(from_row=item, max_rows=1)) - return results[item] + return results[0] else: raise TypeError("Invalid argument type") @@ -1262,9 +1257,15 @@ def and_(self, new_query): raise ApiError("Cannot have multiple 'where' clauses") return self.where(new_query) - def _perform_query(self): - for item in self.results: + def _perform_query(self, from_row=0, max_rows=-1): + returned_rows = 0 + for index, item in enumerate(self.results): + if index < from_row: + continue yield item + returned_rows += 1 + if 0 < max_rows <= returned_rows: + break def sort(self, new_sort): """ @@ -1373,8 +1374,8 @@ def __getitem__(self, item): else: raise TypeError("invalid type") - def _perform_query(self, start=0, numrows=0): - for item in self._search(start=start, rows=numrows): + def _perform_query(self, from_row=0, max_rows=0): + for item in self._search(start=from_row, rows=max_rows): yield self._doc_class._new_object(self._cb, item) def batch_size(self, new_batch_size): @@ -2412,8 +2413,13 @@ def _search(self, start=0, rows=0): result = self._cb.get_object(result_url, query_parameters=query_parameters) return self._doc_class(self._cb, model_unique_id=self._query_token, initial_data=result) - def _perform_query(self): - return self.results + def _perform_query(self, from_row=0, max_rows=-1): + if max_rows > 0: + return self.results[from_row:from_row + max_rows] + elif from_row > 0: + return self.results[from_row:] + else: + return self.results @property def results(self): diff --git a/src/cbc_sdk/platform/alerts.py b/src/cbc_sdk/platform/alerts.py index d66aaf26..da474b88 100644 --- a/src/cbc_sdk/platform/alerts.py +++ b/src/cbc_sdk/platform/alerts.py @@ -1392,21 +1392,21 @@ def _count(self): return self._total_results - def _perform_query(self, from_row=1, max_rows=-1): + def _perform_query(self, from_row=0, max_rows=-1): """ Performs the query and returns the results of the query in an iterable fashion. Alerts v6 API uses base 1 instead of 0. Args: - from_row (int): The row to start the query at (default 1). + from_row (int): The row to start the query at (default 0). max_rows (int): The maximum number of rows to be returned (default -1, meaning "all"). Returns: Iterable: The iterated query. """ url = self._build_url("/_search") - current = from_row + current = from_row + 1 numrows = 0 still_querying = True while still_querying: @@ -1449,7 +1449,7 @@ def _perform_query(self, from_row=1, max_rows=-1): still_querying = False break - from_row = current + from_row = current - 1 if current >= self._total_results: still_querying = False break @@ -1704,19 +1704,19 @@ def get_alert_search_query(self): return alert_search_query - def _perform_query(self, from_row=1, max_rows=-1): + def _perform_query(self, from_row=0, max_rows=-1): """ Performs the query and returns the results of the query in an iterable fashion. Args: - from_row (int): The row to start the query at (default 1). + from_row (int): The row to start the query at (default 0). max_rows (int): The maximum number of rows to be returned (default -1, meaning "all"). Returns: Iterable: The iterated query. """ url = self._build_url("/_search") - current = from_row + current = from_row + 1 numrows = 0 still_querying = True while still_querying: @@ -1744,7 +1744,7 @@ def _perform_query(self, from_row=1, max_rows=-1): still_querying = False break - from_row = current + from_row = current - 1 if current >= self._total_results: still_querying = False break diff --git a/src/cbc_sdk/platform/devices.py b/src/cbc_sdk/platform/devices.py index 30d62d82..0d3446a5 100644 --- a/src/cbc_sdk/platform/devices.py +++ b/src/cbc_sdk/platform/devices.py @@ -999,25 +999,22 @@ def _count(self): return self._total_results - def _perform_query(self, from_row=1, max_rows=-1): + def _perform_query(self, from_row=0, max_rows=-1): """ Performs the query and returns the results of the query in an iterable fashion. - Note: - Device v6 API uses base 1 instead of 0. - Required Permissions: device(READ) Args: - from_row (int): The row to start the query at (default 1). + from_row (int): The row to start the query at (default 0). max_rows (int): The maximum number of rows to be returned (default -1, meaning "all"). Yields: Device: The individual devices which match the query. """ url = self._build_url("/_search") - current = from_row + current = from_row + 1 numrows = 0 still_querying = True while still_querying: @@ -1038,7 +1035,7 @@ def _perform_query(self, from_row=1, max_rows=-1): still_querying = False break - from_row = current + from_row = current - 1 if current >= self._total_results: still_querying = False break diff --git a/src/cbc_sdk/platform/events.py b/src/cbc_sdk/platform/events.py index 24e728df..1251a44e 100644 --- a/src/cbc_sdk/platform/events.py +++ b/src/cbc_sdk/platform/events.py @@ -281,8 +281,13 @@ def _get_query_parameters(self): args["process_guid"] = q return args - def _perform_query(self): - return self.results + def _perform_query(self, from_row=0, max_rows=-1): + if max_rows > 0: + return self.results[from_row:from_row + max_rows] + elif from_row > 0: + return self.results[from_row:] + else: + return self.results def _submit(self): args = self._get_query_parameters() diff --git a/src/cbc_sdk/platform/processes.py b/src/cbc_sdk/platform/processes.py index 5cbe7169..19f4081b 100644 --- a/src/cbc_sdk/platform/processes.py +++ b/src/cbc_sdk/platform/processes.py @@ -1028,15 +1028,28 @@ def _search(self, start=0, rows=0): else: raise ApiError(f"Failed to get Process Tree: {result['exception']}") - def _perform_query(self): + def _perform_query(self, from_row=0, max_rows=-1): """ Iterate over the results of the query. Required Permissions: org.search.events(CREATE, READ) + + Args: + from_row (int): Row to start iterating from (default 0). + max_rows(int): Number of rows to enumerate (default -1, meaning "all rows"). + + Yields: + Process.Summary or Process.Tree: The enumerated results. """ - for item in self.results: + returned_rows = 0 + for ndx, item in enumerate(self.results): + if ndx < from_row: + continue yield item + returned_rows += 1 + if 0 < max_rows <= returned_rows: + break @property def results(self): diff --git a/src/cbc_sdk/platform/vulnerability_assessment.py b/src/cbc_sdk/platform/vulnerability_assessment.py index 5f102cec..0a67b81b 100644 --- a/src/cbc_sdk/platform/vulnerability_assessment.py +++ b/src/cbc_sdk/platform/vulnerability_assessment.py @@ -294,10 +294,14 @@ def set_severity(self, severity): self._severity = severity return self - def _perform_query(self): + def _perform_query(self, from_row=0, max_rows=-1): """ Performs the query and returns the Vulnerability.OrgSummary + Args: + from_row (int): Not used, retained for compatibility. + max_rows (int): Not used, retained for compatibility. + Returns: Vulnerability.OrgSummary: The vulnerabilty summary for an organization """ diff --git a/src/tests/unit/platform/test_platform_events.py b/src/tests/unit/platform/test_platform_events.py index 003086a3..62a14059 100644 --- a/src/tests/unit/platform/test_platform_events.py +++ b/src/tests/unit/platform/test_platform_events.py @@ -100,7 +100,7 @@ def test_event_query_select_with_where(cbcsdk_mock): # test .where(process_guid=...) events = api.select(Event).where(process_guid=guid) - results = [res for res in events._perform_query(numrows=10)] + results = [res for res in events._perform_query(max_rows=10)] assert len(results) == 10 first_event = results[0] assert first_event.process_guid == guid @@ -117,7 +117,7 @@ def test_event_query_select_with_where(cbcsdk_mock): EVENT_SEARCH_VALIDATION_RESP) events = api.select(Event).where('process_guid:J7G6DTLN-006633e3-00000334-00000000-1d677bedfbb1c2e') - results = [res for res in events._perform_query(numrows=10)] + results = [res for res in events._perform_query(max_rows=10)] first_event = results[0] assert first_event.process_guid == guid @@ -125,7 +125,7 @@ def test_event_query_select_with_where(cbcsdk_mock): assert len(results) == 10 # test ._perform_query(numrows) - results = [result for result in events._perform_query(numrows=100)] + results = [result for result in events._perform_query(max_rows=100)] assert len(results) == 100 first_result = results[0] assert first_result.process_guid == guid From 6aae4bfd78381f836d262a1292e1c66bf152ef20 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Thu, 11 Apr 2024 10:40:01 -0600 Subject: [PATCH 26/95] bring up test coverage --- src/cbc_sdk/base.py | 6 +++--- .../platform/test_vulnerability_assessment.py | 18 +++++++++++++++++- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/cbc_sdk/base.py b/src/cbc_sdk/base.py index c26787dc..47ff45a3 100644 --- a/src/cbc_sdk/base.py +++ b/src/cbc_sdk/base.py @@ -147,7 +147,7 @@ def __new__(mcs, name, bases, clsdict): setattr(cls, field_name, FieldDescriptor(field_name)) for fk_name, fk_info in iter(foreign_keys.items()): - setattr(cls, fk_name, ForeignKeyFieldDescriptor(fk_name, fk_info[0], fk_info[1])) + setattr(cls, fk_name, ForeignKeyFieldDescriptor(fk_name, fk_info[0], fk_info[1])) # pragma: no cover return cls @@ -1067,7 +1067,7 @@ def one(self): label = str(self._query) if self._query else "" if len(res) == 0: raise ObjectNotFoundError("query_uri", message="0 results for query {0:s}".format(label)) - if len(res) > 1: + if len(res) > 1: # pragma: no cover raise MoreThanOneResultError( message="{0:d} results found for query {1:s}".format(len(self), label), results=self.all() @@ -1111,7 +1111,7 @@ def __getitem__(self, item): elif isinstance(item, int): results = list(self._perform_query(from_row=item, max_rows=1)) return results[0] - else: + else: # pragma: no cover raise TypeError("Invalid argument type") def __iter__(self): diff --git a/src/tests/unit/platform/test_vulnerability_assessment.py b/src/tests/unit/platform/test_vulnerability_assessment.py index 2d901877..5030ccd1 100644 --- a/src/tests/unit/platform/test_vulnerability_assessment.py +++ b/src/tests/unit/platform/test_vulnerability_assessment.py @@ -93,8 +93,11 @@ def test_get_vulnerability_summary_per_severity_fail(cbcsdk_mock): cbcsdk_mock.mock_request("GET", "/vulnerability/assessment/api/v1/orgs/test/vulnerabilities/summary", GET_VULNERABILITY_SUMMARY_ORG_LEVEL_PER_SEVERITY) api = cbcsdk_mock.api + query = api.select(Vulnerability.OrgSummary) with pytest.raises(ApiError): - api.select(Vulnerability.OrgSummary).set_severity('ERROR') + query.set_severity('ERROR') + with pytest.raises(ApiError): + query.set_visibility('BOGUS') def test_get_vulnerability_summary_per_severity_per_vcenter(cbcsdk_mock): @@ -176,6 +179,13 @@ def test_get_all_vulnerabilities(cbcsdk_mock): assert query._count() == len(results) +def test_vulnerability_query_visibility_fail(cb): + """Test that setting visibility on vulnerability query to a bogus value returns an error.""" + query = cb.select(Vulnerability) + with pytest.raises(ApiError): + query.set_visibility('BOGUS') + + def test_export_vulnerabilities(cbcsdk_mock): """Test Export Vulnerabilities""" cbcsdk_mock.mock_request("POST", @@ -295,6 +305,7 @@ def post_validate(url, body, **kwargs): assert crits['highest_risk_score'] == {"value": 10, "operator": "LESS_THAN"} assert crits['last_sync_ts'] == {"value": "2020-01-02T03:04:05Z", "operator": "EQUALS"} assert crits['name'] == {"value": "test", "operator": "EQUALS"} + assert crits['deployment_type'] == {"value": "ENDPOINT", "operator": "EQUALS"} assert crits['os_arch'] == {"value": "x86_64", "operator": "EQUALS"} assert crits['os_name'] == {"value": "Red Hat Enterprise Linux Server", "operator": "EQUALS"} assert crits['os_type'] == {"value": "MAC", "operator": "EQUALS"} @@ -315,6 +326,7 @@ def post_validate(url, body, **kwargs): .set_highest_risk_score(10, 'LESS_THAN') \ .set_last_sync_ts('2020-01-02T03:04:05Z', 'EQUALS') \ .set_name('test', 'EQUALS') \ + .set_deployment_type('ENDPOINT', 'EQUALS') \ .set_os_arch('x86_64', 'EQUALS') \ .set_os_name('Red Hat Enterprise Linux Server', 'EQUALS') \ .set_os_type('MAC', 'EQUALS') \ @@ -357,6 +369,10 @@ def post_validate(url, body, **kwargs): api.select(Vulnerability).set_name('', 'EQUALS') assert ex.value.message == 'Invalid name' + with pytest.raises(ApiError) as ex: + api.select(Vulnerability).set_deployment_type('', 'EQUALS') + assert ex.value.message == 'Invalid deployment type' + with pytest.raises(ApiError) as ex: api.select(Vulnerability).set_os_arch('', 'EQUALS') assert ex.value.message == 'Invalid os architecture' From 1359cc21e1854a6b30b87346de1d7e9618d0c920 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Thu, 4 Apr 2024 15:00:54 -0600 Subject: [PATCH 27/95] initial draft of new script --- examples/platform/identify_silent_devices.py | 57 ++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 examples/platform/identify_silent_devices.py diff --git a/examples/platform/identify_silent_devices.py b/examples/platform/identify_silent_devices.py new file mode 100644 index 00000000..3643101f --- /dev/null +++ b/examples/platform/identify_silent_devices.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python +# ******************************************************* +# Copyright (c) VMware, Inc. 2020-2024. All Rights Reserved. +# SPDX-License-Identifier: MIT +# ******************************************************* +# * +# * DISCLAIMER. THIS PROGRAM IS PROVIDED TO YOU "AS IS" WITHOUT +# * WARRANTIES OR CONDITIONS OF ANY KIND, WHETHER ORAL OR WRITTEN, +# * EXPRESS OR IMPLIED. THE AUTHOR SPECIFICALLY DISCLAIMS ANY IMPLIED +# * WARRANTIES OR CONDITIONS OF MERCHANTABILITY, SATISFACTORY QUALITY, +# * NON-INFRINGEMENT AND FITNESS FOR A PARTICULAR PURPOSE. + +""" +This script identifies silent devices in the current organization. A "silent device" is one which has checked in +with the Carbon Black Cloud Server during a recent period of time, the "checkin window," but has not sent any +events within a certain period of time, the "event threshold." The script allows configuration of the checkin window +(in days) and the event threshold (in minutes), as well as specifying to only report on devices running selected +operating systems. +""" + +import sys +from cbc_sdk.helpers import build_cli_parser, get_cb_cloud_object +from cbc_sdk.platform import Device +from dateutil import parser + + +def main(): + """Main function for the script.""" + cmdparser = build_cli_parser("Identify silent devices") + cmdparser.add_argument("-w", "--window", type=int, default=1, + help="The checkin window for devices (specified in days, default 1)") + cmdparser.add_argument("-t", "--threshold", type=int, default=60, + help="The event threshold for devices (specified in minutes, default 60)") + cmdparser.add_argument("-o", "--os", action='append', nargs='+', + help="Restrict query to these operating systems (multiple values permitted)") + + args = cmdparser.parse_args() + cb = get_cb_cloud_object(args) + + device_query = cb.select(Device).set_last_contact_time(range=f"-{args.window}d").set_status(["ACTIVE"]) + if args.os: + device_query.add_criteria("os", args.os) + devices = list(device_query) + print(f"{len(devices)} device(s) have checked in during the last {args.window} day(s)") + + for device in devices: + delta = parser.parse(device.last_contact_time) - parser.parse(device.last_reported_time) + delta_minutes = round(delta.total_seconds() / 60) + if delta_minutes >= args.threshold: + print(f"Device {device.name} (ID={device.id}, OS={device.os}) " + f"last checked in = '{device.last_contact_time}', last reported data = '{device.last_reported_time}, " + f"delta = {delta_minutes} minutes") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From 9a93f3e561d85e2261d5450f8e14deeed84e2c66 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Mon, 8 Apr 2024 16:28:25 -0600 Subject: [PATCH 28/95] minor correction to OS parameter handling --- examples/platform/identify_silent_devices.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/platform/identify_silent_devices.py b/examples/platform/identify_silent_devices.py index 3643101f..56bfd422 100644 --- a/examples/platform/identify_silent_devices.py +++ b/examples/platform/identify_silent_devices.py @@ -39,7 +39,8 @@ def main(): device_query = cb.select(Device).set_last_contact_time(range=f"-{args.window}d").set_status(["ACTIVE"]) if args.os: - device_query.add_criteria("os", args.os) + for sublist in args.os: + device_query.add_criteria("os", sublist) devices = list(device_query) print(f"{len(devices)} device(s) have checked in during the last {args.window} day(s)") From 2c3c56c02a608c910a282d0503c36dd57bcadb6c Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Mon, 8 Apr 2024 16:31:36 -0600 Subject: [PATCH 29/95] deflake8'd --- examples/platform/identify_silent_devices.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/examples/platform/identify_silent_devices.py b/examples/platform/identify_silent_devices.py index 56bfd422..84ee8821 100644 --- a/examples/platform/identify_silent_devices.py +++ b/examples/platform/identify_silent_devices.py @@ -11,11 +11,12 @@ # * NON-INFRINGEMENT AND FITNESS FOR A PARTICULAR PURPOSE. """ -This script identifies silent devices in the current organization. A "silent device" is one which has checked in -with the Carbon Black Cloud Server during a recent period of time, the "checkin window," but has not sent any -events within a certain period of time, the "event threshold." The script allows configuration of the checkin window -(in days) and the event threshold (in minutes), as well as specifying to only report on devices running selected -operating systems. +This script identifies silent devices in the current organization. + +A "silent device" is one which has checked in with the Carbon Black Cloud Server during a recent period of time, +the "checkin window," but has not sent any events within a certain period of time, the "event threshold." The script +allows configuration of the checkin window (in days) and the event threshold (in minutes), as well as specifying to +only report on devices running selected operating systems. """ import sys From 73ef3741f91cb3dfec48c01c8517c93e68e5e081 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Thu, 28 Mar 2024 12:17:59 -0600 Subject: [PATCH 30/95] removed parameter display from docstrings of alert classes --- src/cbc_sdk/base.py | 40 ++++++++++--------- src/cbc_sdk/platform/models/alert.yaml | 2 + .../platform/models/alert_cb_analytic.yaml | 2 + .../models/alert_container_runtime.yaml | 2 + .../platform/models/alert_device_control.yaml | 2 + .../models/alert_host_based_firewall.yaml | 2 + .../alert_intrusion_detection_system.yaml | 2 + .../platform/models/alert_watchlist.yaml | 2 + 8 files changed, 36 insertions(+), 18 deletions(-) diff --git a/src/cbc_sdk/base.py b/src/cbc_sdk/base.py index 47ff45a3..4d5dc768 100644 --- a/src/cbc_sdk/base.py +++ b/src/cbc_sdk/base.py @@ -89,24 +89,28 @@ def __new__(mcs, name, bases, clsdict): model_data = yaml.load( open(os.path.join(mcs.model_base_directory, swagger_meta_file), 'rb').read(), SwaggerLoader) - # clsdict["__doc__"] = "Represents a %s object in the Carbon Black server.\n\n" % (name,) - # for field_name, field_info in iter(model_data.get("properties", {}).items()): - # docstring = field_info.get("description", None) - # if docstring: - # clsdict["__doc__"] += ":ivar %s: %s\n" % (field_name, docstring) - - class_docstr = clsdict.get('__doc__', None) - if not class_docstr: - class_docstr = f"Represents a {name} object in the Carbon Black Cloud." # pragma: no cover - need_header = True - for field_name, field_info in iter(model_data.get("properties", {}).items()): - docstring = field_info.get("description", None) - if docstring: - if need_header: - class_docstr += "\n\nParameters:" - need_header = False - class_docstr += f"\n {field_name}: {docstring}" - clsdict['__doc__'] = class_docstr + options = model_data.get('x-options', []) + + if 'nodocstring' not in options: + + # clsdict["__doc__"] = "Represents a %s object in the Carbon Black server.\n\n" % (name,) + # for field_name, field_info in iter(model_data.get("properties", {}).items()): + # docstring = field_info.get("description", None) + # if docstring: + # clsdict["__doc__"] += ":ivar %s: %s\n" % (field_name, docstring) + + class_docstr = clsdict.get('__doc__', None) + if not class_docstr: + class_docstr = f"Represents a {name} object in the Carbon Black Cloud." # pragma: no cover + need_header = True + for field_name, field_info in iter(model_data.get("properties", {}).items()): + docstring = field_info.get("description", None) + if docstring: + if need_header: + class_docstr += "\n\nParameters:" + need_header = False + class_docstr += f"\n {field_name}: {docstring}" + clsdict['__doc__'] = class_docstr foreign_keys = clsdict.pop("foreign_keys", {}) diff --git a/src/cbc_sdk/platform/models/alert.yaml b/src/cbc_sdk/platform/models/alert.yaml index 619ca1c9..509f8cf4 100644 --- a/src/cbc_sdk/platform/models/alert.yaml +++ b/src/cbc_sdk/platform/models/alert.yaml @@ -1,4 +1,6 @@ type: object +x-options: + - nodocstring properties: additional_events_present: type: boolean diff --git a/src/cbc_sdk/platform/models/alert_cb_analytic.yaml b/src/cbc_sdk/platform/models/alert_cb_analytic.yaml index a054c63e..41d9a9e6 100644 --- a/src/cbc_sdk/platform/models/alert_cb_analytic.yaml +++ b/src/cbc_sdk/platform/models/alert_cb_analytic.yaml @@ -1,4 +1,6 @@ type: object +x-options: + - nodocstring properties: additional_events_present: type: boolean diff --git a/src/cbc_sdk/platform/models/alert_container_runtime.yaml b/src/cbc_sdk/platform/models/alert_container_runtime.yaml index 054d9497..e3161ebc 100644 --- a/src/cbc_sdk/platform/models/alert_container_runtime.yaml +++ b/src/cbc_sdk/platform/models/alert_container_runtime.yaml @@ -1,4 +1,6 @@ type: object +x-options: + - nodocstring properties: alert_notes_present: type: boolean diff --git a/src/cbc_sdk/platform/models/alert_device_control.yaml b/src/cbc_sdk/platform/models/alert_device_control.yaml index c9bad81a..ea888dae 100644 --- a/src/cbc_sdk/platform/models/alert_device_control.yaml +++ b/src/cbc_sdk/platform/models/alert_device_control.yaml @@ -1,4 +1,6 @@ type: object +x-options: + - nodocstring properties: alert_notes_present: type: boolean diff --git a/src/cbc_sdk/platform/models/alert_host_based_firewall.yaml b/src/cbc_sdk/platform/models/alert_host_based_firewall.yaml index 4ccba400..5fb5cabb 100644 --- a/src/cbc_sdk/platform/models/alert_host_based_firewall.yaml +++ b/src/cbc_sdk/platform/models/alert_host_based_firewall.yaml @@ -1,4 +1,6 @@ type: object +x-options: + - nodocstring properties: additional_events_present: type: boolean diff --git a/src/cbc_sdk/platform/models/alert_intrusion_detection_system.yaml b/src/cbc_sdk/platform/models/alert_intrusion_detection_system.yaml index c10c1ce3..02c1e97a 100644 --- a/src/cbc_sdk/platform/models/alert_intrusion_detection_system.yaml +++ b/src/cbc_sdk/platform/models/alert_intrusion_detection_system.yaml @@ -1,4 +1,6 @@ type: object +x-options: + - nodocstring properties: additional_events_present: type: boolean diff --git a/src/cbc_sdk/platform/models/alert_watchlist.yaml b/src/cbc_sdk/platform/models/alert_watchlist.yaml index 3951694f..e2d9eb21 100644 --- a/src/cbc_sdk/platform/models/alert_watchlist.yaml +++ b/src/cbc_sdk/platform/models/alert_watchlist.yaml @@ -1,4 +1,6 @@ type: object +x-options: + - nodocstring properties: additional_events_present: type: boolean From 51c3421c3e05394f262368c5f29a0158b6986072 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Mon, 1 Apr 2024 16:43:38 -0600 Subject: [PATCH 31/95] beginning a pass through Alert docstrings --- src/cbc_sdk/platform/alerts.py | 95 ++++++++++++++++++++-------- src/cbc_sdk/platform/observations.py | 3 + 2 files changed, 70 insertions(+), 28 deletions(-) diff --git a/src/cbc_sdk/platform/alerts.py b/src/cbc_sdk/platform/alerts.py index da474b88..1abe0641 100644 --- a/src/cbc_sdk/platform/alerts.py +++ b/src/cbc_sdk/platform/alerts.py @@ -11,7 +11,27 @@ # * WARRANTIES OR CONDITIONS OF MERCHANTABILITY, SATISFACTORY QUALITY, # * NON-INFRINGEMENT AND FITNESS FOR A PARTICULAR PURPOSE. -"""Model and Query Classes for Platform Alerts and Workflows""" +""" +The model and query classes for supporting alerts and alert workflows. + +*Alerts* indicate suspicious behavior and known threats in the monitored environment. They should be regularly +reviewed to determine whether action must be taken or policies should be modified. The Carbon Black Cloud Python +SDK may be used to retrieve alerts, as well as manage the workflow by modifying alert status or closing alerts. + +The Carbon Black Cloud Python SDK currently implements the Alerts v7 API, as documented on +`the Developer Network `_. +It works with any Carbon Black Cloud product, although certain alert types are only generated by specific products. + +Typical usage example:: + + # assume "cb" is an instance of CBCloudAPI + query = cb.select(Alert).add_criteria("device_os", ["WINDOWS"]).set_minimum_severity(3) + query.set_time_range(range="-1d").set_rows(1000).add_exclusions("type", ["WATCHLIST"]) + for alert in query: + print(f"Alert ID {alert.id} with severity {alert.severity} at {alert.detection_timestamp}") + +""" + import time import datetime @@ -40,7 +60,12 @@ class Alert(PlatformModel): - """Represents a basic alert.""" + """ + Represents a basic alert within the Carbon Black Cloud. + + ``Alert`` objects are typically located through a search (using ``AlertSearchQuery``) before they can be + operated on. + """ REMAPPED_ALERTS_V6_TO_V7 = { "alert_classification.user_feedback": "determination_value", "cluster_name": "k8s_cluster", @@ -196,11 +221,18 @@ def get_process(self, async_mode=False): """ Gets the process corresponding with the alert. + Required Permissions: + org.search.events (CREATE. READ) + Args: - async_mode: True to request process in an asynchronous manner. + async_mode: ``True`` to request process in an asynchronous manner. Returns: Process: The process corresponding to the alert. + + Note: + - When using asynchronous mode, this method returns a Python ``Future``. + You can call ``result()`` on the ``Future`` object to wait for completion and get the results. """ process_guid = self._info.get("process_guid") if not process_guid: @@ -211,10 +243,13 @@ def get_process(self, async_mode=False): def _get_process(self, *args, **kwargs): """ - Implementation of the get_process. + Implementation of the ``get_process`` call. + + Required Permissions: + org.search.events (CREATE. READ) Returns: - Process: The process corresponding to the alert. May return None if no process is found. + Process: The process corresponding to the alert. May return ``None`` if no process is found. """ process_guid = self._info.get("process_guid") try: @@ -224,16 +259,16 @@ def _get_process(self, *args, **kwargs): return process def get_observations(self, timeout=0): - """Requests observations that are associated with the Alert. + """ + Requests observations that are associated with the ``Alert``. - Uses Observations bulk get details. + Uses ``Observation.bulk_get_details``. - Returns: - list: Observations associated with the alert + Required Permissions: + org.search.events (READ, CREATE) - Note: - - When using asynchronous mode, this method returns a python future. - You can call result() on the future object to wait for completion and get the results. + Returns: + list[Observation]: Observations associated with the ``Alert``. """ alert_id = self.get("id") if not alert_id: @@ -244,14 +279,16 @@ def get_observations(self, timeout=0): def get_history(self, threat=False): """ - Get the actions taken on an Alert such as Notes added and workflow state changes. + Get the actions taken on an ``Alert`` such as ``Note``s added and workflow state changes. + + Required Permissions: + org.alerts (READ) Args: - threat (bool): Whether to return the Alert or Threat history + threat (bool): If ``True``, the threat history is returned; if ``False``, the alert history is returned. Returns: - list: The dicts of each determination, note or workflow change - + list: The ``dict``s of each determination, note or workflow change. """ if threat: url = Alert.threat_urlobject_single.format(self._cb.credentials.org_key, self.threat_id) @@ -264,13 +301,13 @@ def get_history(self, threat=False): def get_threat_tags(self): """ - Gets the threat's tags + Gets the threat's tags. Required Permissions: org.alerts.tags (READ) Returns: - (list[str]): The list of current tags + list[str]: The list of current tags """ url = Alert.threat_urlobject_single.format(self._cb.credentials.org_key, self.threat_id) url = f"{url}/tags" @@ -279,19 +316,19 @@ def get_threat_tags(self): def add_threat_tags(self, tags): """ - Adds tags to the threat + Adds tags to the threat. Required Permissions: org.alerts.tags (CREATE) Args: - tags (list[str]): List of tags to add to the threat + tags (list[str]): List of tags to add to the threat. Raises: - ApiError: If tags is not a list of strings + ApiError: If ``tags`` is not a list of strings. Returns: - (list[str]): The list of current tags + list[str]: The list of current tags. """ if not isinstance(tags, list) or not isinstance(tags[0], str): raise ApiError("Tags must be a list of strings") @@ -304,16 +341,16 @@ def add_threat_tags(self, tags): def delete_threat_tag(self, tag): """ - Delete a threat tag + Delete a threat tag. Required Permissions: org.alerts.tags (DELETE) Args: - tag (str): The tag to delete + tag (str): The tag to delete. Returns: - (list[str]): The list of current tags + (list[str]): The list of current tags. """ url = Alert.threat_urlobject_single.format(self._cb.credentials.org_key, self.threat_id) url = f"{url}/tags/{tag}" @@ -322,7 +359,9 @@ def delete_threat_tag(self, tag): return resp_json.get("tags", []) class Note(PlatformModel): - """Represents a note within an alert.""" + """ + Represents a note placed on an alert. + """ REMAPPED_NOTES_V6_TO_V7 = { "create_time": "create_timestamp", } @@ -339,13 +378,13 @@ class Note(PlatformModel): def __init__(self, cb, alert, model_unique_id, threat_note=False, initial_data=None): """ - Initialize the Note object. + Initialize the ``Note`` object. Args: cb (BaseAPI): Reference to API object used to communicate with the server. alert (Alert): The alert where the note is saved. model_unique_id (str): ID of the note represented. - threat_note (bool): Whether the note is an Alert or Threat note + threat_note (bool): ``True`` if the note is a threat note, ``False`` if the note is an alert note.`` initial_data (dict): Initial data used to populate the note. """ super(Alert.Note, self).__init__(cb, model_unique_id, initial_data) diff --git a/src/cbc_sdk/platform/observations.py b/src/cbc_sdk/platform/observations.py index 7e455f86..38143e16 100644 --- a/src/cbc_sdk/platform/observations.py +++ b/src/cbc_sdk/platform/observations.py @@ -269,6 +269,9 @@ def search_suggestions(cb, query, count=None): def bulk_get_details(cb, alert_id=None, observation_ids=None, timeout=0): """Bulk get details + Required Permissions: + org.search.events (READ, CREATE) + Args: cb (CBCloudAPI): A reference to the CBCloudAPI object. alert_id (str): An alert id to fetch associated observations From daee3dc1e4dcb4b0e37ca289505a48c7aa750512 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Tue, 2 Apr 2024 16:55:20 -0600 Subject: [PATCH 32/95] finished rework of docstrings prior to adding new links --- src/cbc_sdk/platform/alerts.py | 220 +++++++++++++++++++++++---------- src/cbc_sdk/platform/jobs.py | 33 ++--- 2 files changed, 173 insertions(+), 80 deletions(-) diff --git a/src/cbc_sdk/platform/alerts.py b/src/cbc_sdk/platform/alerts.py index 1abe0641..53d24894 100644 --- a/src/cbc_sdk/platform/alerts.py +++ b/src/cbc_sdk/platform/alerts.py @@ -397,8 +397,11 @@ def _refresh(self): """ Rereads the alert data from the server. + Required Permissions: + org.alerts.notes (READ) + Returns: - bool: True if refresh was successful, False if not. + bool: ``True`` if refresh was successful, ``False`` if not. """ _exists_in_list = False if self._is_deleted: @@ -439,7 +442,12 @@ def _query_implementation(cls, cb, **kwargs): raise NonQueryableModel("Notes cannot be queried directly") def delete(self): - """Deletes a note from an alert.""" + """ + Deletes a note from an alert. + + Required Permissions: + org.alerts.notes (DELETE) + """ if self._threat_note: url = self.threat_urlobject.format(self._cb.credentials.org_key, self._alert.threat_id) else: @@ -492,10 +500,16 @@ def __getattr__(self, item): def notes_(self, threat_note=False): """ - Retrieves all notes for an alert. + Retrieves all notes for this alert. + + Required Permissions: + org.alerts.notes (READ) Args: - threat_note (bool): Whether to return the Alert notes or Threat notes + threat_note (bool): ``True`` to retrieve threat notes, ``False`` to retrieve alert notes. + + Returns: + list[Note]: The list of notes for the alert. """ if threat_note: url = Alert.Note.threat_urlobject.format(self._cb.credentials.org_key, self.threat_id) @@ -509,11 +523,17 @@ def notes_(self, threat_note=False): def create_note(self, note, threat_note=False): """ - Creates a new note. + Creates a new note for this alert. + + Required Permissions: + org.alerts.notes (CREATE) Args: - note (str): Note content to add - threat_note (bool): Whether to add the note to the Alert or Threat + note (str): Note content to add. + threat_note (bool): ``True`` to add this alert to the treat, ``False`` to add this note to the alert. + + Returns: + Note: The newly-added note. """ request = {"note": note} if threat_note: @@ -542,8 +562,11 @@ def _refresh(self): """ Rereads the alert data from the server. + Required Permissions: + org.alerts (READ) + Returns: - bool: True if refresh was successful, False if not. + bool: ``True`` if refresh was successful, ``False`` if not. """ url = self.urlobject_single.format(self._cb.credentials.org_key, self._model_unique_id) resp = self._cb.get_object(url) @@ -566,10 +589,11 @@ def deobfuscate_cmdline(self): Deobfuscates the command line of the process pointed to by the alert and returns the deobfuscated result. Required Permissions: - script.deobfuscation(EXECUTE) + script.deobfuscation (EXECUTE) Returns: - dict: A dict containing information about the obfuscated command line, including the deobfuscated result. + dict: A ``dict`` containing information about the obfuscated command line, including the + deobfuscated result. """ body = {"input": self.process_cmdline} result = self._cb.post_object(f"/tau/v2/orgs/{self._cb.credentials.org_key}/reveal", body) @@ -579,6 +603,14 @@ def close(self, closure_reason=None, determination=None, note=None): """ Closes this alert. + Note: + - This is an asynchronous call that returns a ``Job``. If you want to wait and block on the results + you can call ``await_completion()`` to get a ``Future`` then ``result()`` on the ``future`` object + to wait for completion and get the results. + + Required Permissions: + org.alerts.close (EXECUTE), jobs.status (READ) + Args: closure_reason (str): the closure reason for this alert, either "NO_REASON", "RESOLVED", \ "RESOLVED_BENIGN_KNOWN_GOOD", "DUPLICATE_CLEANUP", "OTHER" @@ -586,19 +618,14 @@ def close(self, closure_reason=None, determination=None, note=None): "FALSE_POSITIVE", or "NONE" note (str): The comment to set for the alert. - Note: - - This is an asynchronus call that returns a Job. If you want to wait and block on the results - you can call await_completion() to get a Futre then result() on the future object to wait for - completion and get the results. + Returns: + Job: The ``Job`` object for the alert workflow action. Example: >>> alert = cb.select(Alert, "708d7dbf-2020-42d4-9cbc-0cddd0ffa31a") >>> job = alert.close("RESOLVED", "FALSE_POSITIVE", "Normal behavior") >>> completed_job = job.await_completion().result() >>> alert.refresh() - - Returns: - Job: The Job object for the alert workflow action. """ job = self._cb.select(Alert).add_criteria("id", [self.get("id")]) \ ._update_status("CLOSED", closure_reason, note, determination) @@ -610,27 +637,30 @@ def update(self, status, closure_reason=None, determination=None, note=None): """ Update the Alert with optional closure_reason, determination, note, or status. + Note: + - This is an asynchronous call that returns a ``Job``. If you want to wait and block on the results + you can call ``await_completion()`` to get a ``Future`` then ``result()`` on the ``future`` object + to wait for completion and get the results. + + Required Permissions: + org.alerts.close (EXECUTE), jobs.status (READ) + Args: status (str): The status to set for this alert, either "OPEN", "IN_PROGRESS", or "CLOSED". - closure_reason (str): the closure reason for this alert, either "NO_REASON", "RESOLVED", \ - "RESOLVED_BENIGN_KNOWN_GOOD", "DUPLICATE_CLEANUP", "OTHER" - determination (str): The determination status to set for the alert, either "TRUE_POSITIVE", \ - "FALSE_POSITIVE", or "NONE" + closure_reason (str): the closure reason for this alert, either "NO_REASON", "RESOLVED", + "RESOLVED_BENIGN_KNOWN_GOOD", "DUPLICATE_CLEANUP", "OTHER" + determination (str): The determination status to set for the alert, either "TRUE_POSITIVE", + "FALSE_POSITIVE", or "NONE" note (str): The comment to set for the alert. - Note: - - This is an asynchronus call that returns a Job. If you want to wait and block on the results - you can call await_completion() to get a Futre then result() on the future object to wait for - completion and get the results. + Returns: + Job: The ``Job`` object for the alert workflow action. Example: >>> alert = cb.select(Alert, "708d7dbf-2020-42d4-9cbc-0cddd0ffa31a") >>> job = alert.update("IN_PROGESS", "NO_REASON", "NONE", "Starting Investigation") >>> completed_job = job.await_completion().result() >>> alert.refresh() - - Returns: - Job: The Job object for the alert workflow action. """ job = self._cb.select(Alert).add_criteria("id", [self.get("id")]) \ ._update_status(status, closure_reason, note, determination) @@ -642,6 +672,9 @@ def _update_threat_workflow_status(self, state, remediation, comment): """ Updates the workflow status of all future alerts with the same threat ID. + Required Permissions: + org.alerts.dismiss (EXECUTE) + Args: state (str): The state to set for this alert, either "OPEN" or "DISMISSED". remediation (str): The remediation status to set for the alert. @@ -660,6 +693,9 @@ def dismiss_threat(self, remediation=None, comment=None): """ Dismisses all future alerts assigned to the threat_id. + Required Permissions: + org.alerts.dismiss (EXECUTE) + Args: remediation (str): The remediation status to set for the alert. comment (str): The comment to set for the alert. @@ -674,6 +710,9 @@ def update_threat(self, remediation=None, comment=None): """ Updates all future alerts assigned to the threat_id to the OPEN state. + Required Permissions: + org.alerts.dismiss (EXECUTE) + Args: remediation (str): The remediation status to set for the alert. comment (str): The comment to set for the alert. @@ -689,6 +728,9 @@ def search_suggestions(cb, query): """ Returns suggestions for keys and field values that can be used in a search. + Required Permissions: + org.alerts (READ) + Args: cb (CBCloudAPI): A reference to the CBCloudAPI object. query (str): A search query to use. @@ -827,7 +869,9 @@ def get(self, item, default_val=None): class WatchlistAlert(Alert): - """Represents watch list alerts.""" + """ + A specialization of the base ``Alert`` class that represents a watchlist alert. + """ urlobject = "/api/alerts/v7/orgs/{0}/alerts" type = ["WATCHLIST"] swagger_meta_file = "platform/models/alert_watchlist.yaml" @@ -865,7 +909,9 @@ def get_watchlist_objects(self): class CBAnalyticsAlert(Alert): - """Represents CB Analytics alerts.""" + """ + A specialization of the base ``Alert`` class that represents a CB Analytics alert. + """ urlobject = "/api/alerts/v7/orgs/{0}/alerts" type = ["CB_ANALYTICS"] swagger_meta_file = "platform/models/alert_cb_analytic.yaml" @@ -885,7 +931,8 @@ def _query_implementation(cls, cb, **kwargs): return AlertSearchQuery(cls, cb).add_criteria("type", ["CB_ANALYTICS"]) def get_events(self, timeout=0, async_mode=False): - """Removed in CBC SDK 1.5.0 because Enriched Events are deprecated. + """ + Removed in CBC SDK 1.5.0 because Enriched Events are deprecated. Previously requested enriched events detailed results. Update to use get_observations() instead. See `Developer Network Observations Migration @@ -911,7 +958,9 @@ def get_events(self, timeout=0, async_mode=False): class DeviceControlAlert(Alert): - """Represents Device Control alerts.""" + """ + A specialization of the base ``Alert`` class that represents a Device Control alert. + """ urlobject = "/api/alerts/v7/orgs/{0}/alerts" swagger_meta_file = "platform/models/alert_device_control.yaml" @@ -931,7 +980,9 @@ def _query_implementation(cls, cb, **kwargs): class ContainerRuntimeAlert(Alert): - """Represents Container Runtime alerts.""" + """ + A specialization of the base ``Alert`` class that represents a Container Runtime alert. + """ urlobject = "/api/alerts/v7/orgs/{0}/alerts" swagger_meta_file = "platform/models/alert_container_runtime.yaml" type = ["CONTAINER_RUNTIME"] @@ -952,7 +1003,9 @@ def _query_implementation(cls, cb, **kwargs): class HostBasedFirewallAlert(Alert): - """Represents Host Based Firewall alerts.""" + """ + A specialization of the base ``Alert`` class that represents a host-based firewall alert. + """ urlobject = "/api/alerts/v7/orgs/{0}/alerts" swagger_meta_file = "platform/models/alert_host_based_firewall.yaml" type = ["HOST_BASED_FIREWALL"] @@ -963,7 +1016,6 @@ def _query_implementation(cls, cb, **kwargs): Returns the appropriate query object for this alert type. Args: - cb (BaseAPI): Reference to API object used to communicate with the server. cb (BaseAPI): Reference to API object used to communicate with the server. **kwargs (dict): Not used, retained for compatibility. @@ -974,7 +1026,9 @@ def _query_implementation(cls, cb, **kwargs): class IntrusionDetectionSystemAlert(Alert): - """Represents Intrusion Detection System alerts.""" + """ + A specialization of the base ``Alert`` class that represents an intrusion detection system alert. + """ urlobject = "/api/alerts/v7/orgs/{0}/alerts" swagger_meta_file = "platform/models/alert_intrusion_detection_system.yaml" type = ["INTRUSION_DETECTION_SYSTEM"] @@ -995,7 +1049,7 @@ def _query_implementation(cls, cb, **kwargs): def get_network_threat_metadata(self): """ - The NetworkThreatMetadata associated with this IDS alert if it exists. + Retrun the ``NetworkThreatMetadata`` associated with this IDS alert if it exists. Example: >>> alert_threat_metadata = ids_alert.get_network_threat_metadata() @@ -1010,7 +1064,10 @@ def get_network_threat_metadata(self): class GroupedAlert(PlatformModel): - """Represents Grouped alerts.""" + """ + Represents alerts that have been grouped together based on a common characteristic, to allow viewing of similar + alerts across multiple endpoints. + """ urlobject = "/api/alerts/v7/orgs/{0}/grouped_alerts" swagger_meta_file = "platform/models/grouped_alert.yaml" @@ -1101,13 +1158,18 @@ def get_alerts(self): class AlertSearchQuery(BaseQuery, QueryBuilderSupportMixin, IterableQueryMixin, LegacyAlertSearchQueryCriterionMixin, CriteriaBuilderSupportMixin, ExclusionBuilderSupportMixin): - """Represents a query that is used to locate Alert objects.""" + """ + Query object that is used to locate ``Alert`` objects. + + The ``AlertSearchQuery`` is constructed via SDK functions like the ``select()`` method on ``CBCloudAPI``. + The user would then add a query and/or criteria to it before iterating over the results. + """ DEPRECATED_FACET_FIELDS = ["ALERT_TYPE", "CATEGORY", "REPUTATION", "WORKFLOW", "TAG", "POLICY_ID", "POLICY_NAME", "APPLICATION_HASH", "APPLICATION_NAME", "STATUS", "POLICY_APPLIED_STATE"] def __init__(self, doc_class, cb): """ - Initialize the AlertSearchQuery. + Initialize the ``AlertSearchQuery``. Args: doc_class (class): The model class that will be returned by this query. @@ -1289,9 +1351,9 @@ def _create_valid_time_filter(self, kwargs): Args: kwargs (dict): Used to specify start= for start time, end= for end time, and range= for range. Values are - either timestamp ISO 8601 strings or datetime objects for start and end time. For range the time range to - execute the result search, ending on the current time. Should be in the form "-2w", - where y=year, w=week, d=day, h=hour, m=minute, s=second. + either timestamp ISO 8601 strings or datetime objects for start and end time. For range the time + range to execute the result search, ending on the current time. Should be in the form "-2w", + where y=year, w=week, d=day, h=hour, m=minute, s=second. Returns: filter object to be applied to the global time range or a specific field @@ -1415,6 +1477,9 @@ def _count(self): """ Returns the number of results from the run of this query. + Required Permissions: + org.alerts (READ) + Returns: int: The number of results from the run of this query. """ @@ -1437,6 +1502,9 @@ def _perform_query(self, from_row=0, max_rows=-1): Alerts v6 API uses base 1 instead of 0. + Required Permissions: + org.alerts (READ) + Args: from_row (int): The row to start the query at (default 0). max_rows (int): The maximum number of rows to be returned (default -1, meaning "all"). @@ -1497,6 +1565,9 @@ def facets(self, fieldlist, max_rows=0): """ Return information about the facets for this alert by search, using the defined criteria. + Required Permissions: + org.alerts (READ) + Args: fieldlist (list): List of facet field names. max_rows (int): The maximum number of rows to return. 0 means return all rows. @@ -1529,15 +1600,18 @@ def _update_status(self, status, closure_reason, note, determination): """ Updates the status of all alerts matching the given query. + Required Permissions: + org.alerts.close (EXECUTE), jobs.status (READ) + Args: status (str): The status to set for this alert, either "OPEN", "IN_PROGRESS", or "CLOSED". closure_reason (str): the closure reason for this alert, either "TRUE_POSITIVE", "FALSE_POSITIVE", or "NONE" note (str): The comment to set for the alert. - determination (str): The determination status to set for the alert, either "NO_REASON", "RESOLVED", \ - "RESOLVED_BENIGN_KNOWN_GOOD", "DUPLICATE_CLEANUP", "OTHER" + determination (str): The determination status to set for the alert, either "NO_REASON", "RESOLVED", + "RESOLVED_BENIGN_KNOWN_GOOD", "DUPLICATE_CLEANUP", "OTHER" Returns: - Job: The Job object for the bulk workflow action. + Job: The ``Job`` object for the bulk workflow action. """ request = self._build_request(0, -1) del request["rows"] @@ -1558,6 +1632,9 @@ def update(self, status, closure_reason=None, determination=None, note=None): """ Update all alerts matching the given query. + Required Permissions: + org.alerts.close (EXECUTE), jobs.status (READ) + Args: status (str): The status to set for this alert, either "OPEN", "IN_PROGRESS", or "CLOSED". closure_reason (str): the closure reason for this alert, either "NO_REASON", "RESOLVED", \ @@ -1567,12 +1644,12 @@ def update(self, status, closure_reason=None, determination=None, note=None): note (str): The comment to set for the alert. Returns: - Job: The Job object for the bulk workflow action. + Job: The ``Job`` object for the bulk workflow action. Note: - - This is an asynchronus call that returns a Job. If you want to wait and block on the results - you can call await_completion() to get a Futre then result() on the future object to wait for - completion and get the results. + - This is an asynchronous call that returns a ``Job``. If you want to wait and block on the results + you can call ``await_completion()`` to get a ``Future`` then ``result()`` on the ``Future`` object + to wait for completion and get the results. Example: >>> alert_query = cb.select(Alert).add_criteria("threat_id", ["19261158DBBF00775959F8AA7F7551A1"]) @@ -1585,6 +1662,9 @@ def close(self, closure_reason=None, determination=None, note=None, ): """ Close all alerts matching the given query. The alerts will be left in a CLOSED state after this request. + Required Permissions: + org.alerts.close (EXECUTE), jobs.status (READ) + Args: closure_reason (str): the closure reason for this alert, either "NO_REASON", "RESOLVED", \ "RESOLVED_BENIGN_KNOWN_GOOD", "DUPLICATE_CLEANUP", "OTHER" @@ -1593,12 +1673,12 @@ def close(self, closure_reason=None, determination=None, note=None, ): note (str): The comment to set for the alert. Returns: - Job: The Job object for the bulk workflow action. + Job: The ``Job`` object for the bulk workflow action. Note: - - This is an asynchronus call that returns a Job. If you want to wait and block on the results - you can call await_completion() to get a Futre then result() on the future object to wait for - completion and get the results. + - This is an asynchronous call that returns a ``Job``. If you want to wait and block on the results + you can call ``await_completion()`` to get a ``Future`` then ``result()`` on the ``Future`` object + to wait for completion and get the results. Example: >>> alert_query = cb.select(Alert).add_criteria("threat_id", ["19261158DBBF00775959F8AA7F7551A1"]) @@ -1674,13 +1754,13 @@ def set_remote_is_private(self, is_private, exclude=False): def set_group_by(self, field): """ - Converts the AlertSearchQuery to a GroupAlertSearchQuery grouped by the argument + Converts the ``AlertSearchQuery`` to a ``GroupAlertSearchQuery`` grouped by the argument. Args: - field (string): The field to group by, defaults to "threat_id" + field (string): The field to group by, defaults to "threat_id." Returns: - AlertSearchQuery + GroupedAlertSearchQuery: New query instance. Note: Does not preserve sort criterion @@ -1695,7 +1775,12 @@ def set_group_by(self, field): class GroupedAlertSearchQuery(AlertSearchQuery): - """Represents a query that is used to group Alert objects by a given field.""" + """ + Query object that is used to locate ``Alert`` objects. + + This query is constructed by using the ``select()`` method on ``CBCloudAPI`` to create an ``AlertSearchQuery,`` + then using that query's ``set_group_by()`` method to specify grouping. + """ def __init__(self, *args, **kwargs): """Initialize the GroupAlertSearchQuery.""" super().__init__(*args, **kwargs) @@ -1730,11 +1815,13 @@ def _build_request(self, from_row, max_rows, add_sort=True): def get_alert_search_query(self): """ - Converts the GroupedAlertSearchQuery into a nongrouped AlertSearchQuery + Converts the ``GroupedAlertSearchQuery`` into a nongrouped ``AlertSearchQuery``. - Returns: AlertSearchQuery + Returns: + AlertSearchQuery: New query instance. - Note: Does not preserve sort criterion + Note: + Does not preserve sort criterion. """ alert_search_query = self._cb.select(Alert) for key, value in vars(alert_search_query).items(): @@ -1747,6 +1834,9 @@ def _perform_query(self, from_row=0, max_rows=-1): """ Performs the query and returns the results of the query in an iterable fashion. + Required Permissions: + org.alerts (READ) + Args: from_row (int): The row to start the query at (default 0). max_rows (int): The maximum number of rows to be returned (default -1, meaning "all"). @@ -1821,15 +1911,17 @@ def facets(self, fieldlist, max_rows=0, filter_values=False): """ Return information about the facets for this alert by search, using the defined criteria. + Required Permissions: + org.alerts (READ) + Args: fieldlist (list): List of facet field names. max_rows (int): The maximum number of rows to return. 0 means return all rows. filter_values (boolean): A flag to indicate whether any filters on a term should be applied to facet - calculation. When false (default), a filter on the term is ignored while calculating facets + calculation. When ``False`` (default), a filter on the term is ignored while calculating facets. Returns: - list: A list of facet information specified as dicts. - error: invalid enum + list: A list of facet information specified as ``dict``s. Raises: FunctionalityDecommissioned: If the requested attribute is no longer available. diff --git a/src/cbc_sdk/platform/jobs.py b/src/cbc_sdk/platform/jobs.py index 79cd133b..515bb93e 100644 --- a/src/cbc_sdk/platform/jobs.py +++ b/src/cbc_sdk/platform/jobs.py @@ -41,10 +41,6 @@ def __init__(self, cb, model_unique_id, initial_data=None): cb (BaseAPI): Reference to API object used to communicate with the server. model_unique_id (int): ID of the job. initial_data (dict): Initial data used to populate the job. - - Keyword Args: - wait_status (bool): If ``True``, causes the job to wait on change in status instead of relying on the - progress API call (workaround for server issue). Default is ``False``. """ super(Job, self).__init__(cb, model_unique_id, initial_data) self._wait_status = False @@ -70,7 +66,12 @@ def _query_implementation(cls, cb, **kwargs): return JobQuery(cls, cb) def _refresh(self): - """Reload this object from the server.""" + """ + Reload this object from the server. + + Required Permissions: + jobs.status (READ) + """ url = self.urlobject_single.format(self._cb.credentials.org_key, self._model_unique_id) resp = self._cb.get_object(url) self._info = resp @@ -83,7 +84,7 @@ def get_progress(self): Get and return the current progress information for the job. Required Permissions: - jobs.status(READ) + jobs.status (READ) Returns: int: Total number of items to be operated on by this job. @@ -100,7 +101,7 @@ def _await_completion(self, timeout=0): Waits for this job to complete by examining the progress data. Required Permissions: - jobs.status(READ) + jobs.status (READ) Args: timeout (int): The timeout for this wait in milliseconds. If this is 0, the default value will be used. @@ -149,20 +150,20 @@ def _await_completion(self, timeout=0): def await_completion(self, timeout=0): """ - Create a Python Future to check for job completion and return results when available. + Create a Python ``Future`` to check for job completion and return results when available. - Returns a Future object which can be used to await results that are ready to fetch. This function call + Returns a ``Future`` object which can be used to await results that are ready to fetch. This function call does not block. Required Permissions: - jobs.status(READ) + jobs.status (READ) Args: timeout (int): The timeout for this wait in milliseconds. If this is 0, the default value will be used. Returns: - Future: A future which can be used to wait for this job's completion. When complete, the result of the - Future will be this object. + Future: A ``Future`` which can be used to wait for this job's completion. When complete, the result of the + ``Future`` will be this object. """ return self._cb._async_submit(lambda arg, kwarg: arg[0]._await_completion(timeout), self) @@ -171,7 +172,7 @@ def get_output_as_stream(self, output): Export the results from the job, writing the results to the given stream. Required Permissions: - jobs.status(READ) + jobs.status (READ) Args: output (RawIOBase): Stream to write the CSV data from the request to. @@ -184,7 +185,7 @@ def get_output_as_string(self): Export the results from the job, returning the results as a string. Required Permissions: - jobs.status(READ) + jobs.status (READ) Returns: str: The results from the job. @@ -198,7 +199,7 @@ def get_output_as_file(self, filename): Export the results from the job, writing the results to the given file. Required Permissions: - jobs.status(READ) + jobs.status (READ) Args: filename (str): Name of the file to write the results to. @@ -214,7 +215,7 @@ def get_output_as_lines(self): CSV. If a job outputs structured text like JSON or XML, this method should not be used. Required Permissions: - jobs.status(READ) + jobs.status (READ) Returns: iterable: An iterable that can be used to get each line of text in turn as a string. From 8cfd7f3241499bc4008677573579944b85dbad91 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Wed, 3 Apr 2024 16:49:05 -0600 Subject: [PATCH 33/95] included the Dev Network links --- src/cbc_sdk/platform/alerts.py | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/src/cbc_sdk/platform/alerts.py b/src/cbc_sdk/platform/alerts.py index 53d24894..92a98558 100644 --- a/src/cbc_sdk/platform/alerts.py +++ b/src/cbc_sdk/platform/alerts.py @@ -65,6 +65,10 @@ class Alert(PlatformModel): ``Alert`` objects are typically located through a search (using ``AlertSearchQuery``) before they can be operated on. + + The complete list of alert fields is too large to be reproduced here; please see the list of available fields + for each alert type on `the Developer Network + `_. """ REMAPPED_ALERTS_V6_TO_V7 = { "alert_classification.user_feedback": "determination_value", @@ -628,7 +632,7 @@ def close(self, closure_reason=None, determination=None, note=None): >>> alert.refresh() """ job = self._cb.select(Alert).add_criteria("id", [self.get("id")]) \ - ._update_status("CLOSED", closure_reason, note, determination) + ._update_status("CLOSED", closure_reason, note, determination) self._last_refresh_time = time.time() return job @@ -663,7 +667,7 @@ def update(self, status, closure_reason=None, determination=None, note=None): >>> alert.refresh() """ job = self._cb.select(Alert).add_criteria("id", [self.get("id")]) \ - ._update_status(status, closure_reason, note, determination) + ._update_status(status, closure_reason, note, determination) self._last_refresh_time = time.time() return job @@ -871,6 +875,10 @@ def get(self, item, default_val=None): class WatchlistAlert(Alert): """ A specialization of the base ``Alert`` class that represents a watchlist alert. + + The complete list of alert fields is too large to be reproduced here; please see the list of available fields + for each alert type on `the Developer Network + `_. """ urlobject = "/api/alerts/v7/orgs/{0}/alerts" type = ["WATCHLIST"] @@ -911,6 +919,10 @@ def get_watchlist_objects(self): class CBAnalyticsAlert(Alert): """ A specialization of the base ``Alert`` class that represents a CB Analytics alert. + + The complete list of alert fields is too large to be reproduced here; please see the list of available fields + for each alert type on `the Developer Network + `_. """ urlobject = "/api/alerts/v7/orgs/{0}/alerts" type = ["CB_ANALYTICS"] @@ -960,6 +972,10 @@ def get_events(self, timeout=0, async_mode=False): class DeviceControlAlert(Alert): """ A specialization of the base ``Alert`` class that represents a Device Control alert. + + The complete list of alert fields is too large to be reproduced here; please see the list of available fields + for each alert type on `the Developer Network + `_. """ urlobject = "/api/alerts/v7/orgs/{0}/alerts" swagger_meta_file = "platform/models/alert_device_control.yaml" @@ -982,6 +998,10 @@ def _query_implementation(cls, cb, **kwargs): class ContainerRuntimeAlert(Alert): """ A specialization of the base ``Alert`` class that represents a Container Runtime alert. + + The complete list of alert fields is too large to be reproduced here; please see the list of available fields + for each alert type on `the Developer Network + `_. """ urlobject = "/api/alerts/v7/orgs/{0}/alerts" swagger_meta_file = "platform/models/alert_container_runtime.yaml" @@ -1005,6 +1025,10 @@ def _query_implementation(cls, cb, **kwargs): class HostBasedFirewallAlert(Alert): """ A specialization of the base ``Alert`` class that represents a host-based firewall alert. + + The complete list of alert fields is too large to be reproduced here; please see the list of available fields + for each alert type on `the Developer Network + `_. """ urlobject = "/api/alerts/v7/orgs/{0}/alerts" swagger_meta_file = "platform/models/alert_host_based_firewall.yaml" @@ -1028,6 +1052,10 @@ def _query_implementation(cls, cb, **kwargs): class IntrusionDetectionSystemAlert(Alert): """ A specialization of the base ``Alert`` class that represents an intrusion detection system alert. + + The complete list of alert fields is too large to be reproduced here; please see the list of available fields + for each alert type on `the Developer Network + `_. """ urlobject = "/api/alerts/v7/orgs/{0}/alerts" swagger_meta_file = "platform/models/alert_intrusion_detection_system.yaml" @@ -1781,6 +1809,7 @@ class GroupedAlertSearchQuery(AlertSearchQuery): This query is constructed by using the ``select()`` method on ``CBCloudAPI`` to create an ``AlertSearchQuery,`` then using that query's ``set_group_by()`` method to specify grouping. """ + def __init__(self, *args, **kwargs): """Initialize the GroupAlertSearchQuery.""" super().__init__(*args, **kwargs) From ceb0247b11f99831b8fd4d1d755f773ca60b51a3 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Thu, 4 Apr 2024 11:08:25 -0600 Subject: [PATCH 34/95] deflake8'd --- src/cbc_sdk/platform/alerts.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/cbc_sdk/platform/alerts.py b/src/cbc_sdk/platform/alerts.py index 92a98558..09493a66 100644 --- a/src/cbc_sdk/platform/alerts.py +++ b/src/cbc_sdk/platform/alerts.py @@ -363,9 +363,7 @@ def delete_threat_tag(self, tag): return resp_json.get("tags", []) class Note(PlatformModel): - """ - Represents a note placed on an alert. - """ + """Represents a note placed on an alert.""" REMAPPED_NOTES_V6_TO_V7 = { "create_time": "create_timestamp", } @@ -1093,8 +1091,9 @@ def get_network_threat_metadata(self): class GroupedAlert(PlatformModel): """ - Represents alerts that have been grouped together based on a common characteristic, to allow viewing of similar - alerts across multiple endpoints. + Represents alerts that have been grouped together based on a common characteristic. + + This allows viewing of similar alerts across multiple endpoints. """ urlobject = "/api/alerts/v7/orgs/{0}/grouped_alerts" swagger_meta_file = "platform/models/grouped_alert.yaml" From fef21b92438897bb6029f97a94944bc1703beae5 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Tue, 9 Apr 2024 10:25:28 -0600 Subject: [PATCH 35/95] breakthrough supplied by Alex, allowing me to remove the swagger_meta_file link from Alert --- src/cbc_sdk/platform/alerts.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cbc_sdk/platform/alerts.py b/src/cbc_sdk/platform/alerts.py index 09493a66..22c99d39 100644 --- a/src/cbc_sdk/platform/alerts.py +++ b/src/cbc_sdk/platform/alerts.py @@ -206,7 +206,7 @@ class Alert(PlatformModel): urlobject_single = "/api/alerts/v7/orgs/{0}/alerts/{1}" threat_urlobject_single = "/api/alerts/v7/orgs/{0}/threats/{1}" primary_key = "id" - swagger_meta_file = "platform/models/alert.yaml" + # swagger_meta_file = "platform/models/alert.yaml" def __init__(self, cb, model_unique_id, initial_data=None): """ @@ -796,7 +796,7 @@ def __getattr__(self, item): "Alerts v7. In SDK 1.5.0 the".format(item, self.__class__.__name__)) item = Alert.REMAPPED_ALERTS_V6_TO_V7.get(item, item) - if self.get("type") == "CONTAINER_RUNTIME": + if self._info.get('type', None) == "CONTAINER_RUNTIME": item = Alert.REMAPPED_CONTAINER_ALERTS_V6_TO_V7.get(original_item, item) return super(Alert, self).__getattr__(item) except AttributeError: From 4070c0a819d93f831f93523755ba27e5c4146c8c Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Tue, 9 Apr 2024 11:32:51 -0600 Subject: [PATCH 36/95] removed all alert swagger_meta_file references --- src/cbc_sdk/platform/alerts.py | 12 +++--------- src/tests/unit/platform/test_alertsv7_api.py | 2 +- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/src/cbc_sdk/platform/alerts.py b/src/cbc_sdk/platform/alerts.py index 22c99d39..78c3dfe7 100644 --- a/src/cbc_sdk/platform/alerts.py +++ b/src/cbc_sdk/platform/alerts.py @@ -206,7 +206,6 @@ class Alert(PlatformModel): urlobject_single = "/api/alerts/v7/orgs/{0}/alerts/{1}" threat_urlobject_single = "/api/alerts/v7/orgs/{0}/threats/{1}" primary_key = "id" - # swagger_meta_file = "platform/models/alert.yaml" def __init__(self, cb, model_unique_id, initial_data=None): """ @@ -790,7 +789,8 @@ def __getattr__(self, item): raise FunctionalityDecommissioned( "Attribute '{0}' does not exist in object '{1}' because it was deprecated in " "Alerts v7. In SDK 1.5.0 the".format(item, self.__class__.__name__)) - if item in Alert.DEPRECATED_FIELDS_NOT_IN_V7_CONTAINER_ONLY and self.type == "CONTAINER_RUNTIME": + if (item in Alert.DEPRECATED_FIELDS_NOT_IN_V7_CONTAINER_ONLY + and self._info.get('type', None) == "CONTAINER_RUNTIME"): raise FunctionalityDecommissioned( "Attribute '{0}' does not exist in object '{1}' because it was deprecated in " "Alerts v7. In SDK 1.5.0 the".format(item, self.__class__.__name__)) @@ -880,7 +880,6 @@ class WatchlistAlert(Alert): """ urlobject = "/api/alerts/v7/orgs/{0}/alerts" type = ["WATCHLIST"] - swagger_meta_file = "platform/models/alert_watchlist.yaml" @classmethod def _query_implementation(cls, cb, **kwargs): @@ -908,7 +907,7 @@ def get_watchlist_objects(self): list[Watchlist]: A list of Watchlist objects. """ watchlist_objects = [] - for watchlist in self.get("watchlists"): + for watchlist in self._info.get("watchlists"): watchlist_id = watchlist.get("id") watchlist_objects.append(self._cb.select(Watchlist, watchlist_id)) return watchlist_objects @@ -924,7 +923,6 @@ class CBAnalyticsAlert(Alert): """ urlobject = "/api/alerts/v7/orgs/{0}/alerts" type = ["CB_ANALYTICS"] - swagger_meta_file = "platform/models/alert_cb_analytic.yaml" @classmethod def _query_implementation(cls, cb, **kwargs): @@ -976,7 +974,6 @@ class DeviceControlAlert(Alert): `_. """ urlobject = "/api/alerts/v7/orgs/{0}/alerts" - swagger_meta_file = "platform/models/alert_device_control.yaml" @classmethod def _query_implementation(cls, cb, **kwargs): @@ -1002,7 +999,6 @@ class ContainerRuntimeAlert(Alert): `_. """ urlobject = "/api/alerts/v7/orgs/{0}/alerts" - swagger_meta_file = "platform/models/alert_container_runtime.yaml" type = ["CONTAINER_RUNTIME"] @classmethod @@ -1029,7 +1025,6 @@ class HostBasedFirewallAlert(Alert): `_. """ urlobject = "/api/alerts/v7/orgs/{0}/alerts" - swagger_meta_file = "platform/models/alert_host_based_firewall.yaml" type = ["HOST_BASED_FIREWALL"] @classmethod @@ -1056,7 +1051,6 @@ class IntrusionDetectionSystemAlert(Alert): `_. """ urlobject = "/api/alerts/v7/orgs/{0}/alerts" - swagger_meta_file = "platform/models/alert_intrusion_detection_system.yaml" type = ["INTRUSION_DETECTION_SYSTEM"] @classmethod diff --git a/src/tests/unit/platform/test_alertsv7_api.py b/src/tests/unit/platform/test_alertsv7_api.py index 9c308388..42d32de5 100755 --- a/src/tests/unit/platform/test_alertsv7_api.py +++ b/src/tests/unit/platform/test_alertsv7_api.py @@ -2101,7 +2101,7 @@ def on_post(url, body, **kwargs): alert = alerts[0] assert isinstance(alerts, list) - assert alert.get("type") == "WATCHLIST" + # assert alert.get("type") == "WATCHLIST" assert alert.get("threat_id") == group_alert.most_recent_alert.get("threat_id") From bfa9d8cd512e93f02731b9ea71c783b952d5a82f Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Tue, 9 Apr 2024 11:35:38 -0600 Subject: [PATCH 37/95] cleanup: remove CbMetaModel alteration --- src/cbc_sdk/base.py | 40 ++++++++++++++++++---------------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/src/cbc_sdk/base.py b/src/cbc_sdk/base.py index 4d5dc768..47ff45a3 100644 --- a/src/cbc_sdk/base.py +++ b/src/cbc_sdk/base.py @@ -89,28 +89,24 @@ def __new__(mcs, name, bases, clsdict): model_data = yaml.load( open(os.path.join(mcs.model_base_directory, swagger_meta_file), 'rb').read(), SwaggerLoader) - options = model_data.get('x-options', []) - - if 'nodocstring' not in options: - - # clsdict["__doc__"] = "Represents a %s object in the Carbon Black server.\n\n" % (name,) - # for field_name, field_info in iter(model_data.get("properties", {}).items()): - # docstring = field_info.get("description", None) - # if docstring: - # clsdict["__doc__"] += ":ivar %s: %s\n" % (field_name, docstring) - - class_docstr = clsdict.get('__doc__', None) - if not class_docstr: - class_docstr = f"Represents a {name} object in the Carbon Black Cloud." # pragma: no cover - need_header = True - for field_name, field_info in iter(model_data.get("properties", {}).items()): - docstring = field_info.get("description", None) - if docstring: - if need_header: - class_docstr += "\n\nParameters:" - need_header = False - class_docstr += f"\n {field_name}: {docstring}" - clsdict['__doc__'] = class_docstr + # clsdict["__doc__"] = "Represents a %s object in the Carbon Black server.\n\n" % (name,) + # for field_name, field_info in iter(model_data.get("properties", {}).items()): + # docstring = field_info.get("description", None) + # if docstring: + # clsdict["__doc__"] += ":ivar %s: %s\n" % (field_name, docstring) + + class_docstr = clsdict.get('__doc__', None) + if not class_docstr: + class_docstr = f"Represents a {name} object in the Carbon Black Cloud." # pragma: no cover + need_header = True + for field_name, field_info in iter(model_data.get("properties", {}).items()): + docstring = field_info.get("description", None) + if docstring: + if need_header: + class_docstr += "\n\nParameters:" + need_header = False + class_docstr += f"\n {field_name}: {docstring}" + clsdict['__doc__'] = class_docstr foreign_keys = clsdict.pop("foreign_keys", {}) From 02a8658110771b2cb1bff803bbfcb803ed23824b Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Tue, 9 Apr 2024 11:38:27 -0600 Subject: [PATCH 38/95] cleanup: remove orphaned yaml files --- src/cbc_sdk/platform/models/alert.yaml | 685 ------------------ .../platform/models/alert_cb_analytic.yaml | 494 ------------- .../models/alert_container_runtime.yaml | 249 ------- .../platform/models/alert_device_control.yaml | 231 ------ .../models/alert_host_based_firewall.yaml | 485 ------------- .../alert_intrusion_detection_system.yaml | 500 ------------- .../platform/models/alert_watchlist.yaml | 545 -------------- 7 files changed, 3189 deletions(-) delete mode 100644 src/cbc_sdk/platform/models/alert.yaml delete mode 100644 src/cbc_sdk/platform/models/alert_cb_analytic.yaml delete mode 100644 src/cbc_sdk/platform/models/alert_container_runtime.yaml delete mode 100644 src/cbc_sdk/platform/models/alert_device_control.yaml delete mode 100644 src/cbc_sdk/platform/models/alert_host_based_firewall.yaml delete mode 100644 src/cbc_sdk/platform/models/alert_intrusion_detection_system.yaml delete mode 100644 src/cbc_sdk/platform/models/alert_watchlist.yaml diff --git a/src/cbc_sdk/platform/models/alert.yaml b/src/cbc_sdk/platform/models/alert.yaml deleted file mode 100644 index 509f8cf4..00000000 --- a/src/cbc_sdk/platform/models/alert.yaml +++ /dev/null @@ -1,685 +0,0 @@ -type: object -x-options: - - nodocstring -properties: - additional_events_present: - type: boolean - description: Indicator to let API and forwarder users know that they should look up other associated events related to this alert - alert_notes_present: - type: boolean - description: True if notes are present on the alert ID. False if notes are not present. - alert_url: - type: string - description: Link to the alerts page for this alert. Does not vary by alert type - attack_tactic: - type: string - description: A tactic from the MITRE ATT&CK framework; defines a reason for an adversary’s action, such as achieving credential access - attack_technique: - type: string - description: A technique from the MITRE ATT&CK framework; defines an action an adversary takes to accomplish a goal, such as dumping credentials to achieve credential access - backend_timestamp: - type: string - format: date-time - description: Timestamp when the Carbon Black Cloud processed and enabled the alert for searching. Corresponds to the Created column on the Alerts page. - backend_update_timestamp: - type: string - format: date-time - description: Timestamp when the Carbon Black Cloud initiated and processed an update to an alert. Corresponds to the Updated column on the Alerts page. Note that changes made by users do not change this date; those changes are reflected on `user_update_timestamp` - blocked_effective_reputation: - type: string - enum: - - ADAPTIVE_WHITE_LIST - - COMMON_WHITE_LIST - - COMPANY_BLACK_LIST - - COMPANY_WHITE_LIST - - PUP - - TRUSTED_WHITE_LIST - - RESOLVING - - COMPROMISED_OBSOLETE - - DLP_OBSOLETE - - IGNORE - - ADWARE - - HEURISTIC - - SUSPECT_MALWARE - - KNOWN_MALWARE - - ADMIN_RESTRICT_OBSOLETE - - NOT_LISTED - - GRAY_OBSOLETE - - NOT_COMPANY_WHITE_OBSOLETE - - LOCAL_WHITE - - NOT_SUPPORTED - description: Effective reputation of the blocked file or process; applied by the sensor at the time the block occurred - blocked_md5: - type: string - description: MD5 hash of the child process binary; for any process terminated by the sensor - blocked_name: - type: string - description: Tokenized file path of the files blocked by sensor action - blocked_sha256: - type: string - description: SHA-256 hash of the child process binary; for any process terminated by the sensor - category: - type: string - description: Alert category - Monitored vs Threat - enum: - - THREAT - - MONITORED - - INFO - - MINOR - - SERIOUS - - CRITICAL - childproc_cmdline: - type: string - description: Command line for the child process - childproc_effective_reputation: - type: string - enum: - - ADAPTIVE_WHITE_LIST - - COMMON_WHITE_LIST - - COMPANY_BLACK_LIST - - COMPANY_WHITE_LIST - - PUP - - TRUSTED_WHITE_LIST - - RESOLVING - - COMPROMISED_OBSOLETE - - DLP_OBSOLETE - - IGNORE - - ADWARE - - HEURISTIC - - SUSPECT_MALWARE - - KNOWN_MALWARE - - ADMIN_RESTRICT_OBSOLETE - - NOT_LISTED - - GRAY_OBSOLETE - - NOT_COMPANY_WHITE_OBSOLETE - - LOCAL_WHITE - - NOT_SUPPORTED - description: Effective reputation of the child process; applied by the sensor at the time the event occurred - childproc_guid: - type: string - description: Unique process identifier assigned to the child process - childproc_md5: - type: string - description: Hash of the child process' binary (Enterprise EDR) - childproc_name: - type: string - description: Filesystem path of the child process' binary - childproc_sha256: - type: string - description: Hash of the child process' binary (Endpoint Standard) - childproc_username: - type: string - description: User context in which the child process was executed - connection_type: - type: string - enum: - - INTERNAL_INBOUND - - INTERNAL_OUTBOUND - - INGRESS - - EGRESS - description: Connection Type - detection_timestamp: - type: string - format: date-time - description: For sensor-sent alerts, this is the time of the event on the sensor. For alerts generated on the backend, this is the time the backend system triggered the alert. - determination: - description: User-updatable determination of the alert - type: object - properties: - change_timestamp: - type: string - format: date-time - description: When the determination was updated. - changed_by: - type: string - description: User the determination was changed by. - changed_by_type: - type: string - description: - enum: - - SYSTEM - - USER - - API - - AUTOMATION - value: - type: string - description: Determination of the alert set by a user - enum: - - NONE - - TRUE_POSITIVE - - FALSE_POSITIVE - device_external_ip: - type: string - description: IP address of the endpoint according to the Carbon Black Cloud; can differ from device_internal_ip due to network proxy or NAT; either IPv4 (dotted decimal notation) or IPv6 (proprietary format) - device_id: - type: integer - format: int64 - description: ID of devices - device_internal_ip: - type: string - description: IP address of the endpoint reported by the sensor; either IPv4 (dotted decimal notation) or IPv6 (proprietary format) - device_location: - type: string - enum: - - ONSITE - - OFFSITE - - UNKNOWN - description: Whether the device was on or off premises when the alert started, based on the current IP address and the device’s registered DNS domain suffix - device_name: - type: string - description: Device name - device_os: - type: string - enum: - - WINDOWS - - ANDROID - - MAC - - LINUX - - OTHER - description: Device Operating Systems - device_os_version: - type: string - example: Windows 10 x64 - description: The operating system and version of the endpoint. Requires Windows CBC sensor version 3.5 or later. - device_policy: - type: string - description: Device policy - device_policy_id: - type: integer - format: int64 - description: Device policy id - device_target_value: - type: string - enum: - - LOW - - MEDIUM - - HIGH - - MISSION_CRITICAL - description: Target value assigned to the device, set from the policy - device_uem_id: - type: string - description: Device correlation with WS1/EUC, required for our Workspace ONE Intelligence integration to function - device_username: - type: string - description: Logged on user during the alert. This is filled on a best-effort - approach. If the user is not available it may be populated with the device - owner (empty for Container Runtime alerts) - egress_group_id: - type: string - description: Unique identifier for the egress group - egress_group_name: - type: string - description: Name of the egress group - external_device_friendly_name: - type: string - description: Human-readable external device names - first_event_timestamp: - type: string - format: date-time - description: Timestamp when the first event in the alert occurred - id: - type: string - description: Unique IDs of alerts - ioc_field: - type: string - description: The field the indicator of comprise (IOC) hit contains - ioc_hit: - type: string - description: IOC field value or IOC query that matches - ioc_id: - type: string - description: Unique identifier of the IOC that generated the watchlist hit - ip_reputation: - type: integer - format: int64 - description: Range of reputations to accept for the remote IP 0- unknown 1-20 high risk 21-40 suspicious 41-60 moderate 61-80 low risk 81-100 trustworthy There must be two values in this list. The first is the lower end of the range (inclusive) the second is the upper end of the range (inclusive) - is_updated: - type: boolean - description: Boolean that describes whether or not this is the original copy of the alert - k8s_cluster: - type: string - description: K8s Cluster name - k8s_kind: - type: string - description: K8s Workload kind - k8s_namespace: - type: string - description: K8s namespace - k8s_pod_name: - type: string - description: Name of the pod within a workload - k8s_policy: - type: string - description: Name of the K8s policy - k8s_policy_id: - type: string - description: Unique identifier for the K8s policy - k8s_rule: - type: string - description: Name of the K8s policy rule - k8s_rule_id: - type: string - description: Unique identifier for the K8s policy rule - k8s_workload_name: - type: string - description: K8s Workload Name - last_event_timestamp: - type: string - format: date-time - description: Timestamp when the last event in the alert occurred - mdr_alert: - type: boolean - description: Is Mdr alert - mdr_alert_notes_present: - type: boolean - description: Customer visible notes at the alert level that were added by a MDR analyst - mdr_determination: - type: object - description: Mdr updatable classification of the alert - properties: - change_timestamp: - type: string - description: When the last MDR classification change occurred - format: date-time - value: - type: string - description: A record that identifies the whether the alert was determined to represent a likely or unlikely threat. - enum: - - NOT_ENOUGH_INFO - - NOT_REVIEWED - - NONE - - UNLIKELY_THREAT - - LIKELY_THREAT - mdr_threat_notes_present: - type: boolean - description: Customer visible notes at the threat level that were added by a MDR analyst - mdr_workflow: - type: object - description: MDR-updatable workflow of the alert - properties: - change_timestamp: - description: When the last MDR status change occurred - type: string - format: date-time - is_assigned: - type: boolean - description: - status: - type: string - description: Primary value used to capture status change during MD Analyst's alert triage - enum: - - UNCLAIMED - - IN_PROGRESS - - TRIAGE_COMPLETE - - ACTION_REQUESTED - - PENDING_RESPONSE - - RESPONCE_RECEIVED - ml_classification_final_verdict: - type: string - enum: - - NOT_CLASSIFIED - - NOT_ANOMALOUS - - ANOMALOUS - description: Final verdict of the alert, based on the ML models that were used to make the prediction. - ml_classification_global_prevalence: - type: string - enum: - - UNKNOWN - - LOW - - MEDIUM - - HIGH - description: Categories (low/medium/high) used to describe the prevalence of alerts across all regional organizations. - ml_classification_org_prevalence: - type: string - enum: - - UNKNOWN - - LOW - - MEDIUM - - HIGH - description: Categories (low/medium/high) used to describe the prevalence of alerts within an organization. - netconn_local_ip: - type: string - description: IP address of the remote side of the network connection; stored as dotted decimal - netconn_local_ipv4: - type: string - description: IPv4 address of the local side of the network connection; stored as a dotted decimal. Only one of ipv4 and ipv6 fields will be populated. - netconn_local_ipv6: - type: string - description: IPv6 address of the local side of the network connection; stored as a string without octet-separating colon characters. Only one of ipv4 and ipv6 fields will be populated. - netconn_local_port: - type: integer - format: int64 - description: TCP or UDP port used by the local side of the network connection - netconn_protocol: - type: string - description: Network protocol of the network connection - netconn_remote_domain: - type: string - description: Domain name (FQDN) associated with the remote end of the network connection, if available - netconn_remote_ip: - type: string - description: IP address of the local side of the network connection; stored as dotted decimal - netconn_remote_ipv4: - type: string - description: IPv4 address of the remote side of the network connection; stored as dotted decimal. Only one of ipv4 and ipv6 fields will be populated. - netconn_remote_ipv6: - type: string - description: IPv6 address of the remote side of the network connection; stored as a string without octet-separating colon characters. Only one of ipv4 and ipv6 fields will be populated. - netconn_remote_port: - type: integer - format: int64 - description: TCP or UDP port used by the remote side of the network connection; same as netconn_port and event_network_remote_port - org_key: - type: string - description: Unique alphanumeric string that identifies your organization in the Carbon Black Cloud - parent_cmdline: - type: string - description: Command line of the parent process - parent_effective_reputation: - type: string - enum: - - ADAPTIVE_WHITE_LIST - - COMMON_WHITE_LIST - - COMPANY_BLACK_LIST - - COMPANY_WHITE_LIST - - PUP - - TRUSTED_WHITE_LIST - - RESOLVING - - COMPROMISED_OBSOLETE - - DLP_OBSOLETE - - IGNORE - - ADWARE - - HEURISTIC - - SUSPECT_MALWARE - - KNOWN_MALWARE - - ADMIN_RESTRICT_OBSOLETE - - NOT_LISTED - - GRAY_OBSOLETE - - NOT_COMPANY_WHITE_OBSOLETE - - LOCAL_WHITE - - NOT_SUPPORTED - description: Effective reputation of the parent process; applied by the sensor when the event occurred - parent_guid: - type: string - description: Unique process identifier assigned to the parent process - parent_md5: - type: string - description: MD5 hash of the parent process binary - parent_name: - type: string - description: Filesystem path of the parent process binary - parent_pid: - type: integer - format: int64 - description: Identifier assigned by the operating system to the parent process - parent_reputation: - type: string - enum: - - ADAPTIVE_WHITE_LIST - - COMMON_WHITE_LIST - - COMPANY_BLACK_LIST - - COMPANY_WHITE_LIST - - PUP - - TRUSTED_WHITE_LIST - - RESOLVING - - COMPROMISED_OBSOLETE - - DLP_OBSOLETE - - IGNORE - - ADWARE - - HEURISTIC - - SUSPECT_MALWARE - - KNOWN_MALWARE - - ADMIN_RESTRICT_OBSOLETE - - NOT_LISTED - - GRAY_OBSOLETE - - NOT_COMPANY_WHITE_OBSOLETE - - LOCAL_WHITE - - NOT_SUPPORTED - description: Reputation of the parent process; applied by the Carbon Black Cloud when the event is initially processed - parent_sha256: - type: string - description: SHA-256 hash of the parent process binary - parent_username: - type: string - description: User context in which the parent process was executed - policy_applied: - type: string - enum: - - APPLIED - - NOT_APPLIED - description: Indicates whether or not a policy has been applied to any event associated with this alert - primary_event_id: - type: string - description: ID of the primary event in the alert - process_cmdline: - type: string - description: Command line executed by the actor process - process_effective_reputation: - type: string - enum: - - ADAPTIVE_WHITE_LIST - - COMMON_WHITE_LIST - - COMPANY_BLACK_LIST - - COMPANY_WHITE_LIST - - PUP - - TRUSTED_WHITE_LIST - - RESOLVING - - COMPROMISED_OBSOLETE - - DLP_OBSOLETE - - IGNORE - - ADWARE - - HEURISTIC - - SUSPECT_MALWARE - - KNOWN_MALWARE - - ADMIN_RESTRICT_OBSOLETE - - NOT_LISTED - - GRAY_OBSOLETE - - NOT_COMPANY_WHITE_OBSOLETE - - LOCAL_WHITE - - NOT_SUPPORTED - description: Effective reputation of the actor hash - process_guid: - type: string - description: Guid of the process that has fired the alert (optional) - process_issuer: - type: string - description: - process_md5: - type: string - description: MD5 hash of the actor process binary - process_name: - type: string - description: Process names of an alert - process_pid: - type: integer - format: int64 - description: PID of the process that has fired the alert (optional) - process_publisher: - type: string - description: - process_reputation: - type: string - enum: - - ADAPTIVE_WHITE_LIST - - COMMON_WHITE_LIST - - COMPANY_BLACK_LIST - - COMPANY_WHITE_LIST - - PUP - - TRUSTED_WHITE_LIST - - RESOLVING - - COMPROMISED_OBSOLETE - - DLP_OBSOLETE - - IGNORE - - ADWARE - - HEURISTIC - - SUSPECT_MALWARE - - KNOWN_MALWARE - - ADMIN_RESTRICT_OBSOLETE - - NOT_LISTED - - GRAY_OBSOLETE - - NOT_COMPANY_WHITE_OBSOLETE - - LOCAL_WHITE - - NOT_SUPPORTED - description: Reputation of the actor process; applied when event is processed by the Carbon Black Cloud - process_sha256: - type: string - description: SHA-256 hash of the actor process binary - process_username: - type: string - description: User context in which the actor process was executed. MacOS - all users for the PID for fork() and exec() transitions. Linux - process user for exec() events, but in a future sensor release can be multi-valued due to setuid(). - product_id: - type: string - description: IDs of the product that identifies USB devices - product_name: - type: string - description: Names of the product that identifies USB devices - reason: - type: string - description: A spoken language written explanation of the what and why the alert occurred and any action taken, usually consisting of 1 to 3 sentences. - reason_code: - type: string - description: A unique short-hand code or GUID identifying the particular alert reason - remote_is_private: - type: boolean - description: Is the remote information private - remote_k8s_kind: - type: string - description: Kind of remote workload; set if the remote side is another workload in the same cluster - remote_k8s_namespace: - type: string - description: Namespace within the remote workload’s cluster; set if the remote side is another workload in the same cluster - remote_k8s_pod_name: - type: string - description: Remote workload pod name; set if the remote side is another workload in the same cluster - remote_k8s_workload_name: - type: string - description: Name of the remote workload; set if the remote side is another workload in the same cluster - report_description: - type: string - description: Description of the watchlist report associated with the alert - report_id: - type: string - description: Report IDs that contained the IOC that caused a hit - report_link: - type: string - description: Link of reports that contained the IOC that caused a hit - report_name: - type: string - description: Name of the watchlist report - report_tags: - type: string[] - description: Tags associated with the watchlist report - rule_category_id: - type: string - description: ID representing the category of the rule_id for certain alert types - rule_config_category: - type: string - description: Types of rule configs - rule_id: - type: string - description: ID of the rule that triggered an alert; applies to Intrusion Detection System, Host-Based Firewall, TAU Intelligence, and USB Device Control alerts - run_state: - type: string - enum: - - DID_NOT_RUN - - RAN - - UNKNOWN - description: Whether the threat in the alert actually ran - sensor_action: - type: string - enum: - - ALLOW - - ALLOW_AND_LOG - - DENY - - TERMINATE - description: Actions taken by the sensor, according to the rules of a policy - serial_number: - type: string - description: Serial numbers of the specific devices - severity: - type: integer - format: int64 - description: integer representation of the impact of alert if true positive - tags: - type: array - description: Tags added to the threat ID of the alert - items: - type: string - threat_id: - type: string - description: ID assigned to a group of alerts with common criteria, based on alert type - threat_name: - type: string - description: Name of the threat - threat_notes_present: - type: boolean - description: True if notes are present on the threat ID. False if notes are not present. - tms_rule_id: - type: string - description: Detection id - ttps: - type: string - description: Other potential malicious activities involved in a threat - type: - type: string - enum: - - CB_ANALYTICS - - WATCHLIST - - DEVICE_CONTROL - - CONTAINER_RUNTIME - - HOST_BASED_FIREWALL - - INTRUSION_DETECTION_SYSTEM - - NETWORK_TRAFFIC_ANALYSIS - description: Type of alert generated - user_update_timestamp: - type: string - format: date-time - description: Timestamp of the last property of an alert changed by a user, such as the alert workflow or determination - vendor_id: - type: string - description: IDs of the vendors who produced the devices - vendor_name: - type: string - description: Names of the vendors who produced the devices - watchlists: - type: object - description: List of watchlists associated with an alert. Alerts are batched hourly - properties: - id: - type: string - description: - name: - type: string - description: - workflow: - type: object - description: Current workflow state of an alert. The workflow represents the flow from `OPEN` to `IN_PROGRESS` to `CLOSED` and captures who moved the alert into the current state. The history of these state transitions is available via the alert history route. - properties: - change_timestamp: - type: string - format: date-time - description: When the last status change occurred - changed_by: - type: string - description: Who (or what) made the last status change - workflow_changed_by_rule_id: - type: string - description: - changed_by_type: - type: string - enum: - - SYSTEM - - USER - - API - - AUTOMATION - description: - closure_reason: - type: string `NO_REASON`, `RESOLVED`, `RESOLVED_BENIGN_KNOWN_GOOD`, `DUPLICATE_CLEANUP`, `OTHER` - description: A more detailed description of why the alert was resolved - status: - type: string - enum: - - OPEN - - IN_PROGRESS - - CLOSED - description: primary value used to determine if the alert is active or inactive and displayed in the UI by default \ No newline at end of file diff --git a/src/cbc_sdk/platform/models/alert_cb_analytic.yaml b/src/cbc_sdk/platform/models/alert_cb_analytic.yaml deleted file mode 100644 index 41d9a9e6..00000000 --- a/src/cbc_sdk/platform/models/alert_cb_analytic.yaml +++ /dev/null @@ -1,494 +0,0 @@ -type: object -x-options: - - nodocstring -properties: - additional_events_present: - type: boolean - description: Indicator to let API and forwarder users know that they should look up other associated events related to this alert - alert_notes_present: - type: boolean - description: True if notes are present on the alert ID. False if notes are not present. - alert_url: - type: string - description: Link to the alerts page for this alert. Does not vary by alert type - attack_tactic: - type: string - description: A tactic from the MITRE ATT&CK framework; defines a reason for an adversary’s action, such as achieving credential access - attack_technique: - type: string - description: A technique from the MITRE ATT&CK framework; defines an action an adversary takes to accomplish a goal, such as dumping credentials to achieve credential access - backend_timestamp: - type: string - format: date-time - description: Timestamp when the Carbon Black Cloud processed and enabled the alert for searching. Corresponds to the Created column on the Alerts page. - backend_update_timestamp: - type: string - format: date-time - description: Timestamp when the Carbon Black Cloud initiated and processed an update to an alert. Corresponds to the Updated column on the Alerts page. Note that changes made by users do not change this date; those changes are reflected on `user_update_timestamp` - blocked_effective_reputation: - type: string - enum: - - ADAPTIVE_WHITE_LIST - - COMMON_WHITE_LIST - - COMPANY_BLACK_LIST - - COMPANY_WHITE_LIST - - PUP - - TRUSTED_WHITE_LIST - - RESOLVING - - COMPROMISED_OBSOLETE - - DLP_OBSOLETE - - IGNORE - - ADWARE - - HEURISTIC - - SUSPECT_MALWARE - - KNOWN_MALWARE - - ADMIN_RESTRICT_OBSOLETE - - NOT_LISTED - - GRAY_OBSOLETE - - NOT_COMPANY_WHITE_OBSOLETE - - LOCAL_WHITE - - NOT_SUPPORTED - description: Effective reputation of the blocked file or process; applied by the sensor at the time the block occurred - blocked_md5: - type: string - description: MD5 hash of the child process binary; for any process terminated by the sensor - blocked_name: - type: string - description: Tokenized file path of the files blocked by sensor action - blocked_sha256: - type: string - description: SHA-256 hash of the child process binary; for any process terminated by the sensor - category: - type: string - description: Alert category - Monitored vs Threat - enum: - - THREAT - - MONITORED - - INFO - - MINOR - - SERIOUS - - CRITICAL - childproc_cmdline: - type: string - description: Command line for the child process - childproc_effective_reputation: - type: string - enum: - - ADAPTIVE_WHITE_LIST - - COMMON_WHITE_LIST - - COMPANY_BLACK_LIST - - COMPANY_WHITE_LIST - - PUP - - TRUSTED_WHITE_LIST - - RESOLVING - - COMPROMISED_OBSOLETE - - DLP_OBSOLETE - - IGNORE - - ADWARE - - HEURISTIC - - SUSPECT_MALWARE - - KNOWN_MALWARE - - ADMIN_RESTRICT_OBSOLETE - - NOT_LISTED - - GRAY_OBSOLETE - - NOT_COMPANY_WHITE_OBSOLETE - - LOCAL_WHITE - - NOT_SUPPORTED - description: Effective reputation of the child process; applied by the sensor at the time the event occurred - childproc_guid: - type: string - description: Unique process identifier assigned to the child process - childproc_md5: - type: string - description: Hash of the child process' binary (Enterprise EDR) - childproc_name: - type: string - description: Filesystem path of the child process' binary - childproc_sha256: - type: string - description: Hash of the child process' binary (Endpoint Standard) - childproc_username: - type: string - description: User context in which the child process was executed - detection_timestamp: - type: string - format: date-time - description: For sensor-sent alerts, this is the time of the event on the sensor. For alerts generated on the backend, this is the time the backend system triggered the alert. - determination: - description: User-updatable determination of the alert - type: object - properties: - change_timestamp: - type: string - format: date-time - description: When the determination was updated. - changed_by: - type: string - description: User the determination was changed by. - changed_by_type: - type: string - description: - enum: - - SYSTEM - - USER - - API - - AUTOMATION - value: - type: string - description: Determination of the alert set by a user - enum: - - NONE - - TRUE_POSITIVE - - FALSE_POSITIVE - device_external_ip: - type: string - description: IP address of the endpoint according to the Carbon Black Cloud; can differ from device_internal_ip due to network proxy or NAT; either IPv4 (dotted decimal notation) or IPv6 (proprietary format) - device_id: - type: integer - format: int64 - description: ID of devices - device_internal_ip: - type: string - description: IP address of the endpoint reported by the sensor; either IPv4 (dotted decimal notation) or IPv6 (proprietary format) - device_location: - type: string - enum: - - ONSITE - - OFFSITE - - UNKNOWN - description: Whether the device was on or off premises when the alert started, based on the current IP address and the device’s registered DNS domain suffix - device_name: - type: string - description: Device name - device_os: - type: string - enum: - - WINDOWS - - ANDROID - - MAC - - LINUX - - OTHER - description: Device Operating Systems - device_os_version: - type: string - example: Windows 10 x64 - description: The operating system and version of the endpoint. Requires Windows CBC sensor version 3.5 or later. - device_policy: - type: string - description: Device policy - device_policy_id: - type: integer - format: int64 - description: Device policy id - device_target_value: - type: string - enum: - - LOW - - MEDIUM - - HIGH - - MISSION_CRITICAL - description: Target value assigned to the device, set from the policy - device_uem_id: - type: string - description: Device correlation with WS1/EUC, required for our Workspace ONE Intelligence integration to function - device_username: - type: string - description: Logged on user during the alert. This is filled on a best-effort - approach. If the user is not available it may be populated with the device - owner (empty for Container Runtime alerts) - first_event_timestamp: - type: string - format: date-time - description: Timestamp when the first event in the alert occurred - id: - type: string - description: Unique IDs of alerts - is_updated: - type: boolean - description: Boolean that describes whether or not this is the original copy of the alert - last_event_timestamp: - type: string - format: date-time - description: Timestamp when the last event in the alert occurred - netconn_local_ip: - type: string - description: IP address of the remote side of the network connection; stored as dotted decimal - netconn_local_ipv4: - type: string - description: IPv4 address of the local side of the network connection; stored as a dotted decimal. Only one of ipv4 and ipv6 fields will be populated. - netconn_local_ipv6: - type: string - description: IPv6 address of the local side of the network connection; stored as a string without octet-separating colon characters. Only one of ipv4 and ipv6 fields will be populated. - netconn_local_port: - type: integer - format: int64 - description: TCP or UDP port used by the local side of the network connection - netconn_protocol: - type: string - description: Network protocol of the network connection - netconn_remote_domain: - type: string - description: Domain name (FQDN) associated with the remote end of the network connection, if available - netconn_remote_ip: - type: string - description: IP address of the local side of the network connection; stored as dotted decimal - netconn_remote_ipv4: - type: string - description: IPv4 address of the remote side of the network connection; stored as dotted decimal. Only one of ipv4 and ipv6 fields will be populated. - netconn_remote_ipv6: - type: string - description: IPv6 address of the remote side of the network connection; stored as a string without octet-separating colon characters. Only one of ipv4 and ipv6 fields will be populated. - netconn_remote_port: - type: integer - format: int64 - description: TCP or UDP port used by the remote side of the network connection; same as netconn_port and event_network_remote_port - org_key: - type: string - description: Unique alphanumeric string that identifies your organization in the Carbon Black Cloud - parent_cmdline: - type: string - description: Command line of the parent process - parent_effective_reputation: - type: string - enum: - - ADAPTIVE_WHITE_LIST - - COMMON_WHITE_LIST - - COMPANY_BLACK_LIST - - COMPANY_WHITE_LIST - - PUP - - TRUSTED_WHITE_LIST - - RESOLVING - - COMPROMISED_OBSOLETE - - DLP_OBSOLETE - - IGNORE - - ADWARE - - HEURISTIC - - SUSPECT_MALWARE - - KNOWN_MALWARE - - ADMIN_RESTRICT_OBSOLETE - - NOT_LISTED - - GRAY_OBSOLETE - - NOT_COMPANY_WHITE_OBSOLETE - - LOCAL_WHITE - - NOT_SUPPORTED - description: Effective reputation of the parent process; applied by the sensor when the event occurred - parent_guid: - type: string - description: Unique process identifier assigned to the parent process - parent_md5: - type: string - description: MD5 hash of the parent process binary - parent_name: - type: string - description: Filesystem path of the parent process binary - parent_pid: - type: integer - format: int64 - description: Identifier assigned by the operating system to the parent process - parent_reputation: - type: string - enum: - - ADAPTIVE_WHITE_LIST - - COMMON_WHITE_LIST - - COMPANY_BLACK_LIST - - COMPANY_WHITE_LIST - - PUP - - TRUSTED_WHITE_LIST - - RESOLVING - - COMPROMISED_OBSOLETE - - DLP_OBSOLETE - - IGNORE - - ADWARE - - HEURISTIC - - SUSPECT_MALWARE - - KNOWN_MALWARE - - ADMIN_RESTRICT_OBSOLETE - - NOT_LISTED - - GRAY_OBSOLETE - - NOT_COMPANY_WHITE_OBSOLETE - - LOCAL_WHITE - - NOT_SUPPORTED - description: Reputation of the parent process; applied by the Carbon Black Cloud when the event is initially processed - parent_sha256: - type: string - description: SHA-256 hash of the parent process binary - parent_username: - type: string - description: User context in which the parent process was executed - policy_applied: - type: string - enum: - - APPLIED - - NOT_APPLIED - description: Indicates whether or not a policy has been applied to any event associated with this alert - primary_event_id: - type: string - description: ID of the primary event in the alert - process_cmdline: - type: string - description: Command line executed by the actor process - process_effective_reputation: - type: string - enum: - - ADAPTIVE_WHITE_LIST - - COMMON_WHITE_LIST - - COMPANY_BLACK_LIST - - COMPANY_WHITE_LIST - - PUP - - TRUSTED_WHITE_LIST - - RESOLVING - - COMPROMISED_OBSOLETE - - DLP_OBSOLETE - - IGNORE - - ADWARE - - HEURISTIC - - SUSPECT_MALWARE - - KNOWN_MALWARE - - ADMIN_RESTRICT_OBSOLETE - - NOT_LISTED - - GRAY_OBSOLETE - - NOT_COMPANY_WHITE_OBSOLETE - - LOCAL_WHITE - - NOT_SUPPORTED - description: Effective reputation of the actor hash - process_guid: - type: string - description: Guid of the process that has fired the alert (optional) - process_issuer: - type: string - description: - process_md5: - type: string - description: MD5 hash of the actor process binary - process_name: - type: string - description: Process names of an alert - process_pid: - type: integer - format: int64 - description: PID of the process that has fired the alert (optional) - process_publisher: - type: string - description: - process_reputation: - type: string - enum: - - ADAPTIVE_WHITE_LIST - - COMMON_WHITE_LIST - - COMPANY_BLACK_LIST - - COMPANY_WHITE_LIST - - PUP - - TRUSTED_WHITE_LIST - - RESOLVING - - COMPROMISED_OBSOLETE - - DLP_OBSOLETE - - IGNORE - - ADWARE - - HEURISTIC - - SUSPECT_MALWARE - - KNOWN_MALWARE - - ADMIN_RESTRICT_OBSOLETE - - NOT_LISTED - - GRAY_OBSOLETE - - NOT_COMPANY_WHITE_OBSOLETE - - LOCAL_WHITE - - NOT_SUPPORTED - description: Reputation of the actor process; applied when event is processed by the Carbon Black Cloud - process_sha256: - type: string - description: SHA-256 hash of the actor process binary - process_username: - type: string - description: User context in which the actor process was executed. MacOS - all users for the PID for fork() and exec() transitions. Linux - process user for exec() events, but in a future sensor release can be multi-valued due to setuid(). - reason: - type: string - description: A spoken language written explanation of the what and why the alert occurred and any action taken, usually consisting of 1 to 3 sentences. - reason_code: - type: string - description: A unique short-hand code or GUID identifying the particular alert reason - rule_category_id: - type: string - description: ID representing the category of the rule_id for certain alert types - rule_id: - type: string - description: ID of the rule that triggered an alert; applies to Intrusion Detection System, Host-Based Firewall, TAU Intelligence, and USB Device Control alerts - run_state: - type: string - enum: - - DID_NOT_RUN - - RAN - - UNKNOWN - description: Whether the threat in the alert actually ran - sensor_action: - type: string - enum: - - ALLOW - - ALLOW_AND_LOG - - DENY - - TERMINATE - description: Actions taken by the sensor, according to the rules of a policy - severity: - type: integer - format: int64 - description: integer representation of the impact of alert if true positive - tags: - type: array - description: Tags added to the threat ID of the alert - items: - type: string - threat_id: - type: string - description: ID assigned to a group of alerts with common criteria, based on alert type - threat_notes_present: - type: boolean - description: True if notes are present on the threat ID. False if notes are not present. - ttps: - type: string - description: Other potential malicious activities involved in a threat - type: - type: string - enum: - - CB_ANALYTICS - - WATCHLIST - - DEVICE_CONTROL - - CONTAINER_RUNTIME - - HOST_BASED_FIREWALL - - INTRUSION_DETECTION_SYSTEM - - NETWORK_TRAFFIC_ANALYSIS - description: Type of alert generated - user_update_timestamp: - type: string - format: date-time - description: Timestamp of the last property of an alert changed by a user, such as the alert workflow or determination - workflow: - type: object - description: Current workflow state of an alert. The workflow represents the flow from `OPEN` to `IN_PROGRESS` to `CLOSED` and captures who moved the alert into the current state. The history of these state transitions is available via the alert history route. - properties: - change_timestamp: - type: string - format: date-time - description: When the last status change occurred - workflow_changed_by: - type: string - description: Who (or what) made the last status change - workflow_changed_by_rule_id: - type: string - description: - workflow_changed_by_type: - type: string - enum: - - SYSTEM - - USER - - API - - AUTOMATION - description: - workflow_closure_reason: - type: string `NO_REASON`, `RESOLVED`, `RESOLVED_BENIGN_KNOWN_GOOD`, `DUPLICATE_CLEANUP`, `OTHER` - description: A more detailed description of why the alert was resolved - workflow_status: - type: string - enum: - - OPEN - - IN_PROGRESS - - CLOSED - description: primary value used to determine if the alert is active or inactive and displayed in the UI by default \ No newline at end of file diff --git a/src/cbc_sdk/platform/models/alert_container_runtime.yaml b/src/cbc_sdk/platform/models/alert_container_runtime.yaml deleted file mode 100644 index e3161ebc..00000000 --- a/src/cbc_sdk/platform/models/alert_container_runtime.yaml +++ /dev/null @@ -1,249 +0,0 @@ -type: object -x-options: - - nodocstring -properties: - alert_notes_present: - type: boolean - description: True if notes are present on the alert ID. False if notes are not present. - alert_url: - type: string - description: Link to the alerts page for this alert. Does not vary by alert type - backend_timestamp: - type: string - format: date-time - description: Timestamp when the Carbon Black Cloud processed and enabled the alert for searching. Corresponds to the Created column on the Alerts page. - backend_update_timestamp: - type: string - format: date-time - description: Timestamp when the Carbon Black Cloud initiated and processed an update to an alert. Corresponds to the Updated column on the Alerts page. Note that changes made by users do not change this date; those changes are reflected on `user_update_timestamp` - connection_type: - type: string - enum: - - INTERNAL_INBOUND - - INTERNAL_OUTBOUND - - INGRESS - - EGRESS - description: Connection Type - detection_timestamp: - type: string - format: date-time - description: For sensor-sent alerts, this is the time of the event on the sensor. For alerts generated on the backend, this is the time the backend system triggered the alert. - determination: - description: User-updatable determination of the alert - type: object - properties: - change_timestamp: - type: string - format: date-time - description: When the determination was updated. - changed_by: - type: string - description: User the determination was changed by. - changed_by_type: - type: string - description: - enum: - - SYSTEM - - USER - - API - - AUTOMATION - value: - type: string - description: Determination of the alert set by a user - enum: - - NONE - - TRUE_POSITIVE - - FALSE_POSITIVE - egress_group_id: - type: string - description: Unique identifier for the egress group - egress_group_name: - type: string - description: Name of the egress group - first_event_timestamp: - type: string - format: date-time - description: Timestamp when the first event in the alert occurred - id: - type: string - description: Unique IDs of alerts - ip_reputation: - type: integer - format: int64 - description: Range of reputations to accept for the remote IP 0- unknown 1-20 high risk 21-40 suspicious 41-60 moderate 61-80 low risk 81-100 trustworthy There must be two values in this list. The first is the lower end of the range (inclusive) the second is the upper end of the range (inclusive) - is_updated: - type: boolean - description: Boolean that describes whether or not this is the original copy of the alert - k8s_cluster: - type: string - description: K8s Cluster name - k8s_kind: - type: string - description: K8s Workload kind - k8s_namespace: - type: string - description: K8s namespace - k8s_pod_name: - type: string - description: Name of the pod within a workload - k8s_policy: - type: string - description: Name of the K8s policy - k8s_policy_id: - type: string - description: Unique identifier for the K8s policy - k8s_rule: - type: string - description: Name of the K8s policy rule - k8s_rule_id: - type: string - description: Unique identifier for the K8s policy rule - k8s_workload_name: - type: string - description: K8s Workload Name - last_event_timestamp: - type: string - format: date-time - description: Timestamp when the last event in the alert occurred - netconn_local_ip: - type: string - description: IP address of the remote side of the network connection; stored as dotted decimal - netconn_local_ipv4: - type: string - description: IPv4 address of the local side of the network connection; stored as a dotted decimal. Only one of ipv4 and ipv6 fields will be populated. - netconn_local_ipv6: - type: string - description: IPv6 address of the local side of the network connection; stored as a string without octet-separating colon characters. Only one of ipv4 and ipv6 fields will be populated. - netconn_local_port: - type: integer - format: int64 - description: TCP or UDP port used by the local side of the network connection - netconn_protocol: - type: string - description: Network protocol of the network connection - netconn_remote_domain: - type: string - description: Domain name (FQDN) associated with the remote end of the network connection, if available - netconn_remote_ip: - type: string - description: IP address of the local side of the network connection; stored as dotted decimal - netconn_remote_ipv4: - type: string - description: IPv4 address of the remote side of the network connection; stored as dotted decimal. Only one of ipv4 and ipv6 fields will be populated. - netconn_remote_ipv6: - type: string - description: IPv6 address of the remote side of the network connection; stored as a string without octet-separating colon characters. Only one of ipv4 and ipv6 fields will be populated. - netconn_remote_port: - type: integer - format: int64 - description: TCP or UDP port used by the remote side of the network connection; same as netconn_port and event_network_remote_port - org_key: - type: string - description: Unique alphanumeric string that identifies your organization in the Carbon Black Cloud - policy_applied: - type: string - enum: - - APPLIED - - NOT_APPLIED - description: Indicates whether or not a policy has been applied to any event associated with this alert - primary_event_id: - type: string - description: ID of the primary event in the alert - reason: - type: string - description: A spoken language written explanation of the what and why the alert occurred and any action taken, usually consisting of 1 to 3 sentences. - reason_code: - type: string - description: A unique short-hand code or GUID identifying the particular alert reason - remote_is_private: - type: boolean - description: Is the remote information private - remote_k8s_kind: - type: string - description: Kind of remote workload; set if the remote side is another workload in the same cluster - remote_k8s_namespace: - type: string - description: Namespace within the remote workload’s cluster; set if the remote side is another workload in the same cluster - remote_k8s_pod_name: - type: string - description: Remote workload pod name; set if the remote side is another workload in the same cluster - remote_k8s_workload_name: - type: string - description: Name of the remote workload; set if the remote side is another workload in the same cluster - run_state: - type: string - enum: - - DID_NOT_RUN - - RAN - - UNKNOWN - description: Whether the threat in the alert actually ran - sensor_action: - type: string - enum: - - ALLOW - - ALLOW_AND_LOG - - DENY - - TERMINATE - description: Actions taken by the sensor, according to the rules of a policy - severity: - type: integer - format: int64 - description: integer representation of the impact of alert if true positive - tags: - type: array - description: Tags added to the threat ID of the alert - items: - type: string - threat_id: - type: string - description: ID assigned to a group of alerts with common criteria, based on alert type - threat_notes_present: - type: boolean - description: True if notes are present on the threat ID. False if notes are not present. - type: - type: string - enum: - - CB_ANALYTICS - - WATCHLIST - - DEVICE_CONTROL - - CONTAINER_RUNTIME - - HOST_BASED_FIREWALL - - INTRUSION_DETECTION_SYSTEM - - NETWORK_TRAFFIC_ANALYSIS - description: Type of alert generated - user_update_timestamp: - type: string - format: date-time - description: Timestamp of the last property of an alert changed by a user, such as the alert workflow or determination - workflow: - type: object - description: Current workflow state of an alert. The workflow represents the flow from `OPEN` to `IN_PROGRESS` to `CLOSED` and captures who moved the alert into the current state. The history of these state transitions is available via the alert history route. - properties: - change_timestamp: - type: string - format: date-time - description: When the last status change occurred - workflow_changed_by: - type: string - description: Who (or what) made the last status change - workflow_changed_by_rule_id: - type: string - description: - workflow_changed_by_type: - type: string - enum: - - SYSTEM - - USER - - API - - AUTOMATION - description: - workflow_closure_reason: - type: string `NO_REASON`, `RESOLVED`, `RESOLVED_BENIGN_KNOWN_GOOD`, `DUPLICATE_CLEANUP`, `OTHER` - description: A more detailed description of why the alert was resolved - workflow_status: - type: string - enum: - - OPEN - - IN_PROGRESS - - CLOSED - description: primary value used to determine if the alert is active or inactive and displayed in the UI by default \ No newline at end of file diff --git a/src/cbc_sdk/platform/models/alert_device_control.yaml b/src/cbc_sdk/platform/models/alert_device_control.yaml deleted file mode 100644 index ea888dae..00000000 --- a/src/cbc_sdk/platform/models/alert_device_control.yaml +++ /dev/null @@ -1,231 +0,0 @@ -type: object -x-options: - - nodocstring -properties: - alert_notes_present: - type: boolean - description: True if notes are present on the alert ID. False if notes are not present. - alert_url: - type: string - description: Link to the alerts page for this alert. Does not vary by alert type - backend_timestamp: - type: string - format: date-time - description: Timestamp when the Carbon Black Cloud processed and enabled the alert for searching. Corresponds to the Created column on the Alerts page. - backend_update_timestamp: - type: string - format: date-time - description: Timestamp when the Carbon Black Cloud initiated and processed an update to an alert. Corresponds to the Updated column on the Alerts page. Note that changes made by users do not change this date; those changes are reflected on `user_update_timestamp` - detection_timestamp: - type: string - format: date-time - description: For sensor-sent alerts, this is the time of the event on the sensor. For alerts generated on the backend, this is the time the backend system triggered the alert. - determination: - description: User-updatable determination of the alert - type: object - properties: - change_timestamp: - type: string - format: date-time - description: When the determination was updated. - changed_by: - type: string - description: User the determination was changed by. - changed_by_type: - type: string - description: - enum: - - SYSTEM - - USER - - API - - AUTOMATION - value: - type: string - description: Determination of the alert set by a user - enum: - - NONE - - TRUE_POSITIVE - - FALSE_POSITIVE - device_external_ip: - type: string - description: IP address of the endpoint according to the Carbon Black Cloud; can differ from device_internal_ip due to network proxy or NAT; either IPv4 (dotted decimal notation) or IPv6 (proprietary format) - device_id: - type: integer - format: int64 - description: ID of devices - device_internal_ip: - type: string - description: IP address of the endpoint reported by the sensor; either IPv4 (dotted decimal notation) or IPv6 (proprietary format) - device_location: - type: string - enum: - - ONSITE - - OFFSITE - - UNKNOWN - description: Whether the device was on or off premises when the alert started, based on the current IP address and the device’s registered DNS domain suffix - device_name: - type: string - description: Device name - device_os: - type: string - enum: - - WINDOWS - - ANDROID - - MAC - - LINUX - - OTHER - description: Device Operating Systems - device_os_version: - type: string - example: Windows 10 x64 - description: The operating system and version of the endpoint. Requires Windows CBC sensor version 3.5 or later. - device_policy: - type: string - description: Device policy - device_policy_id: - type: integer - format: int64 - description: Device policy id - device_target_value: - type: string - enum: - - LOW - - MEDIUM - - HIGH - - MISSION_CRITICAL - description: Target value assigned to the device, set from the policy - device_uem_id: - type: string - description: Device correlation with WS1/EUC, required for our Workspace ONE Intelligence integration to function - device_username: - type: string - description: Logged on user during the alert. This is filled on a best-effort - approach. If the user is not available it may be populated with the device - owner (empty for Container Runtime alerts) - external_device_friendly_name: - type: string - description: Human-readable external device names - first_event_timestamp: - type: string - format: date-time - description: Timestamp when the first event in the alert occurred - id: - type: string - description: Unique IDs of alerts - is_updated: - type: boolean - description: Boolean that describes whether or not this is the original copy of the alert - last_event_timestamp: - type: string - format: date-time - description: Timestamp when the last event in the alert occurred - org_key: - type: string - description: Unique alphanumeric string that identifies your organization in the Carbon Black Cloud - policy_applied: - type: string - enum: - - APPLIED - - NOT_APPLIED - description: Indicates whether or not a policy has been applied to any event associated with this alert - primary_event_id: - type: string - description: ID of the primary event in the alert - product_id: - type: string - description: IDs of the product that identifies USB devices - product_name: - type: string - description: Names of the product that identifies USB devices - reason: - type: string - description: A spoken language written explanation of the what and why the alert occurred and any action taken, usually consisting of 1 to 3 sentences. - reason_code: - type: string - description: A unique short-hand code or GUID identifying the particular alert reason - run_state: - type: string - enum: - - DID_NOT_RUN - - RAN - - UNKNOWN - description: Whether the threat in the alert actually ran - sensor_action: - type: string - enum: - - ALLOW - - ALLOW_AND_LOG - - DENY - - TERMINATE - description: Actions taken by the sensor, according to the rules of a policy - serial_number: - type: string - description: Serial numbers of the specific devices - severity: - type: integer - format: int64 - description: integer representation of the impact of alert if true positive - tags: - type: array - description: Tags added to the threat ID of the alert - items: - type: string - threat_id: - type: string - description: ID assigned to a group of alerts with common criteria, based on alert type - threat_notes_present: - type: boolean - description: True if notes are present on the threat ID. False if notes are not present. - type: - type: string - enum: - - CB_ANALYTICS - - WATCHLIST - - DEVICE_CONTROL - - CONTAINER_RUNTIME - - HOST_BASED_FIREWALL - - INTRUSION_DETECTION_SYSTEM - - NETWORK_TRAFFIC_ANALYSIS - description: Type of alert generated - user_update_timestamp: - type: string - format: date-time - description: Timestamp of the last property of an alert changed by a user, such as the alert workflow or determination - vendor_id: - type: string - description: IDs of the vendors who produced the devices - vendor_name: - type: string - description: Names of the vendors who produced the devices - workflow: - type: object - description: Current workflow state of an alert. The workflow represents the flow from `OPEN` to `IN_PROGRESS` to `CLOSED` and captures who moved the alert into the current state. The history of these state transitions is available via the alert history route. - properties: - change_timestamp: - type: string - format: date-time - description: When the last status change occurred - workflow_changed_by: - type: string - description: Who (or what) made the last status change - workflow_changed_by_rule_id: - type: string - description: - workflow_changed_by_type: - type: string - enum: - - SYSTEM - - USER - - API - - AUTOMATION - description: - workflow_closure_reason: - type: string `NO_REASON`, `RESOLVED`, `RESOLVED_BENIGN_KNOWN_GOOD`, `DUPLICATE_CLEANUP`, `OTHER` - description: A more detailed description of why the alert was resolved - workflow_status: - type: string - enum: - - OPEN - - IN_PROGRESS - - CLOSED - description: primary value used to determine if the alert is active or inactive and displayed in the UI by default \ No newline at end of file diff --git a/src/cbc_sdk/platform/models/alert_host_based_firewall.yaml b/src/cbc_sdk/platform/models/alert_host_based_firewall.yaml deleted file mode 100644 index 5fb5cabb..00000000 --- a/src/cbc_sdk/platform/models/alert_host_based_firewall.yaml +++ /dev/null @@ -1,485 +0,0 @@ -type: object -x-options: - - nodocstring -properties: - additional_events_present: - type: boolean - description: Indicator to let API and forwarder users know that they should look up other associated events related to this alert - alert_notes_present: - type: boolean - description: True if notes are present on the alert ID. False if notes are not present. - alert_url: - type: string - description: Link to the alerts page for this alert. Does not vary by alert type - backend_timestamp: - type: string - format: date-time - description: Timestamp when the Carbon Black Cloud processed and enabled the alert for searching. Corresponds to the Created column on the Alerts page. - backend_update_timestamp: - type: string - format: date-time - description: Timestamp when the Carbon Black Cloud initiated and processed an update to an alert. Corresponds to the Updated column on the Alerts page. Note that changes made by users do not change this date; those changes are reflected on `user_update_timestamp` - blocked_effective_reputation: - type: string - enum: - - ADAPTIVE_WHITE_LIST - - COMMON_WHITE_LIST - - COMPANY_BLACK_LIST - - COMPANY_WHITE_LIST - - PUP - - TRUSTED_WHITE_LIST - - RESOLVING - - COMPROMISED_OBSOLETE - - DLP_OBSOLETE - - IGNORE - - ADWARE - - HEURISTIC - - SUSPECT_MALWARE - - KNOWN_MALWARE - - ADMIN_RESTRICT_OBSOLETE - - NOT_LISTED - - GRAY_OBSOLETE - - NOT_COMPANY_WHITE_OBSOLETE - - LOCAL_WHITE - - NOT_SUPPORTED - description: Effective reputation of the blocked file or process; applied by the sensor at the time the block occurred - blocked_md5: - type: string - description: MD5 hash of the child process binary; for any process terminated by the sensor - blocked_name: - type: string - description: Tokenized file path of the files blocked by sensor action - blocked_sha256: - type: string - description: SHA-256 hash of the child process binary; for any process terminated by the sensor - category: - type: string - description: Alert category - Monitored vs Threat - enum: - - THREAT - - MONITORED - - INFO - - MINOR - - SERIOUS - - CRITICAL - childproc_cmdline: - type: string - description: Command line for the child process - childproc_effective_reputation: - type: string - enum: - - ADAPTIVE_WHITE_LIST - - COMMON_WHITE_LIST - - COMPANY_BLACK_LIST - - COMPANY_WHITE_LIST - - PUP - - TRUSTED_WHITE_LIST - - RESOLVING - - COMPROMISED_OBSOLETE - - DLP_OBSOLETE - - IGNORE - - ADWARE - - HEURISTIC - - SUSPECT_MALWARE - - KNOWN_MALWARE - - ADMIN_RESTRICT_OBSOLETE - - NOT_LISTED - - GRAY_OBSOLETE - - NOT_COMPANY_WHITE_OBSOLETE - - LOCAL_WHITE - - NOT_SUPPORTED - description: Effective reputation of the child process; applied by the sensor at the time the event occurred - childproc_guid: - type: string - description: Unique process identifier assigned to the child process - childproc_md5: - type: string - description: Hash of the child process' binary (Enterprise EDR) - childproc_name: - type: string - description: Filesystem path of the child process' binary - childproc_sha256: - type: string - description: Hash of the child process' binary (Endpoint Standard) - childproc_username: - type: string - description: User context in which the child process was executed - detection_timestamp: - type: string - format: date-time - description: For sensor-sent alerts, this is the time of the event on the sensor. For alerts generated on the backend, this is the time the backend system triggered the alert. - determination: - description: User-updatable determination of the alert - type: object - properties: - change_timestamp: - type: string - format: date-time - description: When the determination was updated. - changed_by: - type: string - description: User the determination was changed by. - changed_by_type: - type: string - description: - enum: - - SYSTEM - - USER - - API - - AUTOMATION - value: - type: string - description: Determination of the alert set by a user - enum: - - NONE - - TRUE_POSITIVE - - FALSE_POSITIVE - device_external_ip: - type: string - description: IP address of the endpoint according to the Carbon Black Cloud; can differ from device_internal_ip due to network proxy or NAT; either IPv4 (dotted decimal notation) or IPv6 (proprietary format) - device_id: - type: integer - format: int64 - description: ID of devices - device_internal_ip: - type: string - description: IP address of the endpoint reported by the sensor; either IPv4 (dotted decimal notation) or IPv6 (proprietary format) - device_location: - type: string - enum: - - ONSITE - - OFFSITE - - UNKNOWN - description: Whether the device was on or off premises when the alert started, based on the current IP address and the device’s registered DNS domain suffix - device_name: - type: string - description: Device name - device_os: - type: string - enum: - - WINDOWS - - ANDROID - - MAC - - LINUX - - OTHER - description: Device Operating Systems - device_os_version: - type: string - example: Windows 10 x64 - description: The operating system and version of the endpoint. Requires Windows CBC sensor version 3.5 or later. - device_policy: - type: string - description: Device policy - device_policy_id: - type: integer - format: int64 - description: Device policy id - device_target_value: - type: string - enum: - - LOW - - MEDIUM - - HIGH - - MISSION_CRITICAL - description: Target value assigned to the device, set from the policy - device_uem_id: - type: string - description: Device correlation with WS1/EUC, required for our Workspace ONE Intelligence integration to function - device_username: - type: string - description: Logged on user during the alert. This is filled on a best-effort - approach. If the user is not available it may be populated with the device - owner (empty for Container Runtime alerts) - first_event_timestamp: - type: string - format: date-time - description: Timestamp when the first event in the alert occurred - id: - type: string - description: Unique IDs of alerts - is_updated: - type: boolean - description: Boolean that describes whether or not this is the original copy of the alert - last_event_timestamp: - type: string - format: date-time - description: Timestamp when the last event in the alert occurred - netconn_local_ip: - type: string - description: IP address of the remote side of the network connection; stored as dotted decimal - netconn_local_ipv4: - type: string - description: IPv4 address of the local side of the network connection; stored as a dotted decimal. Only one of ipv4 and ipv6 fields will be populated. - netconn_local_ipv6: - type: string - description: IPv6 address of the local side of the network connection; stored as a string without octet-separating colon characters. Only one of ipv4 and ipv6 fields will be populated. - netconn_local_port: - type: integer - format: int64 - description: TCP or UDP port used by the local side of the network connection - netconn_protocol: - type: string - description: Network protocol of the network connection - netconn_remote_domain: - type: string - description: Domain name (FQDN) associated with the remote end of the network connection, if available - netconn_remote_ip: - type: string - description: IP address of the local side of the network connection; stored as dotted decimal - netconn_remote_ipv4: - type: string - description: IPv4 address of the remote side of the network connection; stored as dotted decimal. Only one of ipv4 and ipv6 fields will be populated. - netconn_remote_ipv6: - type: string - description: IPv6 address of the remote side of the network connection; stored as a string without octet-separating colon characters. Only one of ipv4 and ipv6 fields will be populated. - netconn_remote_port: - type: integer - format: int64 - description: TCP or UDP port used by the remote side of the network connection; same as netconn_port and event_network_remote_port - org_key: - type: string - description: Unique alphanumeric string that identifies your organization in the Carbon Black Cloud - parent_cmdline: - type: string - description: Command line of the parent process - parent_effective_reputation: - type: string - enum: - - ADAPTIVE_WHITE_LIST - - COMMON_WHITE_LIST - - COMPANY_BLACK_LIST - - COMPANY_WHITE_LIST - - PUP - - TRUSTED_WHITE_LIST - - RESOLVING - - COMPROMISED_OBSOLETE - - DLP_OBSOLETE - - IGNORE - - ADWARE - - HEURISTIC - - SUSPECT_MALWARE - - KNOWN_MALWARE - - ADMIN_RESTRICT_OBSOLETE - - NOT_LISTED - - GRAY_OBSOLETE - - NOT_COMPANY_WHITE_OBSOLETE - - LOCAL_WHITE - - NOT_SUPPORTED - description: Effective reputation of the parent process; applied by the sensor when the event occurred - parent_guid: - type: string - description: Unique process identifier assigned to the parent process - parent_md5: - type: string - description: MD5 hash of the parent process binary - parent_name: - type: string - description: Filesystem path of the parent process binary - parent_pid: - type: integer - format: int64 - description: Identifier assigned by the operating system to the parent process - parent_reputation: - type: string - enum: - - ADAPTIVE_WHITE_LIST - - COMMON_WHITE_LIST - - COMPANY_BLACK_LIST - - COMPANY_WHITE_LIST - - PUP - - TRUSTED_WHITE_LIST - - RESOLVING - - COMPROMISED_OBSOLETE - - DLP_OBSOLETE - - IGNORE - - ADWARE - - HEURISTIC - - SUSPECT_MALWARE - - KNOWN_MALWARE - - ADMIN_RESTRICT_OBSOLETE - - NOT_LISTED - - GRAY_OBSOLETE - - NOT_COMPANY_WHITE_OBSOLETE - - LOCAL_WHITE - - NOT_SUPPORTED - description: Reputation of the parent process; applied by the Carbon Black Cloud when the event is initially processed - parent_sha256: - type: string - description: SHA-256 hash of the parent process binary - parent_username: - type: string - description: User context in which the parent process was executed - policy_applied: - type: string - enum: - - APPLIED - - NOT_APPLIED - description: Indicates whether or not a policy has been applied to any event associated with this alert - primary_event_id: - type: string - description: ID of the primary event in the alert - process_cmdline: - type: string - description: Command line executed by the actor process - process_effective_reputation: - type: string - enum: - - ADAPTIVE_WHITE_LIST - - COMMON_WHITE_LIST - - COMPANY_BLACK_LIST - - COMPANY_WHITE_LIST - - PUP - - TRUSTED_WHITE_LIST - - RESOLVING - - COMPROMISED_OBSOLETE - - DLP_OBSOLETE - - IGNORE - - ADWARE - - HEURISTIC - - SUSPECT_MALWARE - - KNOWN_MALWARE - - ADMIN_RESTRICT_OBSOLETE - - NOT_LISTED - - GRAY_OBSOLETE - - NOT_COMPANY_WHITE_OBSOLETE - - LOCAL_WHITE - - NOT_SUPPORTED - description: Effective reputation of the actor hash - process_guid: - type: string - description: Guid of the process that has fired the alert (optional) - process_issuer: - type: string - description: - process_md5: - type: string - description: MD5 hash of the actor process binary - process_name: - type: string - description: Process names of an alert - process_pid: - type: integer - format: int64 - description: PID of the process that has fired the alert (optional) - process_publisher: - type: string - description: - process_reputation: - type: string - enum: - - ADAPTIVE_WHITE_LIST - - COMMON_WHITE_LIST - - COMPANY_BLACK_LIST - - COMPANY_WHITE_LIST - - PUP - - TRUSTED_WHITE_LIST - - RESOLVING - - COMPROMISED_OBSOLETE - - DLP_OBSOLETE - - IGNORE - - ADWARE - - HEURISTIC - - SUSPECT_MALWARE - - KNOWN_MALWARE - - ADMIN_RESTRICT_OBSOLETE - - NOT_LISTED - - GRAY_OBSOLETE - - NOT_COMPANY_WHITE_OBSOLETE - - LOCAL_WHITE - - NOT_SUPPORTED - description: Reputation of the actor process; applied when event is processed by the Carbon Black Cloud - process_sha256: - type: string - description: SHA-256 hash of the actor process binary - process_username: - type: string - description: User context in which the actor process was executed. MacOS - all users for the PID for fork() and exec() transitions. Linux - process user for exec() events, but in a future sensor release can be multi-valued due to setuid(). - reason: - type: string - description: A spoken language written explanation of the what and why the alert occurred and any action taken, usually consisting of 1 to 3 sentences. - reason_code: - type: string - description: A unique short-hand code or GUID identifying the particular alert reason - rule_category_id: - type: string - description: ID representing the category of the rule_id for certain alert types - rule_id: - type: string - description: ID of the rule that triggered an alert; applies to Intrusion Detection System, Host-Based Firewall, TAU Intelligence, and USB Device Control alerts - run_state: - type: string - enum: - - DID_NOT_RUN - - RAN - - UNKNOWN - description: Whether the threat in the alert actually ran - sensor_action: - type: string - enum: - - ALLOW - - ALLOW_AND_LOG - - DENY - - TERMINATE - description: Actions taken by the sensor, according to the rules of a policy - severity: - type: integer - format: int64 - description: integer representation of the impact of alert if true positive - tags: - type: array - description: Tags added to the threat ID of the alert - items: - type: string - threat_id: - type: string - description: ID assigned to a group of alerts with common criteria, based on alert type - threat_notes_present: - type: boolean - description: True if notes are present on the threat ID. False if notes are not present. - type: - type: string - enum: - - CB_ANALYTICS - - WATCHLIST - - DEVICE_CONTROL - - CONTAINER_RUNTIME - - HOST_BASED_FIREWALL - - INTRUSION_DETECTION_SYSTEM - - NETWORK_TRAFFIC_ANALYSIS - description: Type of alert generated - user_update_timestamp: - type: string - format: date-time - description: Timestamp of the last property of an alert changed by a user, such as the alert workflow or determination - workflow: - type: object - description: Current workflow state of an alert. The workflow represents the flow from `OPEN` to `IN_PROGRESS` to `CLOSED` and captures who moved the alert into the current state. The history of these state transitions is available via the alert history route. - properties: - change_timestamp: - type: string - format: date-time - description: When the last status change occurred - workflow_changed_by: - type: string - description: Who (or what) made the last status change - workflow_changed_by_rule_id: - type: string - description: - workflow_changed_by_type: - type: string - enum: - - SYSTEM - - USER - - API - - AUTOMATION - description: - workflow_closure_reason: - type: string `NO_REASON`, `RESOLVED`, `RESOLVED_BENIGN_KNOWN_GOOD`, `DUPLICATE_CLEANUP`, `OTHER` - description: A more detailed description of why the alert was resolved - workflow_status: - type: string - enum: - - OPEN - - IN_PROGRESS - - CLOSED - description: primary value used to determine if the alert is active or inactive and displayed in the UI by default \ No newline at end of file diff --git a/src/cbc_sdk/platform/models/alert_intrusion_detection_system.yaml b/src/cbc_sdk/platform/models/alert_intrusion_detection_system.yaml deleted file mode 100644 index 02c1e97a..00000000 --- a/src/cbc_sdk/platform/models/alert_intrusion_detection_system.yaml +++ /dev/null @@ -1,500 +0,0 @@ -type: object -x-options: - - nodocstring -properties: - additional_events_present: - type: boolean - description: Indicator to let API and forwarder users know that they should look up other associated events related to this alert - alert_notes_present: - type: boolean - description: True if notes are present on the alert ID. False if notes are not present. - alert_url: - type: string - description: Link to the alerts page for this alert. Does not vary by alert type - attack_tactic: - type: string - description: A tactic from the MITRE ATT&CK framework; defines a reason for an adversary’s action, such as achieving credential access - attack_technique: - type: string - description: A technique from the MITRE ATT&CK framework; defines an action an adversary takes to accomplish a goal, such as dumping credentials to achieve credential access - backend_timestamp: - type: string - format: date-time - description: Timestamp when the Carbon Black Cloud processed and enabled the alert for searching. Corresponds to the Created column on the Alerts page. - backend_update_timestamp: - type: string - format: date-time - description: Timestamp when the Carbon Black Cloud initiated and processed an update to an alert. Corresponds to the Updated column on the Alerts page. Note that changes made by users do not change this date; those changes are reflected on `user_update_timestamp` - blocked_effective_reputation: - type: string - enum: - - ADAPTIVE_WHITE_LIST - - COMMON_WHITE_LIST - - COMPANY_BLACK_LIST - - COMPANY_WHITE_LIST - - PUP - - TRUSTED_WHITE_LIST - - RESOLVING - - COMPROMISED_OBSOLETE - - DLP_OBSOLETE - - IGNORE - - ADWARE - - HEURISTIC - - SUSPECT_MALWARE - - KNOWN_MALWARE - - ADMIN_RESTRICT_OBSOLETE - - NOT_LISTED - - GRAY_OBSOLETE - - NOT_COMPANY_WHITE_OBSOLETE - - LOCAL_WHITE - - NOT_SUPPORTED - description: Effective reputation of the blocked file or process; applied by the sensor at the time the block occurred - blocked_md5: - type: string - description: MD5 hash of the child process binary; for any process terminated by the sensor - blocked_name: - type: string - description: Tokenized file path of the files blocked by sensor action - blocked_sha256: - type: string - description: SHA-256 hash of the child process binary; for any process terminated by the sensor - category: - type: string - description: Alert category - Monitored vs Threat - enum: - - THREAT - - MONITORED - - INFO - - MINOR - - SERIOUS - - CRITICAL - childproc_cmdline: - type: string - description: Command line for the child process - childproc_effective_reputation: - type: string - enum: - - ADAPTIVE_WHITE_LIST - - COMMON_WHITE_LIST - - COMPANY_BLACK_LIST - - COMPANY_WHITE_LIST - - PUP - - TRUSTED_WHITE_LIST - - RESOLVING - - COMPROMISED_OBSOLETE - - DLP_OBSOLETE - - IGNORE - - ADWARE - - HEURISTIC - - SUSPECT_MALWARE - - KNOWN_MALWARE - - ADMIN_RESTRICT_OBSOLETE - - NOT_LISTED - - GRAY_OBSOLETE - - NOT_COMPANY_WHITE_OBSOLETE - - LOCAL_WHITE - - NOT_SUPPORTED - description: Effective reputation of the child process; applied by the sensor at the time the event occurred - childproc_guid: - type: string - description: Unique process identifier assigned to the child process - childproc_md5: - type: string - description: Hash of the child process' binary (Enterprise EDR) - childproc_name: - type: string - description: Filesystem path of the child process' binary - childproc_sha256: - type: string - description: Hash of the child process' binary (Endpoint Standard) - childproc_username: - type: string - description: User context in which the child process was executed - detection_timestamp: - type: string - format: date-time - description: For sensor-sent alerts, this is the time of the event on the sensor. For alerts generated on the backend, this is the time the backend system triggered the alert. - determination: - description: User-updatable determination of the alert - type: object - properties: - change_timestamp: - type: string - format: date-time - description: When the determination was updated. - changed_by: - type: string - description: User the determination was changed by. - changed_by_type: - type: string - description: - enum: - - SYSTEM - - USER - - API - - AUTOMATION - value: - type: string - description: Determination of the alert set by a user - enum: - - NONE - - TRUE_POSITIVE - - FALSE_POSITIVE - device_external_ip: - type: string - description: IP address of the endpoint according to the Carbon Black Cloud; can differ from device_internal_ip due to network proxy or NAT; either IPv4 (dotted decimal notation) or IPv6 (proprietary format) - device_id: - type: integer - format: int64 - description: ID of devices - device_internal_ip: - type: string - description: IP address of the endpoint reported by the sensor; either IPv4 (dotted decimal notation) or IPv6 (proprietary format) - device_location: - type: string - enum: - - ONSITE - - OFFSITE - - UNKNOWN - description: Whether the device was on or off premises when the alert started, based on the current IP address and the device’s registered DNS domain suffix - device_name: - type: string - description: Device name - device_os: - type: string - enum: - - WINDOWS - - ANDROID - - MAC - - LINUX - - OTHER - description: Device Operating Systems - device_os_version: - type: string - example: Windows 10 x64 - description: The operating system and version of the endpoint. Requires Windows CBC sensor version 3.5 or later. - device_policy: - type: string - description: Device policy - device_policy_id: - type: integer - format: int64 - description: Device policy id - device_target_value: - type: string - enum: - - LOW - - MEDIUM - - HIGH - - MISSION_CRITICAL - description: Target value assigned to the device, set from the policy - device_uem_id: - type: string - description: Device correlation with WS1/EUC, required for our Workspace ONE Intelligence integration to function - device_username: - type: string - description: Logged on user during the alert. This is filled on a best-effort - approach. If the user is not available it may be populated with the device - owner (empty for Container Runtime alerts) - first_event_timestamp: - type: string - format: date-time - description: Timestamp when the first event in the alert occurred - id: - type: string - description: Unique IDs of alerts - is_updated: - type: boolean - description: Boolean that describes whether or not this is the original copy of the alert - last_event_timestamp: - type: string - format: date-time - description: Timestamp when the last event in the alert occurred - netconn_local_ip: - type: string - description: IP address of the remote side of the network connection; stored as dotted decimal - netconn_local_ipv4: - type: string - description: IPv4 address of the local side of the network connection; stored as a dotted decimal. Only one of ipv4 and ipv6 fields will be populated. - netconn_local_ipv6: - type: string - description: IPv6 address of the local side of the network connection; stored as a string without octet-separating colon characters. Only one of ipv4 and ipv6 fields will be populated. - netconn_local_port: - type: integer - format: int64 - description: TCP or UDP port used by the local side of the network connection - netconn_protocol: - type: string - description: Network protocol of the network connection - netconn_remote_domain: - type: string - description: Domain name (FQDN) associated with the remote end of the network connection, if available - netconn_remote_ip: - type: string - description: IP address of the local side of the network connection; stored as dotted decimal - netconn_remote_ipv4: - type: string - description: IPv4 address of the remote side of the network connection; stored as dotted decimal. Only one of ipv4 and ipv6 fields will be populated. - netconn_remote_ipv6: - type: string - description: IPv6 address of the remote side of the network connection; stored as a string without octet-separating colon characters. Only one of ipv4 and ipv6 fields will be populated. - netconn_remote_port: - type: integer - format: int64 - description: TCP or UDP port used by the remote side of the network connection; same as netconn_port and event_network_remote_port - org_key: - type: string - description: Unique alphanumeric string that identifies your organization in the Carbon Black Cloud - parent_cmdline: - type: string - description: Command line of the parent process - parent_effective_reputation: - type: string - enum: - - ADAPTIVE_WHITE_LIST - - COMMON_WHITE_LIST - - COMPANY_BLACK_LIST - - COMPANY_WHITE_LIST - - PUP - - TRUSTED_WHITE_LIST - - RESOLVING - - COMPROMISED_OBSOLETE - - DLP_OBSOLETE - - IGNORE - - ADWARE - - HEURISTIC - - SUSPECT_MALWARE - - KNOWN_MALWARE - - ADMIN_RESTRICT_OBSOLETE - - NOT_LISTED - - GRAY_OBSOLETE - - NOT_COMPANY_WHITE_OBSOLETE - - LOCAL_WHITE - - NOT_SUPPORTED - description: Effective reputation of the parent process; applied by the sensor when the event occurred - parent_guid: - type: string - description: Unique process identifier assigned to the parent process - parent_md5: - type: string - description: MD5 hash of the parent process binary - parent_name: - type: string - description: Filesystem path of the parent process binary - parent_pid: - type: integer - format: int64 - description: Identifier assigned by the operating system to the parent process - parent_reputation: - type: string - enum: - - ADAPTIVE_WHITE_LIST - - COMMON_WHITE_LIST - - COMPANY_BLACK_LIST - - COMPANY_WHITE_LIST - - PUP - - TRUSTED_WHITE_LIST - - RESOLVING - - COMPROMISED_OBSOLETE - - DLP_OBSOLETE - - IGNORE - - ADWARE - - HEURISTIC - - SUSPECT_MALWARE - - KNOWN_MALWARE - - ADMIN_RESTRICT_OBSOLETE - - NOT_LISTED - - GRAY_OBSOLETE - - NOT_COMPANY_WHITE_OBSOLETE - - LOCAL_WHITE - - NOT_SUPPORTED - description: Reputation of the parent process; applied by the Carbon Black Cloud when the event is initially processed - parent_sha256: - type: string - description: SHA-256 hash of the parent process binary - parent_username: - type: string - description: User context in which the parent process was executed - policy_applied: - type: string - enum: - - APPLIED - - NOT_APPLIED - description: Indicates whether or not a policy has been applied to any event associated with this alert - primary_event_id: - type: string - description: ID of the primary event in the alert - process_cmdline: - type: string - description: Command line executed by the actor process - process_effective_reputation: - type: string - enum: - - ADAPTIVE_WHITE_LIST - - COMMON_WHITE_LIST - - COMPANY_BLACK_LIST - - COMPANY_WHITE_LIST - - PUP - - TRUSTED_WHITE_LIST - - RESOLVING - - COMPROMISED_OBSOLETE - - DLP_OBSOLETE - - IGNORE - - ADWARE - - HEURISTIC - - SUSPECT_MALWARE - - KNOWN_MALWARE - - ADMIN_RESTRICT_OBSOLETE - - NOT_LISTED - - GRAY_OBSOLETE - - NOT_COMPANY_WHITE_OBSOLETE - - LOCAL_WHITE - - NOT_SUPPORTED - description: Effective reputation of the actor hash - process_guid: - type: string - description: Guid of the process that has fired the alert (optional) - process_issuer: - type: string - description: - process_md5: - type: string - description: MD5 hash of the actor process binary - process_name: - type: string - description: Process names of an alert - process_pid: - type: integer - format: int64 - description: PID of the process that has fired the alert (optional) - process_publisher: - type: string - description: - process_reputation: - type: string - enum: - - ADAPTIVE_WHITE_LIST - - COMMON_WHITE_LIST - - COMPANY_BLACK_LIST - - COMPANY_WHITE_LIST - - PUP - - TRUSTED_WHITE_LIST - - RESOLVING - - COMPROMISED_OBSOLETE - - DLP_OBSOLETE - - IGNORE - - ADWARE - - HEURISTIC - - SUSPECT_MALWARE - - KNOWN_MALWARE - - ADMIN_RESTRICT_OBSOLETE - - NOT_LISTED - - GRAY_OBSOLETE - - NOT_COMPANY_WHITE_OBSOLETE - - LOCAL_WHITE - - NOT_SUPPORTED - description: Reputation of the actor process; applied when event is processed by the Carbon Black Cloud - process_sha256: - type: string - description: SHA-256 hash of the actor process binary - process_username: - type: string - description: User context in which the actor process was executed. MacOS - all users for the PID for fork() and exec() transitions. Linux - process user for exec() events, but in a future sensor release can be multi-valued due to setuid(). - reason: - type: string - description: A spoken language written explanation of the what and why the alert occurred and any action taken, usually consisting of 1 to 3 sentences. - reason_code: - type: string - description: A unique short-hand code or GUID identifying the particular alert reason - rule_category_id: - type: string - description: ID representing the category of the rule_id for certain alert types - rule_id: - type: string - description: ID of the rule that triggered an alert; applies to Intrusion Detection System, Host-Based Firewall, TAU Intelligence, and USB Device Control alerts - run_state: - type: string - enum: - - DID_NOT_RUN - - RAN - - UNKNOWN - description: Whether the threat in the alert actually ran - sensor_action: - type: string - enum: - - ALLOW - - ALLOW_AND_LOG - - DENY - - TERMINATE - description: Actions taken by the sensor, according to the rules of a policy - severity: - type: integer - format: int64 - description: integer representation of the impact of alert if true positive - tags: - type: array - description: Tags added to the threat ID of the alert - items: - type: string - threat_id: - type: string - description: ID assigned to a group of alerts with common criteria, based on alert type - threat_name: - type: string - description: Name of the threat - threat_notes_present: - type: boolean - description: True if notes are present on the threat ID. False if notes are not present. - tms_rule_id: - type: string - description: Detection id - ttps: - type: string - description: Other potential malicious activities involved in a threat - type: - type: string - enum: - - CB_ANALYTICS - - WATCHLIST - - DEVICE_CONTROL - - CONTAINER_RUNTIME - - HOST_BASED_FIREWALL - - INTRUSION_DETECTION_SYSTEM - - NETWORK_TRAFFIC_ANALYSIS - description: Type of alert generated - user_update_timestamp: - type: string - format: date-time - description: Timestamp of the last property of an alert changed by a user, such as the alert workflow or determination - workflow: - type: object - description: Current workflow state of an alert. The workflow represents the flow from `OPEN` to `IN_PROGRESS` to `CLOSED` and captures who moved the alert into the current state. The history of these state transitions is available via the alert history route. - properties: - change_timestamp: - type: string - format: date-time - description: When the last status change occurred - workflow_changed_by: - type: string - description: Who (or what) made the last status change - workflow_changed_by_rule_id: - type: string - description: - workflow_changed_by_type: - type: string - enum: - - SYSTEM - - USER - - API - - AUTOMATION - description: - workflow_closure_reason: - type: string `NO_REASON`, `RESOLVED`, `RESOLVED_BENIGN_KNOWN_GOOD`, `DUPLICATE_CLEANUP`, `OTHER` - description: A more detailed description of why the alert was resolved - workflow_status: - type: string - enum: - - OPEN - - IN_PROGRESS - - CLOSED - description: primary value used to determine if the alert is active or inactive and displayed in the UI by default \ No newline at end of file diff --git a/src/cbc_sdk/platform/models/alert_watchlist.yaml b/src/cbc_sdk/platform/models/alert_watchlist.yaml deleted file mode 100644 index e2d9eb21..00000000 --- a/src/cbc_sdk/platform/models/alert_watchlist.yaml +++ /dev/null @@ -1,545 +0,0 @@ -type: object -x-options: - - nodocstring -properties: - additional_events_present: - type: boolean - description: Indicator to let API and forwarder users know that they should look up other associated events related to this alert - alert_notes_present: - type: boolean - description: True if notes are present on the alert ID. False if notes are not present. - alert_url: - type: string - description: Link to the alerts page for this alert. Does not vary by alert type - attack_tactic: - type: string - description: A tactic from the MITRE ATT&CK framework; defines a reason for an adversary’s action, such as achieving credential access - attack_technique: - type: string - description: A technique from the MITRE ATT&CK framework; defines an action an adversary takes to accomplish a goal, such as dumping credentials to achieve credential access - backend_timestamp: - type: string - format: date-time - description: Timestamp when the Carbon Black Cloud processed and enabled the alert for searching. Corresponds to the Created column on the Alerts page. - backend_update_timestamp: - type: string - format: date-time - description: Timestamp when the Carbon Black Cloud initiated and processed an update to an alert. Corresponds to the Updated column on the Alerts page. Note that changes made by users do not change this date; those changes are reflected on `user_update_timestamp` - blocked_effective_reputation: - type: string - enum: - - ADAPTIVE_WHITE_LIST - - COMMON_WHITE_LIST - - COMPANY_BLACK_LIST - - COMPANY_WHITE_LIST - - PUP - - TRUSTED_WHITE_LIST - - RESOLVING - - COMPROMISED_OBSOLETE - - DLP_OBSOLETE - - IGNORE - - ADWARE - - HEURISTIC - - SUSPECT_MALWARE - - KNOWN_MALWARE - - ADMIN_RESTRICT_OBSOLETE - - NOT_LISTED - - GRAY_OBSOLETE - - NOT_COMPANY_WHITE_OBSOLETE - - LOCAL_WHITE - - NOT_SUPPORTED - description: Effective reputation of the blocked file or process; applied by the sensor at the time the block occurred - blocked_md5: - type: string - description: MD5 hash of the child process binary; for any process terminated by the sensor - blocked_name: - type: string - description: Tokenized file path of the files blocked by sensor action - blocked_sha256: - type: string - description: SHA-256 hash of the child process binary; for any process terminated by the sensor - category: - type: string - description: Alert category - Monitored vs Threat - enum: - - THREAT - - MONITORED - - INFO - - MINOR - - SERIOUS - - CRITICAL - childproc_cmdline: - type: string - description: Command line for the child process - childproc_effective_reputation: - type: string - enum: - - ADAPTIVE_WHITE_LIST - - COMMON_WHITE_LIST - - COMPANY_BLACK_LIST - - COMPANY_WHITE_LIST - - PUP - - TRUSTED_WHITE_LIST - - RESOLVING - - COMPROMISED_OBSOLETE - - DLP_OBSOLETE - - IGNORE - - ADWARE - - HEURISTIC - - SUSPECT_MALWARE - - KNOWN_MALWARE - - ADMIN_RESTRICT_OBSOLETE - - NOT_LISTED - - GRAY_OBSOLETE - - NOT_COMPANY_WHITE_OBSOLETE - - LOCAL_WHITE - - NOT_SUPPORTED - description: Effective reputation of the child process; applied by the sensor at the time the event occurred - childproc_guid: - type: string - description: Unique process identifier assigned to the child process - childproc_md5: - type: string - description: Hash of the child process' binary (Enterprise EDR) - childproc_name: - type: string - description: Filesystem path of the child process' binary - childproc_sha256: - type: string - description: Hash of the child process' binary (Endpoint Standard) - childproc_username: - type: string - description: User context in which the child process was executed - detection_timestamp: - type: string - format: date-time - description: For sensor-sent alerts, this is the time of the event on the sensor. For alerts generated on the backend, this is the time the backend system triggered the alert. - determination: - description: User-updatable determination of the alert - type: object - properties: - change_timestamp: - type: string - format: date-time - description: When the determination was updated. - changed_by: - type: string - description: User the determination was changed by. - changed_by_type: - type: string - description: - enum: - - SYSTEM - - USER - - API - - AUTOMATION - value: - type: string - description: Determination of the alert set by a user - enum: - - NONE - - TRUE_POSITIVE - - FALSE_POSITIVE - device_external_ip: - type: string - description: IP address of the endpoint according to the Carbon Black Cloud; can differ from device_internal_ip due to network proxy or NAT; either IPv4 (dotted decimal notation) or IPv6 (proprietary format) - device_id: - type: integer - format: int64 - description: ID of devices - device_internal_ip: - type: string - description: IP address of the endpoint reported by the sensor; either IPv4 (dotted decimal notation) or IPv6 (proprietary format) - device_location: - type: string - enum: - - ONSITE - - OFFSITE - - UNKNOWN - description: Whether the device was on or off premises when the alert started, based on the current IP address and the device’s registered DNS domain suffix - device_name: - type: string - description: Device name - device_os: - type: string - enum: - - WINDOWS - - ANDROID - - MAC - - LINUX - - OTHER - description: Device Operating Systems - device_os_version: - type: string - example: Windows 10 x64 - description: The operating system and version of the endpoint. Requires Windows CBC sensor version 3.5 or later. - device_policy: - type: string - description: Device policy - device_policy_id: - type: integer - format: int64 - description: Device policy id - device_target_value: - type: string - enum: - - LOW - - MEDIUM - - HIGH - - MISSION_CRITICAL - description: Target value assigned to the device, set from the policy - device_uem_id: - type: string - description: Device correlation with WS1/EUC, required for our Workspace ONE Intelligence integration to function - device_username: - type: string - description: Logged on user during the alert. This is filled on a best-effort - approach. If the user is not available it may be populated with the device - owner (empty for Container Runtime alerts) - first_event_timestamp: - type: string - format: date-time - description: Timestamp when the first event in the alert occurred - id: - type: string - description: Unique IDs of alerts - ioc_field: - type: string - description: The field the indicator of comprise (IOC) hit contains - ioc_hit: - type: string - description: IOC field value or IOC query that matches - ioc_id: - type: string - description: Unique identifier of the IOC that generated the watchlist hit - is_updated: - type: boolean - description: Boolean that describes whether or not this is the original copy of the alert - last_event_timestamp: - type: string - format: date-time - description: Timestamp when the last event in the alert occurred - ml_classification_final_verdict: - type: string - enum: - - NOT_CLASSIFIED - - NOT_ANOMALOUS - - ANOMALOUS - description: Final verdict of the alert, based on the ML models that were used to make the prediction. - ml_classification_global_prevalence: - type: string - enum: - - UNKNOWN - - LOW - - MEDIUM - - HIGH - description: Categories (low/medium/high) used to describe the prevalence of alerts across all regional organizations. - ml_classification_org_prevalence: - type: string - enum: - - UNKNOWN - - LOW - - MEDIUM - - HIGH - description: Categories (low/medium/high) used to describe the prevalence of alerts within an organization. - netconn_local_ip: - type: string - description: IP address of the remote side of the network connection; stored as dotted decimal - netconn_local_ipv4: - type: string - description: IPv4 address of the local side of the network connection; stored as a dotted decimal. Only one of ipv4 and ipv6 fields will be populated. - netconn_local_ipv6: - type: string - description: IPv6 address of the local side of the network connection; stored as a string without octet-separating colon characters. Only one of ipv4 and ipv6 fields will be populated. - netconn_local_port: - type: integer - format: int64 - description: TCP or UDP port used by the local side of the network connection - netconn_protocol: - type: string - description: Network protocol of the network connection - netconn_remote_domain: - type: string - description: Domain name (FQDN) associated with the remote end of the network connection, if available - netconn_remote_ip: - type: string - description: IP address of the local side of the network connection; stored as dotted decimal - netconn_remote_ipv4: - type: string - description: IPv4 address of the remote side of the network connection; stored as dotted decimal. Only one of ipv4 and ipv6 fields will be populated. - netconn_remote_ipv6: - type: string - description: IPv6 address of the remote side of the network connection; stored as a string without octet-separating colon characters. Only one of ipv4 and ipv6 fields will be populated. - netconn_remote_port: - type: integer - format: int64 - description: TCP or UDP port used by the remote side of the network connection; same as netconn_port and event_network_remote_port - org_key: - type: string - description: Unique alphanumeric string that identifies your organization in the Carbon Black Cloud - parent_cmdline: - type: string - description: Command line of the parent process - parent_effective_reputation: - type: string - enum: - - ADAPTIVE_WHITE_LIST - - COMMON_WHITE_LIST - - COMPANY_BLACK_LIST - - COMPANY_WHITE_LIST - - PUP - - TRUSTED_WHITE_LIST - - RESOLVING - - COMPROMISED_OBSOLETE - - DLP_OBSOLETE - - IGNORE - - ADWARE - - HEURISTIC - - SUSPECT_MALWARE - - KNOWN_MALWARE - - ADMIN_RESTRICT_OBSOLETE - - NOT_LISTED - - GRAY_OBSOLETE - - NOT_COMPANY_WHITE_OBSOLETE - - LOCAL_WHITE - - NOT_SUPPORTED - description: Effective reputation of the parent process; applied by the sensor when the event occurred - parent_guid: - type: string - description: Unique process identifier assigned to the parent process - parent_md5: - type: string - description: MD5 hash of the parent process binary - parent_name: - type: string - description: Filesystem path of the parent process binary - parent_pid: - type: integer - format: int64 - description: Identifier assigned by the operating system to the parent process - parent_reputation: - type: string - enum: - - ADAPTIVE_WHITE_LIST - - COMMON_WHITE_LIST - - COMPANY_BLACK_LIST - - COMPANY_WHITE_LIST - - PUP - - TRUSTED_WHITE_LIST - - RESOLVING - - COMPROMISED_OBSOLETE - - DLP_OBSOLETE - - IGNORE - - ADWARE - - HEURISTIC - - SUSPECT_MALWARE - - KNOWN_MALWARE - - ADMIN_RESTRICT_OBSOLETE - - NOT_LISTED - - GRAY_OBSOLETE - - NOT_COMPANY_WHITE_OBSOLETE - - LOCAL_WHITE - - NOT_SUPPORTED - description: Reputation of the parent process; applied by the Carbon Black Cloud when the event is initially processed - parent_sha256: - type: string - description: SHA-256 hash of the parent process binary - parent_username: - type: string - description: User context in which the parent process was executed - policy_applied: - type: string - enum: - - APPLIED - - NOT_APPLIED - description: Indicates whether or not a policy has been applied to any event associated with this alert - primary_event_id: - type: string - description: ID of the primary event in the alert - process_cmdline: - type: string - description: Command line executed by the actor process - process_effective_reputation: - type: string - enum: - - ADAPTIVE_WHITE_LIST - - COMMON_WHITE_LIST - - COMPANY_BLACK_LIST - - COMPANY_WHITE_LIST - - PUP - - TRUSTED_WHITE_LIST - - RESOLVING - - COMPROMISED_OBSOLETE - - DLP_OBSOLETE - - IGNORE - - ADWARE - - HEURISTIC - - SUSPECT_MALWARE - - KNOWN_MALWARE - - ADMIN_RESTRICT_OBSOLETE - - NOT_LISTED - - GRAY_OBSOLETE - - NOT_COMPANY_WHITE_OBSOLETE - - LOCAL_WHITE - - NOT_SUPPORTED - description: Effective reputation of the actor hash - process_guid: - type: string - description: Guid of the process that has fired the alert (optional) - process_issuer: - type: string - description: - process_md5: - type: string - description: MD5 hash of the actor process binary - process_name: - type: string - description: Process names of an alert - process_pid: - type: integer - format: int64 - description: PID of the process that has fired the alert (optional) - process_publisher: - type: string - description: - process_reputation: - type: string - enum: - - ADAPTIVE_WHITE_LIST - - COMMON_WHITE_LIST - - COMPANY_BLACK_LIST - - COMPANY_WHITE_LIST - - PUP - - TRUSTED_WHITE_LIST - - RESOLVING - - COMPROMISED_OBSOLETE - - DLP_OBSOLETE - - IGNORE - - ADWARE - - HEURISTIC - - SUSPECT_MALWARE - - KNOWN_MALWARE - - ADMIN_RESTRICT_OBSOLETE - - NOT_LISTED - - GRAY_OBSOLETE - - NOT_COMPANY_WHITE_OBSOLETE - - LOCAL_WHITE - - NOT_SUPPORTED - description: Reputation of the actor process; applied when event is processed by the Carbon Black Cloud - process_sha256: - type: string - description: SHA-256 hash of the actor process binary - process_username: - type: string - description: User context in which the actor process was executed. MacOS - all users for the PID for fork() and exec() transitions. Linux - process user for exec() events, but in a future sensor release can be multi-valued due to setuid(). - reason: - type: string - description: A spoken language written explanation of the what and why the alert occurred and any action taken, usually consisting of 1 to 3 sentences. - reason_code: - type: string - description: A unique short-hand code or GUID identifying the particular alert reason - report_description: - type: string - description: Description of the watchlist report associated with the alert - report_id: - type: string - description: Report IDs that contained the IOC that caused a hit - report_link: - type: string - description: Link of reports that contained the IOC that caused a hit - report_name: - type: string - description: Name of the watchlist report - report_tags: - type: string[] - description: Tags associated with the watchlist report - run_state: - type: string - enum: - - DID_NOT_RUN - - RAN - - UNKNOWN - description: Whether the threat in the alert actually ran - sensor_action: - type: string - enum: - - ALLOW - - ALLOW_AND_LOG - - DENY - - TERMINATE - description: Actions taken by the sensor, according to the rules of a policy - severity: - type: integer - format: int64 - description: integer representation of the impact of alert if true positive - tags: - type: array - description: Tags added to the threat ID of the alert - items: - type: string - threat_id: - type: string - description: ID assigned to a group of alerts with common criteria, based on alert type - threat_notes_present: - type: boolean - description: True if notes are present on the threat ID. False if notes are not present. - ttps: - type: string - description: Other potential malicious activities involved in a threat - type: - type: string - enum: - - CB_ANALYTICS - - WATCHLIST - - DEVICE_CONTROL - - CONTAINER_RUNTIME - - HOST_BASED_FIREWALL - - INTRUSION_DETECTION_SYSTEM - - NETWORK_TRAFFIC_ANALYSIS - description: Type of alert generated - user_update_timestamp: - type: string - format: date-time - description: Timestamp of the last property of an alert changed by a user, such as the alert workflow or determination - watchlists: - type: object - description: List of watchlists associated with an alert. Alerts are batched hourly - properties: - id: - type: string - description: - name: - type: string - description: - workflow: - type: object - description: Current workflow state of an alert. The workflow represents the flow from `OPEN` to `IN_PROGRESS` to `CLOSED` and captures who moved the alert into the current state. The history of these state transitions is available via the alert history route. - properties: - change_timestamp: - type: string - format: date-time - description: When the last status change occurred - workflow_changed_by: - type: string - description: Who (or what) made the last status change - workflow_changed_by_rule_id: - type: string - description: - workflow_changed_by_type: - type: string - enum: - - SYSTEM - - USER - - API - - AUTOMATION - description: - workflow_closure_reason: - type: string `NO_REASON`, `RESOLVED`, `RESOLVED_BENIGN_KNOWN_GOOD`, `DUPLICATE_CLEANUP`, `OTHER` - description: A more detailed description of why the alert was resolved - workflow_status: - type: string - enum: - - OPEN - - IN_PROGRESS - - CLOSED - description: primary value used to determine if the alert is active or inactive and displayed in the UI by default \ No newline at end of file From 90294ab207f0da27b1258697739d0c0c946b324e Mon Sep 17 00:00:00 2001 From: vsvvj-cb Date: Tue, 23 Apr 2024 14:09:11 +0530 Subject: [PATCH 39/95] 1. Added support for collapse field parameter for process search job. 2. Added unit test case --- src/cbc_sdk/base.py | 3 +++ src/cbc_sdk/platform/processes.py | 11 +++++++++++ .../unit/platform/test_platform_process.py | 19 +++++++++++++++++++ 3 files changed, 33 insertions(+) diff --git a/src/cbc_sdk/base.py b/src/cbc_sdk/base.py index 47ff45a3..c61a7f80 100644 --- a/src/cbc_sdk/base.py +++ b/src/cbc_sdk/base.py @@ -1870,6 +1870,7 @@ def __init__(self, doc_class, cb): self._time_range = {} self._fields = ["*"] self._default_args = {} + self._collapse_field = [] def _add_exclusions(self, key, newlist): """ @@ -1973,6 +1974,8 @@ def _get_query_parameters(self): if 'process_guid:' in args.get('query', ''): q = args['query'].split('process_guid:', 1)[1].split(' ', 1)[0] args["process_guid"] = q + if self._collapse_field: + args['collapse_field'] = self._collapse_field if args.get("sort", None) is not None and args.get("fields", None) is None: # Add default fields if only sort is specified diff --git a/src/cbc_sdk/platform/processes.py b/src/cbc_sdk/platform/processes.py index 19f4081b..c58a5f8d 100644 --- a/src/cbc_sdk/platform/processes.py +++ b/src/cbc_sdk/platform/processes.py @@ -667,6 +667,17 @@ def set_rows(self, rows): self._batch_size = rows return self + def set_collapse_field(self, field): + """ + Sets the 'collapse_field' query parameter, which queries the file name depending on field + Args: + field (list): query parameters to get file details. + """ + if not isinstance(field, list): + raise ApiError(f"Field must be list. {field} is a {type(field)}.") + self._collapse_field = field + return self + def _submit(self): """ Submits the query to the server. diff --git a/src/tests/unit/platform/test_platform_process.py b/src/tests/unit/platform/test_platform_process.py index ed197f80..7d31a6ff 100644 --- a/src/tests/unit/platform/test_platform_process.py +++ b/src/tests/unit/platform/test_platform_process.py @@ -815,6 +815,25 @@ def test_process_query_start_rows(cbcsdk_mock): assert process_q_params == expected_params assert process._batch_size == 102 +def test_process_query_collapse_field(cbcsdk_mock): + """Testing AsyncProcessQuery.set_collapse_field()""" + api = cbcsdk_mock.api + # use the update methods + process = api.select(Process).where("event_type:modload").add_criteria("device_id", [1234]).add_exclusions( + "crossproc_effective_reputation", ["REP_WHITE"]) + process = process.set_collapse_field(["process_sha256"]) + + process_q_params = process._get_query_parameters() + expected_params = {"query": "event_type:modload", + "criteria": { + "device_id": [1234] + }, + "exclusions": { + "crossproc_effective_reputation": ["REP_WHITE"] + }, + "collapse_field": ["process_sha256"] + } + assert process_q_params == expected_params def test_process_sort_by(cbcsdk_mock): """Testing AsyncProcessQuery.sort_by().""" From 372f31778188c9c78eb39b1fab4b7b4621a2f6de Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Fri, 26 Apr 2024 14:47:45 -0600 Subject: [PATCH 40/95] deflake8'd --- src/cbc_sdk/platform/processes.py | 3 ++- src/tests/unit/platform/test_platform_process.py | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/cbc_sdk/platform/processes.py b/src/cbc_sdk/platform/processes.py index c58a5f8d..4093ab7f 100644 --- a/src/cbc_sdk/platform/processes.py +++ b/src/cbc_sdk/platform/processes.py @@ -669,7 +669,8 @@ def set_rows(self, rows): def set_collapse_field(self, field): """ - Sets the 'collapse_field' query parameter, which queries the file name depending on field + Sets the 'collapse_field' query parameter, which queries the file name depending on field. + Args: field (list): query parameters to get file details. """ diff --git a/src/tests/unit/platform/test_platform_process.py b/src/tests/unit/platform/test_platform_process.py index 7d31a6ff..d6308d44 100644 --- a/src/tests/unit/platform/test_platform_process.py +++ b/src/tests/unit/platform/test_platform_process.py @@ -815,6 +815,7 @@ def test_process_query_start_rows(cbcsdk_mock): assert process_q_params == expected_params assert process._batch_size == 102 + def test_process_query_collapse_field(cbcsdk_mock): """Testing AsyncProcessQuery.set_collapse_field()""" api = cbcsdk_mock.api @@ -835,6 +836,7 @@ def test_process_query_collapse_field(cbcsdk_mock): } assert process_q_params == expected_params + def test_process_sort_by(cbcsdk_mock): """Testing AsyncProcessQuery.sort_by().""" api = cbcsdk_mock.api From 62b48c4c50ee08cb18dda8bc9fe767b67d0ebbfd Mon Sep 17 00:00:00 2001 From: Hristo Karagitliev Date: Fri, 30 Jun 2023 11:30:11 +0300 Subject: [PATCH 41/95] initial commit --- src/cbc_sdk/workload/compliance_assessment.py | 385 ++++++++++++++++++ src/cbc_sdk/workload/models/compliance.yaml | 7 + 2 files changed, 392 insertions(+) create mode 100644 src/cbc_sdk/workload/compliance_assessment.py create mode 100644 src/cbc_sdk/workload/models/compliance.yaml diff --git a/src/cbc_sdk/workload/compliance_assessment.py b/src/cbc_sdk/workload/compliance_assessment.py new file mode 100644 index 00000000..44860822 --- /dev/null +++ b/src/cbc_sdk/workload/compliance_assessment.py @@ -0,0 +1,385 @@ +#!/usr/bin/env python3 + +# ******************************************************* +# Copyright (c) VMware, Inc. 2021-2023. All Rights Reserved. +# SPDX-License-Identifier: MIT +# ******************************************************* +# * +# * DISCLAIMER. THIS PROGRAM IS PROVIDED TO YOU "AS IS" WITHOUT +# * WARRANTIES OR CONDITIONS OF ANY KIND, WHETHER ORAL OR WRITTEN, +# * EXPRESS OR IMPLIED. THE AUTHOR SPECIFICALLY DISCLAIMS ANY IMPLIED +# * WARRANTIES OR CONDITIONS OF MERCHANTABILITY, SATISFACTORY QUALITY, +# * NON-INFRINGEMENT AND FITNESS FOR A PARTICULAR PURPOSE. + +"""Model and Query Classes for Compliance Assessment API""" + +from cbc_sdk.base import (NewBaseModel, BaseQuery, QueryBuilder, QueryBuilderSupportMixin, + IterableQueryMixin, AsyncQueryMixin, UnrefreshableModel) +from cbc_sdk.errors import ApiError, MoreThanOneResultError, ObjectNotFoundError +import logging + +log = logging.getLogger(__name__) + +""" Compliance models """ + + +class ComplianceBenchmark(NewBaseModel): + """ + """ + urlobject = '/compliance/assessment/api/v1/orgs/{}/benchmark_sets/' + swagger_meta_file = "workload/models/compliance.yaml" + primary_key = "benchmark_set_id" + + def __init__(self, cb, model_unique_id=None, initial_data=None): + """ + """ + super(ComplianceBenchmark, self).__init__(cb, model_unique_id, initial_data) + + if model_unique_id is not None and initial_data is None: + self._refresh() + self._full_init = True + + def _refresh(self): + """ + """ + return True + + @classmethod + def _query_implementation(cls, cb, **kwargs): + """ + """ + return ComplianceBenchmarkQuery(cls, cb) + + @staticmethod + def get_org_compliance_schedule(cb): + """ + Gets the compliance scan schedule and timezone for the organization associated with the specified CBCloudAPI instance. + + Args: + cb (CBCloudAPI): An instance of CBCloudAPI representing the Carbon Black Cloud API. + + Raises: + ApiError: If cb is not an instance of CBCloudAPI. + + Returns: + The JSON response from the Carbon Black Cloud API as a Python dictionary. + + Example: + >>> cb = CBCloudAPI(profile="example_profile") + >>> descriptions = AuthEvent.get_auth_events_descriptions(cb) + >>> print(descriptions) + """ + if cb.__class__.__name__ != "CBCloudAPI": + message = "cb argument should be instance of CBCloudAPI." + message += "\nExample:\ncb = CBCloudAPI(profile='example_profile')" + message += "\nCompliance.get_org_settings(cb)" + raise ApiError(message) + + url = f"/compliance/assessment/api/v1/orgs/{cb.credentials.org_key}/settings" + + return cb.get_object(url) + + @staticmethod + def set_org_compliance_schedule(cb, scan_schedule, scan_timezone): + """ + Sets the compliance scan schedule and timezone for the organization associated with the specified CBCloudAPI instance. + + Args: + cb (CBCloudAPI): An instance of CBCloudAPI representing the Carbon Black Cloud API. + scan_schedule (str): The scan schedule, specified in RFC 5545 format. Example: "RRULE:FREQ=DAILY;BYHOUR=17;BYMINUTE=30;BYSECOND=0". + scan_timezone (str): The timezone in which the scan will run, specified as a timezone string. Example: "UTC". + + Raises: + ApiError: If cb is not an instance of CBCloudAPI, or if scan_schedule or scan_timezone are not provided. + + Returns: + The JSON response from the Carbon Black Cloud API as a Python dictionary. + + Example: + >>> cb = CBCloudAPI(profile="example_profile") + >>> response = Compliance.set_org_compliance_schedule(cb, scan_schedule="RRULE:FREQ=DAILY;BYHOUR=17;BYMINUTE=30;BYSECOND=0", scan_timezone="UTC") + >>> print(response) + """ + if cb.__class__.__name__ != "CBCloudAPI": + message = "cb argument should be instance of CBCloudAPI." + message += "\nExample:\ncb = CBCloudAPI(profile='example_profile')" + message += "\nCompliance.set_org_compliance_schedule(cb, scan_schedule='RRULE:FREQ=DAILY;BYHOUR=17;BYMINUTE=30;BYSECOND=0', scan_timezone='UTC')" + raise ApiError(message) + if not scan_schedule or not scan_timezone or scan_schedule == "" or scan_timezone == "": + raise ApiError("scan_schedule and scan_timezone are required. http://developer.carbonblack.com/reference/carbon-black-cloud/cb-liveops/latest/livequery-api/#recurrence-rules") + + args = {"scan_schedule": scan_schedule, "scan_timezone": scan_timezone} + url = f"/compliance/assessment/api/v1/orgs/{cb.credentials.org_key}/settings" + + return cb.put_object(url, body=args).json() + + +class ComplianceBenchmarkQuery(BaseQuery, QueryBuilderSupportMixin, + IterableQueryMixin, AsyncQueryMixin): + """ + """ + def __init__(self, doc_class, cb): + """ + """ + self._doc_class = doc_class + self._cb = cb + self._count_valid = False + super(BaseQuery, self).__init__() + + self._query_builder = QueryBuilder() + self._criteria = {} + self._sortcriteria = {} + self._total_results = 0 + + self._benchmark_set_id = None + self._rule_id = None + + def add_criteria(self, key, value, operator='EQUALS'): + """ + """ + self._update_criteria(key, value, operator) + return self + + def _update_criteria(self, key, value, operator, overwrite=False): + """ + """ + if self._criteria.get(key, None) is None or overwrite: + self._criteria[key] = dict(value=value, operator=operator) + + def _build_request(self, from_row, max_rows, add_sort=True): + """ + Build the request dictionary for the API query. + + Args: + from_row (int): The starting row for the query. + max_rows (int): The maximum number of rows to retrieve. + add_sort (bool): Flag indicating whether to add sorting criteria to the request. + + Returns: + dict: The constructed request dictionary. + """ + request = { + "criteria": self._criteria, + "query": self._query_builder._collapse(), + "rows": 1000 if max_rows < 0 else max_rows + } + + if from_row > 0: + request["start"] = from_row + + if add_sort and self._sortcriteria != {}: + request["sort"] = [self._sortcriteria] + + return request + + def _build_url(self, tail_end): + """ + Build the URL for the API request. + + Args: + tail_end (str): The tail end of the URL to be appended. + + Returns: + str: The constructed URL. + """ + return self._doc_class.urlobject.format(self._cb.credentials.org_key) + tail_end + + def _count(self): + """ + Get the total number of results. + + Returns: + int: The total number of results. + """ + if self._count_valid: + return self._total_results + + url = self._build_url("_search") + request = self._build_request(0, -1) + result = self._cb.post_object(url, body=request).json() + + self._total_results = result["num_found"] + self._count_valid = True + + return self._total_results + + def _perform_query(self, from_row=0, max_rows=-1): + """ + Perform a query and retrieve the results. + + Args: + from_row (int): The starting row index for the query (default is 0). + max_rows (int): The maximum number of rows to retrieve (-1 retrieves all rows). + + Yields: + obj: An instance of the document class containing each query result. + """ + url = self._build_url("_search") + current = from_row + numrows = 0 + while True: + request = self._build_request(current, max_rows) + if self._benchmark_set_id and self._rule_id: + resp = self._cb.get_object(url) + else: + resp = self._cb.post_object(url, body=request) + result = resp.json() + + self._total_results = result["num_found"] + self._count_valid = True + + results = result.get("results", []) + for item in results: + yield self._doc_class(self._cb, initial_data=item) + current += 1 + numrows += 1 + + if max_rows > 0 and numrows == max_rows: + return + + if current >= self._total_results: + return + + def _run_async_query(self, context): + """ + Run an asynchronous query and retrieve the results. + + Args: + context: The context for the query. + + Returns: + dict: The JSON response containing the query results. + """ + url = self._build_url("_search") + request = self._build_request(0, -1) + return self._cb.post_object(url, body=request).json() + + def sort_by(self, key, direction='ASC'): + """ + Set the sort criteria for the search. + + Args: + key (str): The field to sort by. + direction (str, optional): The sort direction. Defaults to "ASC". + Valid values are "ASC" (ascending) and "DESC" (descending). + + Returns: + self: The current instance with the updated sort criteria. + + Raises: + ApiError: If an invalid sort direction is specified. + + Example: + To sort by a field in descending order: + + >>> cb = CBCloudAPI(profile="example_profile") + >>> benchmark_sets = cb.select(ComplianceBenchmark).sort_by("name", direction="DESC") + >>> print(*benchmark_sets) + """ + if direction.upper() not in ('ASC', 'DESC', 'asc', 'desc'): + raise ApiError('invalid sort direction specified') + self._sortcriteria = {'field': key, 'order': direction} + return self + + def set_benchmark_set_id(self, benchmark_set_id): + """ + Set the benchmark set ID for the current instance. + + Args: + benchmark_set_id (str): The ID of the benchmark set to set. + + Returns: + self: The current instance with the updated benchmark set ID. + + Example: + >>> cb = CBCloudAPI(profile="example_profile") + >>> benchmark_set = cb.select(ComplianceBenchmark).set_benchmark_set_id('benchmark123') + >>> print(*benchmark_set) + """ + self._benchmark_set_id = benchmark_set_id + return self + + def search_rules(self, rule_id=None): + """ + Search for rules within the benchmark set. + + Args: + rule_id (str): Optional rule ID to search for. If provided, only the rule with the specified ID will be returned. + + Yields: + Rule document: Yields rule documents that match the search criteria. + + Raises: + ApiError: If the benchmark_set_id is not set. + + Returns: + None + + Example: + >>> cb = CBCloudAPI(profile="example_profile") + >>> benchmark_set = cb.select(ComplianceBenchmark).set_benchmark_set_id('benchmark123') + >>> # To return a single rule document, add the ID. + >>> rule = benchmark_set.search_rules('FDBFA982-2EA2-4720-83CC-E515CEFB795D') + >>> print(*rule) + >>> # To return all rules within a benchmark set, leave empty. + >>> rules = benchmark_set.search_rules() + >>> print(*rules) + """ + if not self._benchmark_set_id: + raise ApiError("internal error: benchmark_set_id is required.") + + if rule_id is not None: + self._rule_id = rule_id + url = self._build_url(f"{self._benchmark_set_id}/rules/{self._rule_id}") + result = self._cb.get_object(url) + yield self._doc_class(self._cb, initial_data=result) + return + + url = self._build_url(f"{self._benchmark_set_id}/rules/_search") + current = 0 + max_rows = 1000 + while True: + request = self._build_request(current, max_rows) + resp = self._cb.post_object(url, body=request) + result = resp.json() + + self._total_results = result["num_found"] + self._count_valid = True + + results = result.get("results", []) + for item in results: + yield self._doc_class(self._cb, initial_data=item) + + current += len(results) + if max_rows > 0 and current >= max_rows: + break + + if current >= self._total_results: + break + + def get_benchmark_set_sections(self): + """ + Retrieves the sections of a compliance benchmark set. + + Returns an iterator over the sections of the benchmark set. Each section is represented as an instance of the + compliance benchmark section model. + + Raises: + ApiError: If the benchmark_set_id is not set. + + Yields: + ComplianceBenchmarkSection: An instance of the compliance benchmark section model. + + Example: + >>> cb = CBCloudAPI(profile="example_profile") + >>> benchmark = cb.select(ComplianceBenchmark).set_benchmark_set_id('benchmark123') + >>> for section in benchmark.get_benchmark_set_sections(): + ... print(section.section_name, section.section_id) + """ + if not self._benchmark_set_id: + raise ApiError("internal error: benchmark_set_id is required.") + + url = self._build_url(f"{self._benchmark_set_id}/sections") + results = self._cb.get_object(url) + for item in results: + yield self._doc_class(self._cb, initial_data=item) diff --git a/src/cbc_sdk/workload/models/compliance.yaml b/src/cbc_sdk/workload/models/compliance.yaml new file mode 100644 index 00000000..bab4ce97 --- /dev/null +++ b/src/cbc_sdk/workload/models/compliance.yaml @@ -0,0 +1,7 @@ +type: object +properties: + affected_assets: + type: array + items: + type: string + description: List of affected assets From 0218425da9680daa824a9933abdcb50eeae0bba6 Mon Sep 17 00:00:00 2001 From: Hristo Karagitliev Date: Wed, 20 Sep 2023 11:55:30 +0300 Subject: [PATCH 42/95] update code --- src/cbc_sdk/workload/__init__.py | 2 + src/cbc_sdk/workload/compliance_assessment.py | 263 ++++++++++-------- 2 files changed, 149 insertions(+), 116 deletions(-) diff --git a/src/cbc_sdk/workload/__init__.py b/src/cbc_sdk/workload/__init__.py index 2039ff70..e92bc689 100644 --- a/src/cbc_sdk/workload/__init__.py +++ b/src/cbc_sdk/workload/__init__.py @@ -3,6 +3,8 @@ from cbc_sdk.workload.vm_workloads_search import VCenterComputeResource, AWSComputeResource from cbc_sdk.workload.sensor_lifecycle import SensorKit from cbc_sdk.workload.nsx_remediation import NSXRemediationJob +from cbc_sdk.workload.compliance_assessment import ComplianceBenchmark + # Maintain link for easier migration from cbc_sdk.platform.vulnerability_assessment import Vulnerability diff --git a/src/cbc_sdk/workload/compliance_assessment.py b/src/cbc_sdk/workload/compliance_assessment.py index 44860822..a32153c8 100644 --- a/src/cbc_sdk/workload/compliance_assessment.py +++ b/src/cbc_sdk/workload/compliance_assessment.py @@ -13,7 +13,7 @@ """Model and Query Classes for Compliance Assessment API""" -from cbc_sdk.base import (NewBaseModel, BaseQuery, QueryBuilder, QueryBuilderSupportMixin, +from cbc_sdk.base import (NewBaseModel, BaseQuery, QueryBuilder, CriteriaBuilderSupportMixin, IterableQueryMixin, AsyncQueryMixin, UnrefreshableModel) from cbc_sdk.errors import ApiError, MoreThanOneResultError, ObjectNotFoundError import logging @@ -30,7 +30,7 @@ class ComplianceBenchmark(NewBaseModel): swagger_meta_file = "workload/models/compliance.yaml" primary_key = "benchmark_set_id" - def __init__(self, cb, model_unique_id=None, initial_data=None): + def __init__(self, cb, initial_data, model_unique_id=None): """ """ super(ComplianceBenchmark, self).__init__(cb, model_unique_id, initial_data) @@ -41,7 +41,12 @@ def __init__(self, cb, model_unique_id=None, initial_data=None): def _refresh(self): """ + CHANGES: Refresh should make a get to get the benchmark """ + url = self.urlobject_single.format(self._cb.credentials.org_key, self._model_unique_id) + resp = self._cb.get_object(url) + self._info = resp + self._last_refresh_time = time.time() return True @classmethod @@ -51,7 +56,7 @@ def _query_implementation(cls, cb, **kwargs): return ComplianceBenchmarkQuery(cls, cb) @staticmethod - def get_org_compliance_schedule(cb): + def get_compliance_schedule(cb): """ Gets the compliance scan schedule and timezone for the organization associated with the specified CBCloudAPI instance. @@ -66,13 +71,13 @@ def get_org_compliance_schedule(cb): Example: >>> cb = CBCloudAPI(profile="example_profile") - >>> descriptions = AuthEvent.get_auth_events_descriptions(cb) - >>> print(descriptions) + >>> schedule = ComplianceBenchmark.get_compliance_schedule(cb) + >>> print(schedule) """ if cb.__class__.__name__ != "CBCloudAPI": message = "cb argument should be instance of CBCloudAPI." message += "\nExample:\ncb = CBCloudAPI(profile='example_profile')" - message += "\nCompliance.get_org_settings(cb)" + message += "\nComplianceBenchmark.get_compliance_schedule(cb)" raise ApiError(message) url = f"/compliance/assessment/api/v1/orgs/{cb.credentials.org_key}/settings" @@ -80,7 +85,7 @@ def get_org_compliance_schedule(cb): return cb.get_object(url) @staticmethod - def set_org_compliance_schedule(cb, scan_schedule, scan_timezone): + def set_compliance_schedule(cb, scan_schedule, scan_timezone): """ Sets the compliance scan schedule and timezone for the organization associated with the specified CBCloudAPI instance. @@ -97,27 +102,156 @@ def set_org_compliance_schedule(cb, scan_schedule, scan_timezone): Example: >>> cb = CBCloudAPI(profile="example_profile") - >>> response = Compliance.set_org_compliance_schedule(cb, scan_schedule="RRULE:FREQ=DAILY;BYHOUR=17;BYMINUTE=30;BYSECOND=0", scan_timezone="UTC") - >>> print(response) + >>> schedule = ComplianceBenchmark.set_compliance_schedule(cb, scan_schedule="RRULE:FREQ=DAILY;BYHOUR=17;BYMINUTE=30;BYSECOND=0", scan_timezone="UTC") + >>> print(schedule) """ if cb.__class__.__name__ != "CBCloudAPI": message = "cb argument should be instance of CBCloudAPI." message += "\nExample:\ncb = CBCloudAPI(profile='example_profile')" - message += "\nCompliance.set_org_compliance_schedule(cb, scan_schedule='RRULE:FREQ=DAILY;BYHOUR=17;BYMINUTE=30;BYSECOND=0', scan_timezone='UTC')" + message += "\nComplianceBenchmark.set_compliance_schedule(cb, scan_schedule='RRULE:FREQ=DAILY;BYHOUR=17;BYMINUTE=30;BYSECOND=0', scan_timezone='UTC')" raise ApiError(message) if not scan_schedule or not scan_timezone or scan_schedule == "" or scan_timezone == "": - raise ApiError("scan_schedule and scan_timezone are required. http://developer.carbonblack.com/reference/carbon-black-cloud/cb-liveops/latest/livequery-api/#recurrence-rules") + raise ApiError( + "scan_schedule and scan_timezone are required. http://developer.carbonblack.com/reference/carbon-black-cloud/cb-liveops/latest/livequery-api/#recurrence-rules") args = {"scan_schedule": scan_schedule, "scan_timezone": scan_timezone} url = f"/compliance/assessment/api/v1/orgs/{cb.credentials.org_key}/settings" return cb.put_object(url, body=args).json() + def search_rules(self, rule_id=None): + """ + Fetches compliance rules associated with the benchmark set. + + Args: + rule_id (str, optional): The rule ID to fetch a specific rule. Defaults to None. + + Yields: + ComplianceBenchmark: A ComplianceBenchmark object representing a compliance rule. + + Example: + >>> cb = CBCloudAPI(profile="example_profile") + >>> benchmark_sets = cb.select(ComplianceBenchmark) + >>> # To return all rules within a benchmark set, leave search_rules empty. + >>> rules = list(benchmark_sets[0].search_rules()) + >>> print(*rules) + >>> # To return a single rule document, add the rule ID. + >>> rule = list(benchmark_sets.search_rules('00869D86-6E61-4D7D-A0A3-6F5CDE2E5753')) + >>> print(rule) + """ + if rule_id is not None: + self._rule_id = rule_id + url = self.urlobject.format(self._cb.credentials.org_key) + f"{self.id}/rules/{rule_id}" + result = self._cb.get_object(url) + yield ComplianceBenchmark(self._cb, initial_data=result) + return -class ComplianceBenchmarkQuery(BaseQuery, QueryBuilderSupportMixin, - IterableQueryMixin, AsyncQueryMixin): + url = self.urlobject.format(self._cb.credentials.org_key) + f"{self.id}/rules/_search" + current = 0 + max_rows = 80000 + while True: + # request = self._build_request(current, max_rows) + resp = self._cb.post_object(url, body={}) # FIXME fix the body + result = resp.json() + + self._total_results = result["num_found"] + self._count_valid = True + + results = result.get("results", []) + for item in results: + yield ComplianceBenchmark(self._cb, initial_data=item) + + current += len(results) + if max_rows > 0 and current >= max_rows: + break + + if current >= self._total_results: + break + + def get_set_sections(self): + """ + Get Sections of the Benchmark Set. + + Returns: + generator of ComplianceBenchmark: A generator yielding ComplianceBenchmark instances + for each section in the benchmark set. + + Example: + >>> cb = CBCloudAPI(profile="example_profile") + >>> benchmark = cb.select(ComplianceBenchmark).set_benchmark_set_id('benchmark123') + >>> for section in benchmark.get_benchmark_set_sections(): + ... print(section.section_name, section.section_id) + """ + url = self.urlobject.format(self._cb.credentials.org_key) + f"{self.id}/sections" + results = self._cb.get_object(url) + for item in results: + yield ComplianceBenchmark(self._cb, initial_data=item) + + def execute_action(self, action, device_ids=None): + """ + Execute a specified action on devices within a Benchmark Set. + + Args: + action (str): The action to be executed. Available: + 'ENABLE': Enable the object. + 'DISABLE': Disable the object. + 'REASSESS': Reassess the object. + + device_ids (str or list, optional): IDs of devices on which the action will be executed. + If specified as a string, only one device will be targeted. If specified as a list, + the action will be executed on multiple devices. Default is None. + + Returns: + dict: JSON response containing information about the executed action. + + Raises: + ApiError: If the provided action is not one of the allowed actions. + + Example: + To reassess an object: + ``` + benchmark_sets = cb.select(ComplianceBenchmark) + benchmark_sets[0].execute_action('REASSESS') + """ + ACTIONS = ('ENABLE', 'DISABLE', 'REASSESS') + + if action.upper() not in self.ACTIONS: + message = ( + f"\nAction type is required." + f"\nAvailable action types: {', '.join(self.ACTIONS)}" + f"\nExample:\nbenchmark_sets = cb.select(ComplianceBenchmark)" + f"\nbenchmark_sets[0].execute_action('REASSESS')" + ) + raise ApiError(message) + + args = {"action": action} + if device_ids: + args['device_ids'] = [device_ids] if isinstance(device_ids, str) else device_ids + + url = self.urlobject.format(self._cb.credentials.org_key) + f"{self.id}/actions" + return self._cb.post_object(url, body=args).json() + + def get_benchmark_set_summary(self): + """ + Fetches the compliance summary for the current benchmark set. + + This method constructs the URL for the compliance summary of the benchmark set associated with the current instance, + fetches the summary data using the Carbon Black API, and yields ComplianceBenchmark objects for each item in the summary. + + Returns: + generator of ComplianceBenchmark: A generator yielding ComplianceBenchmark objects, each representing a summary item. + """ + url = self.urlobject.format(self._cb.credentials.org_key) + f"{self.id}/compliance/summary" + results = self._cb.get_object(url) + for item in results: + yield ComplianceBenchmark(self._cb, initial_data=item) + + +class ComplianceBenchmarkQuery(BaseQuery, CriteriaBuilderSupportMixin, + IterableQueryMixin, AsyncQueryMixin): """ """ + def __init__(self, doc_class, cb): """ """ @@ -280,106 +414,3 @@ def sort_by(self, key, direction='ASC'): raise ApiError('invalid sort direction specified') self._sortcriteria = {'field': key, 'order': direction} return self - - def set_benchmark_set_id(self, benchmark_set_id): - """ - Set the benchmark set ID for the current instance. - - Args: - benchmark_set_id (str): The ID of the benchmark set to set. - - Returns: - self: The current instance with the updated benchmark set ID. - - Example: - >>> cb = CBCloudAPI(profile="example_profile") - >>> benchmark_set = cb.select(ComplianceBenchmark).set_benchmark_set_id('benchmark123') - >>> print(*benchmark_set) - """ - self._benchmark_set_id = benchmark_set_id - return self - - def search_rules(self, rule_id=None): - """ - Search for rules within the benchmark set. - - Args: - rule_id (str): Optional rule ID to search for. If provided, only the rule with the specified ID will be returned. - - Yields: - Rule document: Yields rule documents that match the search criteria. - - Raises: - ApiError: If the benchmark_set_id is not set. - - Returns: - None - - Example: - >>> cb = CBCloudAPI(profile="example_profile") - >>> benchmark_set = cb.select(ComplianceBenchmark).set_benchmark_set_id('benchmark123') - >>> # To return a single rule document, add the ID. - >>> rule = benchmark_set.search_rules('FDBFA982-2EA2-4720-83CC-E515CEFB795D') - >>> print(*rule) - >>> # To return all rules within a benchmark set, leave empty. - >>> rules = benchmark_set.search_rules() - >>> print(*rules) - """ - if not self._benchmark_set_id: - raise ApiError("internal error: benchmark_set_id is required.") - - if rule_id is not None: - self._rule_id = rule_id - url = self._build_url(f"{self._benchmark_set_id}/rules/{self._rule_id}") - result = self._cb.get_object(url) - yield self._doc_class(self._cb, initial_data=result) - return - - url = self._build_url(f"{self._benchmark_set_id}/rules/_search") - current = 0 - max_rows = 1000 - while True: - request = self._build_request(current, max_rows) - resp = self._cb.post_object(url, body=request) - result = resp.json() - - self._total_results = result["num_found"] - self._count_valid = True - - results = result.get("results", []) - for item in results: - yield self._doc_class(self._cb, initial_data=item) - - current += len(results) - if max_rows > 0 and current >= max_rows: - break - - if current >= self._total_results: - break - - def get_benchmark_set_sections(self): - """ - Retrieves the sections of a compliance benchmark set. - - Returns an iterator over the sections of the benchmark set. Each section is represented as an instance of the - compliance benchmark section model. - - Raises: - ApiError: If the benchmark_set_id is not set. - - Yields: - ComplianceBenchmarkSection: An instance of the compliance benchmark section model. - - Example: - >>> cb = CBCloudAPI(profile="example_profile") - >>> benchmark = cb.select(ComplianceBenchmark).set_benchmark_set_id('benchmark123') - >>> for section in benchmark.get_benchmark_set_sections(): - ... print(section.section_name, section.section_id) - """ - if not self._benchmark_set_id: - raise ApiError("internal error: benchmark_set_id is required.") - - url = self._build_url(f"{self._benchmark_set_id}/sections") - results = self._cb.get_object(url) - for item in results: - yield self._doc_class(self._cb, initial_data=item) From 6632dad79ed2a4a5c7845129d9704740492faba1 Mon Sep 17 00:00:00 2001 From: Hristo Karagitliev Date: Wed, 27 Sep 2023 00:45:00 +0300 Subject: [PATCH 43/95] add model files --- src/cbc_sdk/workload/models/compliance.yaml | 106 +++++++++++++++++- .../models/complianceDeviceSummaries.yaml | 0 .../complianceInformationSummaries.yaml | 0 3 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 src/cbc_sdk/workload/models/complianceDeviceSummaries.yaml create mode 100644 src/cbc_sdk/workload/models/complianceInformationSummaries.yaml diff --git a/src/cbc_sdk/workload/models/compliance.yaml b/src/cbc_sdk/workload/models/compliance.yaml index bab4ce97..07f987c1 100644 --- a/src/cbc_sdk/workload/models/compliance.yaml +++ b/src/cbc_sdk/workload/models/compliance.yaml @@ -1,7 +1,107 @@ type: object properties: - affected_assets: + id: + type: string + description: Unique identifier for the benchmark set. + name: + type: string + description: Name of the benchmark set. + version: + type: string + description: Version of the benchmark set. + os_family: + type: string + description: Operating system family associated with the benchmark set (e.g., WINDOWS_SERVER). + enabled: + type: boolean + description: Indicates whether the benchmark set is enabled or not. + type: + type: string + description: Type of the benchmark set (e.g., Custom). + supported_os_info: type: array + description: Array of supported operating system information. items: - type: string - description: List of affected assets + type: object + properties: + os_metadata_id: + type: string + description: Unique identifier for the OS metadata. + os_type: + type: string + description: Type of the operating system (e.g., WINDOWS). + os_name: + type: string + description: Name of the operating system. + cis_version: + type: string + description: CIS (Center for Internet Security) version associated with the OS. + created_by: + type: string + description: Name of the user who created the benchmark set. + updated_by: + type: string + description: Email of the user who last updated the benchmark set. + create_time: + type: string + description: Timestamp indicating when the benchmark set was created (in ISO 8601 format). + update_time: + type: string + description: Timestamp indicating when the benchmark set was last updated (in ISO 8601 format). + sections: + type: array + description: Array of benchmark sections. + items: + type: object + properties: + id: + type: string + description: Unique identifier for the benchmark section. + name: + type: string + description: Name of the benchmark section. + description: + type: string + description: Description of the benchmark section. + sections: + type: array + description: Array of subsections within the benchmark section. + items: + type: object + properties: + id: + type: string + description: Unique identifier for the subsection. + name: + type: string + description: Name of the subsection. + description: + type: string + description: Description of the subsection. + sections: + type: array + description: Array of nested subsections within the subsection. + items: + type: object + properties: {} + rules: + type: array + description: Array of rules within the subsection. + items: + type: object + properties: + id: + type: string + description: Unique identifier for the rule. + rule_name: + type: string + description: Name of the rule. + enabled: + type: boolean + description: Indicates whether the rule is enabled or not. + section_id: + type: string + description: Identifier of the parent section for the rule. + section_name: + type: string + description: Name of the parent section for the rule. diff --git a/src/cbc_sdk/workload/models/complianceDeviceSummaries.yaml b/src/cbc_sdk/workload/models/complianceDeviceSummaries.yaml new file mode 100644 index 00000000..e69de29b diff --git a/src/cbc_sdk/workload/models/complianceInformationSummaries.yaml b/src/cbc_sdk/workload/models/complianceInformationSummaries.yaml new file mode 100644 index 00000000..e69de29b From d3b2bf5eb45b38f14eced4edf81e96ec0e290da2 Mon Sep 17 00:00:00 2001 From: Hristo Karagitliev Date: Wed, 27 Sep 2023 16:13:01 +0300 Subject: [PATCH 44/95] fix docstrings --- src/cbc_sdk/workload/compliance_assessment.py | 69 ++++++++++++++++--- 1 file changed, 59 insertions(+), 10 deletions(-) diff --git a/src/cbc_sdk/workload/compliance_assessment.py b/src/cbc_sdk/workload/compliance_assessment.py index a32153c8..05e0f91c 100644 --- a/src/cbc_sdk/workload/compliance_assessment.py +++ b/src/cbc_sdk/workload/compliance_assessment.py @@ -25,6 +25,7 @@ class ComplianceBenchmark(NewBaseModel): """ + Class representing Compliance Benchmarks. """ urlobject = '/compliance/assessment/api/v1/orgs/{}/benchmark_sets/' swagger_meta_file = "workload/models/compliance.yaml" @@ -32,6 +33,15 @@ class ComplianceBenchmark(NewBaseModel): def __init__(self, cb, initial_data, model_unique_id=None): """ + Initialize a ComplianceBenchmark instance. + + Args: + cb (CBCloudAPI): Instance of CBCloudAPI. + initial_data (dict): Initial data for the instance. + model_unique_id (str): Unique identifier for the model. + + Returns: + ComplianceBenchmark: An instance of ComplianceBenchmark. """ super(ComplianceBenchmark, self).__init__(cb, model_unique_id, initial_data) @@ -41,7 +51,10 @@ def __init__(self, cb, initial_data, model_unique_id=None): def _refresh(self): """ - CHANGES: Refresh should make a get to get the benchmark + Refresh the ComplianceBenchmark instance by making a GET request to get the benchmark. + + Returns: + bool: True if the refresh is successful, else False. """ url = self.urlobject_single.format(self._cb.credentials.org_key, self._model_unique_id) resp = self._cb.get_object(url) @@ -52,6 +65,13 @@ def _refresh(self): @classmethod def _query_implementation(cls, cb, **kwargs): """ + Get the query implementation for ComplianceBenchmark. + + Args: + cb (CBCloudAPI): Instance of CBCloudAPI. + + Returns: + ComplianceBenchmarkQuery: Query implementation for ComplianceBenchmark. """ return ComplianceBenchmarkQuery(cls, cb) @@ -127,7 +147,7 @@ def search_rules(self, rule_id=None): rule_id (str, optional): The rule ID to fetch a specific rule. Defaults to None. Yields: - ComplianceBenchmark: A ComplianceBenchmark object representing a compliance rule. + ComplianceBenchmark: A Compliance object representing a compliance rule. Example: >>> cb = CBCloudAPI(profile="example_profile") @@ -173,7 +193,7 @@ def get_set_sections(self): Get Sections of the Benchmark Set. Returns: - generator of ComplianceBenchmark: A generator yielding ComplianceBenchmark instances + generator of ComplianceBenchmark: A generator yielding Compliance instances for each section in the benchmark set. Example: @@ -189,7 +209,8 @@ def get_set_sections(self): def execute_action(self, action, device_ids=None): """ - Execute a specified action on devices within a Benchmark Set. + Execute a specified action on devices within on a Benchmark Set, or specific devives + within a Benchmark Set only. Args: action (str): The action to be executed. Available: @@ -209,16 +230,15 @@ def execute_action(self, action, device_ids=None): Example: To reassess an object: - ``` benchmark_sets = cb.select(ComplianceBenchmark) benchmark_sets[0].execute_action('REASSESS') """ ACTIONS = ('ENABLE', 'DISABLE', 'REASSESS') - if action.upper() not in self.ACTIONS: + if action.upper() not in ACTIONS: message = ( f"\nAction type is required." - f"\nAvailable action types: {', '.join(self.ACTIONS)}" + f"\nAvailable action types: {', '.join(ACTIONS)}" f"\nExample:\nbenchmark_sets = cb.select(ComplianceBenchmark)" f"\nbenchmark_sets[0].execute_action('REASSESS')" ) @@ -236,10 +256,10 @@ def get_benchmark_set_summary(self): Fetches the compliance summary for the current benchmark set. This method constructs the URL for the compliance summary of the benchmark set associated with the current instance, - fetches the summary data using the Carbon Black API, and yields ComplianceBenchmark objects for each item in the summary. + fetches the summary data using the Carbon Black API, and yields Compliance objects for each item in the summary. Returns: - generator of ComplianceBenchmark: A generator yielding ComplianceBenchmark objects, each representing a summary item. + generator of ComplianceBenchmark: A generator yielding Compliance objects, each representing a summary item. """ url = self.urlobject.format(self._cb.credentials.org_key) + f"{self.id}/compliance/summary" results = self._cb.get_object(url) @@ -250,10 +270,19 @@ def get_benchmark_set_summary(self): class ComplianceBenchmarkQuery(BaseQuery, CriteriaBuilderSupportMixin, IterableQueryMixin, AsyncQueryMixin): """ + A class representing a query for Compliance Benchmark. """ def __init__(self, doc_class, cb): """ + Initialize a ComplianceBenchmarkQuery instance. + + Args: + doc_class (class): The document class for this query. + cb (CBCloudAPI): An instance of CBCloudAPI. + + Returns: + ComplianceBenchmarkQuery: An instance of ComplianceBenchmarkQuery. """ self._doc_class = doc_class self._cb = cb @@ -270,19 +299,39 @@ def __init__(self, doc_class, cb): def add_criteria(self, key, value, operator='EQUALS'): """ + Add a criteria to the query. + + Args: + key (str): The key for the criteria. + value: The value for the criteria. + operator (str, optional): The operator for the criteria. Defaults to 'EQUALS'. + + Returns: + ComplianceBenchmarkQuery: The current instance with the updated criteria. """ self._update_criteria(key, value, operator) return self def _update_criteria(self, key, value, operator, overwrite=False): """ + Update the criteria for the query. + + Args: + key (str): The key for the criteria. + value: The value for the criteria. + operator (str): The operator for the criteria. + overwrite (bool, optional): Whether to overwrite existing criteria with the same key. + Defaults to False. + + Returns: + None """ if self._criteria.get(key, None) is None or overwrite: self._criteria[key] = dict(value=value, operator=operator) def _build_request(self, from_row, max_rows, add_sort=True): """ - Build the request dictionary for the API query. + Build the request dictionary for the API query. Args: from_row (int): The starting row for the query. From 20cc70357a7d46976fd808a8224a42037d849b5b Mon Sep 17 00:00:00 2001 From: Hristo Karagitliev Date: Wed, 27 Sep 2023 16:13:32 +0300 Subject: [PATCH 45/95] add device summaries model --- .../models/complianceDeviceSummaries.yaml | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/cbc_sdk/workload/models/complianceDeviceSummaries.yaml b/src/cbc_sdk/workload/models/complianceDeviceSummaries.yaml index e69de29b..b155d7dd 100644 --- a/src/cbc_sdk/workload/models/complianceDeviceSummaries.yaml +++ b/src/cbc_sdk/workload/models/complianceDeviceSummaries.yaml @@ -0,0 +1,26 @@ +type: object +properties: + device_id: + type: string + description: "Unique identifier for the device." + device_name: + type: string + description: "Name of the device." + os_version: + type: string + description: "Operating system version of the device." + compliance_percentage: + type: number + description: "Compliance percentage for the device." + last_assess_time: + type: string + description: "Timestamp of the last assessment for compliance (in ISO 8601 format)." + excluded_on: + type: string + description: "Timestamp indicating when the device was excluded (in ISO 8601 format)." + excluded_by: + type: string + description: "Name of the user who excluded the device." + reason: + type: string + description: "Reason for excluding the device." From dea5b6fbf99ee63e1f1102f5984db66fbe3c844d Mon Sep 17 00:00:00 2001 From: Alex Van Brunt Date: Thu, 11 Apr 2024 14:55:16 -0600 Subject: [PATCH 46/95] Adjust core CIS Benchmark code --- src/cbc_sdk/workload/compliance_assessment.py | 350 +++++++++--------- 1 file changed, 184 insertions(+), 166 deletions(-) diff --git a/src/cbc_sdk/workload/compliance_assessment.py b/src/cbc_sdk/workload/compliance_assessment.py index 05e0f91c..f9ebc92f 100644 --- a/src/cbc_sdk/workload/compliance_assessment.py +++ b/src/cbc_sdk/workload/compliance_assessment.py @@ -13,9 +13,9 @@ """Model and Query Classes for Compliance Assessment API""" -from cbc_sdk.base import (NewBaseModel, BaseQuery, QueryBuilder, CriteriaBuilderSupportMixin, +from cbc_sdk.base import (BaseQuery, QueryBuilder, CriteriaBuilderSupportMixin, IterableQueryMixin, AsyncQueryMixin, UnrefreshableModel) -from cbc_sdk.errors import ApiError, MoreThanOneResultError, ObjectNotFoundError +from cbc_sdk.errors import ApiError import logging log = logging.getLogger(__name__) @@ -23,10 +23,8 @@ """ Compliance models """ -class ComplianceBenchmark(NewBaseModel): - """ - Class representing Compliance Benchmarks. - """ +class ComplianceBenchmark(UnrefreshableModel): + """Class representing Compliance Benchmarks.""" urlobject = '/compliance/assessment/api/v1/orgs/{}/benchmark_sets/' swagger_meta_file = "workload/models/compliance.yaml" primary_key = "benchmark_set_id" @@ -46,22 +44,12 @@ def __init__(self, cb, initial_data, model_unique_id=None): super(ComplianceBenchmark, self).__init__(cb, model_unique_id, initial_data) if model_unique_id is not None and initial_data is None: - self._refresh() + benchmark = cb.select(ComplianceBenchmark).add_criteria("id", [model_unique_id]).first() + if benchmark is None: + raise ApiError(f"Benchmark {model_unique_id} not found") + self._info = benchmark._info self._full_init = True - def _refresh(self): - """ - Refresh the ComplianceBenchmark instance by making a GET request to get the benchmark. - - Returns: - bool: True if the refresh is successful, else False. - """ - url = self.urlobject_single.format(self._cb.credentials.org_key, self._model_unique_id) - resp = self._cb.get_object(url) - self._info = resp - self._last_refresh_time = time.time() - return True - @classmethod def _query_implementation(cls, cb, **kwargs): """ @@ -75,10 +63,21 @@ def _query_implementation(cls, cb, **kwargs): """ return ComplianceBenchmarkQuery(cls, cb) + def get_benchmark_set_summary(self): + """ + Fetches the compliance summary for the current benchmark set. + + Returns: + dict: The benchmark compliance summary + """ + url = self.urlobject.format(self._cb.credentials.org_key) + f"{self.id}/compliance/summary" + results = self._cb.get_object(url).json() + return results + @staticmethod def get_compliance_schedule(cb): """ - Gets the compliance scan schedule and timezone for the organization associated with the specified CBCloudAPI instance. + Gets the compliance scan schedule and timezone configured for the Organization. Args: cb (CBCloudAPI): An instance of CBCloudAPI representing the Carbon Black Cloud API. @@ -87,7 +86,7 @@ def get_compliance_schedule(cb): ApiError: If cb is not an instance of CBCloudAPI. Returns: - The JSON response from the Carbon Black Cloud API as a Python dictionary. + dict: The configured organization settings for Compliance Assessment. Example: >>> cb = CBCloudAPI(profile="example_profile") @@ -95,128 +94,115 @@ def get_compliance_schedule(cb): >>> print(schedule) """ if cb.__class__.__name__ != "CBCloudAPI": - message = "cb argument should be instance of CBCloudAPI." - message += "\nExample:\ncb = CBCloudAPI(profile='example_profile')" - message += "\nComplianceBenchmark.get_compliance_schedule(cb)" - raise ApiError(message) + raise ApiError("cb argument should be instance of CBCloudAPI") url = f"/compliance/assessment/api/v1/orgs/{cb.credentials.org_key}/settings" - return cb.get_object(url) + return cb.get_object(url).json() @staticmethod def set_compliance_schedule(cb, scan_schedule, scan_timezone): """ - Sets the compliance scan schedule and timezone for the organization associated with the specified CBCloudAPI instance. + Sets the compliance scan schedule and timezone for the organization. Args: cb (CBCloudAPI): An instance of CBCloudAPI representing the Carbon Black Cloud API. - scan_schedule (str): The scan schedule, specified in RFC 5545 format. Example: "RRULE:FREQ=DAILY;BYHOUR=17;BYMINUTE=30;BYSECOND=0". - scan_timezone (str): The timezone in which the scan will run, specified as a timezone string. Example: "UTC". + scan_schedule (str): The scan schedule, specified in RFC 5545 format. + Example: "RRULE:FREQ=DAILY;BYHOUR=17;BYMINUTE=30;BYSECOND=0". + scan_timezone (str): The timezone in which the scan will run, + specified as a timezone string. Example: "UTC". Raises: ApiError: If cb is not an instance of CBCloudAPI, or if scan_schedule or scan_timezone are not provided. Returns: - The JSON response from the Carbon Black Cloud API as a Python dictionary. + dict: The configured organization settings for Compliance Assessment. Example: >>> cb = CBCloudAPI(profile="example_profile") - >>> schedule = ComplianceBenchmark.set_compliance_schedule(cb, scan_schedule="RRULE:FREQ=DAILY;BYHOUR=17;BYMINUTE=30;BYSECOND=0", scan_timezone="UTC") + >>> schedule = ComplianceBenchmark.set_compliance_schedule(cb, + scan_schedule="RRULE:FREQ=DAILY;BYHOUR=17;BYMINUTE=30;BYSECOND=0", + scan_timezone="UTC") >>> print(schedule) """ if cb.__class__.__name__ != "CBCloudAPI": - message = "cb argument should be instance of CBCloudAPI." - message += "\nExample:\ncb = CBCloudAPI(profile='example_profile')" - message += "\nComplianceBenchmark.set_compliance_schedule(cb, scan_schedule='RRULE:FREQ=DAILY;BYHOUR=17;BYMINUTE=30;BYSECOND=0', scan_timezone='UTC')" - raise ApiError(message) + raise ApiError("cb argument should be instance of CBCloudAPI") if not scan_schedule or not scan_timezone or scan_schedule == "" or scan_timezone == "": - raise ApiError( - "scan_schedule and scan_timezone are required. http://developer.carbonblack.com/reference/carbon-black-cloud/cb-liveops/latest/livequery-api/#recurrence-rules") + raise ApiError("scan_schedule and scan_timezone are required") args = {"scan_schedule": scan_schedule, "scan_timezone": scan_timezone} url = f"/compliance/assessment/api/v1/orgs/{cb.credentials.org_key}/settings" return cb.put_object(url, body=args).json() - def search_rules(self, rule_id=None): + def get_sections(self): + """ + Get Sections of the Benchmark Set. + + Returns: + list[dict]: List of sections within the Benchmark Set. + + Example: + >>> cb = CBCloudAPI(profile="example_profile") + >>> benchmark = cb.select(ComplianceBenchmark).first() + >>> for section in benchmark.get_sections(): + ... print(section.section_name, section.section_id) + """ + url = self.urlobject.format(self._cb.credentials.org_key) + f"{self.id}/sections" + results = self._cb.get_object(url).json() + return results + + def get_rules(self, rule_id=None): """ Fetches compliance rules associated with the benchmark set. Args: rule_id (str, optional): The rule ID to fetch a specific rule. Defaults to None. - Yields: - ComplianceBenchmark: A Compliance object representing a compliance rule. + Returns: + [dict]: List of Benchmark Rules Example: >>> cb = CBCloudAPI(profile="example_profile") - >>> benchmark_sets = cb.select(ComplianceBenchmark) - >>> # To return all rules within a benchmark set, leave search_rules empty. - >>> rules = list(benchmark_sets[0].search_rules()) - >>> print(*rules) - >>> # To return a single rule document, add the rule ID. - >>> rule = list(benchmark_sets.search_rules('00869D86-6E61-4D7D-A0A3-6F5CDE2E5753')) - >>> print(rule) + >>> benchmark_set = cb.select(ComplianceBenchmark).first() + >>> # To return all rules within a benchmark set, leave get_rules empty. + >>> rules = benchmark_set.get_rules() """ if rule_id is not None: self._rule_id = rule_id url = self.urlobject.format(self._cb.credentials.org_key) + f"{self.id}/rules/{rule_id}" - result = self._cb.get_object(url) - yield ComplianceBenchmark(self._cb, initial_data=result) - return + return [self._cb.get_object(url)] url = self.urlobject.format(self._cb.credentials.org_key) + f"{self.id}/rules/_search" current = 0 - max_rows = 80000 + rules = [] while True: - # request = self._build_request(current, max_rows) - resp = self._cb.post_object(url, body={}) # FIXME fix the body + resp = self._cb.post_object(url, body={ + "rows": 10000, + "start": current, + "sort": [ + { + "field": "section_name", + "order": "DESC" + } + ] + }) result = resp.json() - self._total_results = result["num_found"] - self._count_valid = True - - results = result.get("results", []) - for item in results: - yield ComplianceBenchmark(self._cb, initial_data=item) - - current += len(results) - if max_rows > 0 and current >= max_rows: - break + rules.extend(result.get("results", [])) + current += len(result) - if current >= self._total_results: + if current >= result["num_found"]: break - def get_set_sections(self): - """ - Get Sections of the Benchmark Set. - - Returns: - generator of ComplianceBenchmark: A generator yielding Compliance instances - for each section in the benchmark set. - - Example: - >>> cb = CBCloudAPI(profile="example_profile") - >>> benchmark = cb.select(ComplianceBenchmark).set_benchmark_set_id('benchmark123') - >>> for section in benchmark.get_benchmark_set_sections(): - ... print(section.section_name, section.section_id) - """ - url = self.urlobject.format(self._cb.credentials.org_key) + f"{self.id}/sections" - results = self._cb.get_object(url) - for item in results: - yield ComplianceBenchmark(self._cb, initial_data=item) + return rules def execute_action(self, action, device_ids=None): """ - Execute a specified action on devices within on a Benchmark Set, or specific devives - within a Benchmark Set only. + Execute a specified action for the Benchmark Set for all devices or a specified subset. Args: - action (str): The action to be executed. Available: - 'ENABLE': Enable the object. - 'DISABLE': Disable the object. - 'REASSESS': Reassess the object. + action (str): The action to be executed. Options: ENABLE, DISABLE, REASSESS device_ids (str or list, optional): IDs of devices on which the action will be executed. If specified as a string, only one device will be targeted. If specified as a list, @@ -236,42 +222,99 @@ def execute_action(self, action, device_ids=None): ACTIONS = ('ENABLE', 'DISABLE', 'REASSESS') if action.upper() not in ACTIONS: - message = ( - f"\nAction type is required." - f"\nAvailable action types: {', '.join(ACTIONS)}" - f"\nExample:\nbenchmark_sets = cb.select(ComplianceBenchmark)" - f"\nbenchmark_sets[0].execute_action('REASSESS')" - ) - raise ApiError(message) - - args = {"action": action} + raise ApiError("Action is not supported. Options: ENABLE, DISABLE, REASSESS") + + args = {"action": action.upper()} if device_ids: args['device_ids'] = [device_ids] if isinstance(device_ids, str) else device_ids url = self.urlobject.format(self._cb.credentials.org_key) + f"{self.id}/actions" return self._cb.post_object(url, body=args).json() - def get_benchmark_set_summary(self): + def get_device_compliance(self, query=""): """ - Fetches the compliance summary for the current benchmark set. + Fetches devices compliance associated with the benchmark set. - This method constructs the URL for the compliance summary of the benchmark set associated with the current instance, - fetches the summary data using the Carbon Black API, and yields Compliance objects for each item in the summary. + Args: + query (str, optional): The query to filter results. Returns: - generator of ComplianceBenchmark: A generator yielding Compliance objects, each representing a summary item. + [dict]: List of Device Compliances + + Example: + >>> cb = CBCloudAPI(profile="example_profile") + >>> benchmark_set = cb.select(ComplianceBenchmark).first() + >>> rules = benchmark_set.get_device_compliance() """ - url = self.urlobject.format(self._cb.credentials.org_key) + f"{self.id}/compliance/summary" - results = self._cb.get_object(url) - for item in results: - yield ComplianceBenchmark(self._cb, initial_data=item) + url = self.urlobject.format(self._cb.credentials.org_key) + f"{self.id}/compliance/devices/_search" + current = 0 + device_compliances = [] + while True: + resp = self._cb.post_object(url, body={ + "query": query, + "rows": 10000, + "start": current, + "sort": [ + { + "field": "device_name", + "order": "DESC" + } + ] + }) + result = resp.json() + + device_compliances.extend(result.get("results", [])) + current += len(result) + + if current >= result["num_found"]: + break + + return device_compliances + + def get_rule_compliance(self, query=""): + """ + Fetches rule compliance associated with the benchmark set. + + Args: + query (str, optional): The query to filter results. + + Returns: + [dict]: List of Rule Compliances + + Example: + >>> cb = CBCloudAPI(profile="example_profile") + >>> benchmark_set = cb.select(ComplianceBenchmark).first() + >>> rules = benchmark_set.get_rule_compliance() + """ + url = self.urlobject.format(self._cb.credentials.org_key) + f"{self.id}/compliance/rules/_search" + current = 0 + rule_compliances = [] + while True: + resp = self._cb.post_object(url, body={ + "query": query, + "rows": 10000, + "start": current, + "sort": [ + { + "field": "device_name", + "order": "DESC" + } + ] + }) + result = resp.json() + + rule_compliances.extend(result.get("results", [])) + current += len(result) + + if current >= result["num_found"]: + break + + return rule_compliances class ComplianceBenchmarkQuery(BaseQuery, CriteriaBuilderSupportMixin, IterableQueryMixin, AsyncQueryMixin): - """ - A class representing a query for Compliance Benchmark. - """ + """A class representing a query for Compliance Benchmark.""" def __init__(self, doc_class, cb): """ @@ -297,37 +340,31 @@ def __init__(self, doc_class, cb): self._benchmark_set_id = None self._rule_id = None - def add_criteria(self, key, value, operator='EQUALS'): + def sort_by(self, key, direction='ASC'): """ - Add a criteria to the query. + Sets the sorting behavior on a query's results. - Args: - key (str): The key for the criteria. - value: The value for the criteria. - operator (str, optional): The operator for the criteria. Defaults to 'EQUALS'. + Arguments: + key (str): The key in the schema to sort by. + direction (str): The sort order, either "ASC" or "DESC". Returns: - ComplianceBenchmarkQuery: The current instance with the updated criteria. - """ - self._update_criteria(key, value, operator) - return self + Query: The query with sorting parameters. - def _update_criteria(self, key, value, operator, overwrite=False): - """ - Update the criteria for the query. + Raises: + ApiError: If an invalid sort direction is specified. - Args: - key (str): The key for the criteria. - value: The value for the criteria. - operator (str): The operator for the criteria. - overwrite (bool, optional): Whether to overwrite existing criteria with the same key. - Defaults to False. + Example: + To sort by a field in descending order: - Returns: - None + >>> cb = CBCloudAPI(profile="example_profile") + >>> benchmark_sets = cb.select(ComplianceBenchmark).sort_by("name", direction="DESC") + >>> print(*benchmark_sets) """ - if self._criteria.get(key, None) is None or overwrite: - self._criteria[key] = dict(value=value, operator=operator) + if direction.upper() not in ('ASC', 'DESC'): + raise ApiError('invalid sort direction specified') + self._sortcriteria = {'field': key, 'order': direction} + return self def _build_request(self, from_row, max_rows, add_sort=True): """ @@ -402,10 +439,7 @@ def _perform_query(self, from_row=0, max_rows=-1): numrows = 0 while True: request = self._build_request(current, max_rows) - if self._benchmark_set_id and self._rule_id: - resp = self._cb.get_object(url) - else: - resp = self._cb.post_object(url, body=request) + resp = self._cb.post_object(url, body=request) result = resp.json() self._total_results = result["num_found"] @@ -418,10 +452,10 @@ def _perform_query(self, from_row=0, max_rows=-1): numrows += 1 if max_rows > 0 and numrows == max_rows: - return + break if current >= self._total_results: - return + break def _run_async_query(self, context): """ @@ -434,32 +468,16 @@ def _run_async_query(self, context): dict: The JSON response containing the query results. """ url = self._build_url("_search") - request = self._build_request(0, -1) - return self._cb.post_object(url, body=request).json() - - def sort_by(self, key, direction='ASC'): - """ - Set the sort criteria for the search. - - Args: - key (str): The field to sort by. - direction (str, optional): The sort direction. Defaults to "ASC". - Valid values are "ASC" (ascending) and "DESC" (descending). - - Returns: - self: The current instance with the updated sort criteria. + output = [] + while not self._count_valid or len(output) < self._total_results: + request = self._build_request(len(output), -1) + resp = self._cb.post_object(url, body=request) + result = resp.json() - Raises: - ApiError: If an invalid sort direction is specified. + if not self._count_valid: + self._total_results = result["num_found"] + self._count_valid = True - Example: - To sort by a field in descending order: - - >>> cb = CBCloudAPI(profile="example_profile") - >>> benchmark_sets = cb.select(ComplianceBenchmark).sort_by("name", direction="DESC") - >>> print(*benchmark_sets) - """ - if direction.upper() not in ('ASC', 'DESC', 'asc', 'desc'): - raise ApiError('invalid sort direction specified') - self._sortcriteria = {'field': key, 'order': direction} - return self + results = result.get("results", []) + output += [self._doc_class(self._cb, item["id"], item) for item in results] + return output From f7d62c873c76eadc04a4552823ad0e248df5a38a Mon Sep 17 00:00:00 2001 From: Alex Van Brunt Date: Fri, 12 Apr 2024 18:41:18 -0600 Subject: [PATCH 47/95] Add additional helpers. Remove unused files --- src/cbc_sdk/workload/compliance_assessment.py | 144 ++++++++++++++++-- src/cbc_sdk/workload/models/compliance.yaml | 107 ------------- .../models/complianceDeviceSummaries.yaml | 26 ---- .../complianceInformationSummaries.yaml | 0 .../workload/models/compliance_benchmark.yaml | 53 +++++++ 5 files changed, 181 insertions(+), 149 deletions(-) delete mode 100644 src/cbc_sdk/workload/models/compliance.yaml delete mode 100644 src/cbc_sdk/workload/models/complianceDeviceSummaries.yaml delete mode 100644 src/cbc_sdk/workload/models/complianceInformationSummaries.yaml create mode 100644 src/cbc_sdk/workload/models/compliance_benchmark.yaml diff --git a/src/cbc_sdk/workload/compliance_assessment.py b/src/cbc_sdk/workload/compliance_assessment.py index f9ebc92f..3cdb7807 100644 --- a/src/cbc_sdk/workload/compliance_assessment.py +++ b/src/cbc_sdk/workload/compliance_assessment.py @@ -26,7 +26,7 @@ class ComplianceBenchmark(UnrefreshableModel): """Class representing Compliance Benchmarks.""" urlobject = '/compliance/assessment/api/v1/orgs/{}/benchmark_sets/' - swagger_meta_file = "workload/models/compliance.yaml" + swagger_meta_file = "workload/models/compliance_benchmark.yaml" primary_key = "benchmark_set_id" def __init__(self, cb, initial_data, model_unique_id=None): @@ -197,6 +197,32 @@ def get_rules(self, rule_id=None): return rules + def update_rules(self, rule_ids, enabled): + """ + Update compliance rules associated with the benchmark set. + + Args: + rule_ids (list[str]): The rule IDs to update their enabled/disabled status. + enabled (bool): Whether the rule is enabled or disabled. + + Returns: + [dict]: List of Updated Benchmark Rules + + Example: + >>> cb = CBCloudAPI(profile="example_profile") + >>> benchmark_set = cb.select(ComplianceBenchmark).first() + >>> # To return all rules within a benchmark set, leave get_rules empty. + >>> rules = benchmark_set.update_rules(["2A65B63E-89D9-4844-8290-5042FDF2A27B"], True) + """ + request = [] + for rule_id in rule_ids: + request.append({ + "rule_id": rule_id, + "enabled": enabled + }) + url = self.urlobject.format(self._cb.credentials.org_key) + f"{self.id}/rules" + return self._cb.post_object(url, body=request).json() + def execute_action(self, action, device_ids=None): """ Execute a specified action for the Benchmark Set for all devices or a specified subset. @@ -231,24 +257,66 @@ def execute_action(self, action, device_ids=None): url = self.urlobject.format(self._cb.credentials.org_key) + f"{self.id}/actions" return self._cb.post_object(url, body=args).json() - def get_device_compliance(self, query=""): - """ - Fetches devices compliance associated with the benchmark set. + # API Not supported + # + # def get_device_compliances(self, query=""): + # """ + # Fetches devices compliance summaries associated with the benchmark set. + # + # Args: + # query (str, optional): The query to filter results. + # + # Returns: + # [dict]: List of Device Compliances + # + # Example: + # >>> cb = CBCloudAPI(profile="example_profile") + # >>> benchmark_set = cb.select(ComplianceBenchmark).first() + # >>> rules = benchmark_set.get_device_compliance() + # """ + # url = self.urlobject.format(self._cb.credentials.org_key) + f"{self.id}/compliance/devices/_search" + # current = 0 + # device_compliances = [] + # while True: + # resp = self._cb.post_object(url, body={ + # "query": query, + # "rows": 10000, + # "start": current, + # "sort": [ + # { + # "field": "device_name", + # "order": "DESC" + # } + # ] + # }) + # result = resp.json() + # + # device_compliances.extend(result.get("results", [])) + # current += len(result) + # + # if current >= result["num_found"]: + # break + # + # return device_compliances + + def get_rule_compliances(self, query=""): + """ + Fetches rule compliance summaries associated with the benchmark set. Args: query (str, optional): The query to filter results. Returns: - [dict]: List of Device Compliances + [dict]: List of Rule Compliances Example: >>> cb = CBCloudAPI(profile="example_profile") >>> benchmark_set = cb.select(ComplianceBenchmark).first() - >>> rules = benchmark_set.get_device_compliance() + >>> rules = benchmark_set.get_rule_compliance() """ - url = self.urlobject.format(self._cb.credentials.org_key) + f"{self.id}/compliance/devices/_search" + url = self.urlobject.format(self._cb.credentials.org_key) + f"{self.id}/compliance/rules/_search" current = 0 - device_compliances = [] + rule_compliances = [] while True: resp = self._cb.post_object(url, body={ "query": query, @@ -256,26 +324,27 @@ def get_device_compliance(self, query=""): "start": current, "sort": [ { - "field": "device_name", + "field": "section_name", "order": "DESC" } ] }) result = resp.json() - device_compliances.extend(result.get("results", [])) + rule_compliances.extend(result.get("results", [])) current += len(result) if current >= result["num_found"]: break - return device_compliances + return rule_compliances - def get_rule_compliance(self, query=""): + def get_device_rule_compliances(self, device_id, query=""): """ - Fetches rule compliance associated with the benchmark set. + Fetches rule compliances for specific device. Args: + device_id (int): Device id to fetch benchmark rule compliance query (str, optional): The query to filter results. Returns: @@ -284,9 +353,9 @@ def get_rule_compliance(self, query=""): Example: >>> cb = CBCloudAPI(profile="example_profile") >>> benchmark_set = cb.select(ComplianceBenchmark).first() - >>> rules = benchmark_set.get_rule_compliance() + >>> rules = benchmark_set.get_device_rule_compliance(123) """ - url = self.urlobject.format(self._cb.credentials.org_key) + f"{self.id}/compliance/rules/_search" + url = self.urlobject.format(self._cb.credentials.org_key) + f"{self.id}/compliance/devices{device_id}/_search" current = 0 rule_compliances = [] while True: @@ -296,7 +365,7 @@ def get_rule_compliance(self, query=""): "start": current, "sort": [ { - "field": "device_name", + "field": "section_name", "order": "DESC" } ] @@ -311,6 +380,49 @@ def get_rule_compliance(self, query=""): return rule_compliances + def get_rule_compliance_devices(self, rule_id, query=""): + """ + Fetches device compliances for a specific rule. + + Args: + rule_id (str): Rule id to fetch device compliances + query (str, optional): The query to filter results. + + Returns: + [dict]: List of Device Compliances + + Example: + >>> cb = CBCloudAPI(profile="example_profile") + >>> benchmark_set = cb.select(ComplianceBenchmark).first() + >>> rules = benchmark_set.get_rule_compliance_devices("BCCAAACA-F0BE-4C0F-BE0A-A09FC1641EE2") + """ + url = self.urlobject.format(self._cb.credentials.org_key) + \ + f"{self.id}/compliance/rules/{rule_id}/devices/_search" + + current = 0 + device_compliances = [] + while True: + resp = self._cb.post_object(url, body={ + "query": query, + "rows": 10000, + "start": current, + "sort": [ + { + "field": "device_name", + "order": "DESC" + } + ] + }) + result = resp.json() + + device_compliances.extend(result.get("results", [])) + current += len(result) + + if current >= result["num_found"]: + break + + return device_compliances + class ComplianceBenchmarkQuery(BaseQuery, CriteriaBuilderSupportMixin, IterableQueryMixin, AsyncQueryMixin): diff --git a/src/cbc_sdk/workload/models/compliance.yaml b/src/cbc_sdk/workload/models/compliance.yaml deleted file mode 100644 index 07f987c1..00000000 --- a/src/cbc_sdk/workload/models/compliance.yaml +++ /dev/null @@ -1,107 +0,0 @@ -type: object -properties: - id: - type: string - description: Unique identifier for the benchmark set. - name: - type: string - description: Name of the benchmark set. - version: - type: string - description: Version of the benchmark set. - os_family: - type: string - description: Operating system family associated with the benchmark set (e.g., WINDOWS_SERVER). - enabled: - type: boolean - description: Indicates whether the benchmark set is enabled or not. - type: - type: string - description: Type of the benchmark set (e.g., Custom). - supported_os_info: - type: array - description: Array of supported operating system information. - items: - type: object - properties: - os_metadata_id: - type: string - description: Unique identifier for the OS metadata. - os_type: - type: string - description: Type of the operating system (e.g., WINDOWS). - os_name: - type: string - description: Name of the operating system. - cis_version: - type: string - description: CIS (Center for Internet Security) version associated with the OS. - created_by: - type: string - description: Name of the user who created the benchmark set. - updated_by: - type: string - description: Email of the user who last updated the benchmark set. - create_time: - type: string - description: Timestamp indicating when the benchmark set was created (in ISO 8601 format). - update_time: - type: string - description: Timestamp indicating when the benchmark set was last updated (in ISO 8601 format). - sections: - type: array - description: Array of benchmark sections. - items: - type: object - properties: - id: - type: string - description: Unique identifier for the benchmark section. - name: - type: string - description: Name of the benchmark section. - description: - type: string - description: Description of the benchmark section. - sections: - type: array - description: Array of subsections within the benchmark section. - items: - type: object - properties: - id: - type: string - description: Unique identifier for the subsection. - name: - type: string - description: Name of the subsection. - description: - type: string - description: Description of the subsection. - sections: - type: array - description: Array of nested subsections within the subsection. - items: - type: object - properties: {} - rules: - type: array - description: Array of rules within the subsection. - items: - type: object - properties: - id: - type: string - description: Unique identifier for the rule. - rule_name: - type: string - description: Name of the rule. - enabled: - type: boolean - description: Indicates whether the rule is enabled or not. - section_id: - type: string - description: Identifier of the parent section for the rule. - section_name: - type: string - description: Name of the parent section for the rule. diff --git a/src/cbc_sdk/workload/models/complianceDeviceSummaries.yaml b/src/cbc_sdk/workload/models/complianceDeviceSummaries.yaml deleted file mode 100644 index b155d7dd..00000000 --- a/src/cbc_sdk/workload/models/complianceDeviceSummaries.yaml +++ /dev/null @@ -1,26 +0,0 @@ -type: object -properties: - device_id: - type: string - description: "Unique identifier for the device." - device_name: - type: string - description: "Name of the device." - os_version: - type: string - description: "Operating system version of the device." - compliance_percentage: - type: number - description: "Compliance percentage for the device." - last_assess_time: - type: string - description: "Timestamp of the last assessment for compliance (in ISO 8601 format)." - excluded_on: - type: string - description: "Timestamp indicating when the device was excluded (in ISO 8601 format)." - excluded_by: - type: string - description: "Name of the user who excluded the device." - reason: - type: string - description: "Reason for excluding the device." diff --git a/src/cbc_sdk/workload/models/complianceInformationSummaries.yaml b/src/cbc_sdk/workload/models/complianceInformationSummaries.yaml deleted file mode 100644 index e69de29b..00000000 diff --git a/src/cbc_sdk/workload/models/compliance_benchmark.yaml b/src/cbc_sdk/workload/models/compliance_benchmark.yaml new file mode 100644 index 00000000..bf248f56 --- /dev/null +++ b/src/cbc_sdk/workload/models/compliance_benchmark.yaml @@ -0,0 +1,53 @@ +type: object +properties: + id: + type: string + description: Unique identifier for the benchmark set. + name: + type: string + description: Name of the benchmark set. + version: + type: string + description: Version of the benchmark set. + os_family: + type: string + description: Operating system family associated with the benchmark set (e.g., WINDOWS_SERVER). + enabled: + type: boolean + description: Indicates whether the benchmark set is enabled or not. + type: + type: string + description: Type of the benchmark set (e.g., Custom). + supported_os_info: + type: array + description: Array of supported operating system information. + items: + type: object + properties: + os_metadata_id: + type: string + description: Unique identifier for the OS metadata. + os_type: + type: string + description: Type of the operating system (e.g., WINDOWS). + os_name: + type: string + description: Name of the operating system. + cis_version: + type: string + description: CIS (Center for Internet Security) version associated with the OS. + created_by: + type: string + description: Name of the user who created the benchmark set. + updated_by: + type: string + description: Email of the user who last updated the benchmark set. + create_time: + type: string + description: Timestamp indicating when the benchmark set was created (in ISO 8601 format). + update_time: + type: string + description: Timestamp indicating when the benchmark set was last updated (in ISO 8601 format). + release_time: + type: string + description: Timestamp indicating when the benchmark set was released (in ISO 8601 format). From 0a08dab63bd3ceb1ed914ce6f768ed0890b22841 Mon Sep 17 00:00:00 2001 From: Alex Van Brunt Date: Mon, 15 Apr 2024 18:10:41 -0600 Subject: [PATCH 48/95] Add compliance tests --- src/cbc_sdk/workload/compliance_assessment.py | 40 ++++++++++--------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/src/cbc_sdk/workload/compliance_assessment.py b/src/cbc_sdk/workload/compliance_assessment.py index 3cdb7807..b50e180d 100644 --- a/src/cbc_sdk/workload/compliance_assessment.py +++ b/src/cbc_sdk/workload/compliance_assessment.py @@ -13,7 +13,7 @@ """Model and Query Classes for Compliance Assessment API""" -from cbc_sdk.base import (BaseQuery, QueryBuilder, CriteriaBuilderSupportMixin, +from cbc_sdk.base import (BaseQuery, QueryBuilder, QueryBuilderSupportMixin, CriteriaBuilderSupportMixin, IterableQueryMixin, AsyncQueryMixin, UnrefreshableModel) from cbc_sdk.errors import ApiError import logging @@ -27,9 +27,9 @@ class ComplianceBenchmark(UnrefreshableModel): """Class representing Compliance Benchmarks.""" urlobject = '/compliance/assessment/api/v1/orgs/{}/benchmark_sets/' swagger_meta_file = "workload/models/compliance_benchmark.yaml" - primary_key = "benchmark_set_id" + primary_key = "id" - def __init__(self, cb, initial_data, model_unique_id=None): + def __init__(self, cb, model_unique_id, initial_data=None): """ Initialize a ComplianceBenchmark instance. @@ -71,7 +71,7 @@ def get_benchmark_set_summary(self): dict: The benchmark compliance summary """ url = self.urlobject.format(self._cb.credentials.org_key) + f"{self.id}/compliance/summary" - results = self._cb.get_object(url).json() + results = self._cb.get_object(url) return results @staticmethod @@ -98,7 +98,7 @@ def get_compliance_schedule(cb): url = f"/compliance/assessment/api/v1/orgs/{cb.credentials.org_key}/settings" - return cb.get_object(url).json() + return cb.get_object(url) @staticmethod def set_compliance_schedule(cb, scan_schedule, scan_timezone): @@ -149,7 +149,7 @@ def get_sections(self): ... print(section.section_name, section.section_id) """ url = self.urlobject.format(self._cb.credentials.org_key) + f"{self.id}/sections" - results = self._cb.get_object(url).json() + results = self._cb.get_object(url) return results def get_rules(self, rule_id=None): @@ -190,8 +190,7 @@ def get_rules(self, rule_id=None): result = resp.json() rules.extend(result.get("results", [])) - current += len(result) - + current = len(rules) if current >= result["num_found"]: break @@ -221,7 +220,7 @@ def update_rules(self, rule_ids, enabled): "enabled": enabled }) url = self.urlobject.format(self._cb.credentials.org_key) + f"{self.id}/rules" - return self._cb.post_object(url, body=request).json() + return self._cb.put_object(url, body=request).json() def execute_action(self, action, device_ids=None): """ @@ -251,10 +250,13 @@ def execute_action(self, action, device_ids=None): raise ApiError("Action is not supported. Options: ENABLE, DISABLE, REASSESS") args = {"action": action.upper()} + url = "" if device_ids: args['device_ids'] = [device_ids] if isinstance(device_ids, str) else device_ids + url = self.urlobject.format(self._cb.credentials.org_key) + f"{self.id}/compliance/device_actions" + else: + url = self.urlobject.format(self._cb.credentials.org_key) + f"{self.id}/actions" - url = self.urlobject.format(self._cb.credentials.org_key) + f"{self.id}/actions" return self._cb.post_object(url, body=args).json() # API Not supported @@ -292,7 +294,7 @@ def execute_action(self, action, device_ids=None): # result = resp.json() # # device_compliances.extend(result.get("results", [])) - # current += len(result) + # current = len(device_compliances) # # if current >= result["num_found"]: # break @@ -332,7 +334,7 @@ def get_rule_compliances(self, query=""): result = resp.json() rule_compliances.extend(result.get("results", [])) - current += len(result) + current = len(rule_compliances) if current >= result["num_found"]: break @@ -355,7 +357,9 @@ def get_device_rule_compliances(self, device_id, query=""): >>> benchmark_set = cb.select(ComplianceBenchmark).first() >>> rules = benchmark_set.get_device_rule_compliance(123) """ - url = self.urlobject.format(self._cb.credentials.org_key) + f"{self.id}/compliance/devices{device_id}/_search" + url = self.urlobject.format(self._cb.credentials.org_key) + url += f"{self.id}/compliance/devices/{device_id}/rules/_search" + current = 0 rule_compliances = [] while True: @@ -373,7 +377,7 @@ def get_device_rule_compliances(self, device_id, query=""): result = resp.json() rule_compliances.extend(result.get("results", [])) - current += len(result) + current = len(rule_compliances) if current >= result["num_found"]: break @@ -416,7 +420,7 @@ def get_rule_compliance_devices(self, rule_id, query=""): result = resp.json() device_compliances.extend(result.get("results", [])) - current += len(result) + current = len(device_compliances) if current >= result["num_found"]: break @@ -424,7 +428,7 @@ def get_rule_compliance_devices(self, rule_id, query=""): return device_compliances -class ComplianceBenchmarkQuery(BaseQuery, CriteriaBuilderSupportMixin, +class ComplianceBenchmarkQuery(BaseQuery, QueryBuilderSupportMixin, CriteriaBuilderSupportMixin, IterableQueryMixin, AsyncQueryMixin): """A class representing a query for Compliance Benchmark.""" @@ -559,7 +563,7 @@ def _perform_query(self, from_row=0, max_rows=-1): results = result.get("results", []) for item in results: - yield self._doc_class(self._cb, initial_data=item) + yield self._doc_class(self._cb, item[self._doc_class.primary_key], initial_data=item) current += 1 numrows += 1 @@ -591,5 +595,5 @@ def _run_async_query(self, context): self._count_valid = True results = result.get("results", []) - output += [self._doc_class(self._cb, item["id"], item) for item in results] + output += [self._doc_class(self._cb, item[self._doc_class.primary_key], item) for item in results] return output From 0b693d9ce08c61bf3739a376d3f12eeaa408a107 Mon Sep 17 00:00:00 2001 From: Alex Van Brunt Date: Mon, 15 Apr 2024 18:28:41 -0600 Subject: [PATCH 49/95] Add test files --- .../unit/fixtures/workload/mock_compliance.py | 231 +++++++++++++++ src/tests/unit/workload/test_compliance.py | 267 ++++++++++++++++++ 2 files changed, 498 insertions(+) create mode 100644 src/tests/unit/fixtures/workload/mock_compliance.py create mode 100644 src/tests/unit/workload/test_compliance.py diff --git a/src/tests/unit/fixtures/workload/mock_compliance.py b/src/tests/unit/fixtures/workload/mock_compliance.py new file mode 100644 index 00000000..2479789a --- /dev/null +++ b/src/tests/unit/fixtures/workload/mock_compliance.py @@ -0,0 +1,231 @@ +"""Mock data for ComplianceBenchmark""" + +SEARCH_COMPLIANCE_BENCHMARKS = { + "num_found": 1, + "results": [ + { + "id": "eee5e491-9c31-4a38-84d8-50c9163ef559", + "name": "CIS Compliance - Microsoft Windows Server", + "version": "1.0.0.4", + "os_family": "WINDOWS_SERVER", + "bundle_name": "CIS Compliance - Microsoft Windows Server", + "enabled": True, + "type": "Default", + "supported_os_info": [ + { + "os_metadata_id": "1", + "os_type": "WINDOWS", + "os_name": "Windows Server 2012 x64", + "cis_version": "2.3.0" + }, + { + "os_metadata_id": "2", + "os_type": "WINDOWS", + "os_name": "Windows Server 2012 R2 x64", + "cis_version": "2.5.0" + }, + { + "os_metadata_id": "3", + "os_type": "WINDOWS", + "os_name": "Windows Server 2016 x64", + "cis_version": "1.4.0" + }, + { + "os_metadata_id": "4", + "os_type": "WINDOWS", + "os_name": "Windows Server 2019 x64", + "cis_version": "1.3.0" + }, + { + "os_metadata_id": "71", + "os_type": "WINDOWS", + "os_name": "Windows Server 2022 x64", + "cis_version": "1.0.0" + } + ], + "created_by": "CB_ADMIN", + "updated_by": "user@vmware.com", + "create_time": "2023-03-20T13:04:38.557369Z", + "update_time": "2023-07-10T13:56:35.238166Z", + "release_time": "2023-07-10T13:55:59.274881Z" + } + ] +} + +COMPLIANCE_SCHEDULE = { + "scan_schedule": "FREQ=WEEKLY;BYDAY=TU;BYHOUR=11;BYMINUTE=30;BYSECOND=0", + "scan_timezone": "UTC" +} + + +GET_SECTIONS = [ + { + "section_id": "0ABA0288-8A68-83AF-3BAE-A7F45167564B", + "section_name": "Remote Assistance", + "parent_id": "8C180BA0-EE6F-FB08-18F5-8F5FFC9A3FFF" + }, + { + "section_id": "0D69B5D9-B931-5ADB-EB04-F3775256B445", + "section_name": "App runtime", + "parent_id": "F072A0E3-24F6-B29C-6F2E-254F42CA6DF6" + }, + { + "section_id": "0FD8844A-C679-3F8F-748D-CDEAFF892CD4", + "section_name": "System Services", + "parent_id": None + } +] + +SEARCH_RULES = { + "num_found": 4, + "results": [ + { + "id": "39D861A0-3631-442B-BF94-CC442C73C03E", + "rule_name": "(L1) Ensure 'Configure Offer Remote Assistance' is set to 'Disabled'", + "enabled": True, + "section_id": "0ABA0288-8A68-83AF-3BAE-A7F45167564B", + "section_name": "Remote Assistance" + }, + { + "id": "C5632571-24C4-430D-9CCE-542F30B6933A", + "rule_name": "(L1) Ensure 'Configure Solicited Remote Assistance' is set to 'Disabled'", + "enabled": True, + "section_id": "0ABA0288-8A68-83AF-3BAE-A7F45167564B", + "section_name": "Remote Assistance" + }, + { + "id": "6A530464-631B-43E4-AB8C-5B06A747B7D7", + "rule_name": "(L1) Ensure 'Allow Microsoft accounts to be optional' is set to 'Enabled'", + "enabled": True, + "section_id": "0D69B5D9-B931-5ADB-EB04-F3775256B445", + "section_name": "App runtime" + }, + { + "id": "1F65A756-338E-49A8-AA78-3EC07734B96D", + "rule_name": "(L1) Ensure 'Print Spooler (Spooler)' is set to 'Disabled' (DC only)", + "enabled": True, + "section_id": "0FD8844A-C679-3F8F-748D-CDEAFF892CD4", + "section_name": "System Services" + } + ] +} + +GET_RULE = { + "id": "39D861A0-3631-442B-BF94-CC442C73C03E", + "rule_name": "(L1) Ensure 'Configure Offer Remote Assistance' is set to 'Disabled'", + "enabled": True, + "section_id": "0ABA0288-8A68-83AF-3BAE-A7F45167564B", + "section_name": "Remote Assistance", + "supported_os_info": [ + { + "os_metadata_id": "1", + "os_type": "WINDOWS", + "os_name": "Windows Server 2012 x64", + "cis_version": "2.3.0" + }, + { + "os_metadata_id": "2", + "os_type": "WINDOWS", + "os_name": "Windows Server 2012 R2 x64", + "cis_version": "2.5.0" + }, + { + "os_metadata_id": "3", + "os_type": "WINDOWS", + "os_name": "Windows Server 2016 x64", + "cis_version": "1.4.0" + }, + { + "os_metadata_id": "4", + "os_type": "WINDOWS", + "os_name": "Windows Server 2019 x64", + "cis_version": "1.3.0" + }, + { + "os_metadata_id": "71", + "os_type": "WINDOWS", + "os_name": "Windows Server 2022 x64", + "cis_version": "1.0.0" + } + ], + "description": "This policy setting allows you to turn on or turn off Offer (Unsolicited) Remote Assistance on this" + " computer.\n\nHelp desk and support personnel will not be able to proactively offer assistance, although they can" + " still respond to user assistance requests.\n\nThe recommended state for this setting is: `Disabled`.", + "rationale": "A user might be tricked and accept an unsolicited Remote Assistance offer from a malicious user.", + "impact": "None - this is the default behavior.", + "remediation": { + "procedure": "To establish the recommended configuration via GP, set the following UI path to `Disabled`", + "steps": "\n\n```\nComputer Configuration\\Policies\\Administrative Templates\\System\\Remote" + " Assistance\\Configure Offer Remote Assistance\n```\n\n**Note" + }, + "profile": [ + "Level 1 Domain Controller", + "Level 1 Member Server" + ] +} + +RULE_COMPLIANCES = { + "num_found": 2, + "results": [ + { + "rule_id": "39D861A0-3631-442B-BF94-CC442C73C03E", + "rule_name": "(L1) Ensure 'Configure Offer Remote Assistance' is set to 'Disabled'", + "section_id": "0ABA0288-8A68-83AF-3BAE-A7F45167564B", + "section_name": "Remote Assistance", + "compliant_assets": 1, + "non_compliant_assets": 0, + "profile": [ + "Level 1 Domain Controller", + "Level 1 Member Server" + ] + }, + { + "rule_id": "C5632571-24C4-430D-9CCE-542F30B6933A", + "rule_name": "(L1) Ensure 'Configure Solicited Remote Assistance' is set to 'Disabled'", + "section_id": "0ABA0288-8A68-83AF-3BAE-A7F45167564B", + "section_name": "Remote Assistance", + "compliant_assets": 1, + "non_compliant_assets": 0, + "profile": [ + "Level 1 Domain Controller", + "Level 1 Member Server" + ] + } + ] +} + +DEVICE_SPECIFIC_RULE_COMPLIANCE = { + "num_found": 2, + "results": [ + { + "id": "39D861A0-3631-442B-BF94-CC442C73C03E", + "rule_name": "(L1) Ensure 'Configure Offer Remote Assistance' is set to 'Disabled'", + "enabled": True, + "section_id": "0ABA0288-8A68-83AF-3BAE-A7F45167564B", + "section_name": "Remote Assistance", + "compliance_result": True, + "message": "Registry_Terminal_Services_fAllowUnsolicited=0" + }, + { + "id": "C5632571-24C4-430D-9CCE-542F30B6933A", + "rule_name": "(L1) Ensure 'Configure Solicited Remote Assistance' is set to 'Disabled'", + "enabled": True, + "section_id": "0ABA0288-8A68-83AF-3BAE-A7F45167564B", + "section_name": "Remote Assistance", + "compliance_result": True, + "message": "Registry_Terminal_Services_fAllowToGetHelp=0" + } + ] +} + +RULE_COMPLIANCE_DEVICE_SEARCH = { + "num_found": 1, + "results": [ + { + "device_id": 1, + "device_name": "Example\\Win2022", + "os_version": "Windows Server 2022 x64", + "compliance_result": True + } + ] +} diff --git a/src/tests/unit/workload/test_compliance.py b/src/tests/unit/workload/test_compliance.py new file mode 100644 index 00000000..827a42e1 --- /dev/null +++ b/src/tests/unit/workload/test_compliance.py @@ -0,0 +1,267 @@ +#!/usr/bin/env python3 + +# ******************************************************* +# Copyright (c) VMware, Inc. 2021-2024. All Rights Reserved. +# SPDX-License-Identifier: MIT +# ******************************************************* +# * +# * DISCLAIMER. THIS PROGRAM IS PROVIDED TO YOU "AS IS" WITHOUT +# * WARRANTIES OR CONDITIONS OF ANY KIND, WHETHER ORAL OR WRITTEN, +# * EXPRESS OR IMPLIED. THE AUTHOR SPECIFICALLY DISCLAIMS ANY IMPLIED +# * WARRANTIES OR CONDITIONS OF MERCHANTABILITY, SATISFACTORY QUALITY, +# * NON-INFRINGEMENT AND FITNESS FOR A PARTICULAR PURPOSE. + +"""Unit test code for ComplianceBenchmark""" + +import pytest +import logging +from cbc_sdk.rest_api import CBCloudAPI +from cbc_sdk.workload import ComplianceBenchmark +from tests.unit.fixtures.CBCSDKMock import CBCSDKMock + +from tests.unit.fixtures.workload.mock_compliance import (SEARCH_COMPLIANCE_BENCHMARKS, + COMPLIANCE_SCHEDULE, + GET_SECTIONS, + SEARCH_RULES, + GET_RULE, + RULE_COMPLIANCES, + DEVICE_SPECIFIC_RULE_COMPLIANCE, + RULE_COMPLIANCE_DEVICE_SEARCH) + + +logging.basicConfig(format="%(asctime)s %(levelname)s:%(message)s", level=logging.DEBUG, filename="log.txt") + + +@pytest.fixture(scope="function") +def cb(): + """Create CBCloudAPI singleton""" + return CBCloudAPI(url="https://example.com", + org_key="test", + token="abcd/1234", + ssl_verify=False) + + +@pytest.fixture(scope="function") +def cbcsdk_mock(monkeypatch, cb): + """Mocks CBC SDK for unit tests""" + return CBCSDKMock(monkeypatch, cb) + + +# ==================================== UNIT TESTS BELOW ==================================== + + +def test_compliance_benchmark(cbcsdk_mock): + """Tests a simple compliance benchmark query""" + + def post_validate(url, body, **kwargs): + assert body == { + "criteria": {"enabled": [True]}, + "query": "Windows Server", + "rows": 1, + "sort": [{"field": "name", "order": "DESC"}] + } + return SEARCH_COMPLIANCE_BENCHMARKS + + cbcsdk_mock.mock_request("POST", "/compliance/assessment/api/v1/orgs/test/benchmark_sets/_search", + post_validate) + + api = cbcsdk_mock.api + benchmark = api.select(ComplianceBenchmark) \ + .where("Windows Server") \ + .add_criteria("enabled", [True]) \ + .sort_by("name", "DESC").first() + + assert benchmark.name == "CIS Compliance - Microsoft Windows Server" + + +def test_compliance_benchmark_async(cbcsdk_mock): + """Test async compliance benchmark query""" + + def post_validate(url, body, **kwargs): + assert body == { + "criteria": {"enabled": [True]}, + "query": "Windows Server", + "rows": 1000, + "sort": [{"field": "name", "order": "DESC"}] + } + return SEARCH_COMPLIANCE_BENCHMARKS + + cbcsdk_mock.mock_request("POST", "/compliance/assessment/api/v1/orgs/test/benchmark_sets/_search", + post_validate) + + api = cbcsdk_mock.api + future_benchmark = api.select(ComplianceBenchmark) \ + .where("Windows Server") \ + .add_criteria("enabled", [True]) \ + .sort_by("name", "DESC").execute_async() + + benchmarks = future_benchmark.result() + + assert benchmarks[0].name == "CIS Compliance - Microsoft Windows Server" + + +def test_get_compliance_compliance(cbcsdk_mock): + """Test get_compliance_schedule""" + cbcsdk_mock.mock_request("GET", "/compliance/assessment/api/v1/orgs/test/settings", + COMPLIANCE_SCHEDULE) + + api = cbcsdk_mock.api + assert ComplianceBenchmark.get_compliance_schedule(api) == COMPLIANCE_SCHEDULE + + +def test_set_compliance_compliance(cbcsdk_mock): + """Test set_compliance_schedule""" + + def put_validate(url, body, **kwargs): + assert body == COMPLIANCE_SCHEDULE + return body + + cbcsdk_mock.mock_request("PUT", "/compliance/assessment/api/v1/orgs/test/settings", + put_validate) + + api = cbcsdk_mock.api + assert ComplianceBenchmark.set_compliance_schedule(api, + COMPLIANCE_SCHEDULE["scan_schedule"], + COMPLIANCE_SCHEDULE["scan_timezone"]) == COMPLIANCE_SCHEDULE + + +def test_get_sections(cbcsdk_mock): + """Test get_sections""" + cbcsdk_mock.mock_request("POST", "/compliance/assessment/api/v1/orgs/test/benchmark_sets/_search", + SEARCH_COMPLIANCE_BENCHMARKS) + cbcsdk_mock.mock_request("GET", "/compliance/assessment/api/v1/orgs/test/benchmark_sets/" + "eee5e491-9c31-4a38-84d8-50c9163ef559/sections", + GET_SECTIONS) + + api = cbcsdk_mock.api + benchmark = api.select(ComplianceBenchmark, "eee5e491-9c31-4a38-84d8-50c9163ef559") + assert benchmark.name == "CIS Compliance - Microsoft Windows Server" + + assert benchmark.get_sections() == GET_SECTIONS + + +def test_get_rules(cbcsdk_mock): + """Test get_rules""" + cbcsdk_mock.mock_request("POST", "/compliance/assessment/api/v1/orgs/test/benchmark_sets/_search", + SEARCH_COMPLIANCE_BENCHMARKS) + cbcsdk_mock.mock_request("POST", "/compliance/assessment/api/v1/orgs/test/benchmark_sets/" + "eee5e491-9c31-4a38-84d8-50c9163ef559/rules/_search", + SEARCH_RULES) + cbcsdk_mock.mock_request("GET", "/compliance/assessment/api/v1/orgs/test/benchmark_sets/" + "eee5e491-9c31-4a38-84d8-50c9163ef559/rules/39D861A0-3631-442B-BF94-CC442C73C03E", + GET_RULE) + + api = cbcsdk_mock.api + benchmark = api.select(ComplianceBenchmark, "eee5e491-9c31-4a38-84d8-50c9163ef559") + assert benchmark.name == "CIS Compliance - Microsoft Windows Server" + + assert benchmark.get_rules() == SEARCH_RULES["results"] + + assert benchmark.get_rules("39D861A0-3631-442B-BF94-CC442C73C03E") == [GET_RULE] + + +def test_update_rules(cbcsdk_mock): + """Test update_sections""" + def put_validate(url, body, **kwargs): + assert body == [{ + "rule_id": "39D861A0-3631-442B-BF94-CC442C73C03E", + "enabled": True + }] + return [{ + "id": "39D861A0-3631-442B-BF94-CC442C73C03E", + "rule_name": "(L1) Ensure 'Configure Offer Remote Assistance' is set to 'Disabled'", + "enabled": True, + "section_id": "0ABA0288-8A68-83AF-3BAE-A7F45167564B", + "section_name": "Remote Assistance" + }] + + cbcsdk_mock.mock_request("POST", "/compliance/assessment/api/v1/orgs/test/benchmark_sets/_search", + SEARCH_COMPLIANCE_BENCHMARKS) + cbcsdk_mock.mock_request("PUT", "/compliance/assessment/api/v1/orgs/test/benchmark_sets/" + "eee5e491-9c31-4a38-84d8-50c9163ef559/rules", + put_validate) + + api = cbcsdk_mock.api + benchmark = api.select(ComplianceBenchmark, "eee5e491-9c31-4a38-84d8-50c9163ef559") + assert benchmark.name == "CIS Compliance - Microsoft Windows Server" + + assert benchmark.update_rules(["39D861A0-3631-442B-BF94-CC442C73C03E"], True)[0]["enabled"] is True + + +def test_execute_action(cbcsdk_mock): + """Test execute_action""" + def post_validate(url, body, **kwargs): + assert body["action"] == "REASSESS" + if "device_ids" in body: + assert body["device_ids"] == [1] + return { + "status_code": "SUCCESS", + "message": "Action Successful" + } + return { + "status_code": "SUCCESS", + "message": "Successfully triggered Reassess for BenchmarkSet: eee5e491-9c31-4a38-84d8-50c9163ef559" + } + + cbcsdk_mock.mock_request("POST", "/compliance/assessment/api/v1/orgs/test/benchmark_sets/_search", + SEARCH_COMPLIANCE_BENCHMARKS) + cbcsdk_mock.mock_request("POST", "/compliance/assessment/api/v1/orgs/test/benchmark_sets/" + "eee5e491-9c31-4a38-84d8-50c9163ef559/actions", + post_validate) + cbcsdk_mock.mock_request("POST", "/compliance/assessment/api/v1/orgs/test/benchmark_sets/" + "eee5e491-9c31-4a38-84d8-50c9163ef559/compliance/device_actions", + post_validate) + + api = cbcsdk_mock.api + benchmark = api.select(ComplianceBenchmark, "eee5e491-9c31-4a38-84d8-50c9163ef559") + assert benchmark.name == "CIS Compliance - Microsoft Windows Server" + + assert benchmark.execute_action("REASSESS")["status_code"] == "SUCCESS" + assert benchmark.execute_action("REASSESS", [1])["status_code"] == "SUCCESS" + + +def test_get_rule_compliances(cbcsdk_mock): + """Test get_rule_compliances""" + cbcsdk_mock.mock_request("POST", "/compliance/assessment/api/v1/orgs/test/benchmark_sets/_search", + SEARCH_COMPLIANCE_BENCHMARKS) + cbcsdk_mock.mock_request("POST", "/compliance/assessment/api/v1/orgs/test/benchmark_sets/" + "eee5e491-9c31-4a38-84d8-50c9163ef559/compliance/rules/_search", + RULE_COMPLIANCES) + + api = cbcsdk_mock.api + benchmark = api.select(ComplianceBenchmark, "eee5e491-9c31-4a38-84d8-50c9163ef559") + assert benchmark.name == "CIS Compliance - Microsoft Windows Server" + + assert benchmark.get_rule_compliances("Remote Assistance") == RULE_COMPLIANCES["results"] + + +def test_get_device_rule_compliances(cbcsdk_mock): + """Test get_device_rule_compliances""" + cbcsdk_mock.mock_request("POST", "/compliance/assessment/api/v1/orgs/test/benchmark_sets/_search", + SEARCH_COMPLIANCE_BENCHMARKS) + cbcsdk_mock.mock_request("POST", "/compliance/assessment/api/v1/orgs/test/benchmark_sets/" + "eee5e491-9c31-4a38-84d8-50c9163ef559/compliance/devices/1/rules/_search", + DEVICE_SPECIFIC_RULE_COMPLIANCE) + + api = cbcsdk_mock.api + benchmark = api.select(ComplianceBenchmark, "eee5e491-9c31-4a38-84d8-50c9163ef559") + assert benchmark.name == "CIS Compliance - Microsoft Windows Server" + + assert benchmark.get_device_rule_compliances(1, "Remote Assistance") == DEVICE_SPECIFIC_RULE_COMPLIANCE["results"] + + +def test_get_rule_compliance_devices(cbcsdk_mock): + """Test get_rule_compliance_devices""" + cbcsdk_mock.mock_request("POST", "/compliance/assessment/api/v1/orgs/test/benchmark_sets/_search", + SEARCH_COMPLIANCE_BENCHMARKS) + cbcsdk_mock.mock_request("POST", "/compliance/assessment/api/v1/orgs/test/benchmark_sets/" + "eee5e491-9c31-4a38-84d8-50c9163ef559/compliance/rules/" + "39D861A0-3631-442B-BF94-CC442C73C03E/devices/_search", + RULE_COMPLIANCE_DEVICE_SEARCH) + + api = cbcsdk_mock.api + benchmark = api.select(ComplianceBenchmark, "eee5e491-9c31-4a38-84d8-50c9163ef559") + assert benchmark.name == "CIS Compliance - Microsoft Windows Server" + + assert benchmark.get_rule_compliance_devices("39D861A0-3631-442B-BF94-CC442C73C03E", "Example") == \ + RULE_COMPLIANCE_DEVICE_SEARCH["results"] From 8669e347b395af8ab2ca9cb65230eef487e16205 Mon Sep 17 00:00:00 2001 From: Alex Van Brunt Date: Tue, 16 Apr 2024 13:23:46 -0600 Subject: [PATCH 50/95] Add compliance guide --- docs/compliance.rst | 88 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 docs/compliance.rst diff --git a/docs/compliance.rst b/docs/compliance.rst new file mode 100644 index 00000000..e831921d --- /dev/null +++ b/docs/compliance.rst @@ -0,0 +1,88 @@ +Compliance Benchmarks +====== + +CIS benchmarks are configuration guidelines published by the Center for Internet Security. + The CIS Benchmark enable configuration and retrieval of Benchmark Sets and Rules in Carbon Black Cloud, and + retrieval of the results from scans performed using these Rules. + +For more information on CIS Benchmarks, see the `Center for Internet Security `. + CIS benchmarks contain over 100 configuration guidelines created by a global community of cybersecurity experts to safeguard + various systems against attacks targeting configuration vulnerabilities. + +You can use all the operations shown in the API, such as retrieving, filtering, reaccessing and enabling/disabling the benchmark rules. +You can locate the full list of operations and attributes in the :py:mod:`ComplianceBenchmark() ` class. + +Resources +--------- +* `API Documentation `_ on Developer Network +* `User Guide `_ + +Retrieve Compliance Benchmarks +--------------- + +By using the following the example, you can retrieve the list of supported benchmarks + +.. code-block:: python + + >>> from cbc_sdk import CBCloudAPI + >>> from cbc_sdk.workload import ComplianceBenchmark + >>> api = CBCloudAPI(profile='sample') + >>> benchmark_query = api.select(ComplianceBenchmark) + >>> for benchmark in benchmark_query: + >>> print(benchmark) + ComplianceBenchmark object, bound to https://defense-test03.cbdtest.io. + ------------------------------------------------------------------------------- + + bundle_name: CIS Compliance - Microsoft Windows Server + create_time: 2023-03-20T13:44:10.923039Z + created_by: emuthu+csr@carbonblack.com + enabled: True + id: b7d1b266-d899-4e28-bae6-7619019447ba + name: CIS Windows Server Retail application Prod + os_family: WINDOWS_SERVER + release_time: 2023-07-10T13:55:59.274881Z + supported_os_info: [list:5 items]: + [0]: {'os_metadata_id': '1', 'os_type': 'WINDOWS', '... + [1]: {'os_metadata_id': '2', 'os_type': 'WINDOWS', '... + [2]: {'os_metadata_id': '3', 'os_type': 'WINDOWS', '... + [...] + type: Custom + update_time: 2024-04-15T21:24:43.283032Z + updated_by: + version: 1.0.0.4 + + +Modify Compliance Benchmarks Schedule +--------------- + +By using the following the example, you can get and set the benchmark assessment schedule + +.. code-block:: python + + >>> from cbc_sdk import CBCloudAPI + >>> from cbc_sdk.workload import ComplianceBenchmark + >>> api = CBCloudAPI(profile='sample') + >>> schedule = ComplianceBenchmark.get_compliance_schedule(api) + >>> print(schedule) + >>> ComplianceBenchmark.set_compliance_schedule(api, "RRULE:FREQ=DAILY;BYHOUR=17;BYMINUTE=30;BYSECOND=0", "UTC") + { + "scan_schedule": "FREQ=WEEKLY;BYDAY=TU;BYHOUR=11;BYMINUTE=30;BYSECOND=0", + "scan_timezone": "UTC" + } + + +Reassess Compliance Benchmarks +--------------- + +By using the following the example, you can reasses a benchmark + +.. code-block:: python + + >>> from cbc_sdk import CBCloudAPI + >>> from cbc_sdk.workload import ComplianceBenchmark + >>> api = CBCloudAPI(profile='sample') + >>> benchmark = api.select(ComplianceBenchmark).first() + >>> # Execute for all devices matching benchmark + >>> benchmark.execute_action("REASSESS") + >>> # Execute for a specific set of devices + >>> benchmark.execute_action("REASSESS", [ 1, 2, 3 ]) From 8e6f197f7becf648f6f3e673786c2ed20af768e4 Mon Sep 17 00:00:00 2001 From: Alex Van Brunt Date: Tue, 16 Apr 2024 15:00:10 -0600 Subject: [PATCH 51/95] Add to guide page --- docs/guides.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/guides.rst b/docs/guides.rst index 787b2ae8..1f2d9ad7 100755 --- a/docs/guides.rst +++ b/docs/guides.rst @@ -29,6 +29,7 @@ Feature Guides alerts asset-groups audit-log + compliance developing-credential-providers devices device-control @@ -49,6 +50,7 @@ Feature Guides * :doc:`asset-groups` - Create and modify Asset Groups, and preview the impact changes to policy ranking or asset group definition will have. * :doc:`alerts-migration` - Update from SDK 1.4.3 or earlier to SDK 1.5.0 or later to get the benefits of the Alerts v7 API. * :doc:`audit-log` - Retrieve audit log events indicating various "system" events. +* :doc:`compliance` - Search and validate Compliance Benchmarks. * :doc:`devices` - Search for, get information about, and act on endpoints. * :doc:`device-control` - Control the blocking of USB devices on endpoints. * :doc:`differential-analysis` - Provides the ability to compare and understand the changes between two Live Query runs From 36080636e58a3606fd43ec509c2ffedaa11a2e2f Mon Sep 17 00:00:00 2001 From: Alex Van Brunt Date: Tue, 16 Apr 2024 15:04:00 -0600 Subject: [PATCH 52/95] Fix link --- docs/compliance.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/compliance.rst b/docs/compliance.rst index e831921d..d3e5f791 100644 --- a/docs/compliance.rst +++ b/docs/compliance.rst @@ -2,12 +2,12 @@ Compliance Benchmarks ====== CIS benchmarks are configuration guidelines published by the Center for Internet Security. - The CIS Benchmark enable configuration and retrieval of Benchmark Sets and Rules in Carbon Black Cloud, and - retrieval of the results from scans performed using these Rules. +The CIS Benchmark enable configuration and retrieval of Benchmark Sets and Rules in Carbon Black Cloud, and +retrieval of the results from scans performed using these Rules. -For more information on CIS Benchmarks, see the `Center for Internet Security `. - CIS benchmarks contain over 100 configuration guidelines created by a global community of cybersecurity experts to safeguard - various systems against attacks targeting configuration vulnerabilities. +For more information on CIS Benchmarks, see the `Center for Internet Security `_. +CIS benchmarks contain over 100 configuration guidelines created by a global community of cybersecurity experts to safeguard +various systems against attacks targeting configuration vulnerabilities. You can use all the operations shown in the API, such as retrieving, filtering, reaccessing and enabling/disabling the benchmark rules. You can locate the full list of operations and attributes in the :py:mod:`ComplianceBenchmark() ` class. From 693b613200b7236c276237fe2e44fadd2bd9a620 Mon Sep 17 00:00:00 2001 From: Alex Van Brunt Date: Thu, 18 Apr 2024 09:51:19 -0600 Subject: [PATCH 53/95] Address feedback --- docs/compliance.rst | 2 +- src/cbc_sdk/workload/compliance_assessment.py | 33 +++++++++++++++++-- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/docs/compliance.rst b/docs/compliance.rst index d3e5f791..9e5a734d 100644 --- a/docs/compliance.rst +++ b/docs/compliance.rst @@ -62,9 +62,9 @@ By using the following the example, you can get and set the benchmark assessment >>> from cbc_sdk import CBCloudAPI >>> from cbc_sdk.workload import ComplianceBenchmark >>> api = CBCloudAPI(profile='sample') + >>> ComplianceBenchmark.set_compliance_schedule(api, "RRULE:FREQ=DAILY;BYHOUR=17;BYMINUTE=30;BYSECOND=0", "UTC") >>> schedule = ComplianceBenchmark.get_compliance_schedule(api) >>> print(schedule) - >>> ComplianceBenchmark.set_compliance_schedule(api, "RRULE:FREQ=DAILY;BYHOUR=17;BYMINUTE=30;BYSECOND=0", "UTC") { "scan_schedule": "FREQ=WEEKLY;BYDAY=TU;BYHOUR=11;BYMINUTE=30;BYSECOND=0", "scan_timezone": "UTC" diff --git a/src/cbc_sdk/workload/compliance_assessment.py b/src/cbc_sdk/workload/compliance_assessment.py index b50e180d..3f32fee8 100644 --- a/src/cbc_sdk/workload/compliance_assessment.py +++ b/src/cbc_sdk/workload/compliance_assessment.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 - # ******************************************************* -# Copyright (c) VMware, Inc. 2021-2023. All Rights Reserved. +# Copyright (c) VMware, Inc. 2021-2024. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * @@ -67,6 +66,9 @@ def get_benchmark_set_summary(self): """ Fetches the compliance summary for the current benchmark set. + Required Permissions: + complianceAssessment.data(READ) + Returns: dict: The benchmark compliance summary """ @@ -82,6 +84,9 @@ def get_compliance_schedule(cb): Args: cb (CBCloudAPI): An instance of CBCloudAPI representing the Carbon Black Cloud API. + Required Permissions: + complianceAssessment.data(READ) + Raises: ApiError: If cb is not an instance of CBCloudAPI. @@ -105,6 +110,9 @@ def set_compliance_schedule(cb, scan_schedule, scan_timezone): """ Sets the compliance scan schedule and timezone for the organization. + Required Permissions: + complianceAssessment.data(UPDATE) + Args: cb (CBCloudAPI): An instance of CBCloudAPI representing the Carbon Black Cloud API. scan_schedule (str): The scan schedule, specified in RFC 5545 format. @@ -139,6 +147,9 @@ def get_sections(self): """ Get Sections of the Benchmark Set. + Required Permissions: + complianceAssessment.data(READ) + Returns: list[dict]: List of sections within the Benchmark Set. @@ -156,6 +167,9 @@ def get_rules(self, rule_id=None): """ Fetches compliance rules associated with the benchmark set. + Required Permissions: + complianceAssessment.data(READ) + Args: rule_id (str, optional): The rule ID to fetch a specific rule. Defaults to None. @@ -200,6 +214,9 @@ def update_rules(self, rule_ids, enabled): """ Update compliance rules associated with the benchmark set. + Required Permissions: + complianceAssessment.data(UPDATE) + Args: rule_ids (list[str]): The rule IDs to update their enabled/disabled status. enabled (bool): Whether the rule is enabled or disabled. @@ -226,6 +243,9 @@ def execute_action(self, action, device_ids=None): """ Execute a specified action for the Benchmark Set for all devices or a specified subset. + Required Permissions: + complianceAssessment.data(EXECUTE) + Args: action (str): The action to be executed. Options: ENABLE, DISABLE, REASSESS @@ -305,6 +325,9 @@ def get_rule_compliances(self, query=""): """ Fetches rule compliance summaries associated with the benchmark set. + Required Permissions: + complianceAssessment.data(READ) + Args: query (str, optional): The query to filter results. @@ -345,6 +368,9 @@ def get_device_rule_compliances(self, device_id, query=""): """ Fetches rule compliances for specific device. + Required Permissions: + complianceAssessment.data(READ) + Args: device_id (int): Device id to fetch benchmark rule compliance query (str, optional): The query to filter results. @@ -388,6 +414,9 @@ def get_rule_compliance_devices(self, rule_id, query=""): """ Fetches device compliances for a specific rule. + Required Permissions: + complianceAssessment.data(READ) + Args: rule_id (str): Rule id to fetch device compliances query (str, optional): The query to filter results. From cc060db271c9accba038f3c512f6819bb688f604 Mon Sep 17 00:00:00 2001 From: Alex Van Brunt Date: Thu, 25 Apr 2024 13:15:38 -0500 Subject: [PATCH 54/95] Add cis benchmarks to workload file --- docs/cbc_sdk.workload.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/cbc_sdk.workload.rst b/docs/cbc_sdk.workload.rst index d2b36940..ed43d8d5 100644 --- a/docs/cbc_sdk.workload.rst +++ b/docs/cbc_sdk.workload.rst @@ -1,6 +1,14 @@ Workload Package ***************** +CIS Benchmarks +----------------------------------------- + +.. automodule:: cbc_sdk.workload.compliance_assessment + :members: + :inherited-members: + :show-inheritance: + NSX Remediation Module ----------------------------------------- From b2c46f1d45b7e809850cc17ef9ab8eb564d25b12 Mon Sep 17 00:00:00 2001 From: Alex Van Brunt Date: Thu, 25 Apr 2024 13:53:57 -0500 Subject: [PATCH 55/95] Expose get device summaries. Hide get compliance summary --- docs/compliance.rst | 26 +++++ src/cbc_sdk/workload/compliance_assessment.py | 104 +++++++++--------- .../unit/fixtures/workload/mock_compliance.py | 17 +++ src/tests/unit/workload/test_compliance.py | 18 ++- 4 files changed, 112 insertions(+), 53 deletions(-) diff --git a/docs/compliance.rst b/docs/compliance.rst index 9e5a734d..86fa3a03 100644 --- a/docs/compliance.rst +++ b/docs/compliance.rst @@ -86,3 +86,29 @@ By using the following the example, you can reasses a benchmark >>> benchmark.execute_action("REASSESS") >>> # Execute for a specific set of devices >>> benchmark.execute_action("REASSESS", [ 1, 2, 3 ]) + + +Device Compliance Summary +--------------- + +By using the following the example, you can fetch the compliance percentage for each device assessed by the Compliance Benchmark + +.. code-block:: python + + >>> from cbc_sdk import CBCloudAPI + >>> from cbc_sdk.workload import ComplianceBenchmark + >>> api = CBCloudAPI(profile='sample') + >>> benchmark = api.select(ComplianceBenchmark).first() + >>> summaries = benchmark.get_device_compliances() + >>> print(summaries[0]) + { + "device_id": 39074613, + "device_name": "Example\\Win2022", + "os_version": "Windows Server 2022 x64", + "compliance_percentage": 93, + "last_assess_time": "2024-04-16T00:00:00.014765Z", + "excluded_on": None, + "excluded_by": None, + "reason": None, + "deployment_type": "WORKLOAD" + } diff --git a/src/cbc_sdk/workload/compliance_assessment.py b/src/cbc_sdk/workload/compliance_assessment.py index 3f32fee8..e78ffdfe 100644 --- a/src/cbc_sdk/workload/compliance_assessment.py +++ b/src/cbc_sdk/workload/compliance_assessment.py @@ -62,19 +62,21 @@ def _query_implementation(cls, cb, **kwargs): """ return ComplianceBenchmarkQuery(cls, cb) - def get_benchmark_set_summary(self): - """ - Fetches the compliance summary for the current benchmark set. + # API Not Supported + # + # def get_benchmark_set_summary(self): + # """ + # Fetches the compliance summary for the current benchmark set. - Required Permissions: - complianceAssessment.data(READ) + # Required Permissions: + # complianceAssessment.data(READ) - Returns: - dict: The benchmark compliance summary - """ - url = self.urlobject.format(self._cb.credentials.org_key) + f"{self.id}/compliance/summary" - results = self._cb.get_object(url) - return results + # Returns: + # dict: The benchmark compliance summary + # """ + # url = self.urlobject.format(self._cb.credentials.org_key) + f"{self.id}/compliance/summary" + # results = self._cb.get_object(url) + # return results @staticmethod def get_compliance_schedule(cb): @@ -279,47 +281,45 @@ def execute_action(self, action, device_ids=None): return self._cb.post_object(url, body=args).json() - # API Not supported - # - # def get_device_compliances(self, query=""): - # """ - # Fetches devices compliance summaries associated with the benchmark set. - # - # Args: - # query (str, optional): The query to filter results. - # - # Returns: - # [dict]: List of Device Compliances - # - # Example: - # >>> cb = CBCloudAPI(profile="example_profile") - # >>> benchmark_set = cb.select(ComplianceBenchmark).first() - # >>> rules = benchmark_set.get_device_compliance() - # """ - # url = self.urlobject.format(self._cb.credentials.org_key) + f"{self.id}/compliance/devices/_search" - # current = 0 - # device_compliances = [] - # while True: - # resp = self._cb.post_object(url, body={ - # "query": query, - # "rows": 10000, - # "start": current, - # "sort": [ - # { - # "field": "device_name", - # "order": "DESC" - # } - # ] - # }) - # result = resp.json() - # - # device_compliances.extend(result.get("results", [])) - # current = len(device_compliances) - # - # if current >= result["num_found"]: - # break - # - # return device_compliances + def get_device_compliances(self, query=""): + """ + Fetches devices compliance summaries associated with the benchmark set. + + Args: + query (str, optional): The query to filter results. + + Returns: + [dict]: List of Device Compliances + + Example: + >>> cb = CBCloudAPI(profile="example_profile") + >>> benchmark_set = cb.select(ComplianceBenchmark).first() + >>> rules = benchmark_set.get_device_compliance() + """ + url = self.urlobject.format(self._cb.credentials.org_key) + f"{self.id}/compliance/devices/_search" + current = 0 + device_compliances = [] + while True: + resp = self._cb.post_object(url, body={ + "query": query, + "rows": 10000, + "start": current, + "sort": [ + { + "field": "device_name", + "order": "DESC" + } + ] + }) + result = resp.json() + + device_compliances.extend(result.get("results", [])) + current = len(device_compliances) + + if current >= result["num_found"]: + break + + return device_compliances def get_rule_compliances(self, query=""): """ diff --git a/src/tests/unit/fixtures/workload/mock_compliance.py b/src/tests/unit/fixtures/workload/mock_compliance.py index 2479789a..f5f46865 100644 --- a/src/tests/unit/fixtures/workload/mock_compliance.py +++ b/src/tests/unit/fixtures/workload/mock_compliance.py @@ -229,3 +229,20 @@ } ] } + +DEVICE_COMPLIANCES = { + "num_found": 1, + "results": [ + { + "device_id": 39074613, + "device_name": "Example\\Win2022", + "os_version": "Windows Server 2022 x64", + "compliance_percentage": 93, + "last_assess_time": "2024-04-16T00:00:00.014765Z", + "excluded_on": None, + "excluded_by": None, + "reason": None, + "deployment_type": "WORKLOAD" + } + ] +} diff --git a/src/tests/unit/workload/test_compliance.py b/src/tests/unit/workload/test_compliance.py index 827a42e1..38047fd7 100644 --- a/src/tests/unit/workload/test_compliance.py +++ b/src/tests/unit/workload/test_compliance.py @@ -26,7 +26,8 @@ GET_RULE, RULE_COMPLIANCES, DEVICE_SPECIFIC_RULE_COMPLIANCE, - RULE_COMPLIANCE_DEVICE_SEARCH) + RULE_COMPLIANCE_DEVICE_SEARCH, + DEVICE_COMPLIANCES) logging.basicConfig(format="%(asctime)s %(levelname)s:%(message)s", level=logging.DEBUG, filename="log.txt") @@ -220,6 +221,21 @@ def post_validate(url, body, **kwargs): assert benchmark.execute_action("REASSESS", [1])["status_code"] == "SUCCESS" +def test_get_device_compliances(cbcsdk_mock): + """Test get_device_compliances""" + cbcsdk_mock.mock_request("POST", "/compliance/assessment/api/v1/orgs/test/benchmark_sets/_search", + SEARCH_COMPLIANCE_BENCHMARKS) + cbcsdk_mock.mock_request("POST", "/compliance/assessment/api/v1/orgs/test/benchmark_sets/" + "eee5e491-9c31-4a38-84d8-50c9163ef559/compliance/devices/_search", + DEVICE_COMPLIANCES) + + api = cbcsdk_mock.api + benchmark = api.select(ComplianceBenchmark, "eee5e491-9c31-4a38-84d8-50c9163ef559") + assert benchmark.name == "CIS Compliance - Microsoft Windows Server" + + assert benchmark.get_device_compliances("Remote Assistance") == DEVICE_COMPLIANCES["results"] + + def test_get_rule_compliances(cbcsdk_mock): """Test get_rule_compliances""" cbcsdk_mock.mock_request("POST", "/compliance/assessment/api/v1/orgs/test/benchmark_sets/_search", From ec006a9e703a0db1f4f965e36776bcf58981b436 Mon Sep 17 00:00:00 2001 From: Alex Van Brunt Date: Thu, 25 Apr 2024 13:55:29 -0500 Subject: [PATCH 56/95] Fix docstring --- src/cbc_sdk/workload/compliance_assessment.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/cbc_sdk/workload/compliance_assessment.py b/src/cbc_sdk/workload/compliance_assessment.py index e78ffdfe..9ad69981 100644 --- a/src/cbc_sdk/workload/compliance_assessment.py +++ b/src/cbc_sdk/workload/compliance_assessment.py @@ -285,6 +285,9 @@ def get_device_compliances(self, query=""): """ Fetches devices compliance summaries associated with the benchmark set. + Required Permissions: + complianceAssessment.data(READ) + Args: query (str, optional): The query to filter results. @@ -294,7 +297,7 @@ def get_device_compliances(self, query=""): Example: >>> cb = CBCloudAPI(profile="example_profile") >>> benchmark_set = cb.select(ComplianceBenchmark).first() - >>> rules = benchmark_set.get_device_compliance() + >>> device_compliances = benchmark_set.get_device_compliance() """ url = self.urlobject.format(self._cb.credentials.org_key) + f"{self.id}/compliance/devices/_search" current = 0 From 9a55beb73be1d5da196b17edca70f0804aa15886 Mon Sep 17 00:00:00 2001 From: Alex Van Brunt Date: Fri, 26 Apr 2024 14:20:49 -0500 Subject: [PATCH 57/95] Remove summary function --- src/cbc_sdk/workload/compliance_assessment.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/cbc_sdk/workload/compliance_assessment.py b/src/cbc_sdk/workload/compliance_assessment.py index 9ad69981..42d9f9f7 100644 --- a/src/cbc_sdk/workload/compliance_assessment.py +++ b/src/cbc_sdk/workload/compliance_assessment.py @@ -62,22 +62,6 @@ def _query_implementation(cls, cb, **kwargs): """ return ComplianceBenchmarkQuery(cls, cb) - # API Not Supported - # - # def get_benchmark_set_summary(self): - # """ - # Fetches the compliance summary for the current benchmark set. - - # Required Permissions: - # complianceAssessment.data(READ) - - # Returns: - # dict: The benchmark compliance summary - # """ - # url = self.urlobject.format(self._cb.credentials.org_key) + f"{self.id}/compliance/summary" - # results = self._cb.get_object(url) - # return results - @staticmethod def get_compliance_schedule(cb): """ From 996b1480e8de1f6832ba37f96de15fdc0da804b9 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Thu, 11 Apr 2024 13:03:35 -0600 Subject: [PATCH 58/95] first sketch of additions to AuditLogRecord object --- src/cbc_sdk/platform/__init__.py | 4 +- src/cbc_sdk/platform/audit.py | 68 +++++++++++++++++-- .../platform/models/audit_log_record.yaml | 27 ++++++++ 3 files changed, 94 insertions(+), 5 deletions(-) create mode 100644 src/cbc_sdk/platform/models/audit_log_record.yaml diff --git a/src/cbc_sdk/platform/__init__.py b/src/cbc_sdk/platform/__init__.py index e7dc9c00..e5b05c7b 100644 --- a/src/cbc_sdk/platform/__init__.py +++ b/src/cbc_sdk/platform/__init__.py @@ -8,7 +8,9 @@ from cbc_sdk.platform.alerts import Alert as BaseAlert -from cbc_sdk.platform.audit import AuditLog +from cbc_sdk.platform.audit import AuditLogRecord + +from cbc_sdk.platform.audit import AuditLogRecord as AuditLog from cbc_sdk.platform.asset_groups import AssetGroup diff --git a/src/cbc_sdk/platform/audit.py b/src/cbc_sdk/platform/audit.py index 4d9cee3a..c77a9299 100644 --- a/src/cbc_sdk/platform/audit.py +++ b/src/cbc_sdk/platform/audit.py @@ -13,15 +13,39 @@ """Model and Query Classes for Platform Auditing""" -from cbc_sdk.base import UnrefreshableModel +from cbc_sdk.base import (UnrefreshableModel, BaseQuery, QueryBuilder, QueryBuilderSupportMixin, + CriteriaBuilderSupportMixin, IterableQueryMixin, AsyncQueryMixin) -class AuditLog(UnrefreshableModel): +"""Model Class""" + + +class AuditLogRecord(UnrefreshableModel): """Model class which represents audit log events. Mostly for future implementation.""" + urlobject = "/audit_log/v1/orgs/{0}/logs" + swagger_meta_file = "platform/models/audit_log_record.yaml" def __init__(self, cb, model_unique_id, initial_data=None): - """Creation of AuditLog objects is not yet implemented.""" - raise NotImplementedError("AuditLog creation will be in a future implementation") + """ + Creates a new ``AuditLogRecord``. + + Args: + cb (BaseAPI): Reference to API object used to communicate with the server. + model_unique_id (int): Not used. + initial_data (dict): Initial data to fill in the audit log record details. + """ + super(AuditLogRecord, self).__init__(cb, model_unique_id, initial_data) + + @classmethod + def _query_implementation(cls, cb, **kwargs): + """ + Returns the appropriate query object for the ``AuditLogRecord`` type. + + Args: + cb (BaseAPI): Reference to API object used to communicate with the server. + **kwargs (dict): Not used, retained for compatibility. + """ + return AuditLogRecordQuery(cls, cb) @staticmethod def get_auditlogs(cb): @@ -39,3 +63,39 @@ def get_auditlogs(cb): """ res = cb.get_object("/integrationServices/v3/auditlogs") return res.get("notifications", []) + + +"""Query Class""" + + +class AuditLogRecordQuery(BaseQuery, QueryBuilderSupportMixin, CriteriaBuilderSupportMixin, + IterableQueryMixin, AsyncQueryMixin): + """ + Query object that is used to locate ``AuditLogRecord`` objects. + + The ``AuditLogRecordQuery`` is constructed via SDK functions like the ``select()`` method on ``CBCloudAPI``. + The user would then add a query and/or criteria to it before iterating over the results. + """ + def __init__(self, doc_class, cb): + """ + Initialize the ``AuditLogRecordQuery``. + + Args: + doc_class (class): The model class that will be returned by this query. + cb (BaseAPI): Reference to API object used to communicate with the server. + """ + self._doc_class = doc_class + self._cb = cb + self._count_valid = False + super(AuditLogRecordQuery, self).__init__() + + self._query_builder = QueryBuilder() + self._criteria = {} + self._time_filter = {} + self._exclusions = {} + self._sortcriteria = {} + self._search_after = None + self.num_remaining = None + self.num_found = None + self.max_rows = -1 + \ No newline at end of file diff --git a/src/cbc_sdk/platform/models/audit_log_record.yaml b/src/cbc_sdk/platform/models/audit_log_record.yaml new file mode 100644 index 00000000..842ad091 --- /dev/null +++ b/src/cbc_sdk/platform/models/audit_log_record.yaml @@ -0,0 +1,27 @@ +type: object +properties: + actor_ip: + type: string + description: IP address of the entity that caused the creation of this audit log + actor: + type: string + description: Name of the entity that caused the creation of this audit log + create_time: + type: string + format: date-time + description: Timestamp when this audit log was created in ISO-8601 string format + description: + type: string + description: Text description of this audit log + flagged: + type: boolean + description: Whether the audit has been flagged + org_key: + type: string + description: Organization key + request_url: + type: string + description: URL of the request that caused the creation of this audit log + verbose: + type: boolean + description: Whether the audit has been marked verbose From fb3138fa8f6bbeda963328a8e0844be333fc17c5 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Tue, 16 Apr 2024 16:44:01 -0600 Subject: [PATCH 59/95] completed writing the query object --- src/cbc_sdk/platform/audit.py | 248 +++++++++++++++++++++++++++++++++- 1 file changed, 244 insertions(+), 4 deletions(-) diff --git a/src/cbc_sdk/platform/audit.py b/src/cbc_sdk/platform/audit.py index c77a9299..68fd5266 100644 --- a/src/cbc_sdk/platform/audit.py +++ b/src/cbc_sdk/platform/audit.py @@ -13,8 +13,13 @@ """Model and Query Classes for Platform Auditing""" +import datetime from cbc_sdk.base import (UnrefreshableModel, BaseQuery, QueryBuilder, QueryBuilderSupportMixin, - CriteriaBuilderSupportMixin, IterableQueryMixin, AsyncQueryMixin) + CriteriaBuilderSupportMixin, ExclusionBuilderSupportMixin, IterableQueryMixin, + AsyncQueryMixin) +from cbc_sdk.errors import ApiError + +from backports._datetime_fromisoformat import datetime_fromisoformat """Model Class""" @@ -69,12 +74,20 @@ def get_auditlogs(cb): class AuditLogRecordQuery(BaseQuery, QueryBuilderSupportMixin, CriteriaBuilderSupportMixin, - IterableQueryMixin, AsyncQueryMixin): + ExclusionBuilderSupportMixin, IterableQueryMixin, AsyncQueryMixin): """ Query object that is used to locate ``AuditLogRecord`` objects. The ``AuditLogRecordQuery`` is constructed via SDK functions like the ``select()`` method on ``CBCloudAPI``. The user would then add a query and/or criteria to it before iterating over the results. + + The following criteria may be added to the query via the standard ``add_criteria()`` method, or added to query + exclusions via the standard ``add_exclusions()`` method: + + * ``actor_ip`` - IP address of the entity that caused the creation of this audit log. + * ``actor`` - Name of the entity that caused the creation of this audit log. + * ``request_url`` - URL of the request that caused the creation of this audit log. + * ``description`` - Text description of this audit log. """ def __init__(self, doc_class, cb): """ @@ -91,11 +104,238 @@ def __init__(self, doc_class, cb): self._query_builder = QueryBuilder() self._criteria = {} - self._time_filter = {} self._exclusions = {} self._sortcriteria = {} self._search_after = None self.num_remaining = None self.num_found = None self.max_rows = -1 - \ No newline at end of file + + @staticmethod + def _create_valid_time_filter(self, kwargs): + """ + Verifies that an alert criteria key has the timerange functionality + + Args: + kwargs (dict): Used to specify start= for start time, end= for end time, and range= for range. Values are + either timestamp ISO 8601 strings or datetime objects for start and end time. For range the time range + to execute the result search, ending on the current time. Should be in the form "-2w", + where y=year, w=week, d=day, h=hour, m=minute, s=second. + + Returns: + dict: A new filter object. + + Raises: + ApiError: If the argument format is incorrect. + """ + time_filter = {} + if kwargs.get("start", None) and kwargs.get("end", None): + if kwargs.get("range", None): + raise ApiError("cannot specify range= in addition to start= and end=") + stime = kwargs["start"] + etime = kwargs["end"] + try: + if isinstance(stime, str): + stime = datetime_fromisoformat(stime) + if isinstance(etime, str): + etime = datetime_fromisoformat(etime) + if isinstance(stime, datetime.datetime) and isinstance(etime, datetime.datetime): + time_filter = {"start": stime.strftime("%Y-%m-%dT%H:%M:%S.%fZ"), + "end": etime.strftime("%Y-%m-%dT%H:%M:%S.%fZ")} + except: + raise ApiError(f"Start and end time must be a string in ISO 8601 format or an object of datetime. " + f"Start time {stime} is a {type(stime)}. End time {etime} is a {type(etime)}.") + elif kwargs.get("range", None): + if kwargs.get("start", None) or kwargs.get("end", None): + raise ApiError("cannot specify start= or end= in addition to range=") + time_filter = {"range": kwargs["range"]} + else: + raise ApiError("must specify either start= and end= or range=") + return time_filter + + def add_time_criteria(self, **kwargs): + """ + Adds a ``create_time`` criteria value to either criteria or exclusions. + + Args: + kwargs (dict): Keyword arguments to this method. + + Keyword Args: + start (str/datetime): Starting time for the time interval to include in the criteria. Must be either a + ``datetime`` object or a string in ISO 8601 format. Both ``start`` and ``end`` must be specified + if they are to be used. + end (str/datetime): Ending time for the time interval to include in the criteria. Must be either a + ``datetime`` object or a string in ISO 8601 format. Both ``start`` and ``end`` must be specified + if they are to be used. + range (str): Range for the time interval, to be measured backwards from the current time. Cannot + be specified if ``start`` or ``end`` are specified. Must be in the format "-NX", where ``N`` is an + integer value, and ``X`` is a single character specifying the time unit: "y" for years, "w" for weeks, + "d" for days, "h" for hours, "m" for minutes, or "s" for seconds. + exclude (bool): ``True`` if this value is to be applied to exclusions, ``False`` if this value is to be + applied to search criteria. Default ``False.`` + + Returns: + AuditLogRecordQuery: This instance. + + Raises: + ApiError: If the argument format is incorrect. + """ + if kwargs.get("exclude", False): + self._exclusions['create_time'] = self._create_valid_time_filter(kwargs) + else: + self._criteria['create_time'] = self._create_valid_time_filter(kwargs) + return self + + def add_boolean_criteria(self, criteria_name, value, exclude=False): + """ + Adds a Boolean value to either the criteria or exclusions. + + Args: + criteria_name (str): The criteria name to set. May be either "flagged" (to set whether or not the audit + record has been flagged) or "verbose" (so set whether or not the audit record has been marked verbose). + value (bool): The value of the criteria to be set. + exclude (bool): ``True`` if this value is to be applied to exclusions, ``False`` if this value is to be + applied to search criteria. Default ``False.`` + + Returns: + AuditLogRecordQuery: This instance. + """ + if exclude: + self._exclusions[criteria_name] = value + else: + self._criteria[criteria_name] = value + return self + + def sort_by(self, key, direction="ASC"): + """ + Sets the sorting behavior on a query's results. + + Example: + >>> cb.select(AuditLogRecord).sort_by("name") + + Args: + key (str): The key in the schema to sort by. + direction (str): The sort order, either "ASC" or "DESC". + + Returns: + AuditLogRecordQuery: This instance. + """ + if direction not in CriteriaBuilderSupportMixin.VALID_DIRECTIONS: + raise ApiError("invalid sort direction specified") + self._sortcriteria = {"field": key, "order": direction} + return self + + def _build_request(self, from_row, max_rows): + """ + Creates the request body for an API call. + + Args: + from_row (int): The row to start the query at. + max_rows (int): The maximum number of rows to be returned. + + Returns: + dict: The complete request body. + """ + request = {} + if self._criteria: + request['criteria'] = self._criteria + if self._exclusions: + request['exclusions'] = self._exclusions + query = self._query_builder.collapse() + if query: + request['query'] = query + if max_rows > 0: + request['rows'] = max_rows + if from_row > 0: + request['start'] = from_row + if self._sortcriteria: + request['sort'] = [self._sortcriteria] + return request + + def _build_url(self, tail_end): + """ + Creates the URL to be used for an API call. + + Args: + tail_end (str): String to be appended to the end of the generated URL. + + Returns: + str: The complete URL. + """ + url = self._doc_class.urlobject.format(self._cb.credentials.org_key) + tail_end + return url + + def _count(self): + """ + Returns the number of results from the run of this query. + + Returns: + int: The number of results from the run of this query. + """ + if self._count_valid: + return self._total_results + + url = self._build_url("/_search") + request = self._build_request(0, -1) + resp = self._cb.post_object(url, body=request) + result = resp.json() + + self._total_results = result["num_found"] + self._count_valid = True + + return self._total_results + + def _perform_query(self, from_row=0, max_rows=-1): + """ + Performs the query and returns the results of the query in an iterable fashion. + + Args: + from_row (int): The row to start the query at (default 0). + max_rows (int): The maximum number of rows to be returned (default -1, meaning "all"). + + Yields: + AuditLogRecord: The audit log records resulting from the search. + """ + url = self._build_url("/_search") + current = from_row + numrows = 0 + still_querying = True + while still_querying: + request = self._build_request(current, max_rows) + resp = self._cb.post_object(url, body=request) + result = resp.json() + + self._total_results = result["num_found"] + self._count_valid = True + + results = result.get("results", []) + for item in results: + yield self._doc_class(self._cb, 0, item) + current += 1 + numrows += 1 + + if max_rows > 0 and numrows == max_rows: + still_querying = False + break + + from_row = current + if current >= self._total_results: + still_querying = False + break + + def _run_async_query(self, context): + """ + Executed in the background to run an asynchronous query. + + Args: + context (object): Not used. + + Returns: + list[AuditLogRecord]: The results of the query. + """ + url = self._build_url("/_search") + request = self._build_request(0, -1) + resp = self._cb.post_object(url, body=request) + result = resp.json() + results = result.get("results", []) + return [self._doc_class(self._cb, 0, item) for item in results] From d450cfd2af691465b60f6991776bfdc5d0190e88 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Wed, 17 Apr 2024 14:33:34 -0600 Subject: [PATCH 60/95] first unit test added and fixed up --- src/cbc_sdk/platform/audit.py | 10 +-- src/cbc_sdk/rest_api.py | 4 +- .../unit/fixtures/platform/mock_audit.py | 80 +++++++++++++++++++ src/tests/unit/platform/test_audit.py | 44 ++++++++-- 4 files changed, 123 insertions(+), 15 deletions(-) diff --git a/src/cbc_sdk/platform/audit.py b/src/cbc_sdk/platform/audit.py index 68fd5266..d4cabf3a 100644 --- a/src/cbc_sdk/platform/audit.py +++ b/src/cbc_sdk/platform/audit.py @@ -21,7 +21,6 @@ from backports._datetime_fromisoformat import datetime_fromisoformat - """Model Class""" @@ -39,7 +38,7 @@ def __init__(self, cb, model_unique_id, initial_data=None): model_unique_id (int): Not used. initial_data (dict): Initial data to fill in the audit log record details. """ - super(AuditLogRecord, self).__init__(cb, model_unique_id, initial_data) + super(AuditLogRecord, self).__init__(cb, model_unique_id, initial_data, force_init=False, full_doc=True) @classmethod def _query_implementation(cls, cb, **kwargs): @@ -89,6 +88,7 @@ class AuditLogRecordQuery(BaseQuery, QueryBuilderSupportMixin, CriteriaBuilderSu * ``request_url`` - URL of the request that caused the creation of this audit log. * ``description`` - Text description of this audit log. """ + def __init__(self, doc_class, cb): """ Initialize the ``AuditLogRecordQuery``. @@ -112,13 +112,13 @@ def __init__(self, doc_class, cb): self.max_rows = -1 @staticmethod - def _create_valid_time_filter(self, kwargs): + def _create_valid_time_filter(kwargs): """ Verifies that an alert criteria key has the timerange functionality Args: kwargs (dict): Used to specify start= for start time, end= for end time, and range= for range. Values are - either timestamp ISO 8601 strings or datetime objects for start and end time. For range the time range + either timestamp ISO 8601 strings or datetime objects for start and end time. For range, the time range to execute the result search, ending on the current time. Should be in the form "-2w", where y=year, w=week, d=day, h=hour, m=minute, s=second. @@ -241,7 +241,7 @@ def _build_request(self, from_row, max_rows): request['criteria'] = self._criteria if self._exclusions: request['exclusions'] = self._exclusions - query = self._query_builder.collapse() + query = self._query_builder._collapse() if query: request['query'] = query if max_rows > 0: diff --git a/src/cbc_sdk/rest_api.py b/src/cbc_sdk/rest_api.py index 3a075df8..2c5c112c 100644 --- a/src/cbc_sdk/rest_api.py +++ b/src/cbc_sdk/rest_api.py @@ -22,7 +22,7 @@ from cbc_sdk.errors import ApiError, CredentialError, ServerError, InvalidObjectError from cbc_sdk.live_response_api import LiveResponseSessionManager from cbc_sdk.audit_remediation import Run, RunHistory -from cbc_sdk.platform.audit import AuditLog +from cbc_sdk.platform.audit import AuditLogRecord from cbc_sdk.enterprise_edr.threat_intelligence import ReportSeverity import logging import time @@ -210,7 +210,7 @@ def get_auditlogs(self): list[dict]: List of dictionary objects representing the audit logs, or an empty list if none available. """ log.warning("CBCloudAPI.get_auditlogs is deprecated, use AuditLog.get_auditlogs instead") - return AuditLog.get_auditlogs(self) + return AuditLogRecord.get_auditlogs(self) # ---- Device API diff --git a/src/tests/unit/fixtures/platform/mock_audit.py b/src/tests/unit/fixtures/platform/mock_audit.py index 11f065e9..752a518e 100644 --- a/src/tests/unit/fixtures/platform/mock_audit.py +++ b/src/tests/unit/fixtures/platform/mock_audit.py @@ -61,3 +61,83 @@ "success": True, "message": "Success", } + +AUDIT_SEARCH_REQUEST = { + "criteria": { + "actor_ip": ["10.29.99.1"], + "actor": ["ABCDEFGHIJ"], + "request_url": ["https://inclusiveladyship.com"], + "description": ["FOOBAR"], + "flagged": True, + "verbose": False, + "create_time": { + "start": "2024-03-01T00:00:00.000000Z", + "end": "2024-03-31T22:00:00.000000Z" + } + }, + "exclusions": { + "actor_ip": ["10.29.99.254"], + "actor": ["JIHGFEDCBA"], + "request_url": ["https://links.inclusiveladyship.com"], + "description": ["BLORT"], + "flagged": False, + "verbose": True, + "create_time": { + "range": "-5d" + } + }, + "query": "description:FOO", + "sort": [ + { + "field": "actor_ip", + "order": "ASC" + } + ] +} + +AUDIT_SEARCH_RESPONSE = { + "num_found": 5, + "num_available": 5, + "results": [ + { + "org_key": "test", + "actor_ip": "192.168.0.5", + "actor": "DEFGHIJKLM", + "request_url": None, + "description": "Connector DEFGHIJKLM logged in successfully", + "create_time": "2024-04-17T19:18:37.480Z" + }, + { + "org_key": "test", + "actor_ip": "192.168.3.5", + "actor": "BELTALOWDA", + "request_url": None, + "description": "Updated report, 'MCRN threat feed'", + "create_time": "2024-04-17T19:13:01.528Z" + }, + { + "org_key": "test", + "actor_ip": "192.168.3.8", + "actor": "BELTALOWDA", + "request_url": None, + "description": "Updated report, 'Reported by DOP'", + "create_time": "2024-04-17T19:13:01.042Z" + }, + { + "org_key": "test", + "actor_ip": "192.168.3.11", + "actor": "BELTALOWDA", + "request_url": None, + "description": "Updated report, 'Reported by Mao-Kwikowski'", + "create_time": "2024-04-17T19:13:00.235Z" + }, + { + "org_key": "test", + "actor_ip": "192.168.3.14", + "actor": "BELTALOWDA", + "request_url": None, + "description": "Updated report, 'Malware SSL Certificate Fingerprint'", + "create_time": "2024-04-17T19:12:59.693Z" + } + ] +} diff --git a/src/tests/unit/platform/test_audit.py b/src/tests/unit/platform/test_audit.py index 12722d8d..f7cc4c0a 100644 --- a/src/tests/unit/platform/test_audit.py +++ b/src/tests/unit/platform/test_audit.py @@ -12,10 +12,11 @@ """Tests for the audit logs APIs.""" import pytest +import copy from cbc_sdk.rest_api import CBCloudAPI -from cbc_sdk.platform.audit import AuditLog +from cbc_sdk.platform import AuditLog, AuditLogRecord from tests.unit.fixtures.CBCSDKMock import CBCSDKMock -from tests.unit.fixtures.platform.mock_audit import AUDITLOGS_RESP +from tests.unit.fixtures.platform.mock_audit import AUDITLOGS_RESP, AUDIT_SEARCH_REQUEST, AUDIT_SEARCH_RESPONSE @pytest.fixture(scope="function") @@ -35,15 +36,42 @@ def cbcsdk_mock(monkeypatch, cb): # ==================================== UNIT TESTS BELOW ==================================== -def test_no_create_object_for_now(cb): - """Validates that we can't create an AuditLog object. Remove when we have a better implementation.""" - with pytest.raises(NotImplementedError): - AuditLog(cb, 0) - - def test_get_auditlogs(cbcsdk_mock): """Tests getting audit logs.""" cbcsdk_mock.mock_request("GET", "/integrationServices/v3/auditlogs", AUDITLOGS_RESP) api = cbcsdk_mock.api result = AuditLog.get_auditlogs(api) assert len(result) == 5 + + +def test_search_audit_logs_with_all_bells_and_whistles(cbcsdk_mock): + """Tests the generation and execution of a search request.""" + + def on_post(url, body, **kwargs): + assert body == AUDIT_SEARCH_REQUEST + return AUDIT_SEARCH_RESPONSE + + cbcsdk_mock.mock_request("POST", "/audit_log/v1/orgs/test/logs/_search", on_post) + api = cbcsdk_mock.api + query = api.select(AuditLogRecord).where("description:FOO").add_criteria("actor_ip", ["10.29.99.1"]) + query.add_criteria("actor", ["ABCDEFGHIJ"]).add_criteria("request_url", ["https://inclusiveladyship.com"]) + query.add_criteria("description", ["FOOBAR"]).add_boolean_criteria("flagged", True) + query.add_boolean_criteria("verbose", False) + query.add_time_criteria(start="2024-03-01T00:00:00", end="2024-03-31T22:00:00") + query.add_exclusions("actor_ip", ["10.29.99.254"]).add_exclusions("actor", ["JIHGFEDCBA"]) + query.add_exclusions("request_url", ["https://links.inclusiveladyship.com"]) + query.add_exclusions("description", ["BLORT"]).add_boolean_criteria("flagged", False, exclude=True) + query.add_boolean_criteria("verbose", True, exclude=True) + query.add_time_criteria(range="-5d", exclude=True).sort_by("actor_ip", "ASC") + l = list(query) + assert len(l) == 5 + assert l[0].actor == "DEFGHIJKLM" + assert l[0].actor_ip == "192.168.0.5" + assert l[1].actor == "BELTALOWDA" + assert l[1].actor_ip == "192.168.3.5" + assert l[2].actor == "BELTALOWDA" + assert l[2].actor_ip == "192.168.3.8" + assert l[3].actor == "BELTALOWDA" + assert l[3].actor_ip == "192.168.3.11" + assert l[4].actor == "BELTALOWDA" + assert l[4].actor_ip == "192.168.3.14" From c8acc798488c24bdfc8ec41e4b156c00b6c24760 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Wed, 17 Apr 2024 17:14:16 -0600 Subject: [PATCH 61/95] added more tests to put coverage at 97% and deflake8'd --- src/tests/unit/platform/test_audit.py | 67 +++++++++++++++++++++------ 1 file changed, 54 insertions(+), 13 deletions(-) diff --git a/src/tests/unit/platform/test_audit.py b/src/tests/unit/platform/test_audit.py index f7cc4c0a..9a484150 100644 --- a/src/tests/unit/platform/test_audit.py +++ b/src/tests/unit/platform/test_audit.py @@ -12,7 +12,7 @@ """Tests for the audit logs APIs.""" import pytest -import copy +from cbc_sdk.errors import ApiError from cbc_sdk.rest_api import CBCloudAPI from cbc_sdk.platform import AuditLog, AuditLogRecord from tests.unit.fixtures.CBCSDKMock import CBCSDKMock @@ -63,15 +63,56 @@ def on_post(url, body, **kwargs): query.add_exclusions("description", ["BLORT"]).add_boolean_criteria("flagged", False, exclude=True) query.add_boolean_criteria("verbose", True, exclude=True) query.add_time_criteria(range="-5d", exclude=True).sort_by("actor_ip", "ASC") - l = list(query) - assert len(l) == 5 - assert l[0].actor == "DEFGHIJKLM" - assert l[0].actor_ip == "192.168.0.5" - assert l[1].actor == "BELTALOWDA" - assert l[1].actor_ip == "192.168.3.5" - assert l[2].actor == "BELTALOWDA" - assert l[2].actor_ip == "192.168.3.8" - assert l[3].actor == "BELTALOWDA" - assert l[3].actor_ip == "192.168.3.11" - assert l[4].actor == "BELTALOWDA" - assert l[4].actor_ip == "192.168.3.14" + result_list = list(query) + assert len(result_list) == 5 + assert query._count() == 5 + assert result_list[0].actor == "DEFGHIJKLM" + assert result_list[0].actor_ip == "192.168.0.5" + assert result_list[1].actor == "BELTALOWDA" + assert result_list[1].actor_ip == "192.168.3.5" + assert result_list[2].actor == "BELTALOWDA" + assert result_list[2].actor_ip == "192.168.3.8" + assert result_list[3].actor == "BELTALOWDA" + assert result_list[3].actor_ip == "192.168.3.11" + assert result_list[4].actor == "BELTALOWDA" + assert result_list[4].actor_ip == "192.168.3.14" + + +def test_criteria_errors(cb): + """Tests error handling in the criteria-setting functions on the query object.""" + query = cb.select(AuditLogRecord) + with pytest.raises(ApiError): + query.add_time_criteria(start="2024-03-01T00:00:00", end="2024-03-31T22:00:00", range="-5d") + with pytest.raises(ApiError): + query.add_time_criteria(start="2024-03-01T00:00:00") + with pytest.raises(ApiError): + query.add_time_criteria(end="2024-03-31T22:00:00") + with pytest.raises(ApiError): + query.add_time_criteria(start="2024-03-01T00:00:00", range="-5d") + with pytest.raises(ApiError): + query.add_time_criteria(end="2024-03-31T22:00:00", range="-5d") + with pytest.raises(ApiError): + query.add_time_criteria(start="BOGUS", end="2024-03-31T22:00:00") + with pytest.raises(ApiError): + query.sort_by("actor_ip", "BOGUS") + + +def test_async_search_audit_logs(cbcsdk_mock): + """Tests async query of audit logs.""" + cbcsdk_mock.mock_request("POST", "/audit_log/v1/orgs/test/logs/_search", AUDIT_SEARCH_RESPONSE) + api = cbcsdk_mock.api + query = api.select(AuditLogRecord) + future = query.execute_async() + result_list = future.result() + assert isinstance(result_list, list) + assert len(result_list) == 5 + assert result_list[0].actor == "DEFGHIJKLM" + assert result_list[0].actor_ip == "192.168.0.5" + assert result_list[1].actor == "BELTALOWDA" + assert result_list[1].actor_ip == "192.168.3.5" + assert result_list[2].actor == "BELTALOWDA" + assert result_list[2].actor_ip == "192.168.3.8" + assert result_list[3].actor == "BELTALOWDA" + assert result_list[3].actor_ip == "192.168.3.11" + assert result_list[4].actor == "BELTALOWDA" + assert result_list[4].actor_ip == "192.168.3.14" From f004e4cf89459ba70ee3b24a948719cf59fa8f9b Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Thu, 18 Apr 2024 12:32:55 -0600 Subject: [PATCH 62/95] renamed AuditLogRecord back to AuditLog --- src/cbc_sdk/platform/__init__.py | 4 +-- src/cbc_sdk/platform/audit.py | 36 +++++++++---------- .../{audit_log_record.yaml => audit_log.yaml} | 0 src/cbc_sdk/rest_api.py | 4 +-- src/tests/unit/platform/test_audit.py | 8 ++--- 5 files changed, 25 insertions(+), 27 deletions(-) rename src/cbc_sdk/platform/models/{audit_log_record.yaml => audit_log.yaml} (100%) diff --git a/src/cbc_sdk/platform/__init__.py b/src/cbc_sdk/platform/__init__.py index e5b05c7b..e7dc9c00 100644 --- a/src/cbc_sdk/platform/__init__.py +++ b/src/cbc_sdk/platform/__init__.py @@ -8,9 +8,7 @@ from cbc_sdk.platform.alerts import Alert as BaseAlert -from cbc_sdk.platform.audit import AuditLogRecord - -from cbc_sdk.platform.audit import AuditLogRecord as AuditLog +from cbc_sdk.platform.audit import AuditLog from cbc_sdk.platform.asset_groups import AssetGroup diff --git a/src/cbc_sdk/platform/audit.py b/src/cbc_sdk/platform/audit.py index d4cabf3a..98f4c6ff 100644 --- a/src/cbc_sdk/platform/audit.py +++ b/src/cbc_sdk/platform/audit.py @@ -24,32 +24,32 @@ """Model Class""" -class AuditLogRecord(UnrefreshableModel): +class AuditLog(UnrefreshableModel): """Model class which represents audit log events. Mostly for future implementation.""" urlobject = "/audit_log/v1/orgs/{0}/logs" - swagger_meta_file = "platform/models/audit_log_record.yaml" + swagger_meta_file = "platform/models/audit_log.yaml" def __init__(self, cb, model_unique_id, initial_data=None): """ - Creates a new ``AuditLogRecord``. + Creates a new ``AuditLog``. Args: cb (BaseAPI): Reference to API object used to communicate with the server. model_unique_id (int): Not used. initial_data (dict): Initial data to fill in the audit log record details. """ - super(AuditLogRecord, self).__init__(cb, model_unique_id, initial_data, force_init=False, full_doc=True) + super(AuditLog, self).__init__(cb, model_unique_id, initial_data, force_init=False, full_doc=True) @classmethod def _query_implementation(cls, cb, **kwargs): """ - Returns the appropriate query object for the ``AuditLogRecord`` type. + Returns the appropriate query object for the ``AuditLog`` type. Args: cb (BaseAPI): Reference to API object used to communicate with the server. **kwargs (dict): Not used, retained for compatibility. """ - return AuditLogRecordQuery(cls, cb) + return AuditLogQuery(cls, cb) @staticmethod def get_auditlogs(cb): @@ -72,12 +72,12 @@ def get_auditlogs(cb): """Query Class""" -class AuditLogRecordQuery(BaseQuery, QueryBuilderSupportMixin, CriteriaBuilderSupportMixin, - ExclusionBuilderSupportMixin, IterableQueryMixin, AsyncQueryMixin): +class AuditLogQuery(BaseQuery, QueryBuilderSupportMixin, CriteriaBuilderSupportMixin, + ExclusionBuilderSupportMixin, IterableQueryMixin, AsyncQueryMixin): """ - Query object that is used to locate ``AuditLogRecord`` objects. + Query object that is used to locate ``AuditLog`` objects. - The ``AuditLogRecordQuery`` is constructed via SDK functions like the ``select()`` method on ``CBCloudAPI``. + The ``AuditLogQuery`` is constructed via SDK functions like the ``select()`` method on ``CBCloudAPI``. The user would then add a query and/or criteria to it before iterating over the results. The following criteria may be added to the query via the standard ``add_criteria()`` method, or added to query @@ -91,7 +91,7 @@ class AuditLogRecordQuery(BaseQuery, QueryBuilderSupportMixin, CriteriaBuilderSu def __init__(self, doc_class, cb): """ - Initialize the ``AuditLogRecordQuery``. + Initialize the ``AuditLogQuery``. Args: doc_class (class): The model class that will be returned by this query. @@ -100,7 +100,7 @@ def __init__(self, doc_class, cb): self._doc_class = doc_class self._cb = cb self._count_valid = False - super(AuditLogRecordQuery, self).__init__() + super(AuditLogQuery, self).__init__() self._query_builder = QueryBuilder() self._criteria = {} @@ -175,7 +175,7 @@ def add_time_criteria(self, **kwargs): applied to search criteria. Default ``False.`` Returns: - AuditLogRecordQuery: This instance. + AuditLogQuery: This instance. Raises: ApiError: If the argument format is incorrect. @@ -198,7 +198,7 @@ def add_boolean_criteria(self, criteria_name, value, exclude=False): applied to search criteria. Default ``False.`` Returns: - AuditLogRecordQuery: This instance. + AuditLogQuery: This instance. """ if exclude: self._exclusions[criteria_name] = value @@ -211,14 +211,14 @@ def sort_by(self, key, direction="ASC"): Sets the sorting behavior on a query's results. Example: - >>> cb.select(AuditLogRecord).sort_by("name") + >>> cb.select(AuditLog).sort_by("name") Args: key (str): The key in the schema to sort by. direction (str): The sort order, either "ASC" or "DESC". Returns: - AuditLogRecordQuery: This instance. + AuditLogQuery: This instance. """ if direction not in CriteriaBuilderSupportMixin.VALID_DIRECTIONS: raise ApiError("invalid sort direction specified") @@ -294,7 +294,7 @@ def _perform_query(self, from_row=0, max_rows=-1): max_rows (int): The maximum number of rows to be returned (default -1, meaning "all"). Yields: - AuditLogRecord: The audit log records resulting from the search. + AuditLog: The audit log records resulting from the search. """ url = self._build_url("/_search") current = from_row @@ -331,7 +331,7 @@ def _run_async_query(self, context): context (object): Not used. Returns: - list[AuditLogRecord]: The results of the query. + list[AuditLog]: The results of the query. """ url = self._build_url("/_search") request = self._build_request(0, -1) diff --git a/src/cbc_sdk/platform/models/audit_log_record.yaml b/src/cbc_sdk/platform/models/audit_log.yaml similarity index 100% rename from src/cbc_sdk/platform/models/audit_log_record.yaml rename to src/cbc_sdk/platform/models/audit_log.yaml diff --git a/src/cbc_sdk/rest_api.py b/src/cbc_sdk/rest_api.py index 2c5c112c..3a075df8 100644 --- a/src/cbc_sdk/rest_api.py +++ b/src/cbc_sdk/rest_api.py @@ -22,7 +22,7 @@ from cbc_sdk.errors import ApiError, CredentialError, ServerError, InvalidObjectError from cbc_sdk.live_response_api import LiveResponseSessionManager from cbc_sdk.audit_remediation import Run, RunHistory -from cbc_sdk.platform.audit import AuditLogRecord +from cbc_sdk.platform.audit import AuditLog from cbc_sdk.enterprise_edr.threat_intelligence import ReportSeverity import logging import time @@ -210,7 +210,7 @@ def get_auditlogs(self): list[dict]: List of dictionary objects representing the audit logs, or an empty list if none available. """ log.warning("CBCloudAPI.get_auditlogs is deprecated, use AuditLog.get_auditlogs instead") - return AuditLogRecord.get_auditlogs(self) + return AuditLog.get_auditlogs(self) # ---- Device API diff --git a/src/tests/unit/platform/test_audit.py b/src/tests/unit/platform/test_audit.py index 9a484150..44ee7bb5 100644 --- a/src/tests/unit/platform/test_audit.py +++ b/src/tests/unit/platform/test_audit.py @@ -14,7 +14,7 @@ import pytest from cbc_sdk.errors import ApiError from cbc_sdk.rest_api import CBCloudAPI -from cbc_sdk.platform import AuditLog, AuditLogRecord +from cbc_sdk.platform import AuditLog from tests.unit.fixtures.CBCSDKMock import CBCSDKMock from tests.unit.fixtures.platform.mock_audit import AUDITLOGS_RESP, AUDIT_SEARCH_REQUEST, AUDIT_SEARCH_RESPONSE @@ -53,7 +53,7 @@ def on_post(url, body, **kwargs): cbcsdk_mock.mock_request("POST", "/audit_log/v1/orgs/test/logs/_search", on_post) api = cbcsdk_mock.api - query = api.select(AuditLogRecord).where("description:FOO").add_criteria("actor_ip", ["10.29.99.1"]) + query = api.select(AuditLog).where("description:FOO").add_criteria("actor_ip", ["10.29.99.1"]) query.add_criteria("actor", ["ABCDEFGHIJ"]).add_criteria("request_url", ["https://inclusiveladyship.com"]) query.add_criteria("description", ["FOOBAR"]).add_boolean_criteria("flagged", True) query.add_boolean_criteria("verbose", False) @@ -80,7 +80,7 @@ def on_post(url, body, **kwargs): def test_criteria_errors(cb): """Tests error handling in the criteria-setting functions on the query object.""" - query = cb.select(AuditLogRecord) + query = cb.select(AuditLog) with pytest.raises(ApiError): query.add_time_criteria(start="2024-03-01T00:00:00", end="2024-03-31T22:00:00", range="-5d") with pytest.raises(ApiError): @@ -101,7 +101,7 @@ def test_async_search_audit_logs(cbcsdk_mock): """Tests async query of audit logs.""" cbcsdk_mock.mock_request("POST", "/audit_log/v1/orgs/test/logs/_search", AUDIT_SEARCH_RESPONSE) api = cbcsdk_mock.api - query = api.select(AuditLogRecord) + query = api.select(AuditLog) future = query.execute_async() result_list = future.result() assert isinstance(result_list, list) From c43f65276b9e133365a4847faad6feb645a8ae44 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Thu, 18 Apr 2024 13:37:10 -0600 Subject: [PATCH 63/95] did some fixup of docstrings --- src/cbc_sdk/platform/audit.py | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/src/cbc_sdk/platform/audit.py b/src/cbc_sdk/platform/audit.py index 98f4c6ff..cbc54310 100644 --- a/src/cbc_sdk/platform/audit.py +++ b/src/cbc_sdk/platform/audit.py @@ -11,7 +11,15 @@ # * WARRANTIES OR CONDITIONS OF MERCHANTABILITY, SATISFACTORY QUALITY, # * NON-INFRINGEMENT AND FITNESS FOR A PARTICULAR PURPOSE. -"""Model and Query Classes for Platform Auditing""" +""" +Model and query classes for platform audit logs. + +``AuditLog`` can be used to monitor your Carbon Black Cloud organization for actions performed by Carbon Black Cloud +console users and API keys. Audit logs are recorded for most CREATE, UPDATE and DELETE actions as well as a few READ +actions. Audit logs will include a description of the action and indicate the actor who performed the action along +with their IP to help determine if the User/API key are from an expected source. + +""" import datetime from cbc_sdk.base import (UnrefreshableModel, BaseQuery, QueryBuilder, QueryBuilderSupportMixin, @@ -25,13 +33,18 @@ class AuditLog(UnrefreshableModel): - """Model class which represents audit log events. Mostly for future implementation.""" + """ + The model class which represents individual audit log entries. + + Each entry includes the actor performing the action, the IP address of the actor, a description, and a request URL + where available. + """ urlobject = "/audit_log/v1/orgs/{0}/logs" swagger_meta_file = "platform/models/audit_log.yaml" def __init__(self, cb, model_unique_id, initial_data=None): """ - Creates a new ``AuditLog``. + Creates a new ``AuditLog`` object. Args: cb (BaseAPI): Reference to API object used to communicate with the server. @@ -114,7 +127,7 @@ def __init__(self, doc_class, cb): @staticmethod def _create_valid_time_filter(kwargs): """ - Verifies that an alert criteria key has the timerange functionality + Creates the time range used for a "create_time" criteria value. Args: kwargs (dict): Used to specify start= for start time, end= for end time, and range= for range. Values are @@ -155,7 +168,7 @@ def _create_valid_time_filter(kwargs): def add_time_criteria(self, **kwargs): """ - Adds a ``create_time`` criteria value to either criteria or exclusions. + Adds a ``create_time`` value to either criteria or exclusions. Args: kwargs (dict): Keyword arguments to this method. @@ -269,6 +282,9 @@ def _count(self): """ Returns the number of results from the run of this query. + Required Permissions: + org.audits (READ) + Returns: int: The number of results from the run of this query. """ @@ -289,6 +305,9 @@ def _perform_query(self, from_row=0, max_rows=-1): """ Performs the query and returns the results of the query in an iterable fashion. + Required Permissions: + org.audits (READ) + Args: from_row (int): The row to start the query at (default 0). max_rows (int): The maximum number of rows to be returned (default -1, meaning "all"). From bc66e979e2a8a0d572d3a7ec993d79ceb7d7a582 Mon Sep 17 00:00:00 2001 From: Kylie Ebringer Date: Tue, 23 Apr 2024 11:24:33 -0600 Subject: [PATCH 64/95] Updated with new queue endpoint and search. Export not yet implemented. --- examples/platform/audit_log.py | 87 +++++++++++++++++++++++++++++----- 1 file changed, 76 insertions(+), 11 deletions(-) diff --git a/examples/platform/audit_log.py b/examples/platform/audit_log.py index 25e98471..94ff2a69 100644 --- a/examples/platform/audit_log.py +++ b/examples/platform/audit_log.py @@ -24,20 +24,30 @@ from cbc_sdk.helpers import build_cli_parser, get_cb_cloud_object from cbc_sdk.platform import AuditLog +# To see the http requests being made, and the structure of the search requests enable debug logging +import logging +logging.basicConfig(level=logging.DEBUG) -def get_audit_logs(cb, args): - """Polls for audit logs for the period set on input at the specified interval.""" + +def deprecated_get_audit_logs(cb, args): + """Deprecated: Polls for audit logs for the period set on input at the specified interval. + + Uses the deprecated queue endpoint. Use AuditLog.get_queued_auditlogs(), shown in get_audit_logs_from_queue + for equivalent functionality using an updated API signature. + """ + print("The AuditLog.get_auditlogs() method is deprecated.") + print("You should have a look at the new example script and change to a new method.") poll_interval = args.poll_interval run_period = args.run_period while run_period > 0: - events_list = AuditLog.get_auditlogs(cb) + audit_log_records = AuditLog.get_auditlogs(cb) print("Runtime remaining: {0} seconds".format(run_period)) - if len(events_list) == 0: + if len(audit_log_records) == 0: print("No audit logs available") - for event in events_list: - print(f"Event {event['eventId']}:") - for (k, v) in event.items(): + for audit_log in audit_log_records: + print(f"Event {audit_log['eventId']}:") + for (k, v) in audit_log.items(): print(f"\t{k}: {v}") time.sleep(poll_interval) run_period = run_period - poll_interval @@ -45,13 +55,67 @@ def get_audit_logs(cb, args): print("Run time completed") +def get_audit_logs_from_queue(cb, args): + """Polls for audit logs for the period set on input at the specified interval. + + Uses the queue endpoint. + """ + poll_interval = args.poll_interval + run_period = args.run_period + + while run_period > 0: + audit_log_records = AuditLog.get_queued_auditlogs(cb) + print("Runtime remaining: {0} seconds".format(run_period)) + if len(audit_log_records) == 0: + print("No audit logs available") + for audit_log in audit_log_records: + print("New Event:") + print("{}".format(audit_log)) + time.sleep(poll_interval) + run_period = run_period - poll_interval + + print("Run time completed") + + +def search_audit_logs(cb, args): + """Does one request for audit logs exercising search criteria. Uses the /_search endpoint.""" + # add_time_criteria can stake a start and end time, or a range + # add_time_criteria(start="2024-04-23T09:00:00Z", end="2024-04-23T10:40:00Z") + audit_log_records = cb.select(AuditLog).add_time_criteria(range="-3d").add_boolean_criteria("verbose", True)\ + .add_criteria("description", ["Connector (App)"]) + print("Found {} alert records".format(len(audit_log_records))) + + for a in audit_log_records: + print("{}".format(a)) + + print("End of search results.") + + def main(): """Main function for Audit Logs example script.""" + """This script demonstrates how to use Audit Logs in the SDK and the three different APIs. + * Search + * Export + * Queue + + This example does not use command line parsing in order to reduce complexity and focus on the SDK functions. + Review the Authentication section of the Read the Docs for information about Authentication in the SDK + https://carbon-black-cloud-python-sdk.readthedocs.io/en/latest/authentication/ + + This is written for clarity of explanation, not perfect coding practices. + """ + # CBCloudAPI is the connection to the cloud. It holds the credentials for connectivity. + # To execute this script, the profile must have an API key with the following permissions. + # If you are restricted in the actions you're allowed to perform, expect a 403 response for missing permissions. + # Permissions are set on Settings -> API Access -> Access Level and then assigned to an API Key + # Audit Logs - org.audits - READ: View and Export Audits + # Background tasks - Status - jobs.status - READ: To get the status and results of an asynchronous export + + # command line parameters are used for the PROFILE to get connection credentials and polling information. parser = build_cli_parser("Get Audit Logs") subparsers = parser.add_subparsers(dest="command", required=True) - get = subparsers.add_parser("get_audit_logs", help="Get available audit logs") - + get = subparsers.add_parser("get", help="Gets audit logs using Queue, Search, Export and Deprecated queue") get.add_argument('-r', '--run_period', type=int, default=180, help="Time in seconds to continue polling for") # For production use, a longer poll interval of at least one minute should be used get.add_argument('-p', '--poll_interval', type=int, default=30, help="Time in seconds between calling the api") @@ -59,8 +123,9 @@ def main(): args = parser.parse_args() cb = get_cb_cloud_object(args) - if args.command == "get_audit_logs": - get_audit_logs(cb, args) + search_audit_logs(cb, args) + get_audit_logs_from_queue(cb, args) + deprecated_get_audit_logs(cb, args) if __name__ == "__main__": From a064b174b783e500c5937fe3fcf8c0ee463b0eb2 Mon Sep 17 00:00:00 2001 From: Kylie Ebringer Date: Fri, 26 Apr 2024 14:48:38 -0600 Subject: [PATCH 65/95] Added export to demo script --- examples/platform/audit_log.py | 74 ++++++++++++++++++++++++---------- 1 file changed, 53 insertions(+), 21 deletions(-) diff --git a/examples/platform/audit_log.py b/examples/platform/audit_log.py index 94ff2a69..f48b50b7 100644 --- a/examples/platform/audit_log.py +++ b/examples/platform/audit_log.py @@ -12,11 +12,17 @@ """Example script which collects audit logs -The Audit log API provides a read-once queue so no search parameters are requied. -The command line takes the command (get_audit_logs), the total time to run for in seconds and -the polling period, also in seconds. This command will run for 180 seconds (3 minutes) polling for new audit -logs every 30 seconds. -> python examples/platform/audit_log.py --profile DEMO_PROFILE get_audit_logs -r 180 -p 30 +This script demonstrates how to use Audit Logs in the SDK and the three different APIs. + * Search + * Export + * Queue + + This example has minimal command line parsing in order to reduce complexity and focus on the SDK functions. + Review the Authentication section of the Read the Docs for information about Authentication in the SDK + https://carbon-black-cloud-python-sdk.readthedocs.io/en/latest/authentication/ + + This command line will use "DEMO PROFILE" from the credentials file and poll every 30 seconds for three minutes. + > python examples/platform/audit_log.py --profile DEMO_PROFILE get_audit_logs -r 180 -p 30 """ import sys @@ -36,7 +42,8 @@ def deprecated_get_audit_logs(cb, args): for equivalent functionality using an updated API signature. """ print("The AuditLog.get_auditlogs() method is deprecated.") - print("You should have a look at the new example script and change to a new method.") + print("You should have a look at and change to a new method.") + print("Field names have changed from CamelCase to snake_case.") poll_interval = args.poll_interval run_period = args.run_period @@ -52,7 +59,7 @@ def deprecated_get_audit_logs(cb, args): time.sleep(poll_interval) run_period = run_period - poll_interval - print("Run time completed") + print("deprecated_get_audit_logs completed") def get_audit_logs_from_queue(cb, args): @@ -60,6 +67,7 @@ def get_audit_logs_from_queue(cb, args): Uses the queue endpoint. """ + print("Starting get_audit_logs_from_queue") poll_interval = args.poll_interval run_period = args.run_period @@ -74,36 +82,58 @@ def get_audit_logs_from_queue(cb, args): time.sleep(poll_interval) run_period = run_period - poll_interval - print("Run time completed") + print("get_audit_logs_from_queue completed") -def search_audit_logs(cb, args): - """Does one request for audit logs exercising search criteria. Uses the /_search endpoint.""" +def search_audit_logs(cb): + """Shows requests for audit logs exercising search criteria. Uses the /_search endpoint.""" + print("Starting search_audit_logs") # add_time_criteria can stake a start and end time, or a range # add_time_criteria(start="2024-04-23T09:00:00Z", end="2024-04-23T10:40:00Z") audit_log_records = cb.select(AuditLog).add_time_criteria(range="-3d").add_boolean_criteria("verbose", True)\ - .add_criteria("description", ["Connector (App)"]) + .add_criteria("description", ["logged in"]) + print("Found {} alert records".format(len(audit_log_records))) + + # Instead of the criteria function, a lucene style query can be used in a where clause for comparable behaviour + # to the search on the Audit Log page of the Carbon Black Cloud console. + audit_log_records = cb.select(AuditLog).add_time_criteria(range="-3d").where("description:login") print("Found {} alert records".format(len(audit_log_records))) for a in audit_log_records: print("{}".format(a)) - print("End of search results.") + print("search_audit_logs completed") + + +def export_audit_logs(cb): + """Does one request for audit logs exercising search criteria and then exports via the Job Service. + + Uses the /_export endpoint. + """ + print("Starting export_audit_logs") + audit_log_query = cb.select(AuditLog).add_time_criteria(range="-1d") + audit_log_export_job = audit_log_query.export(format="csv") + results = audit_log_export_job.await_completion().result() + print(results) + results = audit_log_export_job.get_output_as_string() + print(results) + + print("Async Export in json format") + audit_log_export_job = cb.select(AuditLog).add_time_criteria(range="-1d").export(format="json") + results = audit_log_export_job.get_output_as_file("/my/home/directory/audit_results.json") + print("export_audit_logs complete") def main(): - """Main function for Audit Logs example script.""" - """This script demonstrates how to use Audit Logs in the SDK and the three different APIs. + """Main function for Audit Logs example script. + + This script demonstrates how to use Audit Logs in the SDK and the three different APIs. * Search * Export * Queue - This example does not use command line parsing in order to reduce complexity and focus on the SDK functions. - Review the Authentication section of the Read the Docs for information about Authentication in the SDK - https://carbon-black-cloud-python-sdk.readthedocs.io/en/latest/authentication/ - - This is written for clarity of explanation, not perfect coding practices. - """ + This is written for clarity of explanation, not perfect coding practices. + """ # CBCloudAPI is the connection to the cloud. It holds the credentials for connectivity. # To execute this script, the profile must have an API key with the following permissions. # If you are restricted in the actions you're allowed to perform, expect a 403 response for missing permissions. @@ -123,9 +153,11 @@ def main(): args = parser.parse_args() cb = get_cb_cloud_object(args) - search_audit_logs(cb, args) + export_audit_logs(cb) + search_audit_logs(cb) get_audit_logs_from_queue(cb, args) deprecated_get_audit_logs(cb, args) + print("The End") if __name__ == "__main__": From 0eaf1ee16fdbf8b64f390562c6792a7140002636 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Fri, 19 Apr 2024 17:08:05 -0600 Subject: [PATCH 66/95] the function is added --- src/cbc_sdk/platform/audit.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/cbc_sdk/platform/audit.py b/src/cbc_sdk/platform/audit.py index cbc54310..c56632d2 100644 --- a/src/cbc_sdk/platform/audit.py +++ b/src/cbc_sdk/platform/audit.py @@ -69,6 +69,9 @@ def get_auditlogs(cb): """ Retrieve queued audit logs from the Carbon Black Cloud server. + Deprecated: + This method uses an outdated API. Use ``get_queued_auditlogs()`` instead. + Required Permissions: org.audits (READ) @@ -81,6 +84,23 @@ def get_auditlogs(cb): res = cb.get_object("/integrationServices/v3/auditlogs") return res.get("notifications", []) + @staticmethod + def get_queued_auditlogs(cb): + """ + Retrieve queued audit logs from the Carbon Black Cloud server. + + Required Permissions: + org.audits (READ) + + Args: + cb (BaseAPI): Reference to API object used to communicate with the server. + + Returns: + list[AuditLog]: List of objects representing the audit logs, or an empty list if none available. + """ + res = cb.get_object(AuditLog.urlobject.format(cb.credentials.org_key) + "/queue") + return [AuditLog(cb, -1, data) for data in res.get("results", [])] + """Query Class""" From db76e7c2d509d6cfb2d320c5dbf63e1fceaadd4f Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Mon, 22 Apr 2024 15:11:36 -0600 Subject: [PATCH 67/95] unit tests added, coverage 97% --- src/cbc_sdk/platform/audit.py | 2 +- src/tests/unit/fixtures/platform/mock_audit.py | 10 ++++++++++ src/tests/unit/platform/test_audit.py | 10 ++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/cbc_sdk/platform/audit.py b/src/cbc_sdk/platform/audit.py index c56632d2..49af6dd0 100644 --- a/src/cbc_sdk/platform/audit.py +++ b/src/cbc_sdk/platform/audit.py @@ -98,7 +98,7 @@ def get_queued_auditlogs(cb): Returns: list[AuditLog]: List of objects representing the audit logs, or an empty list if none available. """ - res = cb.get_object(AuditLog.urlobject.format(cb.credentials.org_key) + "/queue") + res = cb.get_object(AuditLog.urlobject.format(cb.credentials.org_key) + "/_queue") return [AuditLog(cb, -1, data) for data in res.get("results", [])] diff --git a/src/tests/unit/fixtures/platform/mock_audit.py b/src/tests/unit/fixtures/platform/mock_audit.py index 752a518e..0b186da0 100644 --- a/src/tests/unit/fixtures/platform/mock_audit.py +++ b/src/tests/unit/fixtures/platform/mock_audit.py @@ -105,6 +105,8 @@ "actor": "DEFGHIJKLM", "request_url": None, "description": "Connector DEFGHIJKLM logged in successfully", + "flagged": False, + "verbose": False, "create_time": "2024-04-17T19:18:37.480Z" }, { @@ -113,6 +115,8 @@ "actor": "BELTALOWDA", "request_url": None, "description": "Updated report, 'MCRN threat feed'", + "flagged": False, + "verbose": False, "create_time": "2024-04-17T19:13:01.528Z" }, { @@ -121,6 +125,8 @@ "actor": "BELTALOWDA", "request_url": None, "description": "Updated report, 'Reported by DOP'", + "flagged": False, + "verbose": False, "create_time": "2024-04-17T19:13:01.042Z" }, { @@ -129,6 +135,8 @@ "actor": "BELTALOWDA", "request_url": None, "description": "Updated report, 'Reported by Mao-Kwikowski'", + "flagged": False, + "verbose": False, "create_time": "2024-04-17T19:13:00.235Z" }, { @@ -137,6 +145,8 @@ "actor": "BELTALOWDA", "request_url": None, "description": "Updated report, 'Malware SSL Certificate Fingerprint'", + "flagged": False, + "verbose": False, "create_time": "2024-04-17T19:12:59.693Z" } ] diff --git a/src/tests/unit/platform/test_audit.py b/src/tests/unit/platform/test_audit.py index 44ee7bb5..2dc02ad8 100644 --- a/src/tests/unit/platform/test_audit.py +++ b/src/tests/unit/platform/test_audit.py @@ -44,6 +44,16 @@ def test_get_auditlogs(cbcsdk_mock): assert len(result) == 5 +def test_get_queued_auditlogs(cbcsdk_mock): + """Tests the get_queued_auditlogs function.""" + cbcsdk_mock.mock_request("GET", "/audit_log/v1/orgs/test/logs/_queue", AUDIT_SEARCH_RESPONSE) + api = cbcsdk_mock.api + result = AuditLog.get_queued_auditlogs(api) + assert len(result) == 5 + for v in result: + assert isinstance(v, AuditLog) + + def test_search_audit_logs_with_all_bells_and_whistles(cbcsdk_mock): """Tests the generation and execution of a search request.""" From 8b32c4d794c45aeb78bba1203be7bbc3e9331ef8 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Tue, 23 Apr 2024 09:40:46 -0600 Subject: [PATCH 68/95] removed the model_uniqueid argument from the __init__ call (not used). --- src/cbc_sdk/platform/audit.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/cbc_sdk/platform/audit.py b/src/cbc_sdk/platform/audit.py index 49af6dd0..f8d23a96 100644 --- a/src/cbc_sdk/platform/audit.py +++ b/src/cbc_sdk/platform/audit.py @@ -42,7 +42,7 @@ class AuditLog(UnrefreshableModel): urlobject = "/audit_log/v1/orgs/{0}/logs" swagger_meta_file = "platform/models/audit_log.yaml" - def __init__(self, cb, model_unique_id, initial_data=None): + def __init__(self, cb, initial_data=None): """ Creates a new ``AuditLog`` object. @@ -51,7 +51,7 @@ def __init__(self, cb, model_unique_id, initial_data=None): model_unique_id (int): Not used. initial_data (dict): Initial data to fill in the audit log record details. """ - super(AuditLog, self).__init__(cb, model_unique_id, initial_data, force_init=False, full_doc=True) + super(AuditLog, self).__init__(cb, -1, initial_data, force_init=False, full_doc=True) @classmethod def _query_implementation(cls, cb, **kwargs): @@ -99,7 +99,7 @@ def get_queued_auditlogs(cb): list[AuditLog]: List of objects representing the audit logs, or an empty list if none available. """ res = cb.get_object(AuditLog.urlobject.format(cb.credentials.org_key) + "/_queue") - return [AuditLog(cb, -1, data) for data in res.get("results", [])] + return [AuditLog(cb, data) for data in res.get("results", [])] """Query Class""" @@ -349,7 +349,7 @@ def _perform_query(self, from_row=0, max_rows=-1): results = result.get("results", []) for item in results: - yield self._doc_class(self._cb, 0, item) + yield self._doc_class(self._cb, item) current += 1 numrows += 1 @@ -377,4 +377,4 @@ def _run_async_query(self, context): resp = self._cb.post_object(url, body=request) result = resp.json() results = result.get("results", []) - return [self._doc_class(self._cb, 0, item) for item in results] + return [self._doc_class(self._cb, item) for item in results] From 8d9743aaffe5ed5c05a0bac6de18bac5e1ea3694 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Tue, 23 Apr 2024 09:41:29 -0600 Subject: [PATCH 69/95] and remove from docstring... --- src/cbc_sdk/platform/audit.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/cbc_sdk/platform/audit.py b/src/cbc_sdk/platform/audit.py index f8d23a96..bf1e7d3d 100644 --- a/src/cbc_sdk/platform/audit.py +++ b/src/cbc_sdk/platform/audit.py @@ -48,7 +48,6 @@ def __init__(self, cb, initial_data=None): Args: cb (BaseAPI): Reference to API object used to communicate with the server. - model_unique_id (int): Not used. initial_data (dict): Initial data to fill in the audit log record details. """ super(AuditLog, self).__init__(cb, -1, initial_data, force_init=False, full_doc=True) From 75cc445ba4a5c7e623d85da43312f82a8565f23b Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Tue, 23 Apr 2024 11:35:37 -0600 Subject: [PATCH 70/95] added an example section for add_time_criteria --- src/cbc_sdk/platform/audit.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/cbc_sdk/platform/audit.py b/src/cbc_sdk/platform/audit.py index bf1e7d3d..f00e0114 100644 --- a/src/cbc_sdk/platform/audit.py +++ b/src/cbc_sdk/platform/audit.py @@ -189,6 +189,11 @@ def add_time_criteria(self, **kwargs): """ Adds a ``create_time`` value to either criteria or exclusions. + Examples: + >>> query_specify_start_and_end = api.select(AuditLog). + ... add_time_criteria(start="2023-10-20T20:34:07Z", end="2023-10-30T20:34:07Z") + >>> query_specify_exclude_range = api.select(AuditLog).add_time_criteria(range='-3d', exclude=True) + Args: kwargs (dict): Keyword arguments to this method. From 8052a94b1026ba16b204a8e4bc2325c8d222411c Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Tue, 23 Apr 2024 11:42:46 -0600 Subject: [PATCH 71/95] marking where export function will go --- src/cbc_sdk/platform/audit.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/cbc_sdk/platform/audit.py b/src/cbc_sdk/platform/audit.py index f00e0114..8d48688e 100644 --- a/src/cbc_sdk/platform/audit.py +++ b/src/cbc_sdk/platform/audit.py @@ -382,3 +382,6 @@ def _run_async_query(self, context): result = resp.json() results = result.get("results", []) return [self._doc_class(self._cb, item) for item in results] + + def export(self): + pass From fa33b702ca115eec3f3f73f31fd7c5788d65f8da Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Tue, 23 Apr 2024 14:25:47 -0600 Subject: [PATCH 72/95] implementation of export() --- src/cbc_sdk/platform/audit.py | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/cbc_sdk/platform/audit.py b/src/cbc_sdk/platform/audit.py index 8d48688e..788eb758 100644 --- a/src/cbc_sdk/platform/audit.py +++ b/src/cbc_sdk/platform/audit.py @@ -26,9 +26,11 @@ CriteriaBuilderSupportMixin, ExclusionBuilderSupportMixin, IterableQueryMixin, AsyncQueryMixin) from cbc_sdk.errors import ApiError +from cbc_sdk.platform.jobs import Job from backports._datetime_fromisoformat import datetime_fromisoformat + """Model Class""" @@ -120,6 +122,7 @@ class AuditLogQuery(BaseQuery, QueryBuilderSupportMixin, CriteriaBuilderSupportM * ``request_url`` - URL of the request that caused the creation of this audit log. * ``description`` - Text description of this audit log. """ + VALID_EXPORT_FORMATS = ("csv", "json") def __init__(self, doc_class, cb): """ @@ -383,5 +386,26 @@ def _run_async_query(self, context): results = result.get("results", []) return [self._doc_class(self._cb, item) for item in results] - def export(self): - pass + def export(self, format="csv"): + """ + Export audit logs using the Job service. + + The actual results are retrieved by waiting for the resulting job to complete, then calling one of the methods + on ``Job`` to retrieve the results. + + Args: + format (str): Format in which to return results, either "csv" or "json". Default is "csv". + + Returns: + Job: The object representing the export job. + """ + if format not in AuditLogQuery.VALID_EXPORT_FORMATS: + raise ApiError(f"invalid export format '{format}'") + url = self._build_url("/_export") + request = self._build_request(0, -1) + request["format"] = format + resp = self._cb.post_object(url, body=request) + result = resp.json() + if "job_id" in result: + return Job(self._cb, result["job_id"]) + return None From b38215b8ce76f7ae65af64565ad7e21a29be4155 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Tue, 23 Apr 2024 15:10:47 -0600 Subject: [PATCH 73/95] test code written, audit.py coverage at 97% --- src/cbc_sdk/platform/audit.py | 2 +- .../unit/fixtures/platform/mock_audit.py | 17 ++++++++++++ src/tests/unit/platform/test_audit.py | 27 +++++++++++++++++-- 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/src/cbc_sdk/platform/audit.py b/src/cbc_sdk/platform/audit.py index 788eb758..9baf5fdf 100644 --- a/src/cbc_sdk/platform/audit.py +++ b/src/cbc_sdk/platform/audit.py @@ -408,4 +408,4 @@ def export(self, format="csv"): result = resp.json() if "job_id" in result: return Job(self._cb, result["job_id"]) - return None + return None # pragma: no cover diff --git a/src/tests/unit/fixtures/platform/mock_audit.py b/src/tests/unit/fixtures/platform/mock_audit.py index 0b186da0..84e58841 100644 --- a/src/tests/unit/fixtures/platform/mock_audit.py +++ b/src/tests/unit/fixtures/platform/mock_audit.py @@ -151,3 +151,20 @@ } ] } + +AUDIT_EXPORT_REQUEST = { + "query": "description:FOO", + "format": "csv" +} + +MOCK_AUDIT_EXPORT_JOB = { + "id": 4805565, + "type": "EXTERNAL", + "job_parameters": { + "job_parameters": None + }, + "org_key": "test", + "status": "COMPLETED", + "create_time": "2023-02-02T23:16:25.625583Z", + "last_update_time": "2023-02-02T23:16:29.079184Z" +} diff --git a/src/tests/unit/platform/test_audit.py b/src/tests/unit/platform/test_audit.py index 2dc02ad8..49dbdc25 100644 --- a/src/tests/unit/platform/test_audit.py +++ b/src/tests/unit/platform/test_audit.py @@ -14,9 +14,10 @@ import pytest from cbc_sdk.errors import ApiError from cbc_sdk.rest_api import CBCloudAPI -from cbc_sdk.platform import AuditLog +from cbc_sdk.platform import AuditLog, Job from tests.unit.fixtures.CBCSDKMock import CBCSDKMock -from tests.unit.fixtures.platform.mock_audit import AUDITLOGS_RESP, AUDIT_SEARCH_REQUEST, AUDIT_SEARCH_RESPONSE +from tests.unit.fixtures.platform.mock_audit import (AUDITLOGS_RESP, AUDIT_SEARCH_REQUEST, AUDIT_SEARCH_RESPONSE, + AUDIT_EXPORT_REQUEST, MOCK_AUDIT_EXPORT_JOB) @pytest.fixture(scope="function") @@ -126,3 +127,25 @@ def test_async_search_audit_logs(cbcsdk_mock): assert result_list[3].actor_ip == "192.168.3.11" assert result_list[4].actor == "BELTALOWDA" assert result_list[4].actor_ip == "192.168.3.14" + + +def test_export_audit_logs(cbcsdk_mock): + """Tests the basic functionality of the export() function.""" + def on_post(url, body, **kwargs): + assert body == AUDIT_EXPORT_REQUEST + return {"job_id": 4805565} + + cbcsdk_mock.mock_request("POST", "/audit_log/v1/orgs/test/logs/_export", on_post) + cbcsdk_mock.mock_request("GET", "/jobs/v1/orgs/test/jobs/4805565", MOCK_AUDIT_EXPORT_JOB) + api = cbcsdk_mock.api + query = api.select(AuditLog).where("description:FOO") + job = query.export() + assert isinstance(job, Job) + assert job.id == 4805565 + + +def test_export_bad_format(cb): + """Tests calling export() with a bad format name.""" + query = cb.select(AuditLog) + with pytest.raises(ApiError): + query.export("bogusformat") From a112f9bdd3c58c32aa72547f4648800ee13927c1 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Thu, 25 Apr 2024 10:27:48 -0600 Subject: [PATCH 74/95] removed Job /progress code entirely - _await_completion now always relies on status --- src/cbc_sdk/platform/jobs.py | 51 ++++++-------------- src/tests/unit/platform/test_devicev6_api.py | 1 - src/tests/unit/platform/test_jobs.py | 44 +---------------- 3 files changed, 17 insertions(+), 79 deletions(-) diff --git a/src/cbc_sdk/platform/jobs.py b/src/cbc_sdk/platform/jobs.py index 515bb93e..03bd4e26 100644 --- a/src/cbc_sdk/platform/jobs.py +++ b/src/cbc_sdk/platform/jobs.py @@ -31,8 +31,6 @@ class Job(NewBaseModel): primary_key = "id" swagger_meta_file = "platform/models/job.yaml" - JOB_TYPES_WAITING_ON_STATUS = ('ENDPOINTS', ) - def __init__(self, cb, model_unique_id, initial_data=None): """ Initialize the Job object. @@ -43,13 +41,10 @@ def __init__(self, cb, model_unique_id, initial_data=None): initial_data (dict): Initial data used to populate the job. """ super(Job, self).__init__(cb, model_unique_id, initial_data) - self._wait_status = False if model_unique_id is not None and initial_data is None: self._refresh() else: self._full_init = True - if self._info['type'] in self.JOB_TYPES_WAITING_ON_STATUS: - self._wait_status = True @classmethod def _query_implementation(cls, cb, **kwargs): @@ -115,37 +110,21 @@ def _await_completion(self, timeout=0): backoff = BackoffHandler(self._cb, timeout=timeout) with backoff as b: errorcount = 0 - if self._wait_status: - status = "" - while status not in ("FAILED", "COMPLETED"): - b.pause() - try: - self._refresh() - if self.status != status: - status = self.status - b.reset() - except (ServerError, ObjectNotFoundError): - errorcount += 1 - if errorcount == 3: - raise - status = "" - if status == "FAILED": - raise ApiError(f"Job {self.id} reports failure") - else: - progress_data = (1, 0, '') - last_nc = 0 - while progress_data[1] < progress_data[0]: - b.pause() - try: - progress_data = self.get_progress() - if progress_data[1] > last_nc: - last_nc = progress_data[1] - b.reset() - except (ServerError, ObjectNotFoundError): - errorcount += 1 - if errorcount == 3: - raise - progress_data = (1, 0, '') + status = "" + while status not in ("FAILED", "COMPLETED"): + b.pause() + try: + self._refresh() + if self.status != status: + status = self.status + b.reset() + except (ServerError, ObjectNotFoundError): + errorcount += 1 + if errorcount == 3: + raise + status = "" + if status == "FAILED": + raise ApiError(f"Job {self.id} reports failure") return self def await_completion(self, timeout=0): diff --git a/src/tests/unit/platform/test_devicev6_api.py b/src/tests/unit/platform/test_devicev6_api.py index 87b94fd9..eff6bf2f 100755 --- a/src/tests/unit/platform/test_devicev6_api.py +++ b/src/tests/unit/platform/test_devicev6_api.py @@ -433,4 +433,3 @@ def post_validate(url, body, **kwargs): assert job assert isinstance(job, Job) assert job.id == 11608915 - assert job._wait_status diff --git a/src/tests/unit/platform/test_jobs.py b/src/tests/unit/platform/test_jobs.py index 9eb0bb94..57b6e551 100644 --- a/src/tests/unit/platform/test_jobs.py +++ b/src/tests/unit/platform/test_jobs.py @@ -101,46 +101,6 @@ def test_load_job_and_get_progress(cbcsdk_mock, jobid, total, completed, msg, lo def test_job_await_completion(cbcsdk_mock): - """Test the functionality of await_completion().""" - first_time = True - pr_index = 0 - - def on_progress(url, query_params, default): - nonlocal first_time, pr_index - if first_time: - first_time = False - raise ServerError(400, "Not yet") - assert pr_index < len(AWAIT_COMPLETION_PROGRESS), "Too many progress calls made" - return_value = AWAIT_COMPLETION_PROGRESS[pr_index] - pr_index += 1 - return return_value - - cbcsdk_mock.mock_request('GET', '/jobs/v1/orgs/test/jobs/12345', JOB_DETAILS_1) - cbcsdk_mock.mock_request('GET', '/jobs/v1/orgs/test/jobs/12345/progress', on_progress) - api = cbcsdk_mock.api - job = api.select(Job, 12345) - future = job.await_completion() - result = future.result() - assert result is job - assert first_time is False - assert pr_index == len(AWAIT_COMPLETION_PROGRESS) - - -def test_job_await_completion_error(cbcsdk_mock): - """Test that await_completion() throws a ServerError if it gets too many ServerErrors internally.""" - def on_progress(url, query_params, default): - raise ServerError(400, "Ain't happening") - - cbcsdk_mock.mock_request('GET', '/jobs/v1/orgs/test/jobs/12345', JOB_DETAILS_1) - cbcsdk_mock.mock_request('GET', '/jobs/v1/orgs/test/jobs/12345/progress', on_progress) - api = cbcsdk_mock.api - job = api.select(Job, 12345) - future = job.await_completion() - with pytest.raises(ServerError): - future.result() - - -def test_job_await_completion_status(cbcsdk_mock): """Test that await_completion() functions if _wait_status is set.""" pr_index = 0 @@ -162,7 +122,7 @@ def on_details(url, query_params, default): assert pr_index == len(AWAIT_COMPLETION_DETAILS_PROGRESS_1) -def test_job_await_completion_status_error(cbcsdk_mock): +def test_job_await_completion_error(cbcsdk_mock): """Test that await_completion() throws a ServerError if it gets too many ServerErrors internally.""" first_time = True @@ -184,7 +144,7 @@ def on_details(url, query_params, default): future.result() -def test_job_await_completion_status_failure(cbcsdk_mock): +def test_job_await_completion_failure(cbcsdk_mock): """Test that await_completion() throws an ApiError if it gets a FAILURE response.""" pr_index = 0 From 70d1124b4abaf9f3be858e24cd956bd29e1ac2a5 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Thu, 25 Apr 2024 10:34:09 -0600 Subject: [PATCH 75/95] added Example: section to AuditLogQuery.export() docstring and deflake8'd --- src/cbc_sdk/platform/audit.py | 5 +++++ src/tests/unit/fixtures/platform/mock_jobs.py | 18 ------------------ src/tests/unit/platform/test_jobs.py | 5 ++--- 3 files changed, 7 insertions(+), 21 deletions(-) diff --git a/src/cbc_sdk/platform/audit.py b/src/cbc_sdk/platform/audit.py index 9baf5fdf..117c3fb5 100644 --- a/src/cbc_sdk/platform/audit.py +++ b/src/cbc_sdk/platform/audit.py @@ -393,6 +393,11 @@ def export(self, format="csv"): The actual results are retrieved by waiting for the resulting job to complete, then calling one of the methods on ``Job`` to retrieve the results. + Example: + >>> audit_log_query = cb.select(AuditLog).add_time_criteria(range="-1d") + >>> audit_log_export_job = audit_log_query.export(format="csv") + >>> results = audit_log_export_job.await_completion().result() + Args: format (str): Format in which to return results, either "csv" or "json". Default is "csv". diff --git a/src/tests/unit/fixtures/platform/mock_jobs.py b/src/tests/unit/fixtures/platform/mock_jobs.py index 1a929893..5a6e55ad 100644 --- a/src/tests/unit/fixtures/platform/mock_jobs.py +++ b/src/tests/unit/fixtures/platform/mock_jobs.py @@ -56,24 +56,6 @@ "message": "Foo" } -AWAIT_COMPLETION_PROGRESS = [ - { - "num_total": 18, - "num_completed": 0, - "message": "" - }, - { - "num_total": 18, - "num_completed": 10, - "message": "" - }, - { - "num_total": 18, - "num_completed": 18, - "message": "" - }, -] - AWAIT_COMPLETION_DETAILS_PROGRESS_1 = ["CREATED", "CREATED", "CREATED", "COMPLETED"] AWAIT_COMPLETION_DETAILS_PROGRESS_2 = ["CREATED", "CREATED", "CREATED", "FAILED"] diff --git a/src/tests/unit/platform/test_jobs.py b/src/tests/unit/platform/test_jobs.py index 57b6e551..e5ac173c 100644 --- a/src/tests/unit/platform/test_jobs.py +++ b/src/tests/unit/platform/test_jobs.py @@ -11,8 +11,7 @@ from cbc_sdk.rest_api import CBCloudAPI from tests.unit.fixtures.CBCSDKMock import CBCSDKMock from tests.unit.fixtures.platform.mock_jobs import (FIND_ALL_JOBS_RESP, JOB_DETAILS_1, JOB_DETAILS_2, PROGRESS_1, - PROGRESS_2, AWAIT_COMPLETION_PROGRESS, - AWAIT_COMPLETION_DETAILS_PROGRESS_1, + PROGRESS_2, AWAIT_COMPLETION_DETAILS_PROGRESS_1, AWAIT_COMPLETION_DETAILS_PROGRESS_2) @@ -101,7 +100,7 @@ def test_load_job_and_get_progress(cbcsdk_mock, jobid, total, completed, msg, lo def test_job_await_completion(cbcsdk_mock): - """Test that await_completion() functions if _wait_status is set.""" + """Test that await_completion() functions.""" pr_index = 0 def on_details(url, query_params, default): From 222ca489910d99f356accad615acda041eb73a8e Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Thu, 11 Apr 2024 13:03:35 -0600 Subject: [PATCH 76/95] first sketch of additions to AuditLogRecord object --- src/cbc_sdk/platform/__init__.py | 4 +- src/cbc_sdk/platform/audit.py | 353 +----------------- .../platform/models/audit_log_record.yaml | 27 ++ 3 files changed, 49 insertions(+), 335 deletions(-) create mode 100644 src/cbc_sdk/platform/models/audit_log_record.yaml diff --git a/src/cbc_sdk/platform/__init__.py b/src/cbc_sdk/platform/__init__.py index e7dc9c00..e5b05c7b 100644 --- a/src/cbc_sdk/platform/__init__.py +++ b/src/cbc_sdk/platform/__init__.py @@ -8,7 +8,9 @@ from cbc_sdk.platform.alerts import Alert as BaseAlert -from cbc_sdk.platform.audit import AuditLog +from cbc_sdk.platform.audit import AuditLogRecord + +from cbc_sdk.platform.audit import AuditLogRecord as AuditLog from cbc_sdk.platform.asset_groups import AssetGroup diff --git a/src/cbc_sdk/platform/audit.py b/src/cbc_sdk/platform/audit.py index 117c3fb5..c77a9299 100644 --- a/src/cbc_sdk/platform/audit.py +++ b/src/cbc_sdk/platform/audit.py @@ -11,68 +11,47 @@ # * WARRANTIES OR CONDITIONS OF MERCHANTABILITY, SATISFACTORY QUALITY, # * NON-INFRINGEMENT AND FITNESS FOR A PARTICULAR PURPOSE. -""" -Model and query classes for platform audit logs. +"""Model and Query Classes for Platform Auditing""" -``AuditLog`` can be used to monitor your Carbon Black Cloud organization for actions performed by Carbon Black Cloud -console users and API keys. Audit logs are recorded for most CREATE, UPDATE and DELETE actions as well as a few READ -actions. Audit logs will include a description of the action and indicate the actor who performed the action along -with their IP to help determine if the User/API key are from an expected source. - -""" - -import datetime from cbc_sdk.base import (UnrefreshableModel, BaseQuery, QueryBuilder, QueryBuilderSupportMixin, - CriteriaBuilderSupportMixin, ExclusionBuilderSupportMixin, IterableQueryMixin, - AsyncQueryMixin) -from cbc_sdk.errors import ApiError -from cbc_sdk.platform.jobs import Job - -from backports._datetime_fromisoformat import datetime_fromisoformat + CriteriaBuilderSupportMixin, IterableQueryMixin, AsyncQueryMixin) """Model Class""" -class AuditLog(UnrefreshableModel): - """ - The model class which represents individual audit log entries. - - Each entry includes the actor performing the action, the IP address of the actor, a description, and a request URL - where available. - """ +class AuditLogRecord(UnrefreshableModel): + """Model class which represents audit log events. Mostly for future implementation.""" urlobject = "/audit_log/v1/orgs/{0}/logs" - swagger_meta_file = "platform/models/audit_log.yaml" + swagger_meta_file = "platform/models/audit_log_record.yaml" - def __init__(self, cb, initial_data=None): + def __init__(self, cb, model_unique_id, initial_data=None): """ - Creates a new ``AuditLog`` object. + Creates a new ``AuditLogRecord``. Args: cb (BaseAPI): Reference to API object used to communicate with the server. + model_unique_id (int): Not used. initial_data (dict): Initial data to fill in the audit log record details. """ - super(AuditLog, self).__init__(cb, -1, initial_data, force_init=False, full_doc=True) + super(AuditLogRecord, self).__init__(cb, model_unique_id, initial_data) @classmethod def _query_implementation(cls, cb, **kwargs): """ - Returns the appropriate query object for the ``AuditLog`` type. + Returns the appropriate query object for the ``AuditLogRecord`` type. Args: cb (BaseAPI): Reference to API object used to communicate with the server. **kwargs (dict): Not used, retained for compatibility. """ - return AuditLogQuery(cls, cb) + return AuditLogRecordQuery(cls, cb) @staticmethod def get_auditlogs(cb): """ Retrieve queued audit logs from the Carbon Black Cloud server. - Deprecated: - This method uses an outdated API. Use ``get_queued_auditlogs()`` instead. - Required Permissions: org.audits (READ) @@ -85,48 +64,21 @@ def get_auditlogs(cb): res = cb.get_object("/integrationServices/v3/auditlogs") return res.get("notifications", []) - @staticmethod - def get_queued_auditlogs(cb): - """ - Retrieve queued audit logs from the Carbon Black Cloud server. - - Required Permissions: - org.audits (READ) - - Args: - cb (BaseAPI): Reference to API object used to communicate with the server. - - Returns: - list[AuditLog]: List of objects representing the audit logs, or an empty list if none available. - """ - res = cb.get_object(AuditLog.urlobject.format(cb.credentials.org_key) + "/_queue") - return [AuditLog(cb, data) for data in res.get("results", [])] - """Query Class""" -class AuditLogQuery(BaseQuery, QueryBuilderSupportMixin, CriteriaBuilderSupportMixin, - ExclusionBuilderSupportMixin, IterableQueryMixin, AsyncQueryMixin): +class AuditLogRecordQuery(BaseQuery, QueryBuilderSupportMixin, CriteriaBuilderSupportMixin, + IterableQueryMixin, AsyncQueryMixin): """ - Query object that is used to locate ``AuditLog`` objects. + Query object that is used to locate ``AuditLogRecord`` objects. - The ``AuditLogQuery`` is constructed via SDK functions like the ``select()`` method on ``CBCloudAPI``. + The ``AuditLogRecordQuery`` is constructed via SDK functions like the ``select()`` method on ``CBCloudAPI``. The user would then add a query and/or criteria to it before iterating over the results. - - The following criteria may be added to the query via the standard ``add_criteria()`` method, or added to query - exclusions via the standard ``add_exclusions()`` method: - - * ``actor_ip`` - IP address of the entity that caused the creation of this audit log. - * ``actor`` - Name of the entity that caused the creation of this audit log. - * ``request_url`` - URL of the request that caused the creation of this audit log. - * ``description`` - Text description of this audit log. """ - VALID_EXPORT_FORMATS = ("csv", "json") - def __init__(self, doc_class, cb): """ - Initialize the ``AuditLogQuery``. + Initialize the ``AuditLogRecordQuery``. Args: doc_class (class): The model class that will be returned by this query. @@ -135,282 +87,15 @@ def __init__(self, doc_class, cb): self._doc_class = doc_class self._cb = cb self._count_valid = False - super(AuditLogQuery, self).__init__() + super(AuditLogRecordQuery, self).__init__() self._query_builder = QueryBuilder() self._criteria = {} + self._time_filter = {} self._exclusions = {} self._sortcriteria = {} self._search_after = None self.num_remaining = None self.num_found = None self.max_rows = -1 - - @staticmethod - def _create_valid_time_filter(kwargs): - """ - Creates the time range used for a "create_time" criteria value. - - Args: - kwargs (dict): Used to specify start= for start time, end= for end time, and range= for range. Values are - either timestamp ISO 8601 strings or datetime objects for start and end time. For range, the time range - to execute the result search, ending on the current time. Should be in the form "-2w", - where y=year, w=week, d=day, h=hour, m=minute, s=second. - - Returns: - dict: A new filter object. - - Raises: - ApiError: If the argument format is incorrect. - """ - time_filter = {} - if kwargs.get("start", None) and kwargs.get("end", None): - if kwargs.get("range", None): - raise ApiError("cannot specify range= in addition to start= and end=") - stime = kwargs["start"] - etime = kwargs["end"] - try: - if isinstance(stime, str): - stime = datetime_fromisoformat(stime) - if isinstance(etime, str): - etime = datetime_fromisoformat(etime) - if isinstance(stime, datetime.datetime) and isinstance(etime, datetime.datetime): - time_filter = {"start": stime.strftime("%Y-%m-%dT%H:%M:%S.%fZ"), - "end": etime.strftime("%Y-%m-%dT%H:%M:%S.%fZ")} - except: - raise ApiError(f"Start and end time must be a string in ISO 8601 format or an object of datetime. " - f"Start time {stime} is a {type(stime)}. End time {etime} is a {type(etime)}.") - elif kwargs.get("range", None): - if kwargs.get("start", None) or kwargs.get("end", None): - raise ApiError("cannot specify start= or end= in addition to range=") - time_filter = {"range": kwargs["range"]} - else: - raise ApiError("must specify either start= and end= or range=") - return time_filter - - def add_time_criteria(self, **kwargs): - """ - Adds a ``create_time`` value to either criteria or exclusions. - - Examples: - >>> query_specify_start_and_end = api.select(AuditLog). - ... add_time_criteria(start="2023-10-20T20:34:07Z", end="2023-10-30T20:34:07Z") - >>> query_specify_exclude_range = api.select(AuditLog).add_time_criteria(range='-3d', exclude=True) - - Args: - kwargs (dict): Keyword arguments to this method. - - Keyword Args: - start (str/datetime): Starting time for the time interval to include in the criteria. Must be either a - ``datetime`` object or a string in ISO 8601 format. Both ``start`` and ``end`` must be specified - if they are to be used. - end (str/datetime): Ending time for the time interval to include in the criteria. Must be either a - ``datetime`` object or a string in ISO 8601 format. Both ``start`` and ``end`` must be specified - if they are to be used. - range (str): Range for the time interval, to be measured backwards from the current time. Cannot - be specified if ``start`` or ``end`` are specified. Must be in the format "-NX", where ``N`` is an - integer value, and ``X`` is a single character specifying the time unit: "y" for years, "w" for weeks, - "d" for days, "h" for hours, "m" for minutes, or "s" for seconds. - exclude (bool): ``True`` if this value is to be applied to exclusions, ``False`` if this value is to be - applied to search criteria. Default ``False.`` - - Returns: - AuditLogQuery: This instance. - - Raises: - ApiError: If the argument format is incorrect. - """ - if kwargs.get("exclude", False): - self._exclusions['create_time'] = self._create_valid_time_filter(kwargs) - else: - self._criteria['create_time'] = self._create_valid_time_filter(kwargs) - return self - - def add_boolean_criteria(self, criteria_name, value, exclude=False): - """ - Adds a Boolean value to either the criteria or exclusions. - - Args: - criteria_name (str): The criteria name to set. May be either "flagged" (to set whether or not the audit - record has been flagged) or "verbose" (so set whether or not the audit record has been marked verbose). - value (bool): The value of the criteria to be set. - exclude (bool): ``True`` if this value is to be applied to exclusions, ``False`` if this value is to be - applied to search criteria. Default ``False.`` - - Returns: - AuditLogQuery: This instance. - """ - if exclude: - self._exclusions[criteria_name] = value - else: - self._criteria[criteria_name] = value - return self - - def sort_by(self, key, direction="ASC"): - """ - Sets the sorting behavior on a query's results. - - Example: - >>> cb.select(AuditLog).sort_by("name") - - Args: - key (str): The key in the schema to sort by. - direction (str): The sort order, either "ASC" or "DESC". - - Returns: - AuditLogQuery: This instance. - """ - if direction not in CriteriaBuilderSupportMixin.VALID_DIRECTIONS: - raise ApiError("invalid sort direction specified") - self._sortcriteria = {"field": key, "order": direction} - return self - - def _build_request(self, from_row, max_rows): - """ - Creates the request body for an API call. - - Args: - from_row (int): The row to start the query at. - max_rows (int): The maximum number of rows to be returned. - - Returns: - dict: The complete request body. - """ - request = {} - if self._criteria: - request['criteria'] = self._criteria - if self._exclusions: - request['exclusions'] = self._exclusions - query = self._query_builder._collapse() - if query: - request['query'] = query - if max_rows > 0: - request['rows'] = max_rows - if from_row > 0: - request['start'] = from_row - if self._sortcriteria: - request['sort'] = [self._sortcriteria] - return request - - def _build_url(self, tail_end): - """ - Creates the URL to be used for an API call. - - Args: - tail_end (str): String to be appended to the end of the generated URL. - - Returns: - str: The complete URL. - """ - url = self._doc_class.urlobject.format(self._cb.credentials.org_key) + tail_end - return url - - def _count(self): - """ - Returns the number of results from the run of this query. - - Required Permissions: - org.audits (READ) - - Returns: - int: The number of results from the run of this query. - """ - if self._count_valid: - return self._total_results - - url = self._build_url("/_search") - request = self._build_request(0, -1) - resp = self._cb.post_object(url, body=request) - result = resp.json() - - self._total_results = result["num_found"] - self._count_valid = True - - return self._total_results - - def _perform_query(self, from_row=0, max_rows=-1): - """ - Performs the query and returns the results of the query in an iterable fashion. - - Required Permissions: - org.audits (READ) - - Args: - from_row (int): The row to start the query at (default 0). - max_rows (int): The maximum number of rows to be returned (default -1, meaning "all"). - - Yields: - AuditLog: The audit log records resulting from the search. - """ - url = self._build_url("/_search") - current = from_row - numrows = 0 - still_querying = True - while still_querying: - request = self._build_request(current, max_rows) - resp = self._cb.post_object(url, body=request) - result = resp.json() - - self._total_results = result["num_found"] - self._count_valid = True - - results = result.get("results", []) - for item in results: - yield self._doc_class(self._cb, item) - current += 1 - numrows += 1 - - if max_rows > 0 and numrows == max_rows: - still_querying = False - break - - from_row = current - if current >= self._total_results: - still_querying = False - break - - def _run_async_query(self, context): - """ - Executed in the background to run an asynchronous query. - - Args: - context (object): Not used. - - Returns: - list[AuditLog]: The results of the query. - """ - url = self._build_url("/_search") - request = self._build_request(0, -1) - resp = self._cb.post_object(url, body=request) - result = resp.json() - results = result.get("results", []) - return [self._doc_class(self._cb, item) for item in results] - - def export(self, format="csv"): - """ - Export audit logs using the Job service. - - The actual results are retrieved by waiting for the resulting job to complete, then calling one of the methods - on ``Job`` to retrieve the results. - - Example: - >>> audit_log_query = cb.select(AuditLog).add_time_criteria(range="-1d") - >>> audit_log_export_job = audit_log_query.export(format="csv") - >>> results = audit_log_export_job.await_completion().result() - - Args: - format (str): Format in which to return results, either "csv" or "json". Default is "csv". - - Returns: - Job: The object representing the export job. - """ - if format not in AuditLogQuery.VALID_EXPORT_FORMATS: - raise ApiError(f"invalid export format '{format}'") - url = self._build_url("/_export") - request = self._build_request(0, -1) - request["format"] = format - resp = self._cb.post_object(url, body=request) - result = resp.json() - if "job_id" in result: - return Job(self._cb, result["job_id"]) - return None # pragma: no cover + \ No newline at end of file diff --git a/src/cbc_sdk/platform/models/audit_log_record.yaml b/src/cbc_sdk/platform/models/audit_log_record.yaml new file mode 100644 index 00000000..842ad091 --- /dev/null +++ b/src/cbc_sdk/platform/models/audit_log_record.yaml @@ -0,0 +1,27 @@ +type: object +properties: + actor_ip: + type: string + description: IP address of the entity that caused the creation of this audit log + actor: + type: string + description: Name of the entity that caused the creation of this audit log + create_time: + type: string + format: date-time + description: Timestamp when this audit log was created in ISO-8601 string format + description: + type: string + description: Text description of this audit log + flagged: + type: boolean + description: Whether the audit has been flagged + org_key: + type: string + description: Organization key + request_url: + type: string + description: URL of the request that caused the creation of this audit log + verbose: + type: boolean + description: Whether the audit has been marked verbose From bf27a251c90932c56f28dbbcbdb425b891654610 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Tue, 16 Apr 2024 16:44:01 -0600 Subject: [PATCH 77/95] completed writing the query object --- src/cbc_sdk/platform/audit.py | 248 +++++++++++++++++++++++++++++++++- 1 file changed, 244 insertions(+), 4 deletions(-) diff --git a/src/cbc_sdk/platform/audit.py b/src/cbc_sdk/platform/audit.py index c77a9299..68fd5266 100644 --- a/src/cbc_sdk/platform/audit.py +++ b/src/cbc_sdk/platform/audit.py @@ -13,8 +13,13 @@ """Model and Query Classes for Platform Auditing""" +import datetime from cbc_sdk.base import (UnrefreshableModel, BaseQuery, QueryBuilder, QueryBuilderSupportMixin, - CriteriaBuilderSupportMixin, IterableQueryMixin, AsyncQueryMixin) + CriteriaBuilderSupportMixin, ExclusionBuilderSupportMixin, IterableQueryMixin, + AsyncQueryMixin) +from cbc_sdk.errors import ApiError + +from backports._datetime_fromisoformat import datetime_fromisoformat """Model Class""" @@ -69,12 +74,20 @@ def get_auditlogs(cb): class AuditLogRecordQuery(BaseQuery, QueryBuilderSupportMixin, CriteriaBuilderSupportMixin, - IterableQueryMixin, AsyncQueryMixin): + ExclusionBuilderSupportMixin, IterableQueryMixin, AsyncQueryMixin): """ Query object that is used to locate ``AuditLogRecord`` objects. The ``AuditLogRecordQuery`` is constructed via SDK functions like the ``select()`` method on ``CBCloudAPI``. The user would then add a query and/or criteria to it before iterating over the results. + + The following criteria may be added to the query via the standard ``add_criteria()`` method, or added to query + exclusions via the standard ``add_exclusions()`` method: + + * ``actor_ip`` - IP address of the entity that caused the creation of this audit log. + * ``actor`` - Name of the entity that caused the creation of this audit log. + * ``request_url`` - URL of the request that caused the creation of this audit log. + * ``description`` - Text description of this audit log. """ def __init__(self, doc_class, cb): """ @@ -91,11 +104,238 @@ def __init__(self, doc_class, cb): self._query_builder = QueryBuilder() self._criteria = {} - self._time_filter = {} self._exclusions = {} self._sortcriteria = {} self._search_after = None self.num_remaining = None self.num_found = None self.max_rows = -1 - \ No newline at end of file + + @staticmethod + def _create_valid_time_filter(self, kwargs): + """ + Verifies that an alert criteria key has the timerange functionality + + Args: + kwargs (dict): Used to specify start= for start time, end= for end time, and range= for range. Values are + either timestamp ISO 8601 strings or datetime objects for start and end time. For range the time range + to execute the result search, ending on the current time. Should be in the form "-2w", + where y=year, w=week, d=day, h=hour, m=minute, s=second. + + Returns: + dict: A new filter object. + + Raises: + ApiError: If the argument format is incorrect. + """ + time_filter = {} + if kwargs.get("start", None) and kwargs.get("end", None): + if kwargs.get("range", None): + raise ApiError("cannot specify range= in addition to start= and end=") + stime = kwargs["start"] + etime = kwargs["end"] + try: + if isinstance(stime, str): + stime = datetime_fromisoformat(stime) + if isinstance(etime, str): + etime = datetime_fromisoformat(etime) + if isinstance(stime, datetime.datetime) and isinstance(etime, datetime.datetime): + time_filter = {"start": stime.strftime("%Y-%m-%dT%H:%M:%S.%fZ"), + "end": etime.strftime("%Y-%m-%dT%H:%M:%S.%fZ")} + except: + raise ApiError(f"Start and end time must be a string in ISO 8601 format or an object of datetime. " + f"Start time {stime} is a {type(stime)}. End time {etime} is a {type(etime)}.") + elif kwargs.get("range", None): + if kwargs.get("start", None) or kwargs.get("end", None): + raise ApiError("cannot specify start= or end= in addition to range=") + time_filter = {"range": kwargs["range"]} + else: + raise ApiError("must specify either start= and end= or range=") + return time_filter + + def add_time_criteria(self, **kwargs): + """ + Adds a ``create_time`` criteria value to either criteria or exclusions. + + Args: + kwargs (dict): Keyword arguments to this method. + + Keyword Args: + start (str/datetime): Starting time for the time interval to include in the criteria. Must be either a + ``datetime`` object or a string in ISO 8601 format. Both ``start`` and ``end`` must be specified + if they are to be used. + end (str/datetime): Ending time for the time interval to include in the criteria. Must be either a + ``datetime`` object or a string in ISO 8601 format. Both ``start`` and ``end`` must be specified + if they are to be used. + range (str): Range for the time interval, to be measured backwards from the current time. Cannot + be specified if ``start`` or ``end`` are specified. Must be in the format "-NX", where ``N`` is an + integer value, and ``X`` is a single character specifying the time unit: "y" for years, "w" for weeks, + "d" for days, "h" for hours, "m" for minutes, or "s" for seconds. + exclude (bool): ``True`` if this value is to be applied to exclusions, ``False`` if this value is to be + applied to search criteria. Default ``False.`` + + Returns: + AuditLogRecordQuery: This instance. + + Raises: + ApiError: If the argument format is incorrect. + """ + if kwargs.get("exclude", False): + self._exclusions['create_time'] = self._create_valid_time_filter(kwargs) + else: + self._criteria['create_time'] = self._create_valid_time_filter(kwargs) + return self + + def add_boolean_criteria(self, criteria_name, value, exclude=False): + """ + Adds a Boolean value to either the criteria or exclusions. + + Args: + criteria_name (str): The criteria name to set. May be either "flagged" (to set whether or not the audit + record has been flagged) or "verbose" (so set whether or not the audit record has been marked verbose). + value (bool): The value of the criteria to be set. + exclude (bool): ``True`` if this value is to be applied to exclusions, ``False`` if this value is to be + applied to search criteria. Default ``False.`` + + Returns: + AuditLogRecordQuery: This instance. + """ + if exclude: + self._exclusions[criteria_name] = value + else: + self._criteria[criteria_name] = value + return self + + def sort_by(self, key, direction="ASC"): + """ + Sets the sorting behavior on a query's results. + + Example: + >>> cb.select(AuditLogRecord).sort_by("name") + + Args: + key (str): The key in the schema to sort by. + direction (str): The sort order, either "ASC" or "DESC". + + Returns: + AuditLogRecordQuery: This instance. + """ + if direction not in CriteriaBuilderSupportMixin.VALID_DIRECTIONS: + raise ApiError("invalid sort direction specified") + self._sortcriteria = {"field": key, "order": direction} + return self + + def _build_request(self, from_row, max_rows): + """ + Creates the request body for an API call. + + Args: + from_row (int): The row to start the query at. + max_rows (int): The maximum number of rows to be returned. + + Returns: + dict: The complete request body. + """ + request = {} + if self._criteria: + request['criteria'] = self._criteria + if self._exclusions: + request['exclusions'] = self._exclusions + query = self._query_builder.collapse() + if query: + request['query'] = query + if max_rows > 0: + request['rows'] = max_rows + if from_row > 0: + request['start'] = from_row + if self._sortcriteria: + request['sort'] = [self._sortcriteria] + return request + + def _build_url(self, tail_end): + """ + Creates the URL to be used for an API call. + + Args: + tail_end (str): String to be appended to the end of the generated URL. + + Returns: + str: The complete URL. + """ + url = self._doc_class.urlobject.format(self._cb.credentials.org_key) + tail_end + return url + + def _count(self): + """ + Returns the number of results from the run of this query. + + Returns: + int: The number of results from the run of this query. + """ + if self._count_valid: + return self._total_results + + url = self._build_url("/_search") + request = self._build_request(0, -1) + resp = self._cb.post_object(url, body=request) + result = resp.json() + + self._total_results = result["num_found"] + self._count_valid = True + + return self._total_results + + def _perform_query(self, from_row=0, max_rows=-1): + """ + Performs the query and returns the results of the query in an iterable fashion. + + Args: + from_row (int): The row to start the query at (default 0). + max_rows (int): The maximum number of rows to be returned (default -1, meaning "all"). + + Yields: + AuditLogRecord: The audit log records resulting from the search. + """ + url = self._build_url("/_search") + current = from_row + numrows = 0 + still_querying = True + while still_querying: + request = self._build_request(current, max_rows) + resp = self._cb.post_object(url, body=request) + result = resp.json() + + self._total_results = result["num_found"] + self._count_valid = True + + results = result.get("results", []) + for item in results: + yield self._doc_class(self._cb, 0, item) + current += 1 + numrows += 1 + + if max_rows > 0 and numrows == max_rows: + still_querying = False + break + + from_row = current + if current >= self._total_results: + still_querying = False + break + + def _run_async_query(self, context): + """ + Executed in the background to run an asynchronous query. + + Args: + context (object): Not used. + + Returns: + list[AuditLogRecord]: The results of the query. + """ + url = self._build_url("/_search") + request = self._build_request(0, -1) + resp = self._cb.post_object(url, body=request) + result = resp.json() + results = result.get("results", []) + return [self._doc_class(self._cb, 0, item) for item in results] From 7e7dd2215a4ce0e9c7cf7a6b1cb204dccf5aeed0 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Wed, 17 Apr 2024 14:33:34 -0600 Subject: [PATCH 78/95] first unit test added and fixed up --- src/cbc_sdk/platform/audit.py | 10 +++++----- src/cbc_sdk/rest_api.py | 4 ++-- src/tests/unit/platform/test_audit.py | 1 - 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/cbc_sdk/platform/audit.py b/src/cbc_sdk/platform/audit.py index 68fd5266..d4cabf3a 100644 --- a/src/cbc_sdk/platform/audit.py +++ b/src/cbc_sdk/platform/audit.py @@ -21,7 +21,6 @@ from backports._datetime_fromisoformat import datetime_fromisoformat - """Model Class""" @@ -39,7 +38,7 @@ def __init__(self, cb, model_unique_id, initial_data=None): model_unique_id (int): Not used. initial_data (dict): Initial data to fill in the audit log record details. """ - super(AuditLogRecord, self).__init__(cb, model_unique_id, initial_data) + super(AuditLogRecord, self).__init__(cb, model_unique_id, initial_data, force_init=False, full_doc=True) @classmethod def _query_implementation(cls, cb, **kwargs): @@ -89,6 +88,7 @@ class AuditLogRecordQuery(BaseQuery, QueryBuilderSupportMixin, CriteriaBuilderSu * ``request_url`` - URL of the request that caused the creation of this audit log. * ``description`` - Text description of this audit log. """ + def __init__(self, doc_class, cb): """ Initialize the ``AuditLogRecordQuery``. @@ -112,13 +112,13 @@ def __init__(self, doc_class, cb): self.max_rows = -1 @staticmethod - def _create_valid_time_filter(self, kwargs): + def _create_valid_time_filter(kwargs): """ Verifies that an alert criteria key has the timerange functionality Args: kwargs (dict): Used to specify start= for start time, end= for end time, and range= for range. Values are - either timestamp ISO 8601 strings or datetime objects for start and end time. For range the time range + either timestamp ISO 8601 strings or datetime objects for start and end time. For range, the time range to execute the result search, ending on the current time. Should be in the form "-2w", where y=year, w=week, d=day, h=hour, m=minute, s=second. @@ -241,7 +241,7 @@ def _build_request(self, from_row, max_rows): request['criteria'] = self._criteria if self._exclusions: request['exclusions'] = self._exclusions - query = self._query_builder.collapse() + query = self._query_builder._collapse() if query: request['query'] = query if max_rows > 0: diff --git a/src/cbc_sdk/rest_api.py b/src/cbc_sdk/rest_api.py index 3a075df8..2c5c112c 100644 --- a/src/cbc_sdk/rest_api.py +++ b/src/cbc_sdk/rest_api.py @@ -22,7 +22,7 @@ from cbc_sdk.errors import ApiError, CredentialError, ServerError, InvalidObjectError from cbc_sdk.live_response_api import LiveResponseSessionManager from cbc_sdk.audit_remediation import Run, RunHistory -from cbc_sdk.platform.audit import AuditLog +from cbc_sdk.platform.audit import AuditLogRecord from cbc_sdk.enterprise_edr.threat_intelligence import ReportSeverity import logging import time @@ -210,7 +210,7 @@ def get_auditlogs(self): list[dict]: List of dictionary objects representing the audit logs, or an empty list if none available. """ log.warning("CBCloudAPI.get_auditlogs is deprecated, use AuditLog.get_auditlogs instead") - return AuditLog.get_auditlogs(self) + return AuditLogRecord.get_auditlogs(self) # ---- Device API diff --git a/src/tests/unit/platform/test_audit.py b/src/tests/unit/platform/test_audit.py index 49dbdc25..41612e26 100644 --- a/src/tests/unit/platform/test_audit.py +++ b/src/tests/unit/platform/test_audit.py @@ -19,7 +19,6 @@ from tests.unit.fixtures.platform.mock_audit import (AUDITLOGS_RESP, AUDIT_SEARCH_REQUEST, AUDIT_SEARCH_RESPONSE, AUDIT_EXPORT_REQUEST, MOCK_AUDIT_EXPORT_JOB) - @pytest.fixture(scope="function") def cb(): """Create CBCloudAPI singleton""" From 91ff085418190e9f981c1c43b1d350f648a24ef2 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Thu, 18 Apr 2024 12:32:55 -0600 Subject: [PATCH 79/95] renamed AuditLogRecord back to AuditLog --- src/cbc_sdk/platform/__init__.py | 4 +-- src/cbc_sdk/platform/audit.py | 36 +++++++++---------- .../platform/models/audit_log_record.yaml | 27 -------------- src/cbc_sdk/rest_api.py | 4 +-- 4 files changed, 21 insertions(+), 50 deletions(-) delete mode 100644 src/cbc_sdk/platform/models/audit_log_record.yaml diff --git a/src/cbc_sdk/platform/__init__.py b/src/cbc_sdk/platform/__init__.py index e5b05c7b..e7dc9c00 100644 --- a/src/cbc_sdk/platform/__init__.py +++ b/src/cbc_sdk/platform/__init__.py @@ -8,9 +8,7 @@ from cbc_sdk.platform.alerts import Alert as BaseAlert -from cbc_sdk.platform.audit import AuditLogRecord - -from cbc_sdk.platform.audit import AuditLogRecord as AuditLog +from cbc_sdk.platform.audit import AuditLog from cbc_sdk.platform.asset_groups import AssetGroup diff --git a/src/cbc_sdk/platform/audit.py b/src/cbc_sdk/platform/audit.py index d4cabf3a..98f4c6ff 100644 --- a/src/cbc_sdk/platform/audit.py +++ b/src/cbc_sdk/platform/audit.py @@ -24,32 +24,32 @@ """Model Class""" -class AuditLogRecord(UnrefreshableModel): +class AuditLog(UnrefreshableModel): """Model class which represents audit log events. Mostly for future implementation.""" urlobject = "/audit_log/v1/orgs/{0}/logs" - swagger_meta_file = "platform/models/audit_log_record.yaml" + swagger_meta_file = "platform/models/audit_log.yaml" def __init__(self, cb, model_unique_id, initial_data=None): """ - Creates a new ``AuditLogRecord``. + Creates a new ``AuditLog``. Args: cb (BaseAPI): Reference to API object used to communicate with the server. model_unique_id (int): Not used. initial_data (dict): Initial data to fill in the audit log record details. """ - super(AuditLogRecord, self).__init__(cb, model_unique_id, initial_data, force_init=False, full_doc=True) + super(AuditLog, self).__init__(cb, model_unique_id, initial_data, force_init=False, full_doc=True) @classmethod def _query_implementation(cls, cb, **kwargs): """ - Returns the appropriate query object for the ``AuditLogRecord`` type. + Returns the appropriate query object for the ``AuditLog`` type. Args: cb (BaseAPI): Reference to API object used to communicate with the server. **kwargs (dict): Not used, retained for compatibility. """ - return AuditLogRecordQuery(cls, cb) + return AuditLogQuery(cls, cb) @staticmethod def get_auditlogs(cb): @@ -72,12 +72,12 @@ def get_auditlogs(cb): """Query Class""" -class AuditLogRecordQuery(BaseQuery, QueryBuilderSupportMixin, CriteriaBuilderSupportMixin, - ExclusionBuilderSupportMixin, IterableQueryMixin, AsyncQueryMixin): +class AuditLogQuery(BaseQuery, QueryBuilderSupportMixin, CriteriaBuilderSupportMixin, + ExclusionBuilderSupportMixin, IterableQueryMixin, AsyncQueryMixin): """ - Query object that is used to locate ``AuditLogRecord`` objects. + Query object that is used to locate ``AuditLog`` objects. - The ``AuditLogRecordQuery`` is constructed via SDK functions like the ``select()`` method on ``CBCloudAPI``. + The ``AuditLogQuery`` is constructed via SDK functions like the ``select()`` method on ``CBCloudAPI``. The user would then add a query and/or criteria to it before iterating over the results. The following criteria may be added to the query via the standard ``add_criteria()`` method, or added to query @@ -91,7 +91,7 @@ class AuditLogRecordQuery(BaseQuery, QueryBuilderSupportMixin, CriteriaBuilderSu def __init__(self, doc_class, cb): """ - Initialize the ``AuditLogRecordQuery``. + Initialize the ``AuditLogQuery``. Args: doc_class (class): The model class that will be returned by this query. @@ -100,7 +100,7 @@ def __init__(self, doc_class, cb): self._doc_class = doc_class self._cb = cb self._count_valid = False - super(AuditLogRecordQuery, self).__init__() + super(AuditLogQuery, self).__init__() self._query_builder = QueryBuilder() self._criteria = {} @@ -175,7 +175,7 @@ def add_time_criteria(self, **kwargs): applied to search criteria. Default ``False.`` Returns: - AuditLogRecordQuery: This instance. + AuditLogQuery: This instance. Raises: ApiError: If the argument format is incorrect. @@ -198,7 +198,7 @@ def add_boolean_criteria(self, criteria_name, value, exclude=False): applied to search criteria. Default ``False.`` Returns: - AuditLogRecordQuery: This instance. + AuditLogQuery: This instance. """ if exclude: self._exclusions[criteria_name] = value @@ -211,14 +211,14 @@ def sort_by(self, key, direction="ASC"): Sets the sorting behavior on a query's results. Example: - >>> cb.select(AuditLogRecord).sort_by("name") + >>> cb.select(AuditLog).sort_by("name") Args: key (str): The key in the schema to sort by. direction (str): The sort order, either "ASC" or "DESC". Returns: - AuditLogRecordQuery: This instance. + AuditLogQuery: This instance. """ if direction not in CriteriaBuilderSupportMixin.VALID_DIRECTIONS: raise ApiError("invalid sort direction specified") @@ -294,7 +294,7 @@ def _perform_query(self, from_row=0, max_rows=-1): max_rows (int): The maximum number of rows to be returned (default -1, meaning "all"). Yields: - AuditLogRecord: The audit log records resulting from the search. + AuditLog: The audit log records resulting from the search. """ url = self._build_url("/_search") current = from_row @@ -331,7 +331,7 @@ def _run_async_query(self, context): context (object): Not used. Returns: - list[AuditLogRecord]: The results of the query. + list[AuditLog]: The results of the query. """ url = self._build_url("/_search") request = self._build_request(0, -1) diff --git a/src/cbc_sdk/platform/models/audit_log_record.yaml b/src/cbc_sdk/platform/models/audit_log_record.yaml deleted file mode 100644 index 842ad091..00000000 --- a/src/cbc_sdk/platform/models/audit_log_record.yaml +++ /dev/null @@ -1,27 +0,0 @@ -type: object -properties: - actor_ip: - type: string - description: IP address of the entity that caused the creation of this audit log - actor: - type: string - description: Name of the entity that caused the creation of this audit log - create_time: - type: string - format: date-time - description: Timestamp when this audit log was created in ISO-8601 string format - description: - type: string - description: Text description of this audit log - flagged: - type: boolean - description: Whether the audit has been flagged - org_key: - type: string - description: Organization key - request_url: - type: string - description: URL of the request that caused the creation of this audit log - verbose: - type: boolean - description: Whether the audit has been marked verbose diff --git a/src/cbc_sdk/rest_api.py b/src/cbc_sdk/rest_api.py index 2c5c112c..3a075df8 100644 --- a/src/cbc_sdk/rest_api.py +++ b/src/cbc_sdk/rest_api.py @@ -22,7 +22,7 @@ from cbc_sdk.errors import ApiError, CredentialError, ServerError, InvalidObjectError from cbc_sdk.live_response_api import LiveResponseSessionManager from cbc_sdk.audit_remediation import Run, RunHistory -from cbc_sdk.platform.audit import AuditLogRecord +from cbc_sdk.platform.audit import AuditLog from cbc_sdk.enterprise_edr.threat_intelligence import ReportSeverity import logging import time @@ -210,7 +210,7 @@ def get_auditlogs(self): list[dict]: List of dictionary objects representing the audit logs, or an empty list if none available. """ log.warning("CBCloudAPI.get_auditlogs is deprecated, use AuditLog.get_auditlogs instead") - return AuditLogRecord.get_auditlogs(self) + return AuditLog.get_auditlogs(self) # ---- Device API From f7793815e5aab9e093093dedd51d1d86a7917b7c Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Thu, 18 Apr 2024 13:37:10 -0600 Subject: [PATCH 80/95] did some fixup of docstrings --- src/cbc_sdk/platform/audit.py | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/src/cbc_sdk/platform/audit.py b/src/cbc_sdk/platform/audit.py index 98f4c6ff..cbc54310 100644 --- a/src/cbc_sdk/platform/audit.py +++ b/src/cbc_sdk/platform/audit.py @@ -11,7 +11,15 @@ # * WARRANTIES OR CONDITIONS OF MERCHANTABILITY, SATISFACTORY QUALITY, # * NON-INFRINGEMENT AND FITNESS FOR A PARTICULAR PURPOSE. -"""Model and Query Classes for Platform Auditing""" +""" +Model and query classes for platform audit logs. + +``AuditLog`` can be used to monitor your Carbon Black Cloud organization for actions performed by Carbon Black Cloud +console users and API keys. Audit logs are recorded for most CREATE, UPDATE and DELETE actions as well as a few READ +actions. Audit logs will include a description of the action and indicate the actor who performed the action along +with their IP to help determine if the User/API key are from an expected source. + +""" import datetime from cbc_sdk.base import (UnrefreshableModel, BaseQuery, QueryBuilder, QueryBuilderSupportMixin, @@ -25,13 +33,18 @@ class AuditLog(UnrefreshableModel): - """Model class which represents audit log events. Mostly for future implementation.""" + """ + The model class which represents individual audit log entries. + + Each entry includes the actor performing the action, the IP address of the actor, a description, and a request URL + where available. + """ urlobject = "/audit_log/v1/orgs/{0}/logs" swagger_meta_file = "platform/models/audit_log.yaml" def __init__(self, cb, model_unique_id, initial_data=None): """ - Creates a new ``AuditLog``. + Creates a new ``AuditLog`` object. Args: cb (BaseAPI): Reference to API object used to communicate with the server. @@ -114,7 +127,7 @@ def __init__(self, doc_class, cb): @staticmethod def _create_valid_time_filter(kwargs): """ - Verifies that an alert criteria key has the timerange functionality + Creates the time range used for a "create_time" criteria value. Args: kwargs (dict): Used to specify start= for start time, end= for end time, and range= for range. Values are @@ -155,7 +168,7 @@ def _create_valid_time_filter(kwargs): def add_time_criteria(self, **kwargs): """ - Adds a ``create_time`` criteria value to either criteria or exclusions. + Adds a ``create_time`` value to either criteria or exclusions. Args: kwargs (dict): Keyword arguments to this method. @@ -269,6 +282,9 @@ def _count(self): """ Returns the number of results from the run of this query. + Required Permissions: + org.audits (READ) + Returns: int: The number of results from the run of this query. """ @@ -289,6 +305,9 @@ def _perform_query(self, from_row=0, max_rows=-1): """ Performs the query and returns the results of the query in an iterable fashion. + Required Permissions: + org.audits (READ) + Args: from_row (int): The row to start the query at (default 0). max_rows (int): The maximum number of rows to be returned (default -1, meaning "all"). From bfd810a243173e8c7b8b349f728a9d18a428085d Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Fri, 19 Apr 2024 17:08:05 -0600 Subject: [PATCH 81/95] the function is added --- src/cbc_sdk/platform/audit.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/cbc_sdk/platform/audit.py b/src/cbc_sdk/platform/audit.py index cbc54310..c56632d2 100644 --- a/src/cbc_sdk/platform/audit.py +++ b/src/cbc_sdk/platform/audit.py @@ -69,6 +69,9 @@ def get_auditlogs(cb): """ Retrieve queued audit logs from the Carbon Black Cloud server. + Deprecated: + This method uses an outdated API. Use ``get_queued_auditlogs()`` instead. + Required Permissions: org.audits (READ) @@ -81,6 +84,23 @@ def get_auditlogs(cb): res = cb.get_object("/integrationServices/v3/auditlogs") return res.get("notifications", []) + @staticmethod + def get_queued_auditlogs(cb): + """ + Retrieve queued audit logs from the Carbon Black Cloud server. + + Required Permissions: + org.audits (READ) + + Args: + cb (BaseAPI): Reference to API object used to communicate with the server. + + Returns: + list[AuditLog]: List of objects representing the audit logs, or an empty list if none available. + """ + res = cb.get_object(AuditLog.urlobject.format(cb.credentials.org_key) + "/queue") + return [AuditLog(cb, -1, data) for data in res.get("results", [])] + """Query Class""" From 4148ea221fd3208141500a7efbe627c965e92185 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Mon, 22 Apr 2024 15:11:36 -0600 Subject: [PATCH 82/95] unit tests added, coverage 97% --- src/cbc_sdk/platform/audit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cbc_sdk/platform/audit.py b/src/cbc_sdk/platform/audit.py index c56632d2..49af6dd0 100644 --- a/src/cbc_sdk/platform/audit.py +++ b/src/cbc_sdk/platform/audit.py @@ -98,7 +98,7 @@ def get_queued_auditlogs(cb): Returns: list[AuditLog]: List of objects representing the audit logs, or an empty list if none available. """ - res = cb.get_object(AuditLog.urlobject.format(cb.credentials.org_key) + "/queue") + res = cb.get_object(AuditLog.urlobject.format(cb.credentials.org_key) + "/_queue") return [AuditLog(cb, -1, data) for data in res.get("results", [])] From 82519b4beea446b943630d219ed09629985645f1 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Tue, 23 Apr 2024 09:40:46 -0600 Subject: [PATCH 83/95] removed the model_uniqueid argument from the __init__ call (not used). --- src/cbc_sdk/platform/audit.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/cbc_sdk/platform/audit.py b/src/cbc_sdk/platform/audit.py index 49af6dd0..f8d23a96 100644 --- a/src/cbc_sdk/platform/audit.py +++ b/src/cbc_sdk/platform/audit.py @@ -42,7 +42,7 @@ class AuditLog(UnrefreshableModel): urlobject = "/audit_log/v1/orgs/{0}/logs" swagger_meta_file = "platform/models/audit_log.yaml" - def __init__(self, cb, model_unique_id, initial_data=None): + def __init__(self, cb, initial_data=None): """ Creates a new ``AuditLog`` object. @@ -51,7 +51,7 @@ def __init__(self, cb, model_unique_id, initial_data=None): model_unique_id (int): Not used. initial_data (dict): Initial data to fill in the audit log record details. """ - super(AuditLog, self).__init__(cb, model_unique_id, initial_data, force_init=False, full_doc=True) + super(AuditLog, self).__init__(cb, -1, initial_data, force_init=False, full_doc=True) @classmethod def _query_implementation(cls, cb, **kwargs): @@ -99,7 +99,7 @@ def get_queued_auditlogs(cb): list[AuditLog]: List of objects representing the audit logs, or an empty list if none available. """ res = cb.get_object(AuditLog.urlobject.format(cb.credentials.org_key) + "/_queue") - return [AuditLog(cb, -1, data) for data in res.get("results", [])] + return [AuditLog(cb, data) for data in res.get("results", [])] """Query Class""" @@ -349,7 +349,7 @@ def _perform_query(self, from_row=0, max_rows=-1): results = result.get("results", []) for item in results: - yield self._doc_class(self._cb, 0, item) + yield self._doc_class(self._cb, item) current += 1 numrows += 1 @@ -377,4 +377,4 @@ def _run_async_query(self, context): resp = self._cb.post_object(url, body=request) result = resp.json() results = result.get("results", []) - return [self._doc_class(self._cb, 0, item) for item in results] + return [self._doc_class(self._cb, item) for item in results] From 4f87ffc5a0b7405ba312717171fcf1387f5e16b2 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Tue, 23 Apr 2024 09:41:29 -0600 Subject: [PATCH 84/95] and remove from docstring... --- src/cbc_sdk/platform/audit.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/cbc_sdk/platform/audit.py b/src/cbc_sdk/platform/audit.py index f8d23a96..bf1e7d3d 100644 --- a/src/cbc_sdk/platform/audit.py +++ b/src/cbc_sdk/platform/audit.py @@ -48,7 +48,6 @@ def __init__(self, cb, initial_data=None): Args: cb (BaseAPI): Reference to API object used to communicate with the server. - model_unique_id (int): Not used. initial_data (dict): Initial data to fill in the audit log record details. """ super(AuditLog, self).__init__(cb, -1, initial_data, force_init=False, full_doc=True) From df4d7b368dd31f290e422eea31c379b7d25e5624 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Tue, 23 Apr 2024 11:35:37 -0600 Subject: [PATCH 85/95] added an example section for add_time_criteria --- src/cbc_sdk/platform/audit.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/cbc_sdk/platform/audit.py b/src/cbc_sdk/platform/audit.py index bf1e7d3d..f00e0114 100644 --- a/src/cbc_sdk/platform/audit.py +++ b/src/cbc_sdk/platform/audit.py @@ -189,6 +189,11 @@ def add_time_criteria(self, **kwargs): """ Adds a ``create_time`` value to either criteria or exclusions. + Examples: + >>> query_specify_start_and_end = api.select(AuditLog). + ... add_time_criteria(start="2023-10-20T20:34:07Z", end="2023-10-30T20:34:07Z") + >>> query_specify_exclude_range = api.select(AuditLog).add_time_criteria(range='-3d', exclude=True) + Args: kwargs (dict): Keyword arguments to this method. From 22e2bf00a810e7f8883b6ec0088226f8b92d098b Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Tue, 23 Apr 2024 11:42:46 -0600 Subject: [PATCH 86/95] marking where export function will go --- src/cbc_sdk/platform/audit.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/cbc_sdk/platform/audit.py b/src/cbc_sdk/platform/audit.py index f00e0114..8d48688e 100644 --- a/src/cbc_sdk/platform/audit.py +++ b/src/cbc_sdk/platform/audit.py @@ -382,3 +382,6 @@ def _run_async_query(self, context): result = resp.json() results = result.get("results", []) return [self._doc_class(self._cb, item) for item in results] + + def export(self): + pass From 13f58844de56698308e36ea04b3d965fb3448d8a Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Tue, 23 Apr 2024 14:25:47 -0600 Subject: [PATCH 87/95] implementation of export() --- src/cbc_sdk/platform/audit.py | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/cbc_sdk/platform/audit.py b/src/cbc_sdk/platform/audit.py index 8d48688e..788eb758 100644 --- a/src/cbc_sdk/platform/audit.py +++ b/src/cbc_sdk/platform/audit.py @@ -26,9 +26,11 @@ CriteriaBuilderSupportMixin, ExclusionBuilderSupportMixin, IterableQueryMixin, AsyncQueryMixin) from cbc_sdk.errors import ApiError +from cbc_sdk.platform.jobs import Job from backports._datetime_fromisoformat import datetime_fromisoformat + """Model Class""" @@ -120,6 +122,7 @@ class AuditLogQuery(BaseQuery, QueryBuilderSupportMixin, CriteriaBuilderSupportM * ``request_url`` - URL of the request that caused the creation of this audit log. * ``description`` - Text description of this audit log. """ + VALID_EXPORT_FORMATS = ("csv", "json") def __init__(self, doc_class, cb): """ @@ -383,5 +386,26 @@ def _run_async_query(self, context): results = result.get("results", []) return [self._doc_class(self._cb, item) for item in results] - def export(self): - pass + def export(self, format="csv"): + """ + Export audit logs using the Job service. + + The actual results are retrieved by waiting for the resulting job to complete, then calling one of the methods + on ``Job`` to retrieve the results. + + Args: + format (str): Format in which to return results, either "csv" or "json". Default is "csv". + + Returns: + Job: The object representing the export job. + """ + if format not in AuditLogQuery.VALID_EXPORT_FORMATS: + raise ApiError(f"invalid export format '{format}'") + url = self._build_url("/_export") + request = self._build_request(0, -1) + request["format"] = format + resp = self._cb.post_object(url, body=request) + result = resp.json() + if "job_id" in result: + return Job(self._cb, result["job_id"]) + return None From 5fda3a45af9d88e2b3968b10dbdee4f909110418 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Tue, 23 Apr 2024 15:10:47 -0600 Subject: [PATCH 88/95] test code written, audit.py coverage at 97% --- src/cbc_sdk/platform/audit.py | 2 +- src/tests/unit/platform/test_audit.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/cbc_sdk/platform/audit.py b/src/cbc_sdk/platform/audit.py index 788eb758..9baf5fdf 100644 --- a/src/cbc_sdk/platform/audit.py +++ b/src/cbc_sdk/platform/audit.py @@ -408,4 +408,4 @@ def export(self, format="csv"): result = resp.json() if "job_id" in result: return Job(self._cb, result["job_id"]) - return None + return None # pragma: no cover diff --git a/src/tests/unit/platform/test_audit.py b/src/tests/unit/platform/test_audit.py index 41612e26..49dbdc25 100644 --- a/src/tests/unit/platform/test_audit.py +++ b/src/tests/unit/platform/test_audit.py @@ -19,6 +19,7 @@ from tests.unit.fixtures.platform.mock_audit import (AUDITLOGS_RESP, AUDIT_SEARCH_REQUEST, AUDIT_SEARCH_RESPONSE, AUDIT_EXPORT_REQUEST, MOCK_AUDIT_EXPORT_JOB) + @pytest.fixture(scope="function") def cb(): """Create CBCloudAPI singleton""" From 457c0c310f49df07cb5b591bb3d6235a2cb9f5a1 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Thu, 25 Apr 2024 10:34:09 -0600 Subject: [PATCH 89/95] added Example: section to AuditLogQuery.export() docstring and deflake8'd --- src/cbc_sdk/platform/audit.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/cbc_sdk/platform/audit.py b/src/cbc_sdk/platform/audit.py index 9baf5fdf..117c3fb5 100644 --- a/src/cbc_sdk/platform/audit.py +++ b/src/cbc_sdk/platform/audit.py @@ -393,6 +393,11 @@ def export(self, format="csv"): The actual results are retrieved by waiting for the resulting job to complete, then calling one of the methods on ``Job`` to retrieve the results. + Example: + >>> audit_log_query = cb.select(AuditLog).add_time_criteria(range="-1d") + >>> audit_log_export_job = audit_log_query.export(format="csv") + >>> results = audit_log_export_job.await_completion().result() + Args: format (str): Format in which to return results, either "csv" or "json". Default is "csv". From 1dc69ce8872b94cdfcee28117c62207eb1173232 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Thu, 25 Apr 2024 10:43:28 -0600 Subject: [PATCH 90/95] clear out and mark outdated information in Audit Log Guide --- docs/audit-log.rst | 38 ++++++++++++++++---------------------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/docs/audit-log.rst b/docs/audit-log.rst index e21b8dec..8b521780 100644 --- a/docs/audit-log.rst +++ b/docs/audit-log.rst @@ -8,14 +8,18 @@ In the Carbon Black Cloud, *audit logs* are records of various organization-wide * Creation of connectors * LiveResponse events -The Audit Log API allows these records to be retrieved in JSON format, sorted by time in ascending order -(oldest records come first). The API call returns only *new* audit log records that have been added since +The Audit Log API allows these records to be retrieved as objects, sorted by time in ascending order +(oldest records come first). + + +The API call returns only *new* audit log records that have been added since the last time the call was made using the same API Key ID. Once records have been returned, they are *cleared* and will not be included in future responses. When reading audit log records using a *new* API key, the queue for reading audit logs will begin three days earlier. This may lead to duplicate data if audit log records were previously read with a different API key. + .. note:: Future versions of the Carbon Black Cloud and this SDK will support a more flexible API for finding and retrieving audit log records. This Guide will be rewritten to cover this when it is incorporated into the SDK. @@ -23,30 +27,20 @@ earlier. This may lead to duplicate data if audit log records were previously re API Permissions --------------- -To call this API function, use a custom API key created with a role containing the ``READ`` permission on +To call the Audit Log APIs, use a custom API key created with a role containing the ``READ`` permission on ``org.audits``. -Example of API Usage --------------------- - -.. code-block:: python +Retrieving Queued Audit Log Events +---------------------------------- - import time - from cbc_sdk import CBCloudAPI - from cbc_sdk.platform import AuditLog +TK - cb = CBCloudAPI(profile='yourprofile') - running = True +Searching for Audit Log Events +------------------------------ - while running: - events_list = AuditLog.get_auditlogs(cb) - for event in events_list: - print(f"Event {event['eventId']}:") - for (k, v) in event.items(): - print(f"\t{k}: {v}") - # omitted: decide whether running should be set to False - if running: - time.sleep(5) +TK +Exporting Audit Log Events +-------------------------- -Check out the example script ``audit_log.py`` in the examples/platform directory on `GitHub `_. +TK From 17b08799ad49d33bfc9194e174f5380d25ecd274 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Thu, 25 Apr 2024 11:11:37 -0600 Subject: [PATCH 91/95] more slight changes to text --- docs/audit-log.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/audit-log.rst b/docs/audit-log.rst index 8b521780..e9281358 100644 --- a/docs/audit-log.rst +++ b/docs/audit-log.rst @@ -8,8 +8,8 @@ In the Carbon Black Cloud, *audit logs* are records of various organization-wide * Creation of connectors * LiveResponse events -The Audit Log API allows these records to be retrieved as objects, sorted by time in ascending order -(oldest records come first). +The Audit Log API allows these records to be retrieved as objects, either by getting the most recent audit logs, or +through a flexible search API. The API call returns only *new* audit log records that have been added since From 80464a3a6dfbbf917d40952e57703509b38673f9 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Fri, 26 Apr 2024 11:06:57 -0600 Subject: [PATCH 92/95] filled in details of audit log guide page --- docs/audit-log.rst | 51 +++++++++++++++++++++++++++++++--------------- 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/docs/audit-log.rst b/docs/audit-log.rst index e9281358..c0bfb4ab 100644 --- a/docs/audit-log.rst +++ b/docs/audit-log.rst @@ -11,19 +11,6 @@ In the Carbon Black Cloud, *audit logs* are records of various organization-wide The Audit Log API allows these records to be retrieved as objects, either by getting the most recent audit logs, or through a flexible search API. - -The API call returns only *new* audit log records that have been added since -the last time the call was made using the same API Key ID. Once records have been returned, they are *cleared* -and will not be included in future responses. - -When reading audit log records using a *new* API key, the queue for reading audit logs will begin three days -earlier. This may lead to duplicate data if audit log records were previously read with a different API key. - - -.. note:: - Future versions of the Carbon Black Cloud and this SDK will support a more flexible API for finding and retrieving - audit log records. This Guide will be rewritten to cover this when it is incorporated into the SDK. - API Permissions --------------- @@ -33,14 +20,46 @@ To call the Audit Log APIs, use a custom API key created with a role containing Retrieving Queued Audit Log Events ---------------------------------- -TK +The Carbon Black Cloud maintains a queue of audit log events for each API key, which is initialized with the last three +days of audit logs when the API key is created. This demonstrates how to read audit log events from the queue:: + + >>> from cbc_sdk import CBCloudAPI + >>> from cbc_sdk.platform import AuditLog + >>> api = CBCloudAPI(profile='sample') + >>> events = AuditLog.get_queued_auditlogs(api) + >>> for event in events: + ... print(f"{event.create_time}: {event.actor} {event.description}") + +Once audit log events have been retrieved from the queue, they are "cleared" and will not be included in future +responses to a ``get_queued_auditlogs()`` call. + +.. note:: + Reading queued audit log events using *different* API keys may lead to duplicate data. Searching for Audit Log Events ------------------------------ -TK +Audit log events may be searched for in a manner similar to other objects within the SDK:: + + # assume "api" contains our CBCloudAPI reference as above + >>> query = api.select(AuditLog).where("description:login") + >>> query.sort_by("create_time") + >>> for event in query: + ... print(f"{event.create_time}: {event.actor} {event.description}") + +See also the :ref:`searching-guide` guide page for a more detailed discussion of searching. Exporting Audit Log Events -------------------------- -TK +Any search query may also be used to export audit log data, in either CSV or JSON format:: + + # assume "api" contains our CBCloudAPI reference as above + >>> query = api.select(AuditLog).where("description:login") + >>> query.sort_by("create_time") + >>> job = query.export("csv") + >>> result = job.await_completion().result() + >>> print(result) + +Note that the ``export()`` call returns a ``Job`` object, as exports can take some time to complete. The results may +be obtained from the ``Job`` when the export process is completed. From e6cb77b3bc2ff5a501123eb0e62579bef8aaf67d Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Fri, 26 Apr 2024 15:41:44 -0600 Subject: [PATCH 93/95] change example query text --- docs/audit-log.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/audit-log.rst b/docs/audit-log.rst index c0bfb4ab..f17f8a9b 100644 --- a/docs/audit-log.rst +++ b/docs/audit-log.rst @@ -42,7 +42,7 @@ Searching for Audit Log Events Audit log events may be searched for in a manner similar to other objects within the SDK:: # assume "api" contains our CBCloudAPI reference as above - >>> query = api.select(AuditLog).where("description:login") + >>> query = api.select(AuditLog).where("description:Logged in") >>> query.sort_by("create_time") >>> for event in query: ... print(f"{event.create_time}: {event.actor} {event.description}") @@ -55,7 +55,7 @@ Exporting Audit Log Events Any search query may also be used to export audit log data, in either CSV or JSON format:: # assume "api" contains our CBCloudAPI reference as above - >>> query = api.select(AuditLog).where("description:login") + >>> query = api.select(AuditLog).where("description:Logged in") >>> query.sort_by("create_time") >>> job = query.export("csv") >>> result = job.await_completion().result() From aa7e976e6efc914a38c25009a5848903af57d678 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Mon, 29 Apr 2024 13:51:22 -0600 Subject: [PATCH 94/95] update to version numbers for release 1.5.2 of the SDK --- README.md | 4 ++-- VERSION | 2 +- docs/changelog.rst | 38 ++++++++++++++++++++++++++++++++++++++ docs/conf.py | 4 ++-- src/cbc_sdk/__init__.py | 2 +- 5 files changed, 44 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index b2c6c2d0..cd065651 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # VMware Carbon Black Cloud Python SDK -**Latest Version:** 1.5.1 +**Latest Version:** 1.5.2
-**Release Date:** January 30, 2024 +**Release Date:** TBD [![Coverage Status](https://coveralls.io/repos/github/carbonblack/carbon-black-cloud-sdk-python/badge.svg?t=Id6Baf)](https://coveralls.io/github/carbonblack/carbon-black-cloud-sdk-python) [![Codeship Status for carbonblack/carbon-black-cloud-sdk-python](https://app.codeship.com/projects/9e55a370-a772-0138-aae4-129773225755/status?branch=develop)](https://app.codeship.com/projects/402767) diff --git a/VERSION b/VERSION index 26ca5946..4cda8f19 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.5.1 +1.5.2 diff --git a/docs/changelog.rst b/docs/changelog.rst index ae9707f4..d712ec17 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,5 +1,43 @@ Changelog ================================ +CBC SDK 1.5.2 - Released TBD +----------------------------------------- + +New Features: + +* Enhanced Audit Log support with search and export capabilities +* CIS Benchmarking: + + * Schedule compliance scans + * Search, create, update, and delete benchmark sets + * Search and modify benchmark rules within a benchmark set + * Search and export device summaries for benchmark sets + * Enable, disable, and trigger reassessment on benchmark sets or individual devices + * Search benchmark set summaries + * Search and export device compliance summaries + * Search and export rule compliance summaries + * Search rule results for devices + * Get and acknowledge compliance bundle version updates, show differences, get rule info + +Updates: + +* Added `collapse_field` parameter for process searches +* Added an exponential backoff for polling of Job completion status +* Added rule configurations for event reporting and sensor operation exclusions + +Bug Fixes: + +* Fixed implementation of iterable queries for consistency across the SDK +* Fixed parsing of credential files that are encoded in UTF-16 +* Fixed processing of Job so that it doesn't rely on an API call that doesn't give proper answers +* Fixed missing properties in Process + +Documentation: + +* Fixed documentation for Alert and Process to include links to the Developer Network field descriptions +* New example script for identifying devices that have checked in but have not sent any events +* Added guide page for Devices including searching and actions + CBC SDK 1.5.1 - Released January 30, 2024 ----------------------------------------- diff --git a/docs/conf.py b/docs/conf.py index 1cc03df7..6c87a30e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -19,11 +19,11 @@ # -- Project information ----------------------------------------------------- project = 'Carbon Black Cloud Python SDK' -copyright = '2020-2023 VMware Carbon Black' +copyright = '2020-2024 VMware Carbon Black' author = 'Developer Relations' # The full version, including alpha/beta/rc tags -release = '1.5.1' +release = '1.5.2' # -- General configuration --------------------------------------------------- diff --git a/src/cbc_sdk/__init__.py b/src/cbc_sdk/__init__.py index af0fe893..3fd4917b 100644 --- a/src/cbc_sdk/__init__.py +++ b/src/cbc_sdk/__init__.py @@ -4,7 +4,7 @@ __author__ = 'Carbon Black Developer Network' __license__ = 'MIT' __copyright__ = 'Copyright 2020-2024 VMware Carbon Black' -__version__ = '1.5.1' +__version__ = '1.5.2' from .rest_api import CBCloudAPI from .cache import lru From 9ed143f1d5e5364864e0c96d6839232774ec08e0 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Wed, 1 May 2024 08:55:43 -0600 Subject: [PATCH 95/95] set release date for 1.5.2 --- README.md | 2 +- docs/changelog.rst | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index cd065651..f6fc920c 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ **Latest Version:** 1.5.2
-**Release Date:** TBD +**Release Date:** May 1, 2024 [![Coverage Status](https://coveralls.io/repos/github/carbonblack/carbon-black-cloud-sdk-python/badge.svg?t=Id6Baf)](https://coveralls.io/github/carbonblack/carbon-black-cloud-sdk-python) [![Codeship Status for carbonblack/carbon-black-cloud-sdk-python](https://app.codeship.com/projects/9e55a370-a772-0138-aae4-129773225755/status?branch=develop)](https://app.codeship.com/projects/402767) diff --git a/docs/changelog.rst b/docs/changelog.rst index d712ec17..2c05d4d9 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,7 +1,7 @@ Changelog ================================ -CBC SDK 1.5.2 - Released TBD ------------------------------------------ +CBC SDK 1.5.2 - Released May 1, 2024 +------------------------------------ New Features: