From 95279236cb9453ceaad6fdf818d3d3dbaefd6bae Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Tue, 18 Apr 2023 16:03:23 -0600 Subject: [PATCH 01/76] added script for demo which can list core prevention status or set it --- examples/platform/policy_core_prevention.py | 66 +++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 examples/platform/policy_core_prevention.py diff --git a/examples/platform/policy_core_prevention.py b/examples/platform/policy_core_prevention.py new file mode 100644 index 000000000..415bb0b35 --- /dev/null +++ b/examples/platform/policy_core_prevention.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python +# ******************************************************* +# Copyright (c) VMware, Inc. 2020-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. + +"""Example script which sends control messages to devices.""" + +import sys +from cbc_sdk.helpers import build_cli_parser, get_cb_cloud_object +from cbc_sdk.platform import Policy + + +def list_core_prevention_status(policy): + rule_configs = [config for config in policy.object_rule_configs_list if config.category == "core_prevention"] + for config in sorted(rule_configs, key=lambda c: c.name): + print(f"{config.name} = {config.get_assignment_mode()}") + + +def set_core_prevention_status(policy, config_name, mode): + if mode not in ('BLOCK', 'REPORT'): + raise RuntimeError(f"unknown assignment mode '{mode}'") + rule_configs = [config for config in policy.object_rule_configs_list + if config.category == "core_prevention" and config.name.startswith(config_name)] + if not rule_configs: + raise RuntimeError(f"core prevention rule config '{config_name}' not found") + elif len(rule_configs) > 1: + raise RuntimeError(f"ambiguous core prevention rule config name '{config_name}', " + f"possible values are {[c.name for c in rule_configs]}") + rule_configs[0].set_assignment_mode(mode) + rule_configs[0].save() + print(f"{rule_configs[0].name} = {mode}") + + +def main(): + """Main function for Device Actions script.""" + parser = build_cli_parser("View or set core prevention settings on a policy") + parser.add_argument("-p", "--policy", type=int, required=True, help="The ID of the policy to be manipulated") + subparsers = parser.add_subparsers(dest="command", required=True) + + subparsers.add_parser("list", help="List all core prevention statuses") + + set_command = subparsers.add_parser("set", help="Set a core prevention status") + set_command.add_argument("config", help="The core prevention configuration to change") + set_command.add_argument("mode", choices=["BLOCK", "REPORT"], help="The new assignment mode to set") + + args = parser.parse_args() + cb = get_cb_cloud_object(args) + policy = cb.select(Policy, args.policy) + + if args.command == "list": + list_core_prevention_status(policy) + elif args.command == "set": + set_core_prevention_status(policy, args.config, args.mode) + else: + raise NotImplementedError("Unknown command") + + +if __name__ == "__main__": + sys.exit(main()) From c70bd10c28dc0b29fbec9ff670423a5546dcce9e Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Wed, 19 Apr 2023 09:59:37 -0600 Subject: [PATCH 02/76] slight stiffening of script --- examples/platform/policy_core_prevention.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/platform/policy_core_prevention.py b/examples/platform/policy_core_prevention.py index 415bb0b35..16bf5ffdd 100644 --- a/examples/platform/policy_core_prevention.py +++ b/examples/platform/policy_core_prevention.py @@ -26,8 +26,9 @@ def list_core_prevention_status(policy): def set_core_prevention_status(policy, config_name, mode): if mode not in ('BLOCK', 'REPORT'): raise RuntimeError(f"unknown assignment mode '{mode}'") + match_name = config_name.casefold() rule_configs = [config for config in policy.object_rule_configs_list - if config.category == "core_prevention" and config.name.startswith(config_name)] + if config.category == "core_prevention" and config.name.casefold().startswith(match_name)] if not rule_configs: raise RuntimeError(f"core prevention rule config '{config_name}' not found") elif len(rule_configs) > 1: From e740ead49fda79d44904c0b54f0d8a350fab99dc Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Thu, 20 Apr 2023 08:57:29 -0600 Subject: [PATCH 03/76] added return 0 at end --- examples/platform/policy_core_prevention.py | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/platform/policy_core_prevention.py b/examples/platform/policy_core_prevention.py index 16bf5ffdd..7ea06ce78 100644 --- a/examples/platform/policy_core_prevention.py +++ b/examples/platform/policy_core_prevention.py @@ -61,6 +61,7 @@ def main(): set_core_prevention_status(policy, args.config, args.mode) else: raise NotImplementedError("Unknown command") + return 0 if __name__ == "__main__": From 905ab0062e35f58db441847572659ecd0f92a44d Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Thu, 20 Apr 2023 16:23:48 -0600 Subject: [PATCH 04/76] added (pedantic) docstrings --- examples/platform/policy_core_prevention.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/examples/platform/policy_core_prevention.py b/examples/platform/policy_core_prevention.py index 7ea06ce78..c99fd20b9 100644 --- a/examples/platform/policy_core_prevention.py +++ b/examples/platform/policy_core_prevention.py @@ -18,12 +18,29 @@ def list_core_prevention_status(policy): + """ + Lists all core prevention assignment modes. + + Args: + policy (Policy): Policy to perform the list operation on. + """ rule_configs = [config for config in policy.object_rule_configs_list if config.category == "core_prevention"] for config in sorted(rule_configs, key=lambda c: c.name): print(f"{config.name} = {config.get_assignment_mode()}") def set_core_prevention_status(policy, config_name, mode): + """ + Sets a core prevention assignment mode. + + Args: + policy (Policy): Policy to perform the set operation on. + config_name (str): Name of the core prevention rule config to change. Must match a prefix of exactly one name. + mode (str): New assignment mode. Valid values are "BLOCK" and "REPORT." + + Raises: + RuntimeError: If there was an error in one of the parameters. + """ if mode not in ('BLOCK', 'REPORT'): raise RuntimeError(f"unknown assignment mode '{mode}'") match_name = config_name.casefold() From d4afbeb85bc7d0983740d14d32fce1e6a0aaadc8 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Wed, 22 Mar 2023 08:01:24 -0600 Subject: [PATCH 05/76] put in placeholder skeleton --- src/cbc_sdk/platform/policy_ruleconfigs.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/cbc_sdk/platform/policy_ruleconfigs.py b/src/cbc_sdk/platform/policy_ruleconfigs.py index 965bea166..3e009fd1e 100644 --- a/src/cbc_sdk/platform/policy_ruleconfigs.py +++ b/src/cbc_sdk/platform/policy_ruleconfigs.py @@ -272,3 +272,7 @@ def set_assignment_mode(self, mode): if mode not in ("REPORT", "BLOCK"): raise ApiError(f"invalid assignment mode: {mode}") self.set_parameter("WindowsAssignmentMode", mode) + + +class HostBasedFirewallRuleConfig(PolicyRuleConfig): + pass From 54dad8209f1a765f4ad8166ee281b985d7c3c204 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Wed, 5 Apr 2023 16:45:14 -0600 Subject: [PATCH 06/76] more implementation of rules and rule groups within HBFW configuration --- .../platform/models/firewall_rule.yaml | 49 +++++ .../platform/models/firewall_rule_group.yaml | 12 ++ src/cbc_sdk/platform/policy_ruleconfigs.py | 187 +++++++++++++++++- 3 files changed, 247 insertions(+), 1 deletion(-) create mode 100644 src/cbc_sdk/platform/models/firewall_rule.yaml create mode 100644 src/cbc_sdk/platform/models/firewall_rule_group.yaml diff --git a/src/cbc_sdk/platform/models/firewall_rule.yaml b/src/cbc_sdk/platform/models/firewall_rule.yaml new file mode 100644 index 000000000..363896e6b --- /dev/null +++ b/src/cbc_sdk/platform/models/firewall_rule.yaml @@ -0,0 +1,49 @@ +type: object +required: + - action + - direction + - enabled + - protocol + - remote_ip_address +properties: + action: + type: string + description: The action to take when rule is hit + enum: + - ALLOW + - BLOCK + - BLOCK_ALERT + application_path: + type: string + description: The application path to limit the rule + direction: + type: string + description: The direction the network request is being made from + enum: + - IN + - OUT + - BOTH + enabled: + type: boolean + description: Whether the rule is enabled + protocol: + type: string + description: The type of network request + enum: + - TCP + - UDP + local_ip_address: + type: string + description: IPv4 address of the local side of the network connection (stored as dotted decimal) + local_port_ranges: + type: string + description: TCP or UDP port used by the local side of the network connection + remote_ip_address: + type: string + description: IPv4 address of the remote side of the network connection (stored as dotted decimal) + remote_port_ranges: + type: string + description: TCP or UDP port used by the remote side of the network connection + test_mode: + type: boolean + description: Enables host-based firewall hits without blocking network traffic or generating alerts diff --git a/src/cbc_sdk/platform/models/firewall_rule_group.yaml b/src/cbc_sdk/platform/models/firewall_rule_group.yaml new file mode 100644 index 000000000..90a964bc3 --- /dev/null +++ b/src/cbc_sdk/platform/models/firewall_rule_group.yaml @@ -0,0 +1,12 @@ +type: object +properties: + name: + type: string + description: Name of the rule group + description: + type: String + description: Description of the rule group + rules: + type: array + description: List of rules in the rule group + items: !include platform/models/firewall_rule.yaml diff --git a/src/cbc_sdk/platform/policy_ruleconfigs.py b/src/cbc_sdk/platform/policy_ruleconfigs.py index 3e009fd1e..100a91134 100644 --- a/src/cbc_sdk/platform/policy_ruleconfigs.py +++ b/src/cbc_sdk/platform/policy_ruleconfigs.py @@ -275,4 +275,189 @@ def set_assignment_mode(self, mode): class HostBasedFirewallRuleConfig(PolicyRuleConfig): - pass + """ + Represents a host-based firewall rule configuration in the policy. + """ + 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 CorePreventionRuleConfig 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(HostBasedFirewallRuleConfig, self).__init__(cb, parent, model_unique_id, initial_data, force_init, + full_doc) + self._rule_groups = [] + self._rule_groups_valid = False + + class FirewallRuleGroup(MutableBaseModel): + """Represents a group of related firewall rules.""" + swagger_meta_file = "platform/models/firewall_rule_group.yaml" + + def __init__(self, cb, initial_data): + """ + Initialize the FirewallRuleGroup object. + + Args: + cb (BaseAPI): Reference to API object used to communicate with the server. + initial_data (dict): Initial data used to populate the firewall rule group. + """ + super(HostBasedFirewallRuleConfig.FirewallRuleGroup, self).__init__(cb, None, initial_data, False, True) + self._rules = [HostBasedFirewallRuleConfig.FirewallRule(cb, d) for d in initial_data.get("rules", [])] + + def _flatten(self): + """ + Turns this rule group into a dict for transferral to the server. + + Returns: + dict: The information defining the rule group and its constituent rules. + """ + rc = copy.deepcopy(self._info) + rc['rules'] = [rule._flatten() for rule in self._rules] + return rc + + @property + def rules(self): + """ + Returns a list of the firewall rules within this rule group. + + Returns: + list(HostBasedFirewallRuleConfig.FirewallRule): List of contained firewall rules. + """ + return self._rules + + def append_rule(self, rule): + self._rules.append(rule) + + def remove(self): + if self in self.rule_groups: + self.rule_groups.remove(self) + + class FirewallRule(MutableBaseModel): + """Represents a single firewall rule.""" + swagger_meta_file = "platform/models/firewall_rule.yaml" + + def __init__(self, cb, initial_data): + """ + Initialize the FirewallRule object. + + Args: + cb (BaseAPI): Reference to API object used to communicate with the server. + initial_data (dict): Initial data used to populate the firewall rule. + """ + super(HostBasedFirewallRuleConfig.FirewallRule, self).__init__(cb, None, initial_data, False, True) + + def _flatten(self): + """ + Turns this rule into a dict for transferral to the server. + + Returns: + dict: The information defining the rule. + """ + return copy.deepcopy(self._info) + + def remove(self): + group_list = [group for group in self.rule_groups if self in group._rules] + if group_list: + group_list[0]._rules.remove(self) + + def _base_url(self): + """ + Calculates the base URL for these particular rule configs, including the org key and the parent policy ID. + + Returns: + str: The base URL for these particular rule configs. + + Raises: + InvalidObjectError: If the rule config object is unparented. + """ + return super(HostBasedFirewallRuleConfig, self)._base_url() + "/host_based_firewall" + + 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. + """ + return_data = self._cb.get_object(self._base_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._rule_groups = [] + self._rule_groups_valid = False + else: + raise InvalidObjectError(f"invalid host-based firewall ID: {self._model_unique_id}") + return True + + def _update_ruleconfig(self): + """Perform the internal update of the rule configuration object.""" + ... + + def _delete_ruleconfig(self): + """Perform the internal delete of the rule configuration object.""" + ... + + @property + def enabled(self): + return self.get_parameter('enable_host_based_firewall') + + def set_enabled(self, flag): + self.set_parameter('enable_host_based_firewall', flag) + + @property + def default_action(self): + default_rule = self.get_parameter('default_rule') + return default_rule.get("action", "ALLOW") + + def set_default_action(self, action): + if action not in ("ALLOW", "BLOCK"): + raise ApiError(f"invalid default action: {action}") + default_rule = self.get_parameter('default_rule') + default_rule['action'] = action + self.set_parameter("default_rule", default_rule) + + @property + def rule_groups(self): + if not self._rule_groups_valid: + rg_param = self.get_parameter("rule_groups") + if rg_param is not None: + self._rule_groups = [HostBasedFirewallRuleConfig.FirewallRuleGroup(self._cb, d) for d in rg_param] + else: + self._rule_groups = [] + self._rule_groups_valid = True + return self._rule_groups + + def new_rule_group(self, name, description): + return HostBasedFirewallRuleConfig.FirewallRuleGroup(self._cb, {"name": name, "description": description, + "rules": []}) + + def append_rule_group(self, rule_group): + self.rule_groups.append(rule_group) + + def new_rule(self, action, direction, protocol, remote_ip): + if action not in ("ALLOW", "BLOCK", "BLOCK_ALERT"): + raise ApiError(f"invalid rule action: {action}") + if direction not in ("IN", "OUT", "BOTH"): + raise ApiError(f"invalid rule direction: {direction}") + if protocol not in ("TCP", "UDP"): + raise ApiError(f"invalid rule protocol: {protocol}") + return HostBasedFirewallRuleConfig.FirewallRule(self._cb, {"action": action, "application_path": "*", + "direction": direction, "enabled": True, + "protocol": protocol, "local_ip_address": "*", + "local_port_ranges": "*", + "remote_ip_address": remote_ip, + "remote_port_ranges": "*", "test_mode": False}) From b5efbd556175a95b246619b02276ba83f403a130 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Fri, 14 Apr 2023 16:54:15 -0600 Subject: [PATCH 07/76] finished all projected changes to the base SDK code --- src/cbc_sdk/base.py | 5 + src/cbc_sdk/platform/policies.py | 5 +- src/cbc_sdk/platform/policy_ruleconfigs.py | 118 ++++++++++++++++++++- 3 files changed, 123 insertions(+), 5 deletions(-) diff --git a/src/cbc_sdk/base.py b/src/cbc_sdk/base.py index b5a386403..9f74b9037 100644 --- a/src/cbc_sdk/base.py +++ b/src/cbc_sdk/base.py @@ -823,6 +823,10 @@ def __setattr__(self, attrname, val): else: object.__setattr__(self, attrname, val) + def _field_updated(self, attrname): + """Method called whenever a field is updated.""" + pass + def _set(self, attrname, new_value): # ensure that we are operating on the full object first if not self._full_init and self._model_unique_id is not None: @@ -844,6 +848,7 @@ def _set(self, attrname, new_value): self._dirty_attributes[attrname] = self._info.get(attrname, None) # finally, make the change self._info[attrname] = new_value + self._field_updated(attrname) def refresh(self): """Reload this object from the server.""" diff --git a/src/cbc_sdk/platform/policies.py b/src/cbc_sdk/platform/policies.py index 5035ed131..c0350c8e2 100644 --- a/src/cbc_sdk/platform/policies.py +++ b/src/cbc_sdk/platform/policies.py @@ -16,12 +16,13 @@ import json from types import MappingProxyType from cbc_sdk.base import MutableBaseModel, BaseQuery, IterableQueryMixin, AsyncQueryMixin -from cbc_sdk.platform.policy_ruleconfigs import PolicyRuleConfig, CorePreventionRuleConfig +from cbc_sdk.platform.policy_ruleconfigs import PolicyRuleConfig, CorePreventionRuleConfig, HostBasedFirewallRuleConfig from cbc_sdk.errors import ApiError, ServerError, InvalidObjectError SPECIFIC_RULECONFIGS = MappingProxyType({ - "core_prevention": CorePreventionRuleConfig + "core_prevention": CorePreventionRuleConfig, + "host_based_firewall": HostBasedFirewallRuleConfig }) diff --git a/src/cbc_sdk/platform/policy_ruleconfigs.py b/src/cbc_sdk/platform/policy_ruleconfigs.py index 100a91134..a4f5e4e95 100644 --- a/src/cbc_sdk/platform/policy_ruleconfigs.py +++ b/src/cbc_sdk/platform/policy_ruleconfigs.py @@ -51,6 +51,7 @@ def __init__(self, cb, parent, model_unique_id=None, initial_data=None, force_in super(PolicyRuleConfig, self).__init__(cb, model_unique_id=model_unique_id, initial_data=initial_data, force_init=force_init, full_doc=full_doc) self._parent = parent + self._params_changed = False if model_unique_id is None: self.touch(True) @@ -79,13 +80,15 @@ def _refresh(self): Returns: bool: True if the refresh was successful. """ + rc = False if self._model_unique_id is not None: rc = self._parent._refresh() if rc: newobj = self._parent.object_rule_configs.get(self.id, None) if newobj: self._info = newobj._info - return rc + self._params_changed = False + return rc def _update_ruleconfig(self): """Perform the internal update of the rule configuration object.""" @@ -100,6 +103,7 @@ def _update_object(self): """ self._update_ruleconfig() self._full_init = True + self._params_changed = False self._parent._on_updated_rule_config(self) def _delete_ruleconfig(self): @@ -116,6 +120,15 @@ def _delete_object(self): self._delete_ruleconfig() self._parent._on_deleted_rule_config(self) + def is_dirty(self): + """ + Returns whether or not any fields of this object have been changed. + + Returns: + bool: True if any fields of this object have been changed, False if not. + """ + return self._params_changed or super(PolicyRuleConfig, self).is_dirty() + def get_parameter(self, name): """ Returns a parameter value from the rule configuration. @@ -239,6 +252,7 @@ def _refresh(self): 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._params_changed = False else: raise InvalidObjectError(f"invalid core prevention ID: {self._model_unique_id}") return True @@ -312,6 +326,10 @@ def __init__(self, cb, initial_data): super(HostBasedFirewallRuleConfig.FirewallRuleGroup, self).__init__(cb, None, initial_data, False, True) self._rules = [HostBasedFirewallRuleConfig.FirewallRule(cb, d) for d in initial_data.get("rules", [])] + def _field_updated(self, attrname): + """Method called whenever a field is updated.""" + self._params_changed = True + def _flatten(self): """ Turns this rule group into a dict for transferral to the server. @@ -334,11 +352,20 @@ def rules(self): return self._rules def append_rule(self, rule): + """ + Appends a new rule to this rule group. + + Args: + rule (HostBasedFirewallRuleConfig.FirewallRule): The new rule. + """ self._rules.append(rule) + self._params_changed = True def remove(self): + """Removes this rule group from the rule configuration.""" if self in self.rule_groups: self.rule_groups.remove(self) + self._params_changed = True class FirewallRule(MutableBaseModel): """Represents a single firewall rule.""" @@ -354,6 +381,10 @@ def __init__(self, cb, initial_data): """ super(HostBasedFirewallRuleConfig.FirewallRule, self).__init__(cb, None, initial_data, False, True) + def _field_updated(self, attrname): + """Method called whenever a field is updated.""" + self._params_changed = True + def _flatten(self): """ Turns this rule into a dict for transferral to the server. @@ -364,9 +395,11 @@ def _flatten(self): return copy.deepcopy(self._info) def remove(self): + """Removes this rule from the rule group that contains it.""" group_list = [group for group in self.rule_groups if self in group._rules] if group_list: group_list[0]._rules.remove(self) + self._params_changed = True def _base_url(self): """ @@ -399,31 +432,75 @@ def _refresh(self): self._info = ruleconfig_data[0] self._rule_groups = [] self._rule_groups_valid = False + self._params_changed = False else: raise InvalidObjectError(f"invalid host-based firewall ID: {self._model_unique_id}") return True def _update_ruleconfig(self): """Perform the internal update of the rule configuration object.""" - ... + put_data = {"id": self.id, "parameters": {"enable_host_based_firewall": self.enabled, + "default_rule": self.get_parameter('default_rule')}} + if self._rule_groups_valid: + put_data['parameters']['rule_groups'] = [group._flatten() for group in self._rule_groups] + else: + put_data['parameters']['rule_groups'] = self.get_parameter('rule_groups') + resp = self._cb.put_object(self._base_url(), [put_data]) + success = [d for d in resp.get("successful", []) if d.get("id", None) == self.id] + if not success: + raise ApiError("update of host-based firewall failed") + self._info = success[0] + self._rule_groups = [] + self._rule_groups_valid = False + self._params_changed = False def _delete_ruleconfig(self): """Perform the internal delete of the rule configuration object.""" - ... + my_id = self.id + self._cb.delete_object(self._base_url() + f"/{my_id}") + self._info = {"id": my_id} + self._full_init = False # forcing _refresh() next time we read an attribute + self._rule_groups = [] + self._rule_groups_valid = False + self._params_changed = False @property def enabled(self): + """ + Returns whether or not the host-based firewall is enabled. + + Returns: + bool: True if the host-based firewall is enabled, False if not. + """ return self.get_parameter('enable_host_based_firewall') def set_enabled(self, flag): + """ + Sets whether or not the host-based firewall is enabled. + + Args: + flag (bool): True if the host-based firewall should be enabled, False if not. + """ self.set_parameter('enable_host_based_firewall', flag) @property def default_action(self): + """ + Returns the default action of this rule configuration. + + Returns: + str: The default action of this rule configuration, either "ALLOW" or "BLOCK." + """ default_rule = self.get_parameter('default_rule') return default_rule.get("action", "ALLOW") def set_default_action(self, action): + """ + Sets the default action of this rule configuration. + + Args: + action (str): The new default action of this rule configuration. Valid values are "ALLOW" and "BLOCK." + """ if action not in ("ALLOW", "BLOCK"): raise ApiError(f"invalid default action: {action}") default_rule = self.get_parameter('default_rule') @@ -432,6 +509,12 @@ def set_default_action(self, action): @property def rule_groups(self): + """ + Returns the list of rule groups in this rule configuration. + + Returns: + list[FirewallRuleGroup]: The list of rule groups. + """ if not self._rule_groups_valid: rg_param = self.get_parameter("rule_groups") if rg_param is not None: @@ -442,13 +525,42 @@ def rule_groups(self): return self._rule_groups def new_rule_group(self, name, description): + """ + Creates a new FirewallRuleGroup object. + + Args: + name (str): The name of the new rule group. + description (str): The description of the new rule group. + + Returns: + FirewallRuleGroup: The new rule group object. Add it to this rule configuration with append_rule_group. + """ return HostBasedFirewallRuleConfig.FirewallRuleGroup(self._cb, {"name": name, "description": description, "rules": []}) def append_rule_group(self, rule_group): + """ + Appends a rule group to the list of rule groups in the rule configuration. + + Args: + rule_group (FirewallRuleGroup): The rule group to be added. + """ self.rule_groups.append(rule_group) + self._params_changed = True def new_rule(self, action, direction, protocol, remote_ip): + """ + Creates a new FirewallRule object. + + Args: + action (str): The action to be taken by this rule. Valid values are "ALLOW," "BLOCK," and "BLOCK_ALERT." + direction (str): The traffic direction this rule matches. Valid values are "IN," "OUT," and "BOTH." + protocol (str): The network protocol this rule matches. Valid values are "TCP" and "UDP." + remote_ip (str): The remote IP address this rule matches. + + Returns: + FirewallRule: The new firewall rule. Append it to a rule group using the group's append_rule method. + """ if action not in ("ALLOW", "BLOCK", "BLOCK_ALERT"): raise ApiError(f"invalid rule action: {action}") if direction not in ("IN", "OUT", "BOTH"): From d357e2139d6043ccfa30bc2ed5b13f4629996c38 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Wed, 26 Apr 2023 15:11:20 -0600 Subject: [PATCH 08/76] test coverage now 92% --- src/cbc_sdk/platform/policies.py | 18 + src/cbc_sdk/platform/policy_ruleconfigs.py | 70 ++- .../unit/fixtures/platform/mock_policies.py | 129 ++-- .../platform/mock_policy_ruleconfigs.py | 570 ++++++++++++++++++ .../unit/platform/test_policy_ruleconfigs.py | 213 ++++++- 5 files changed, 919 insertions(+), 81 deletions(-) create mode 100644 src/tests/unit/fixtures/platform/mock_policy_ruleconfigs.py diff --git a/src/cbc_sdk/platform/policies.py b/src/cbc_sdk/platform/policies.py index c0350c8e2..f870c4a7a 100644 --- a/src/cbc_sdk/platform/policies.py +++ b/src/cbc_sdk/platform/policies.py @@ -696,6 +696,24 @@ def core_prevention_rule_configs_list(self): """ return [rconf for rconf in self.object_rule_configs.values() if isinstance(rconf, CorePreventionRuleConfig)] + @property + def host_based_firewall_rule_config(self): + """ + Returns the host-based firewall rule configuration for this policy. + + Returns: + HostBasedFirewallRuleConfig: The host-based firewall rule configuration, or None. + + Raises: + InvalidObjectError: If there's more than one host-based firewall rule configuration (should not happen). + """ + tmp = [rconf for rconf in self.object_rule_configs.values() if isinstance(rconf, HostBasedFirewallRuleConfig)] + if not tmp: + return None + if len(tmp) > 1: + raise InvalidObjectError("found multiple host-based firewall rule configurations") + return tmp[0] + def valid_rule_configs(self): """ Returns a dictionary identifying all valid rule configurations for this policy. diff --git a/src/cbc_sdk/platform/policy_ruleconfigs.py b/src/cbc_sdk/platform/policy_ruleconfigs.py index a4f5e4e95..c0e0bf1e0 100644 --- a/src/cbc_sdk/platform/policy_ruleconfigs.py +++ b/src/cbc_sdk/platform/policy_ruleconfigs.py @@ -120,6 +120,15 @@ def _delete_object(self): self._delete_ruleconfig() self._parent._on_deleted_rule_config(self) + def _mark_changed(self, flag=True): + """ + Marks this object as changed. + + Args: + flag (bool): Changed flag, default is True. + """ + self._params_changed = flag + def is_dirty(self): """ Returns whether or not any fields of this object have been changed. @@ -139,6 +148,8 @@ def get_parameter(self, name): Returns: Any: The parameter value, or None if there is no value. """ + if 'parameters' not in self._info: + self.refresh() params = self._info['parameters'] return params.get(name, None) @@ -150,6 +161,8 @@ def set_parameter(self, name, value): name (str): The parameter name. value (Any): The new value to be set. """ + if 'parameters' not in self._info: + self.refresh() params = self._info['parameters'] old_value = params.get(name, None) if old_value != value: @@ -252,7 +265,7 @@ def _refresh(self): 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._params_changed = False + self._mark_changed(False) else: raise InvalidObjectError(f"invalid core prevention ID: {self._model_unique_id}") return True @@ -315,7 +328,7 @@ class FirewallRuleGroup(MutableBaseModel): """Represents a group of related firewall rules.""" swagger_meta_file = "platform/models/firewall_rule_group.yaml" - def __init__(self, cb, initial_data): + def __init__(self, cb, parent, initial_data): """ Initialize the FirewallRuleGroup object. @@ -324,11 +337,13 @@ def __init__(self, cb, initial_data): initial_data (dict): Initial data used to populate the firewall rule group. """ super(HostBasedFirewallRuleConfig.FirewallRuleGroup, self).__init__(cb, None, initial_data, False, True) - self._rules = [HostBasedFirewallRuleConfig.FirewallRule(cb, d) for d in initial_data.get("rules", [])] + self._parent = parent + self._rules = [HostBasedFirewallRuleConfig.FirewallRule(cb, parent, d) + for d in initial_data.get("rules", [])] def _field_updated(self, attrname): """Method called whenever a field is updated.""" - self._params_changed = True + self._parent._mark_changed() def _flatten(self): """ @@ -342,7 +357,7 @@ def _flatten(self): return rc @property - def rules(self): + def rules_(self): """ Returns a list of the firewall rules within this rule group. @@ -359,19 +374,19 @@ def append_rule(self, rule): rule (HostBasedFirewallRuleConfig.FirewallRule): The new rule. """ self._rules.append(rule) - self._params_changed = True + self._parent._mark_changed() def remove(self): """Removes this rule group from the rule configuration.""" if self in self.rule_groups: self.rule_groups.remove(self) - self._params_changed = True + self._parent._mark_changed() class FirewallRule(MutableBaseModel): """Represents a single firewall rule.""" swagger_meta_file = "platform/models/firewall_rule.yaml" - def __init__(self, cb, initial_data): + def __init__(self, cb, parent, initial_data): """ Initialize the FirewallRule object. @@ -380,10 +395,11 @@ def __init__(self, cb, initial_data): initial_data (dict): Initial data used to populate the firewall rule. """ super(HostBasedFirewallRuleConfig.FirewallRule, self).__init__(cb, None, initial_data, False, True) + self._parent = parent def _field_updated(self, attrname): """Method called whenever a field is updated.""" - self._params_changed = True + self._parent._mark_changed() def _flatten(self): """ @@ -399,7 +415,7 @@ def remove(self): group_list = [group for group in self.rule_groups if self in group._rules] if group_list: group_list[0]._rules.remove(self) - self._params_changed = True + self._parent._mark_changed() def _base_url(self): """ @@ -432,7 +448,7 @@ def _refresh(self): self._info = ruleconfig_data[0] self._rule_groups = [] self._rule_groups_valid = False - self._params_changed = False + self._mark_changed(False) else: raise InvalidObjectError(f"invalid host-based firewall ID: {self._model_unique_id}") return True @@ -446,13 +462,14 @@ def _update_ruleconfig(self): else: put_data['parameters']['rule_groups'] = self.get_parameter('rule_groups') resp = self._cb.put_object(self._base_url(), [put_data]) - success = [d for d in resp.get("successful", []) if d.get("id", None) == self.id] + result = resp.json() + success = [d for d in result.get("successful", []) if d.get("id", None) == self.id] if not success: raise ApiError("update of host-based firewall failed") self._info = success[0] self._rule_groups = [] self._rule_groups_valid = False - self._params_changed = False + self._mark_changed(False) def _delete_ruleconfig(self): """Perform the internal delete of the rule configuration object.""" @@ -462,7 +479,7 @@ def _delete_ruleconfig(self): self._full_init = False # forcing _refresh() next time we read an attribute self._rule_groups = [] self._rule_groups_valid = False - self._params_changed = False + self._mark_changed(False) @property def enabled(self): @@ -518,7 +535,7 @@ def rule_groups(self): if not self._rule_groups_valid: rg_param = self.get_parameter("rule_groups") if rg_param is not None: - self._rule_groups = [HostBasedFirewallRuleConfig.FirewallRuleGroup(self._cb, d) for d in rg_param] + self._rule_groups = [HostBasedFirewallRuleConfig.FirewallRuleGroup(self._cb, self, d) for d in rg_param] else: self._rule_groups = [] self._rule_groups_valid = True @@ -535,8 +552,8 @@ def new_rule_group(self, name, description): Returns: FirewallRuleGroup: The new rule group object. Add it to this rule configuration with append_rule_group. """ - return HostBasedFirewallRuleConfig.FirewallRuleGroup(self._cb, {"name": name, "description": description, - "rules": []}) + return HostBasedFirewallRuleConfig.FirewallRuleGroup(self._cb, self, {"name": name, "description": description, + "rules": []}) def append_rule_group(self, rule_group): """ @@ -546,13 +563,14 @@ def append_rule_group(self, rule_group): rule_group (FirewallRuleGroup): The rule group to be added. """ self.rule_groups.append(rule_group) - self._params_changed = True + self._mark_changed() - def new_rule(self, action, direction, protocol, remote_ip): + def new_rule(self, name, action, direction, protocol, remote_ip): """ Creates a new FirewallRule object. Args: + name (str): The name for the new rule. action (str): The action to be taken by this rule. Valid values are "ALLOW," "BLOCK," and "BLOCK_ALERT." direction (str): The traffic direction this rule matches. Valid values are "IN," "OUT," and "BOTH." protocol (str): The network protocol this rule matches. Valid values are "TCP" and "UDP." @@ -567,9 +585,11 @@ def new_rule(self, action, direction, protocol, remote_ip): raise ApiError(f"invalid rule direction: {direction}") if protocol not in ("TCP", "UDP"): raise ApiError(f"invalid rule protocol: {protocol}") - return HostBasedFirewallRuleConfig.FirewallRule(self._cb, {"action": action, "application_path": "*", - "direction": direction, "enabled": True, - "protocol": protocol, "local_ip_address": "*", - "local_port_ranges": "*", - "remote_ip_address": remote_ip, - "remote_port_ranges": "*", "test_mode": False}) + return HostBasedFirewallRuleConfig.FirewallRule(self._cb, self, {"action": action, "application_path": "*", + "direction": direction, "enabled": True, + "name": name, + "protocol": protocol, "local_ip_address": "*", + "local_port_ranges": "*", + "remote_ip_address": remote_ip, + "remote_port_ranges": "*", + "test_mode": False}) diff --git a/src/tests/unit/fixtures/platform/mock_policies.py b/src/tests/unit/fixtures/platform/mock_policies.py index 7f88f6346..de6d2fcda 100644 --- a/src/tests/unit/fixtures/platform/mock_policies.py +++ b/src/tests/unit/fixtures/platform/mock_policies.py @@ -230,6 +230,72 @@ "parameters": { "WindowsAssignmentMode": "BLOCK" } + }, + { + "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. [...].", + "inherited_from": "", + "category": "host_based_firewall", + "parameters": { + "rulesets": [ + { + "description": "Whatever", + "name": "Argon_firewall", + "rules": [ + { + "action": "ALLOW", + "application_path": "*", + "direction": "IN", + "enabled": True, + "local_ip_address": "1.2.3.4", + "local_port_ranges": "1234", + "name": "my_first_rule", + "protocol": "TCP", + "remote_ip_address": "5.6.7.8", + "remote_port_ranges": "5678", + "rule_access_check_guid": "6d36954a-a944-4944-ae94-df6f94b877b8", + "rule_inbound_event_check_guid": "8a39c00b-f907-4085-929f-f2e98e8b7b87", + "rule_outbound_event_check_guid": "7e7a9761-4187-4065-8ae1-b5161fae75a2", + "test_mode": False + } + ], + "ruleset_id": "0c0ce332-6f81-43d9-ad9b-875e82eb53f9" + } + ], + "rule_groups": [ + { + "description": "Whatever", + "name": "Argon_firewall", + "rules": [ + { + "action": "ALLOW", + "application_path": "*", + "direction": "IN", + "enabled": True, + "local_ip_address": "1.2.3.4", + "local_port_ranges": "1234", + "name": "my_first_rule", + "protocol": "TCP", + "remote_ip_address": "5.6.7.8", + "remote_port_ranges": "5678", + "rule_access_check_guid": "6d36954a-a944-4944-ae94-df6f94b877b8", + "rule_inbound_event_check_guid": "8a39c00b-f907-4085-929f-f2e98e8b7b87", + "rule_outbound_event_check_guid": "7e7a9761-4187-4065-8ae1-b5161fae75a2", + "test_mode": False + } + ], + "ruleset_id": "0c0ce332-6f81-43d9-ad9b-875e82eb53f9" + } + ], + "default_rule": { + "action": "ALLOW", + "default_rule_access_check_guid": "08dc129b-ab72-4ed7-8282-8db7f62bc7e8", + "default_rule_inbound_event_check_guid": "40dd836c-e676-4e3b-b98b-c870c4b6faa7", + "default_rule_outbound_event_check_guid": "94283d79-c2d1-472c-b303-77a0fb387bcc" + }, + "enable_host_based_firewall": False + } } ] } @@ -1729,6 +1795,15 @@ } ] }, + { + "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. [...].", + "presentation": { + "category": "hbfw" + }, + "parameters": [] + }, { "id": "c4ed61b3-d5aa-41a9-814f-0f277451532b", "name": "Carbon Black Threat Intel", @@ -1840,57 +1915,3 @@ "WindowsAssignmentMode": "BLOCK" } } - -CORE_PREVENTION_RETURNS = { - "results": [ - { - "id": "1f8a5e4b-34f2-4d31-9f8f-87c56facaec8", - "name": "Advanced Scripting Prevention", - "description": "Addresses malicious fileless and file-backed scripts that leverage native programs [...]", - "inherited_from": "psc:region", - "category": "core_prevention", - "parameters": { - "WindowsAssignmentMode": "BLOCK" - } - }, - { - "id": "ac67fa14-f6be-4df9-93f2-6de0dbd96061", - "name": "Credential Theft", - "description": "Addresses threat actors obtaining credentials and relies on detecting the malicious [...]", - "inherited_from": "psc:region", - "category": "core_prevention", - "parameters": { - "WindowsAssignmentMode": "REPORT" - } - }, - { - "id": "c4ed61b3-d5aa-41a9-814f-0f277451532b", - "name": "Carbon Black Threat Intel", - "description": "Addresses common and pervasive TTPs used for malicious activity as well as [...]", - "inherited_from": "psc:region", - "category": "core_prevention", - "parameters": { - "WindowsAssignmentMode": "REPORT" - } - }, - { - "id": "88b19232-7ebb-48ef-a198-2a75a282de5d", - "name": "Privilege Escalation", - "description": "Addresses behaviors that indicate a threat actor has gained elevated access via [...]", - "inherited_from": "psc:region", - "category": "core_prevention", - "parameters": { - "WindowsAssignmentMode": "BLOCK" - } - } - ] -} - -CORE_PREVENTION_UPDATE_1 = [ - { - "id": "c4ed61b3-d5aa-41a9-814f-0f277451532b", - "parameters": { - "WindowsAssignmentMode": "BLOCK" - } - } -] diff --git a/src/tests/unit/fixtures/platform/mock_policy_ruleconfigs.py b/src/tests/unit/fixtures/platform/mock_policy_ruleconfigs.py new file mode 100644 index 000000000..49fe161cd --- /dev/null +++ b/src/tests/unit/fixtures/platform/mock_policy_ruleconfigs.py @@ -0,0 +1,570 @@ +CORE_PREVENTION_RETURNS = { + "results": [ + { + "id": "1f8a5e4b-34f2-4d31-9f8f-87c56facaec8", + "name": "Advanced Scripting Prevention", + "description": "Addresses malicious fileless and file-backed scripts that leverage native programs [...]", + "inherited_from": "psc:region", + "category": "core_prevention", + "parameters": { + "WindowsAssignmentMode": "BLOCK" + } + }, + { + "id": "ac67fa14-f6be-4df9-93f2-6de0dbd96061", + "name": "Credential Theft", + "description": "Addresses threat actors obtaining credentials and relies on detecting the malicious [...]", + "inherited_from": "psc:region", + "category": "core_prevention", + "parameters": { + "WindowsAssignmentMode": "REPORT" + } + }, + { + "id": "c4ed61b3-d5aa-41a9-814f-0f277451532b", + "name": "Carbon Black Threat Intel", + "description": "Addresses common and pervasive TTPs used for malicious activity as well as [...]", + "inherited_from": "psc:region", + "category": "core_prevention", + "parameters": { + "WindowsAssignmentMode": "REPORT" + } + }, + { + "id": "88b19232-7ebb-48ef-a198-2a75a282de5d", + "name": "Privilege Escalation", + "description": "Addresses behaviors that indicate a threat actor has gained elevated access via [...]", + "inherited_from": "psc:region", + "category": "core_prevention", + "parameters": { + "WindowsAssignmentMode": "BLOCK" + } + } + ] +} + +CORE_PREVENTION_UPDATE_1 = [ + { + "id": "c4ed61b3-d5aa-41a9-814f-0f277451532b", + "parameters": { + "WindowsAssignmentMode": "BLOCK" + } + } +] + +HBFW_GET_RESULT = { + "results": [ + { + "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. [...].", + "inherited_from": "", + "category": "host_based_firewall", + "parameters": { + "rulesets": [ + { + "description": "Whatever", + "name": "Argon_firewall", + "rules": [ + { + "action": "ALLOW", + "application_path": "*", + "direction": "IN", + "enabled": True, + "local_ip_address": "1.2.3.4", + "local_port_ranges": "1234", + "name": "my_first_rule", + "protocol": "TCP", + "remote_ip_address": "5.6.7.8", + "remote_port_ranges": "5678", + "rule_access_check_guid": "6d36954a-a944-4944-ae94-df6f94b877b8", + "rule_inbound_event_check_guid": "8a39c00b-f907-4085-929f-f2e98e8b7b87", + "rule_outbound_event_check_guid": "7e7a9761-4187-4065-8ae1-b5161fae75a2", + "test_mode": False + } + ], + "ruleset_id": "0c0ce332-6f81-43d9-ad9b-875e82eb53f9" + } + ], + "rule_groups": [ + { + "description": "Whatever", + "name": "Argon_firewall", + "rules": [ + { + "action": "ALLOW", + "application_path": "*", + "direction": "IN", + "enabled": True, + "local_ip_address": "1.2.3.4", + "local_port_ranges": "1234", + "name": "my_first_rule", + "protocol": "TCP", + "remote_ip_address": "5.6.7.8", + "remote_port_ranges": "5678", + "rule_access_check_guid": "6d36954a-a944-4944-ae94-df6f94b877b8", + "rule_inbound_event_check_guid": "8a39c00b-f907-4085-929f-f2e98e8b7b87", + "rule_outbound_event_check_guid": "7e7a9761-4187-4065-8ae1-b5161fae75a2", + "test_mode": False + } + ], + "ruleset_id": "0c0ce332-6f81-43d9-ad9b-875e82eb53f9" + } + ], + "default_rule": { + "action": "ALLOW", + "default_rule_access_check_guid": "08dc129b-ab72-4ed7-8282-8db7f62bc7e8", + "default_rule_inbound_event_check_guid": "40dd836c-e676-4e3b-b98b-c870c4b6faa7", + "default_rule_outbound_event_check_guid": "94283d79-c2d1-472c-b303-77a0fb387bcc" + }, + "enable_host_based_firewall": False + } + } + ] +} + +HBFW_MODIFY_PUT_REQUEST = [ + { + "id": "df181779-f623-415d-879e-91c40246535d", + "parameters": { + "rule_groups": [ + { + "description": "Starship go BOOM", + "name": "Argon_firewall", + "rules": [ + { + "action": "ALLOW", + "application_path": "*", + "direction": "BOTH", + "enabled": True, + "local_ip_address": "1.2.3.4", + "local_port_ranges": "1234", + "name": "my_first_rule", + "protocol": "TCP", + "remote_ip_address": "199.201.128.1", + "remote_port_ranges": "5678", + "rule_access_check_guid": "6d36954a-a944-4944-ae94-df6f94b877b8", + "rule_inbound_event_check_guid": "8a39c00b-f907-4085-929f-f2e98e8b7b87", + "rule_outbound_event_check_guid": "7e7a9761-4187-4065-8ae1-b5161fae75a2", + "test_mode": False + } + ], + "ruleset_id": "0c0ce332-6f81-43d9-ad9b-875e82eb53f9" + } + ], + "default_rule": { + "action": "BLOCK", + "default_rule_access_check_guid": "08dc129b-ab72-4ed7-8282-8db7f62bc7e8", + "default_rule_inbound_event_check_guid": "40dd836c-e676-4e3b-b98b-c870c4b6faa7", + "default_rule_outbound_event_check_guid": "94283d79-c2d1-472c-b303-77a0fb387bcc" + }, + "enable_host_based_firewall": True + } + } +] + +HBFW_MODIFY_PUT_RESPONSE = { + "successful": [ + { + "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. [...].", + "inherited_from": "", + "category": "host_based_firewall", + "parameters": { + "rulesets": [ + { + "description": "Whatever", + "name": "Argon_firewall", + "rules": [ + { + "action": "ALLOW", + "application_path": "*", + "direction": "BOTH", + "enabled": True, + "local_ip_address": "1.2.3.4", + "local_port_ranges": "1234", + "name": "my_first_rule", + "protocol": "TCP", + "remote_ip_address": "199.201.128.1", + "remote_port_ranges": "5678", + "rule_access_check_guid": "6d36954a-a944-4944-ae94-df6f94b877b8", + "rule_inbound_event_check_guid": "8a39c00b-f907-4085-929f-f2e98e8b7b87", + "rule_outbound_event_check_guid": "7e7a9761-4187-4065-8ae1-b5161fae75a2", + "test_mode": False + } + ], + "ruleset_id": "0c0ce332-6f81-43d9-ad9b-875e82eb53f9" + } + ], + "rule_groups": [ + { + "description": "Starship go BOOM", + "name": "Argon_firewall", + "rules": [ + { + "action": "ALLOW", + "application_path": "*", + "direction": "BOTH", + "enabled": True, + "local_ip_address": "1.2.3.4", + "local_port_ranges": "1234", + "name": "my_first_rule", + "protocol": "TCP", + "remote_ip_address": "199.201.128.1", + "remote_port_ranges": "5678", + "rule_access_check_guid": "6d36954a-a944-4944-ae94-df6f94b877b8", + "rule_inbound_event_check_guid": "8a39c00b-f907-4085-929f-f2e98e8b7b87", + "rule_outbound_event_check_guid": "7e7a9761-4187-4065-8ae1-b5161fae75a2", + "test_mode": False + } + ], + "ruleset_id": "0c0ce332-6f81-43d9-ad9b-875e82eb53f9" + } + ], + "default_rule": { + "action": "BLOCK", + "default_rule_access_check_guid": "08dc129b-ab72-4ed7-8282-8db7f62bc7e8", + "default_rule_inbound_event_check_guid": "40dd836c-e676-4e3b-b98b-c870c4b6faa7", + "default_rule_outbound_event_check_guid": "94283d79-c2d1-472c-b303-77a0fb387bcc" + }, + "enable_host_based_firewall": True + } + } + ], + "failed": [] +} + +HBFW_ADD_RULE_PUT_REQUEST = [ + { + "id": "df181779-f623-415d-879e-91c40246535d", + "parameters": { + "rule_groups": [ + { + "description": "Whatever", + "name": "Argon_firewall", + "rules": [ + { + "action": "ALLOW", + "application_path": "*", + "direction": "IN", + "enabled": True, + "local_ip_address": "1.2.3.4", + "local_port_ranges": "1234", + "name": "my_first_rule", + "protocol": "TCP", + "remote_ip_address": "5.6.7.8", + "remote_port_ranges": "5678", + "rule_access_check_guid": "6d36954a-a944-4944-ae94-df6f94b877b8", + "rule_inbound_event_check_guid": "8a39c00b-f907-4085-929f-f2e98e8b7b87", + "rule_outbound_event_check_guid": "7e7a9761-4187-4065-8ae1-b5161fae75a2", + "test_mode": False + }, + { + "action": "BLOCK", + "application_path": "C:\\DOOM\\DOOM.EXE", + "direction": "BOTH", + "enabled": True, + "local_ip_address": "10.29.99.1", + "local_port_ranges": "*", + "name": "DoomyDoomsOfDoom", + "protocol": "TCP", + "remote_ip_address": "199.201.128.1", + "remote_port_ranges": "666", + "test_mode": False + } + ], + "ruleset_id": "0c0ce332-6f81-43d9-ad9b-875e82eb53f9" + } + ], + "default_rule": { + "action": "ALLOW", + "default_rule_access_check_guid": "08dc129b-ab72-4ed7-8282-8db7f62bc7e8", + "default_rule_inbound_event_check_guid": "40dd836c-e676-4e3b-b98b-c870c4b6faa7", + "default_rule_outbound_event_check_guid": "94283d79-c2d1-472c-b303-77a0fb387bcc" + }, + "enable_host_based_firewall": False + } + } +] + +HBFW_ADD_RULE_PUT_RESPONSE = { + "successful": [ + { + "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. [...].", + "inherited_from": "", + "category": "host_based_firewall", + "parameters": { + "rulesets": [ + { + "description": "Whatever", + "name": "Argon_firewall", + "rules": [ + { + "action": "ALLOW", + "application_path": "*", + "direction": "IN", + "enabled": True, + "local_ip_address": "1.2.3.4", + "local_port_ranges": "1234", + "name": "my_first_rule", + "protocol": "TCP", + "remote_ip_address": "5.6.7.8", + "remote_port_ranges": "5678", + "rule_access_check_guid": "6d36954a-a944-4944-ae94-df6f94b877b8", + "rule_inbound_event_check_guid": "8a39c00b-f907-4085-929f-f2e98e8b7b87", + "rule_outbound_event_check_guid": "7e7a9761-4187-4065-8ae1-b5161fae75a2", + "test_mode": False + }, + { + "action": "BLOCK", + "application_path": "C:\\DOOM\\DOOM.EXE", + "direction": "BOTH", + "enabled": True, + "local_ip_address": "10.29.99.1", + "local_port_ranges": "*", + "name": "DoomyDoomsOfDoom", + "protocol": "TCP", + "remote_ip_address": "199.201.128.1", + "remote_port_ranges": "666", + "rule_access_check_guid": "6d36954a-a944-4944-ae94-df6f94b877b8", + "rule_inbound_event_check_guid": "8a39c00b-f907-4085-929f-f2e98e8b7b87", + "rule_outbound_event_check_guid": "7e7a9761-4187-4065-8ae1-b5161fae75a2", + "test_mode": False + } + ], + "ruleset_id": "0c0ce332-6f81-43d9-ad9b-875e82eb53f9" + } + ], + "rule_groups": [ + { + "description": "Whatever", + "name": "Argon_firewall", + "rules": [ + { + "action": "ALLOW", + "application_path": "*", + "direction": "IN", + "enabled": True, + "local_ip_address": "1.2.3.4", + "local_port_ranges": "1234", + "name": "my_first_rule", + "protocol": "TCP", + "remote_ip_address": "5.6.7.8", + "remote_port_ranges": "5678", + "rule_access_check_guid": "6d36954a-a944-4944-ae94-df6f94b877b8", + "rule_inbound_event_check_guid": "8a39c00b-f907-4085-929f-f2e98e8b7b87", + "rule_outbound_event_check_guid": "7e7a9761-4187-4065-8ae1-b5161fae75a2", + "test_mode": False + }, + { + "action": "BLOCK", + "application_path": "C:\\DOOM\\DOOM.EXE", + "direction": "BOTH", + "enabled": True, + "local_ip_address": "10.29.99.1", + "local_port_ranges": "*", + "name": "DoomyDoomsOfDoom", + "protocol": "TCP", + "remote_ip_address": "199.201.128.1", + "remote_port_ranges": "666", + "rule_access_check_guid": "6d36954a-a944-4944-ae94-df6f94b877b8", + "rule_inbound_event_check_guid": "8a39c00b-f907-4085-929f-f2e98e8b7b87", + "rule_outbound_event_check_guid": "7e7a9761-4187-4065-8ae1-b5161fae75a2", + "test_mode": False + } + ], + "ruleset_id": "0c0ce332-6f81-43d9-ad9b-875e82eb53f9" + } + ], + "default_rule": { + "action": "ALLOW", + "default_rule_access_check_guid": "08dc129b-ab72-4ed7-8282-8db7f62bc7e8", + "default_rule_inbound_event_check_guid": "40dd836c-e676-4e3b-b98b-c870c4b6faa7", + "default_rule_outbound_event_check_guid": "94283d79-c2d1-472c-b303-77a0fb387bcc" + }, + "enable_host_based_firewall": False + } + } + ], + "failed": [] +} + +HBFW_ADD_RULE_GROUP_PUT_REQUEST = [ + { + "id": "df181779-f623-415d-879e-91c40246535d", + "parameters": { + "rule_groups": [ + { + "description": "Whatever", + "name": "Argon_firewall", + "rules": [ + { + "action": "ALLOW", + "application_path": "*", + "direction": "IN", + "enabled": True, + "local_ip_address": "1.2.3.4", + "local_port_ranges": "1234", + "name": "my_first_rule", + "protocol": "TCP", + "remote_ip_address": "5.6.7.8", + "remote_port_ranges": "5678", + "rule_access_check_guid": "6d36954a-a944-4944-ae94-df6f94b877b8", + "rule_inbound_event_check_guid": "8a39c00b-f907-4085-929f-f2e98e8b7b87", + "rule_outbound_event_check_guid": "7e7a9761-4187-4065-8ae1-b5161fae75a2", + "test_mode": False + } + ], + "ruleset_id": "0c0ce332-6f81-43d9-ad9b-875e82eb53f9" + }, + { + "description": "No playing DOOM!", + "name": "DOOM_firewall", + "rules": [ + { + "action": "BLOCK", + "application_path": "C:\\DOOM\\DOOM.EXE", + "direction": "BOTH", + "enabled": True, + "local_ip_address": "10.29.99.1", + "local_port_ranges": "*", + "name": "DoomyDoomsOfDoom", + "protocol": "TCP", + "remote_ip_address": "199.201.128.1", + "remote_port_ranges": "666", + "test_mode": False + } + ] + } + ], + "default_rule": { + "action": "ALLOW", + "default_rule_access_check_guid": "08dc129b-ab72-4ed7-8282-8db7f62bc7e8", + "default_rule_inbound_event_check_guid": "40dd836c-e676-4e3b-b98b-c870c4b6faa7", + "default_rule_outbound_event_check_guid": "94283d79-c2d1-472c-b303-77a0fb387bcc" + }, + "enable_host_based_firewall": False + } + } +] + +HBFW_ADD_RULE_GROUP_PUT_RESPONSE = { + "successful": [ + { + "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. [...].", + "inherited_from": "", + "category": "host_based_firewall", + "parameters": { + "rulesets": [ + { + "description": "Whatever", + "name": "Argon_firewall", + "rules": [ + { + "action": "ALLOW", + "application_path": "*", + "direction": "IN", + "enabled": True, + "local_ip_address": "1.2.3.4", + "local_port_ranges": "1234", + "name": "my_first_rule", + "protocol": "TCP", + "remote_ip_address": "5.6.7.8", + "remote_port_ranges": "5678", + "rule_access_check_guid": "6d36954a-a944-4944-ae94-df6f94b877b8", + "rule_inbound_event_check_guid": "8a39c00b-f907-4085-929f-f2e98e8b7b87", + "rule_outbound_event_check_guid": "7e7a9761-4187-4065-8ae1-b5161fae75a2", + "test_mode": False + }, + ], + "ruleset_id": "0c0ce332-6f81-43d9-ad9b-875e82eb53f9" + }, + { + "description": "No playing DOOM!", + "name": "DOOM_firewall", + "rules": [ + { + "action": "BLOCK", + "application_path": "C:\\DOOM\\DOOM.EXE", + "direction": "BOTH", + "enabled": True, + "local_ip_address": "10.29.99.1", + "local_port_ranges": "*", + "name": "DoomyDoomsOfDoom", + "protocol": "TCP", + "remote_ip_address": "199.201.128.1", + "remote_port_ranges": "666", + "rule_access_check_guid": "6d36954a-a944-4944-ae94-df6f94b877b8", + "rule_inbound_event_check_guid": "8a39c00b-f907-4085-929f-f2e98e8b7b87", + "rule_outbound_event_check_guid": "7e7a9761-4187-4065-8ae1-b5161fae75a2", + "test_mode": False + } + ], + "ruleset_id": "0c0ce332-6f81-43d9-ad9b-875e82eb53f9" + } + ], + "rule_groups": [ + { + "description": "Whatever", + "name": "Argon_firewall", + "rules": [ + { + "action": "ALLOW", + "application_path": "*", + "direction": "IN", + "enabled": True, + "local_ip_address": "1.2.3.4", + "local_port_ranges": "1234", + "name": "my_first_rule", + "protocol": "TCP", + "remote_ip_address": "5.6.7.8", + "remote_port_ranges": "5678", + "rule_access_check_guid": "6d36954a-a944-4944-ae94-df6f94b877b8", + "rule_inbound_event_check_guid": "8a39c00b-f907-4085-929f-f2e98e8b7b87", + "rule_outbound_event_check_guid": "7e7a9761-4187-4065-8ae1-b5161fae75a2", + "test_mode": False + }, + ], + "ruleset_id": "0c0ce332-6f81-43d9-ad9b-875e82eb53f9" + }, + { + "description": "No playing DOOM!", + "name": "DOOM_firewall", + "rules": [ + { + "action": "BLOCK", + "application_path": "C:\\DOOM\\DOOM.EXE", + "direction": "BOTH", + "enabled": True, + "local_ip_address": "10.29.99.1", + "local_port_ranges": "*", + "name": "DoomyDoomsOfDoom", + "protocol": "TCP", + "remote_ip_address": "199.201.128.1", + "remote_port_ranges": "666", + "rule_access_check_guid": "6d36954a-a944-4944-ae94-df6f94b877b8", + "rule_inbound_event_check_guid": "8a39c00b-f907-4085-929f-f2e98e8b7b87", + "rule_outbound_event_check_guid": "7e7a9761-4187-4065-8ae1-b5161fae75a2", + "test_mode": False + } + ], + "ruleset_id": "0c0ce332-6f81-43d9-ad9b-875e82eb53f9" + } + ], + "default_rule": { + "action": "ALLOW", + "default_rule_access_check_guid": "08dc129b-ab72-4ed7-8282-8db7f62bc7e8", + "default_rule_inbound_event_check_guid": "40dd836c-e676-4e3b-b98b-c870c4b6faa7", + "default_rule_outbound_event_check_guid": "94283d79-c2d1-472c-b303-77a0fb387bcc" + }, + "enable_host_based_firewall": False + } + } + ], + "failed": [] +} diff --git a/src/tests/unit/platform/test_policy_ruleconfigs.py b/src/tests/unit/platform/test_policy_ruleconfigs.py index 548ede8a7..ff44a198e 100644 --- a/src/tests/unit/platform/test_policy_ruleconfigs.py +++ b/src/tests/unit/platform/test_policy_ruleconfigs.py @@ -22,8 +22,13 @@ from tests.unit.fixtures.CBCSDKMock import CBCSDKMock from tests.unit.fixtures.platform.mock_policies import (FULL_POLICY_1, BASIC_CONFIG_TEMPLATE_RETURN, TEMPLATE_RETURN_BOGUS_TYPE, POLICY_CONFIG_PRESENTATION, - REPLACE_RULECONFIG, CORE_PREVENTION_RETURNS, - CORE_PREVENTION_UPDATE_1) + REPLACE_RULECONFIG) +from tests.unit.fixtures.platform.mock_policy_ruleconfigs import (CORE_PREVENTION_RETURNS, CORE_PREVENTION_UPDATE_1, + HBFW_GET_RESULT, HBFW_MODIFY_PUT_REQUEST, + HBFW_MODIFY_PUT_RESPONSE, HBFW_ADD_RULE_PUT_REQUEST, + HBFW_ADD_RULE_PUT_RESPONSE, + HBFW_ADD_RULE_GROUP_PUT_REQUEST, + HBFW_ADD_RULE_GROUP_PUT_RESPONSE) logging.basicConfig(format='%(asctime)s %(levelname)s:%(message)s', level=logging.DEBUG, filename='log.txt') @@ -280,3 +285,207 @@ def on_delete(url, body): assert rule_config.name == 'Carbon Black Threat Intel' rule_config.delete() assert delete_called + + +def test_host_based_firewall_contents(cbcsdk_mock): + """Tests the contents of the host-based firewall rule configuration, along with the refresh.""" + cbcsdk_mock.mock_request('GET', '/policyservice/v1/orgs/test/policies/65536/rule_configs/host_based_firewall', + HBFW_GET_RESULT) + api = cbcsdk_mock.api + policy = Policy(api, 65536, copy.deepcopy(FULL_POLICY_1), False, True) + rule_config = policy.host_based_firewall_rule_config + assert not rule_config.enabled + assert rule_config.default_action == "ALLOW" + groups = rule_config.rule_groups + assert len(groups) == 1 + assert groups[0].name == "Argon_firewall" + assert groups[0].description == "Whatever" + rules = groups[0].rules_ + assert len(rules) == 1 + assert rules[0].action == "ALLOW" + assert rules[0].application_path == "*" + assert rules[0].direction == "IN" + assert rules[0].enabled + assert rules[0].local_ip_address == "1.2.3.4" + assert rules[0].local_port_ranges == "1234" + assert rules[0].name == "my_first_rule" + assert rules[0].protocol == "TCP" + assert rules[0].remote_ip_address == "5.6.7.8" + assert rules[0].remote_port_ranges == "5678" + assert not rules[0].test_mode + rule_config.refresh() + assert not rule_config.enabled + assert rule_config.default_action == "ALLOW" + groups = rule_config.rule_groups + assert len(groups) == 1 + rules = groups[0].rules_ + assert len(rules) == 1 + assert rules[0].local_ip_address == "1.2.3.4" + assert rules[0].remote_ip_address == "5.6.7.8" + + +def test_delete_host_based_firewall(cbcsdk_mock): + """Tests that delete resets the host-based firewall rule configuration.""" + delete_called = False + get_called = 0 + + def on_delete(url, body): + nonlocal delete_called + delete_called = True + return CBCSDKMock.StubResponse(None, scode=204) + + def on_get(url, params, default): + nonlocal delete_called, get_called + assert delete_called + get_called += 1 + return HBFW_GET_RESULT + + cbcsdk_mock.mock_request('DELETE', '/policyservice/v1/orgs/test/policies/65536/rule_configs/host_based_firewall' + '/df181779-f623-415d-879e-91c40246535d', on_delete) + cbcsdk_mock.mock_request('GET', '/policyservice/v1/orgs/test/policies/65536/rule_configs/host_based_firewall', + on_get) + api = cbcsdk_mock.api + policy = Policy(api, 65536, copy.deepcopy(FULL_POLICY_1), False, True) + rule_config = policy.host_based_firewall_rule_config + rule_config.delete() + assert delete_called + assert get_called == 0 + assert not rule_config.enabled + assert rule_config.default_action == "ALLOW" + assert get_called == 1 + + +def test_modify_host_based_firewall(cbcsdk_mock): + """Tests modifying a host-based firewall rule configuration's data.""" + put_called = False + + def on_put(url, body, **kwargs): + nonlocal put_called + assert body == HBFW_MODIFY_PUT_REQUEST + put_called = True + return copy.deepcopy(HBFW_MODIFY_PUT_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/host_based_firewall', + on_put) + api = cbcsdk_mock.api + policy = Policy(api, 65536, copy.deepcopy(FULL_POLICY_1), False, True) + rule_config = policy.host_based_firewall_rule_config + rule_config.set_enabled(True) + rule_config.set_default_action("BLOCK") + groups = rule_config.rule_groups + groups[0].description = "Starship go BOOM" + rules = groups[0].rules_ + rules[0].remote_ip_address = "199.201.128.1" + rules[0].direction = "BOTH" + rule_config.save() + assert put_called + assert rule_config.enabled + assert rule_config.default_action == "BLOCK" + groups = rule_config.rule_groups + assert groups[0].description == "Starship go BOOM" + rules = groups[0].rules_ + assert rules[0].remote_ip_address == "199.201.128.1" + assert rules[0].direction == "BOTH" + + +def test_host_based_firewall_parameter_errors(cb, policy): + """Tests bad values for host-based firewall parameters.""" + rule_config = policy.host_based_firewall_rule_config + with pytest.raises(ApiError): + rule_config.set_default_action("NOTEXIST") + + +def test_modify_add_rule_to_host_based_firewall(cbcsdk_mock): + """Tests modifying a host-based firewall rule configuration by adding a rule.""" + put_called = False + + def on_put(url, body, **kwargs): + nonlocal put_called + assert body == HBFW_ADD_RULE_PUT_REQUEST + put_called = True + return copy.deepcopy(HBFW_ADD_RULE_PUT_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/host_based_firewall', + on_put) + api = cbcsdk_mock.api + policy = Policy(api, 65536, copy.deepcopy(FULL_POLICY_1), False, True) + rule_config = policy.host_based_firewall_rule_config + groups = rule_config.rule_groups + new_rule = rule_config.new_rule("DoomyDoomsOfDoom", "BLOCK", "BOTH", "TCP", "199.201.128.1") + new_rule.remote_port_ranges = "666" + new_rule.local_ip_address = "10.29.99.1" + new_rule.application_path = "C:\\DOOM\\DOOM.EXE" + groups[0].append_rule(new_rule) + rule_config.save() + assert put_called + groups = rule_config.rule_groups + assert len(groups) == 1 + rules = groups[0].rules_ + assert len(rules) == 2 + assert rules[0].name == "my_first_rule" + assert rules[1].name == "DoomyDoomsOfDoom" + assert rules[1].action == "BLOCK" + assert rules[1].application_path == "C:\\DOOM\\DOOM.EXE" + assert rules[1].direction == "BOTH" + assert rules[1].enabled + assert rules[1].local_ip_address == "10.29.99.1" + assert rules[1].local_port_ranges == "*" + assert rules[1].protocol == "TCP" + assert rules[1].remote_ip_address == "199.201.128.1" + assert rules[1].remote_port_ranges == "666" + assert not rules[1].test_mode + + +def test_new_rule_parameter_errors(cb, policy): + """Tests the parameter check errors on the new_rule() method.""" + rule_config = policy.host_based_firewall_rule_config + with pytest.raises(ApiError): + rule_config.new_rule("DoomyDoomsOfDoom", "NOTEXIST", "BOTH", "TCP", "199.201.128.1") + with pytest.raises(ApiError): + rule_config.new_rule("DoomyDoomsOfDoom", "BLOCK", "NOTEXIST", "TCP", "199.201.128.1") + with pytest.raises(ApiError): + rule_config.new_rule("DoomyDoomsOfDoom", "BLOCK", "BOTH", "NOTEXIST", "199.201.128.1") + + +def test_modify_add_rule_group_to_host_based_firewall(cbcsdk_mock): + """Tests modifying a host-based firewall rule configuration by adding a rule group.""" + put_called = False + + def on_put(url, body, **kwargs): + nonlocal put_called + assert body == HBFW_ADD_RULE_GROUP_PUT_REQUEST + put_called = True + return copy.deepcopy(HBFW_ADD_RULE_GROUP_PUT_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/host_based_firewall', + on_put) + api = cbcsdk_mock.api + policy = Policy(api, 65536, copy.deepcopy(FULL_POLICY_1), False, True) + rule_config = policy.host_based_firewall_rule_config + new_group = rule_config.new_rule_group("DOOM_firewall", "No playing DOOM!") + new_rule = rule_config.new_rule("DoomyDoomsOfDoom", "BLOCK", "BOTH", "TCP", "199.201.128.1") + new_rule.remote_port_ranges = "666" + new_rule.local_ip_address = "10.29.99.1" + new_rule.application_path = "C:\\DOOM\\DOOM.EXE" + new_group.append_rule(new_rule) + rule_config.append_rule_group(new_group) + rule_config.save() + assert put_called + groups = rule_config.rule_groups + assert len(groups) == 2 + assert groups[0].name == "Argon_firewall" + assert groups[0].description == "Whatever" + rules = groups[0].rules_ + assert len(rules) == 1 + assert rules[0].name == "my_first_rule" + assert groups[1].name == "DOOM_firewall" + assert groups[1].description == "No playing DOOM!" + rules = groups[1].rules_ + assert len(rules) == 1 + assert rules[0].name == "DoomyDoomsOfDoom" From 910cd6ace4f7929f0a7749f4f7f30a6fdebdfa75 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Thu, 27 Apr 2023 14:37:51 -0600 Subject: [PATCH 09/76] test coverage now 96%, at target --- src/cbc_sdk/platform/policy_ruleconfigs.py | 71 ++- .../unit/fixtures/platform/mock_policies.py | 513 +++++++++++++++++- .../platform/mock_policy_ruleconfigs.py | 341 ++++++++++++ .../unit/platform/test_policy_ruleconfigs.py | 66 ++- 4 files changed, 963 insertions(+), 28 deletions(-) diff --git a/src/cbc_sdk/platform/policy_ruleconfigs.py b/src/cbc_sdk/platform/policy_ruleconfigs.py index c0e0bf1e0..225f0c485 100644 --- a/src/cbc_sdk/platform/policy_ruleconfigs.py +++ b/src/cbc_sdk/platform/policy_ruleconfigs.py @@ -328,7 +328,7 @@ class FirewallRuleGroup(MutableBaseModel): """Represents a group of related firewall rules.""" swagger_meta_file = "platform/models/firewall_rule_group.yaml" - def __init__(self, cb, parent, initial_data): + def __init__(self, cb, initial_data): """ Initialize the FirewallRuleGroup object. @@ -337,13 +337,25 @@ def __init__(self, cb, parent, initial_data): initial_data (dict): Initial data used to populate the firewall rule group. """ super(HostBasedFirewallRuleConfig.FirewallRuleGroup, self).__init__(cb, None, initial_data, False, True) - self._parent = parent - self._rules = [HostBasedFirewallRuleConfig.FirewallRule(cb, parent, d) + self._parent = None + self._rules = [HostBasedFirewallRuleConfig.FirewallRule(cb, d) for d in initial_data.get("rules", [])] + def _init_parent(self, parent): + """ + Initialize the parent of this group and its rules. + + Args: + parent (HostBasedFirewallRuleConfig): The parent to be set. + """ + self._parent = parent + for rule in self._rules: + rule._parent = parent + def _field_updated(self, attrname): """Method called whenever a field is updated.""" - self._parent._mark_changed() + if self._parent: + self._parent._mark_changed() def _flatten(self): """ @@ -373,20 +385,23 @@ def append_rule(self, rule): Args: rule (HostBasedFirewallRuleConfig.FirewallRule): The new rule. """ + rule._parent = self._parent self._rules.append(rule) - self._parent._mark_changed() + if self._parent: + self._parent._mark_changed() def remove(self): """Removes this rule group from the rule configuration.""" - if self in self.rule_groups: - self.rule_groups.remove(self) + if self._parent and self in self._parent.rule_groups: + self._parent.rule_groups.remove(self) self._parent._mark_changed() + self._parent = None class FirewallRule(MutableBaseModel): """Represents a single firewall rule.""" swagger_meta_file = "platform/models/firewall_rule.yaml" - def __init__(self, cb, parent, initial_data): + def __init__(self, cb, initial_data): """ Initialize the FirewallRule object. @@ -395,11 +410,12 @@ def __init__(self, cb, parent, initial_data): initial_data (dict): Initial data used to populate the firewall rule. """ super(HostBasedFirewallRuleConfig.FirewallRule, self).__init__(cb, None, initial_data, False, True) - self._parent = parent + self._parent = None def _field_updated(self, attrname): """Method called whenever a field is updated.""" - self._parent._mark_changed() + if self._parent: + self._parent._mark_changed() def _flatten(self): """ @@ -412,10 +428,12 @@ def _flatten(self): def remove(self): """Removes this rule from the rule group that contains it.""" - group_list = [group for group in self.rule_groups if self in group._rules] - if group_list: - group_list[0]._rules.remove(self) - self._parent._mark_changed() + if self._parent: + group_list = [group for group in self._parent.rule_groups if self in group._rules] + if group_list: + group_list[0]._rules.remove(self) + self._parent._mark_changed() + self._parent = None def _base_url(self): """ @@ -535,7 +553,9 @@ def rule_groups(self): if not self._rule_groups_valid: rg_param = self.get_parameter("rule_groups") if rg_param is not None: - self._rule_groups = [HostBasedFirewallRuleConfig.FirewallRuleGroup(self._cb, self, d) for d in rg_param] + self._rule_groups = [HostBasedFirewallRuleConfig.FirewallRuleGroup(self._cb, d) for d in rg_param] + for group in self._rule_groups: + group._init_parent(self) else: self._rule_groups = [] self._rule_groups_valid = True @@ -552,8 +572,8 @@ def new_rule_group(self, name, description): Returns: FirewallRuleGroup: The new rule group object. Add it to this rule configuration with append_rule_group. """ - return HostBasedFirewallRuleConfig.FirewallRuleGroup(self._cb, self, {"name": name, "description": description, - "rules": []}) + return HostBasedFirewallRuleConfig.FirewallRuleGroup(self._cb, {"name": name, "description": description, + "rules": []}) def append_rule_group(self, rule_group): """ @@ -563,6 +583,7 @@ def append_rule_group(self, rule_group): rule_group (FirewallRuleGroup): The rule group to be added. """ self.rule_groups.append(rule_group) + rule_group._parent = self self._mark_changed() def new_rule(self, name, action, direction, protocol, remote_ip): @@ -585,11 +606,11 @@ def new_rule(self, name, action, direction, protocol, remote_ip): raise ApiError(f"invalid rule direction: {direction}") if protocol not in ("TCP", "UDP"): raise ApiError(f"invalid rule protocol: {protocol}") - return HostBasedFirewallRuleConfig.FirewallRule(self._cb, self, {"action": action, "application_path": "*", - "direction": direction, "enabled": True, - "name": name, - "protocol": protocol, "local_ip_address": "*", - "local_port_ranges": "*", - "remote_ip_address": remote_ip, - "remote_port_ranges": "*", - "test_mode": False}) + return HostBasedFirewallRuleConfig.FirewallRule(self._cb, {"action": action, "application_path": "*", + "direction": direction, "enabled": True, + "name": name, + "protocol": protocol, "local_ip_address": "*", + "local_port_ranges": "*", + "remote_ip_address": remote_ip, + "remote_port_ranges": "*", + "test_mode": False}) diff --git a/src/tests/unit/fixtures/platform/mock_policies.py b/src/tests/unit/fixtures/platform/mock_policies.py index de6d2fcda..2fd28056d 100644 --- a/src/tests/unit/fixtures/platform/mock_policies.py +++ b/src/tests/unit/fixtures/platform/mock_policies.py @@ -1,6 +1,5 @@ """Mock responses for Policy""" - FULL_POLICY_1 = { "id": 65536, "name": "A Dummy Policy", @@ -1915,3 +1914,515 @@ "WindowsAssignmentMode": "BLOCK" } } + +FULL_POLICY_5 = { + "id": 1492, + "name": "Crapco", + "org_key": "test", + "priority_level": "MEDIUM", + "position": -1, + "is_system": False, + "description": "If you buy this, you'll buy ANYTHING!", + "auto_deregister_inactive_vdi_interval_ms": 0, + "auto_deregister_inactive_vm_workloads_interval_ms": 0, + "update_time": 1682625002305, + "av_settings": { + "avira_protection_cloud": { + "enabled": True, + "max_exe_delay": 45, + "max_file_size": 4, + "risk_level": 4 + }, + "on_access_scan": { + "enabled": True, + "mode": "NORMAL" + }, + "on_demand_scan": { + "enabled": True, + "profile": "NORMAL", + "schedule": { + "start_hour": 0, + "range_hours": 0, + "recovery_scan_if_missed": True + }, + "scan_usb": "AUTOSCAN", + "scan_cd_dvd": "AUTOSCAN" + }, + "signature_update": { + "enabled": True, + "schedule": { + "full_interval_hours": 0, + "initial_random_delay_hours": 4, + "interval_hours": 4 + } + }, + "update_servers": { + "servers_override": [], + "servers_for_onsite_devices": [ + { + "server": "http://updates2.cdc.carbonblack.io/update2", + "preferred": False + } + ], + "servers_for_offsite_devices": [ + "http://updates2.cdc.carbonblack.io/update2" + ] + } + }, + "rules": [ + { + "id": 15, + "required": False, + "action": "TERMINATE", + "application": { + "type": "REPUTATION", + "value": "KNOWN_MALWARE" + }, + "operation": "RUN" + }, + { + "id": 16, + "required": False, + "action": "TERMINATE", + "application": { + "type": "REPUTATION", + "value": "COMPANY_BLACK_LIST" + }, + "operation": "RUN" + }, + { + "id": 17, + "required": False, + "action": "TERMINATE", + "application": { + "type": "REPUTATION", + "value": "SUSPECT_MALWARE" + }, + "operation": "RUN" + }, + { + "id": 18, + "required": False, + "action": "TERMINATE", + "application": { + "type": "REPUTATION", + "value": "PUP" + }, + "operation": "RUN" + }, + { + "id": 19, + "required": False, + "action": "TERMINATE", + "application": { + "type": "REPUTATION", + "value": "RESOLVING" + }, + "operation": "MEMORY_SCRAPE" + }, + { + "id": 20, + "required": False, + "action": "TERMINATE", + "application": { + "type": "REPUTATION", + "value": "RESOLVING" + }, + "operation": "RANSOM" + }, + { + "id": 21, + "required": False, + "action": "TERMINATE", + "application": { + "type": "REPUTATION", + "value": "ADAPTIVE_WHITE_LIST" + }, + "operation": "MEMORY_SCRAPE" + }, + { + "id": 22, + "required": False, + "action": "TERMINATE", + "application": { + "type": "REPUTATION", + "value": "ADAPTIVE_WHITE_LIST" + }, + "operation": "RANSOM" + }, + { + "id": 23, + "required": False, + "action": "TERMINATE", + "application": { + "type": "NAME_PATH", + "value": "**\\powershell*.exe" + }, + "operation": "MEMORY_SCRAPE" + }, + { + "id": 24, + "required": False, + "action": "TERMINATE", + "application": { + "type": "NAME_PATH", + "value": "**/python" + }, + "operation": "MEMORY_SCRAPE" + }, + { + "id": 25, + "required": False, + "action": "TERMINATE", + "application": { + "type": "NAME_PATH", + "value": "**\\wscript.exe" + }, + "operation": "MEMORY_SCRAPE" + }, + { + "id": 26, + "required": False, + "action": "TERMINATE", + "application": { + "type": "NAME_PATH", + "value": "**\\cscript.exe" + }, + "operation": "MEMORY_SCRAPE" + }, + { + "id": 27, + "required": False, + "action": "DENY", + "application": { + "type": "NAME_PATH", + "value": "**\\wscript.exe" + }, + "operation": "CODE_INJECTION" + }, + { + "id": 28, + "required": False, + "action": "DENY", + "application": { + "type": "NAME_PATH", + "value": "**\\cscript.exe" + }, + "operation": "CODE_INJECTION" + } + ], + "directory_action_rules": [], + "sensor_settings": [ + { + "name": "ALLOW_UNINSTALL", + "value": "true" + }, + { + "name": "SHOW_UI", + "value": "true" + }, + { + "name": "ENABLE_THREAT_SHARING", + "value": "true" + }, + { + "name": "QUARANTINE_DEVICE", + "value": "false" + }, + { + "name": "LOGGING_LEVEL", + "value": "false" + }, + { + "name": "QUARANTINE_DEVICE_MESSAGE", + "value": "Device has been quarantined by your computer administrator." + }, + { + "name": "SET_SENSOR_MODE", + "value": "0" + }, + { + "name": "SENSOR_RESET", + "value": "0" + }, + { + "name": "BACKGROUND_SCAN", + "value": "true" + }, + { + "name": "POLICY_ACTION_OVERRIDE", + "value": "true" + }, + { + "name": "HELP_MESSAGE", + "value": "" + }, + { + "name": "PRESERVE_SYSTEM_MEMORY_SCAN", + "value": "false" + }, + { + "name": "HASH_MD5", + "value": "false" + }, + { + "name": "SCAN_LARGE_FILE_READ", + "value": "false" + }, + { + "name": "SCAN_EXECUTE_ON_NETWORK_DRIVE", + "value": "true" + }, + { + "name": "DELAY_EXECUTE", + "value": "true" + }, + { + "name": "SCAN_NETWORK_DRIVE", + "value": "false" + }, + { + "name": "BYPASS_AFTER_LOGIN_MINS", + "value": "0" + }, + { + "name": "BYPASS_AFTER_RESTART_MINS", + "value": "0" + }, + { + "name": "SHOW_FULL_UI", + "value": "false" + }, + { + "name": "SECURITY_CENTER_OPT", + "value": "true" + }, + { + "name": "CB_LIVE_RESPONSE", + "value": "false" + }, + { + "name": "ALLOW_INLINE_BLOCKING", + "value": "true" + }, + { + "name": "UNINSTALL_CODE", + "value": "false" + }, + { + "name": "DEFENSE_OPT_OUT", + "value": "false" + }, + { + "name": "UBS_OPT_IN", + "value": "false" + } + ], + "rule_configs": [ + { + "id": "ac67fa14-f6be-4df9-93f2-6de0dbd96061", + "name": "Credential Theft", + "description": "Addresses threat actors obtaining credentials and relies on detecting the malicious [...].", + "inherited_from": "psc:region", + "category": "core_prevention", + "parameters": { + "WindowsAssignmentMode": "REPORT" + } + }, + { + "id": "c4ed61b3-d5aa-41a9-814f-0f277451532b", + "name": "Carbon Black Threat Intel", + "description": "Addresses common and pervasive TTPs used for malicious activity as well as [...].", + "inherited_from": "psc:region", + "category": "core_prevention", + "parameters": { + "WindowsAssignmentMode": "REPORT" + } + }, + { + "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. [...]", + "inherited_from": "", + "category": "host_based_firewall", + "parameters": { + "rulesets": [ + { + "description": "Whatever", + "name": "Crapco_firewall", + "rules": [ + { + "action": "ALLOW", + "application_path": "*", + "direction": "IN", + "enabled": True, + "local_ip_address": "1.2.3.4", + "local_port_ranges": "1234", + "name": "my_first_rule", + "protocol": "TCP", + "remote_ip_address": "5.6.7.8", + "remote_port_ranges": "5678", + "rule_access_check_guid": "935477b8-997a-4476-8160-9179840d9892", + "rule_inbound_event_check_guid": "203d0685-04a6-49d8-bd9b-20ddda2c6c73", + "rule_outbound_event_check_guid": "16b8a622-a6d0-4873-8197-2974295c0f47", + "test_mode": True + }, + { + "action": "BLOCK", + "application_path": "C:\\DOOM\\DOOM.EXE", + "direction": "BOTH", + "enabled": True, + "local_ip_address": "10.29.99.1", + "local_port_ranges": "*", + "name": "DoomyDoomsofDoom", + "protocol": "TCP", + "remote_ip_address": "199.201.128.1", + "remote_port_ranges": "666", + "rule_access_check_guid": "28acfcac-7891-423d-9e99-d887aa4662fc", + "rule_inbound_event_check_guid": "01e26bc9-7729-4c0d-a550-f63a865b8c9f", + "rule_outbound_event_check_guid": "b9b625eb-1599-4f7d-b852-0f12db6c5a19", + "test_mode": False + } + ], + "ruleset_id": "fa3f7254-6d50-4ebf-aca6-d617bcd644b9" + }, + { + "description": "IRC is a sewer", + "name": "Isolate", + "rules": [ + { + "action": "BLOCK_ALERT", + "application_path": "*", + "direction": "BOTH", + "enabled": True, + "local_ip_address": "10.29.99.1", + "local_port_ranges": "*", + "name": "BlockIRC", + "protocol": "TCP", + "remote_ip_address": "26.2.0.74", + "remote_port_ranges": "6667", + "rule_access_check_guid": "b1454c18-f08c-419a-9b57-186c25aa6c9d", + "rule_inbound_event_check_guid": "b80e9216-5f9f-4e9a-9bcb-79a5af78d976", + "rule_outbound_event_check_guid": "765cdf79-4ff9-419c-9775-abb18e6f6518", + "test_mode": False + } + ], + "ruleset_id": "cc7b30e8-b0e5-4253-96e9-93d345fbe642" + } + ], + "rule_groups": [ + { + "description": "Whatever", + "name": "Crapco_firewall", + "rules": [ + { + "action": "ALLOW", + "application_path": "*", + "direction": "IN", + "enabled": True, + "local_ip_address": "1.2.3.4", + "local_port_ranges": "1234", + "name": "my_first_rule", + "protocol": "TCP", + "remote_ip_address": "5.6.7.8", + "remote_port_ranges": "5678", + "rule_access_check_guid": "935477b8-997a-4476-8160-9179840d9892", + "rule_inbound_event_check_guid": "203d0685-04a6-49d8-bd9b-20ddda2c6c73", + "rule_outbound_event_check_guid": "16b8a622-a6d0-4873-8197-2974295c0f47", + "test_mode": False + }, + { + "action": "BLOCK", + "application_path": "C:\\DOOM\\DOOM.EXE", + "direction": "BOTH", + "enabled": True, + "local_ip_address": "10.29.99.1", + "local_port_ranges": "*", + "name": "DoomyDoomsOfDoom", + "protocol": "TCP", + "remote_ip_address": "199.201.128.1", + "remote_port_ranges": "666", + "rule_access_check_guid": "28acfcac-7891-423d-9e99-d887aa4662fc", + "rule_inbound_event_check_guid": "01e26bc9-7729-4c0d-a550-f63a865b8c9f", + "rule_outbound_event_check_guid": "b9b625eb-1599-4f7d-b852-0f12db6c5a19", + "test_mode": False + } + ], + "ruleset_id": "fa3f7254-6d50-4ebf-aca6-d617bcd644b9" + }, + { + "description": "IRC is a sewer", + "name": "Isolate", + "rules": [ + { + "action": "BLOCK_ALERT", + "application_path": "*", + "direction": "BOTH", + "enabled": True, + "local_ip_address": "10.29.99.1", + "local_port_ranges": "*", + "name": "BlockIRC", + "protocol": "TCP", + "remote_ip_address": "26.2.0.74", + "remote_port_ranges": "6667", + "rule_access_check_guid": "b1454c18-f08c-419a-9b57-186c25aa6c9d", + "rule_inbound_event_check_guid": "b80e9216-5f9f-4e9a-9bcb-79a5af78d976", + "rule_outbound_event_check_guid": "765cdf79-4ff9-419c-9775-abb18e6f6518", + "test_mode": False + } + ], + "ruleset_id": "cc7b30e8-b0e5-4253-96e9-93d345fbe642" + } + ], + "default_rule": { + "action": "ALLOW", + "default_rule_access_check_guid": "e6da6ec1-2e04-4fe7-a864-c4db940510c3", + "default_rule_inbound_event_check_guid": "6d38dce5-d2b2-4572-b61c-3d0bbefddbdb", + "default_rule_outbound_event_check_guid": "26257374-2e78-46ea-b252-1e9916a885d4" + }, + "enable_host_based_firewall": False + } + }, + { + "id": "1f8a5e4b-34f2-4d31-9f8f-87c56facaec8", + "name": "Advanced Scripting Prevention", + "description": "Addresses malicious fileless and file-backed scripts that leverage native programs [...].", + "inherited_from": "psc:region", + "category": "core_prevention", + "parameters": { + "WindowsAssignmentMode": "BLOCK" + } + }, + { + "id": "88b19232-7ebb-48ef-a198-2a75a282de5d", + "name": "Privilege Escalation", + "description": "Addresses behaviors that indicate a threat actor has gained elevated access via [...].", + "inherited_from": "psc:region", + "category": "core_prevention", + "parameters": { + "WindowsAssignmentMode": "BLOCK" + } + }, + { + "id": "97a03cc2-5796-4864-b16d-790d06bea20d", + "name": "Defense Evasion", + "description": "Addresses common TTPs/behaviors that threat actors use to avoid detection such as [...].", + "inherited_from": "psc:region", + "category": "core_prevention", + "parameters": { + "WindowsAssignmentMode": "REPORT" + } + }, + { + "id": "8a16234c-9848-473a-a803-f0f0ffaf5f29", + "name": "Persistence", + "description": "Addresses common TTPs/behaviors that threat actors use to retain access to systems [...].", + "inherited_from": "psc:region", + "category": "core_prevention", + "parameters": { + "WindowsAssignmentMode": "BLOCK" + } + } + ], + "sensor_configs": [] +} diff --git a/src/tests/unit/fixtures/platform/mock_policy_ruleconfigs.py b/src/tests/unit/fixtures/platform/mock_policy_ruleconfigs.py index 49fe161cd..4312e04de 100644 --- a/src/tests/unit/fixtures/platform/mock_policy_ruleconfigs.py +++ b/src/tests/unit/fixtures/platform/mock_policy_ruleconfigs.py @@ -568,3 +568,344 @@ ], "failed": [] } + +HBFW_REMOVE_RULE_PUT_REQUEST = [ + { + "id": "df181779-f623-415d-879e-91c40246535d", + "parameters": { + "rule_groups": [ + { + "description": "Whatever", + "name": "Crapco_firewall", + "rules": [ + { + "action": "ALLOW", + "application_path": "*", + "direction": "IN", + "enabled": True, + "local_ip_address": "1.2.3.4", + "local_port_ranges": "1234", + "name": "my_first_rule", + "protocol": "TCP", + "remote_ip_address": "5.6.7.8", + "remote_port_ranges": "5678", + "rule_access_check_guid": "935477b8-997a-4476-8160-9179840d9892", + "rule_inbound_event_check_guid": "203d0685-04a6-49d8-bd9b-20ddda2c6c73", + "rule_outbound_event_check_guid": "16b8a622-a6d0-4873-8197-2974295c0f47", + "test_mode": False + } + ], + "ruleset_id": "fa3f7254-6d50-4ebf-aca6-d617bcd644b9" + }, + { + "description": "IRC is a sewer", + "name": "Isolate", + "rules": [ + { + "action": "BLOCK_ALERT", + "application_path": "*", + "direction": "BOTH", + "enabled": True, + "local_ip_address": "10.29.99.1", + "local_port_ranges": "*", + "name": "BlockIRC", + "protocol": "TCP", + "remote_ip_address": "26.2.0.74", + "remote_port_ranges": "6667", + "rule_access_check_guid": "b1454c18-f08c-419a-9b57-186c25aa6c9d", + "rule_inbound_event_check_guid": "b80e9216-5f9f-4e9a-9bcb-79a5af78d976", + "rule_outbound_event_check_guid": "765cdf79-4ff9-419c-9775-abb18e6f6518", + "test_mode": False + } + ], + "ruleset_id": "cc7b30e8-b0e5-4253-96e9-93d345fbe642" + } + ], + "default_rule": { + "action": "ALLOW", + "default_rule_access_check_guid": "e6da6ec1-2e04-4fe7-a864-c4db940510c3", + "default_rule_inbound_event_check_guid": "6d38dce5-d2b2-4572-b61c-3d0bbefddbdb", + "default_rule_outbound_event_check_guid": "26257374-2e78-46ea-b252-1e9916a885d4" + }, + "enable_host_based_firewall": False + } + } +] + +HBFW_REMOVE_RULE_PUT_RESPONSE = { + "successful": [ + { + "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. [...]", + "inherited_from": "", + "category": "host_based_firewall", + "parameters": { + "rulesets": [ + { + "description": "Whatever", + "name": "Crapco_firewall", + "rules": [ + { + "action": "ALLOW", + "application_path": "*", + "direction": "IN", + "enabled": True, + "local_ip_address": "1.2.3.4", + "local_port_ranges": "1234", + "name": "my_first_rule", + "protocol": "TCP", + "remote_ip_address": "5.6.7.8", + "remote_port_ranges": "5678", + "rule_access_check_guid": "935477b8-997a-4476-8160-9179840d9892", + "rule_inbound_event_check_guid": "203d0685-04a6-49d8-bd9b-20ddda2c6c73", + "rule_outbound_event_check_guid": "16b8a622-a6d0-4873-8197-2974295c0f47", + "test_mode": True + } + ], + "ruleset_id": "fa3f7254-6d50-4ebf-aca6-d617bcd644b9" + }, + { + "description": "IRC is a sewer", + "name": "Isolate", + "rules": [ + { + "action": "BLOCK_ALERT", + "application_path": "*", + "direction": "BOTH", + "enabled": True, + "local_ip_address": "10.29.99.1", + "local_port_ranges": "*", + "name": "BlockIRC", + "protocol": "TCP", + "remote_ip_address": "26.2.0.74", + "remote_port_ranges": "6667", + "rule_access_check_guid": "b1454c18-f08c-419a-9b57-186c25aa6c9d", + "rule_inbound_event_check_guid": "b80e9216-5f9f-4e9a-9bcb-79a5af78d976", + "rule_outbound_event_check_guid": "765cdf79-4ff9-419c-9775-abb18e6f6518", + "test_mode": False + } + ], + "ruleset_id": "cc7b30e8-b0e5-4253-96e9-93d345fbe642" + } + ], + "rule_groups": [ + { + "description": "Whatever", + "name": "Crapco_firewall", + "rules": [ + { + "action": "ALLOW", + "application_path": "*", + "direction": "IN", + "enabled": True, + "local_ip_address": "1.2.3.4", + "local_port_ranges": "1234", + "name": "my_first_rule", + "protocol": "TCP", + "remote_ip_address": "5.6.7.8", + "remote_port_ranges": "5678", + "rule_access_check_guid": "935477b8-997a-4476-8160-9179840d9892", + "rule_inbound_event_check_guid": "203d0685-04a6-49d8-bd9b-20ddda2c6c73", + "rule_outbound_event_check_guid": "16b8a622-a6d0-4873-8197-2974295c0f47", + "test_mode": False + } + ], + "ruleset_id": "fa3f7254-6d50-4ebf-aca6-d617bcd644b9" + }, + { + "description": "IRC is a sewer", + "name": "Isolate", + "rules": [ + { + "action": "BLOCK_ALERT", + "application_path": "*", + "direction": "BOTH", + "enabled": True, + "local_ip_address": "10.29.99.1", + "local_port_ranges": "*", + "name": "BlockIRC", + "protocol": "TCP", + "remote_ip_address": "26.2.0.74", + "remote_port_ranges": "6667", + "rule_access_check_guid": "b1454c18-f08c-419a-9b57-186c25aa6c9d", + "rule_inbound_event_check_guid": "b80e9216-5f9f-4e9a-9bcb-79a5af78d976", + "rule_outbound_event_check_guid": "765cdf79-4ff9-419c-9775-abb18e6f6518", + "test_mode": False + } + ], + "ruleset_id": "cc7b30e8-b0e5-4253-96e9-93d345fbe642" + } + ], + "default_rule": { + "action": "ALLOW", + "default_rule_access_check_guid": "e6da6ec1-2e04-4fe7-a864-c4db940510c3", + "default_rule_inbound_event_check_guid": "6d38dce5-d2b2-4572-b61c-3d0bbefddbdb", + "default_rule_outbound_event_check_guid": "26257374-2e78-46ea-b252-1e9916a885d4" + }, + "enable_host_based_firewall": False + } + } + ], + "failed": [] +} + +HBFW_REMOVE_RULE_GROUP_PUT_REQUEST = [ + { + "id": "df181779-f623-415d-879e-91c40246535d", + "parameters": { + "rule_groups": [ + { + "description": "Whatever", + "name": "Crapco_firewall", + "rules": [ + { + "action": "ALLOW", + "application_path": "*", + "direction": "IN", + "enabled": True, + "local_ip_address": "1.2.3.4", + "local_port_ranges": "1234", + "name": "my_first_rule", + "protocol": "TCP", + "remote_ip_address": "5.6.7.8", + "remote_port_ranges": "5678", + "rule_access_check_guid": "935477b8-997a-4476-8160-9179840d9892", + "rule_inbound_event_check_guid": "203d0685-04a6-49d8-bd9b-20ddda2c6c73", + "rule_outbound_event_check_guid": "16b8a622-a6d0-4873-8197-2974295c0f47", + "test_mode": False + }, + { + "action": "BLOCK", + "application_path": "C:\\DOOM\\DOOM.EXE", + "direction": "BOTH", + "enabled": True, + "local_ip_address": "10.29.99.1", + "local_port_ranges": "*", + "name": "DoomyDoomsOfDoom", + "protocol": "TCP", + "remote_ip_address": "199.201.128.1", + "remote_port_ranges": "666", + "rule_access_check_guid": "28acfcac-7891-423d-9e99-d887aa4662fc", + "rule_inbound_event_check_guid": "01e26bc9-7729-4c0d-a550-f63a865b8c9f", + "rule_outbound_event_check_guid": "b9b625eb-1599-4f7d-b852-0f12db6c5a19", + "test_mode": False + } + ], + "ruleset_id": "fa3f7254-6d50-4ebf-aca6-d617bcd644b9" + } + ], + "default_rule": { + "action": "ALLOW", + "default_rule_access_check_guid": "e6da6ec1-2e04-4fe7-a864-c4db940510c3", + "default_rule_inbound_event_check_guid": "6d38dce5-d2b2-4572-b61c-3d0bbefddbdb", + "default_rule_outbound_event_check_guid": "26257374-2e78-46ea-b252-1e9916a885d4" + }, + "enable_host_based_firewall": False + } + } +] + +HBFW_REMOVE_RULE_GROUP_PUT_RESPONSE = { + "successful": [ + { + "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. [...]", + "inherited_from": "", + "category": "host_based_firewall", + "parameters": { + "rulesets": [ + { + "description": "Whatever", + "name": "Crapco_firewall", + "rules": [ + { + "action": "ALLOW", + "application_path": "*", + "direction": "IN", + "enabled": True, + "local_ip_address": "1.2.3.4", + "local_port_ranges": "1234", + "name": "my_first_rule", + "protocol": "TCP", + "remote_ip_address": "5.6.7.8", + "remote_port_ranges": "5678", + "rule_access_check_guid": "935477b8-997a-4476-8160-9179840d9892", + "rule_inbound_event_check_guid": "203d0685-04a6-49d8-bd9b-20ddda2c6c73", + "rule_outbound_event_check_guid": "16b8a622-a6d0-4873-8197-2974295c0f47", + "test_mode": True + }, + { + "action": "BLOCK", + "application_path": "C:\\DOOM\\DOOM.EXE", + "direction": "BOTH", + "enabled": True, + "local_ip_address": "10.29.99.1", + "local_port_ranges": "*", + "name": "DoomyDoomsofDoom", + "protocol": "TCP", + "remote_ip_address": "199.201.128.1", + "remote_port_ranges": "666", + "rule_access_check_guid": "28acfcac-7891-423d-9e99-d887aa4662fc", + "rule_inbound_event_check_guid": "01e26bc9-7729-4c0d-a550-f63a865b8c9f", + "rule_outbound_event_check_guid": "b9b625eb-1599-4f7d-b852-0f12db6c5a19", + "test_mode": False + } + ], + "ruleset_id": "fa3f7254-6d50-4ebf-aca6-d617bcd644b9" + } + ], + "rule_groups": [ + { + "description": "Whatever", + "name": "Crapco_firewall", + "rules": [ + { + "action": "ALLOW", + "application_path": "*", + "direction": "IN", + "enabled": True, + "local_ip_address": "1.2.3.4", + "local_port_ranges": "1234", + "name": "my_first_rule", + "protocol": "TCP", + "remote_ip_address": "5.6.7.8", + "remote_port_ranges": "5678", + "rule_access_check_guid": "935477b8-997a-4476-8160-9179840d9892", + "rule_inbound_event_check_guid": "203d0685-04a6-49d8-bd9b-20ddda2c6c73", + "rule_outbound_event_check_guid": "16b8a622-a6d0-4873-8197-2974295c0f47", + "test_mode": False + }, + { + "action": "BLOCK", + "application_path": "C:\\DOOM\\DOOM.EXE", + "direction": "BOTH", + "enabled": True, + "local_ip_address": "10.29.99.1", + "local_port_ranges": "*", + "name": "DoomyDoomsOfDoom", + "protocol": "TCP", + "remote_ip_address": "199.201.128.1", + "remote_port_ranges": "666", + "rule_access_check_guid": "28acfcac-7891-423d-9e99-d887aa4662fc", + "rule_inbound_event_check_guid": "01e26bc9-7729-4c0d-a550-f63a865b8c9f", + "rule_outbound_event_check_guid": "b9b625eb-1599-4f7d-b852-0f12db6c5a19", + "test_mode": False + } + ], + "ruleset_id": "fa3f7254-6d50-4ebf-aca6-d617bcd644b9" + } + ], + "default_rule": { + "action": "ALLOW", + "default_rule_access_check_guid": "e6da6ec1-2e04-4fe7-a864-c4db940510c3", + "default_rule_inbound_event_check_guid": "6d38dce5-d2b2-4572-b61c-3d0bbefddbdb", + "default_rule_outbound_event_check_guid": "26257374-2e78-46ea-b252-1e9916a885d4" + }, + "enable_host_based_firewall": False + } + } + ], + "failed": [] +} diff --git a/src/tests/unit/platform/test_policy_ruleconfigs.py b/src/tests/unit/platform/test_policy_ruleconfigs.py index ff44a198e..551cd893f 100644 --- a/src/tests/unit/platform/test_policy_ruleconfigs.py +++ b/src/tests/unit/platform/test_policy_ruleconfigs.py @@ -22,13 +22,17 @@ from tests.unit.fixtures.CBCSDKMock import CBCSDKMock from tests.unit.fixtures.platform.mock_policies import (FULL_POLICY_1, BASIC_CONFIG_TEMPLATE_RETURN, TEMPLATE_RETURN_BOGUS_TYPE, POLICY_CONFIG_PRESENTATION, - REPLACE_RULECONFIG) + REPLACE_RULECONFIG, FULL_POLICY_5) from tests.unit.fixtures.platform.mock_policy_ruleconfigs import (CORE_PREVENTION_RETURNS, CORE_PREVENTION_UPDATE_1, HBFW_GET_RESULT, HBFW_MODIFY_PUT_REQUEST, HBFW_MODIFY_PUT_RESPONSE, HBFW_ADD_RULE_PUT_REQUEST, HBFW_ADD_RULE_PUT_RESPONSE, HBFW_ADD_RULE_GROUP_PUT_REQUEST, - HBFW_ADD_RULE_GROUP_PUT_RESPONSE) + HBFW_ADD_RULE_GROUP_PUT_RESPONSE, + HBFW_REMOVE_RULE_PUT_REQUEST, + HBFW_REMOVE_RULE_PUT_RESPONSE, + HBFW_REMOVE_RULE_GROUP_PUT_REQUEST, + HBFW_REMOVE_RULE_GROUP_PUT_RESPONSE) logging.basicConfig(format='%(asctime)s %(levelname)s:%(message)s', level=logging.DEBUG, filename='log.txt') @@ -489,3 +493,61 @@ def on_put(url, body, **kwargs): rules = groups[1].rules_ assert len(rules) == 1 assert rules[0].name == "DoomyDoomsOfDoom" + + +def test_modify_remove_rule_from_host_based_firewall(cbcsdk_mock): + """Tests modifying a host-based firewall rule configuration by removing a rule.""" + put_called = False + + def on_put(url, body, **kwargs): + nonlocal put_called + assert body == HBFW_REMOVE_RULE_PUT_REQUEST + put_called = True + return copy.deepcopy(HBFW_REMOVE_RULE_PUT_RESPONSE) + + cbcsdk_mock.mock_request('GET', '/policyservice/v1/orgs/test/policies/1492/configs/presentation', + POLICY_CONFIG_PRESENTATION) + cbcsdk_mock.mock_request('PUT', '/policyservice/v1/orgs/test/policies/1492/rule_configs/host_based_firewall', + on_put) + api = cbcsdk_mock.api + policy = Policy(api, 1492, copy.deepcopy(FULL_POLICY_5), False, True) + rule_config = policy.host_based_firewall_rule_config + result_groups = [group for group in rule_config.rule_groups if group.name == "Crapco_firewall"] + assert len(result_groups) == 1 + result_rules = [rule for rule in result_groups[0].rules_ if rule.name == "DoomyDoomsOfDoom"] + assert len(result_rules) == 1 + result_rules[0].remove() + rule_config.save() + assert put_called + result_groups = [group for group in rule_config.rule_groups if group.name == "Crapco_firewall"] + assert len(result_groups) == 1 + result_rules = result_groups[0].rules_ + assert len(result_rules) == 1 + assert result_rules[0].name == "my_first_rule" + + +def test_modify_remove_rule_group_from_host_based_firewall(cbcsdk_mock): + """Tests modifying a host-based firewall rule configuration by removing a rule group.""" + put_called = False + + def on_put(url, body, **kwargs): + nonlocal put_called + assert body == HBFW_REMOVE_RULE_GROUP_PUT_REQUEST + put_called = True + return copy.deepcopy(HBFW_REMOVE_RULE_GROUP_PUT_RESPONSE) + + cbcsdk_mock.mock_request('GET', '/policyservice/v1/orgs/test/policies/1492/configs/presentation', + POLICY_CONFIG_PRESENTATION) + cbcsdk_mock.mock_request('PUT', '/policyservice/v1/orgs/test/policies/1492/rule_configs/host_based_firewall', + on_put) + api = cbcsdk_mock.api + policy = Policy(api, 1492, copy.deepcopy(FULL_POLICY_5), False, True) + rule_config = policy.host_based_firewall_rule_config + result_groups = [group for group in rule_config.rule_groups if group.name == "Isolate"] + assert len(result_groups) == 1 + result_groups[0].remove() + rule_config.save() + assert put_called + result_groups = rule_config.rule_groups + assert len(result_groups) == 1 + assert result_groups[0].name == "Crapco_firewall" From 747cab0a9e878f4a50cea93c966bf8e916f45d27 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Thu, 27 Apr 2023 14:40:29 -0600 Subject: [PATCH 10/76] deflake8'd --- src/cbc_sdk/platform/policy_ruleconfigs.py | 4 +--- src/tests/unit/fixtures/platform/mock_policy_ruleconfigs.py | 3 +++ 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/cbc_sdk/platform/policy_ruleconfigs.py b/src/cbc_sdk/platform/policy_ruleconfigs.py index 225f0c485..785d68f37 100644 --- a/src/cbc_sdk/platform/policy_ruleconfigs.py +++ b/src/cbc_sdk/platform/policy_ruleconfigs.py @@ -302,9 +302,7 @@ def set_assignment_mode(self, mode): class HostBasedFirewallRuleConfig(PolicyRuleConfig): - """ - Represents a host-based firewall rule configuration in the policy. - """ + """Represents a host-based firewall rule configuration in the policy.""" 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): diff --git a/src/tests/unit/fixtures/platform/mock_policy_ruleconfigs.py b/src/tests/unit/fixtures/platform/mock_policy_ruleconfigs.py index 4312e04de..b6bf8779c 100644 --- a/src/tests/unit/fixtures/platform/mock_policy_ruleconfigs.py +++ b/src/tests/unit/fixtures/platform/mock_policy_ruleconfigs.py @@ -1,3 +1,6 @@ +"""Mock responses for PolicyRuleConfig and subclasses""" + + CORE_PREVENTION_RETURNS = { "results": [ { From 5ed8c0dace5d956ebf642828ee77d2b511fbbd66 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Fri, 28 Apr 2023 17:02:48 -0600 Subject: [PATCH 11/76] straightened out parent references in subobjects and the lazy load for firewall rule groups --- src/cbc_sdk/platform/policy_ruleconfigs.py | 71 +++++++++------------- 1 file changed, 29 insertions(+), 42 deletions(-) diff --git a/src/cbc_sdk/platform/policy_ruleconfigs.py b/src/cbc_sdk/platform/policy_ruleconfigs.py index 785d68f37..65f65ae50 100644 --- a/src/cbc_sdk/platform/policy_ruleconfigs.py +++ b/src/cbc_sdk/platform/policy_ruleconfigs.py @@ -138,12 +138,13 @@ def is_dirty(self): """ return self._params_changed or super(PolicyRuleConfig, self).is_dirty() - def get_parameter(self, name): + def get_parameter(self, name, default_value=None): """ Returns a parameter value from the rule configuration. Args: name (str): The parameter name. + default_value (Any): The default value to return if there's no parameter by that name. Default is None. Returns: Any: The parameter value, or None if there is no value. @@ -151,7 +152,7 @@ def get_parameter(self, name): if 'parameters' not in self._info: self.refresh() params = self._info['parameters'] - return params.get(name, None) + return params.get(name, default_value) def set_parameter(self, name, value): """ @@ -320,35 +321,25 @@ def __init__(self, cb, parent, model_unique_id=None, initial_data=None, force_in super(HostBasedFirewallRuleConfig, self).__init__(cb, parent, model_unique_id, initial_data, force_init, full_doc) self._rule_groups = [] - self._rule_groups_valid = False + self._rule_groups_loaded = False class FirewallRuleGroup(MutableBaseModel): """Represents a group of related firewall rules.""" swagger_meta_file = "platform/models/firewall_rule_group.yaml" - def __init__(self, cb, initial_data): + def __init__(self, cb, parent, initial_data): """ Initialize the FirewallRuleGroup object. Args: cb (BaseAPI): Reference to API object used to communicate with the server. initial_data (dict): Initial data used to populate the firewall rule group. + parent (HostBasedFirewallRuleConfig): The parent rule configuration. """ super(HostBasedFirewallRuleConfig.FirewallRuleGroup, self).__init__(cb, None, initial_data, False, True) - self._parent = None - self._rules = [HostBasedFirewallRuleConfig.FirewallRule(cb, d) - for d in initial_data.get("rules", [])] - - def _init_parent(self, parent): - """ - Initialize the parent of this group and its rules. - - Args: - parent (HostBasedFirewallRuleConfig): The parent to be set. - """ self._parent = parent - for rule in self._rules: - rule._parent = parent + self._rules = [HostBasedFirewallRuleConfig.FirewallRule(cb, parent, d) + for d in initial_data.get("rules", [])] def _field_updated(self, attrname): """Method called whenever a field is updated.""" @@ -399,16 +390,17 @@ class FirewallRule(MutableBaseModel): """Represents a single firewall rule.""" swagger_meta_file = "platform/models/firewall_rule.yaml" - def __init__(self, cb, initial_data): + def __init__(self, cb, parent, initial_data): """ Initialize the FirewallRule object. Args: cb (BaseAPI): Reference to API object used to communicate with the server. initial_data (dict): Initial data used to populate the firewall rule. + parent (HostBasedFirewallRuleConfig): The parent rule configuration. """ super(HostBasedFirewallRuleConfig.FirewallRule, self).__init__(cb, None, initial_data, False, True) - self._parent = None + self._parent = parent def _field_updated(self, attrname): """Method called whenever a field is updated.""" @@ -463,7 +455,7 @@ def _refresh(self): if ruleconfig_data: self._info = ruleconfig_data[0] self._rule_groups = [] - self._rule_groups_valid = False + self._rule_groups_loaded = False self._mark_changed(False) else: raise InvalidObjectError(f"invalid host-based firewall ID: {self._model_unique_id}") @@ -473,7 +465,7 @@ def _update_ruleconfig(self): """Perform the internal update of the rule configuration object.""" put_data = {"id": self.id, "parameters": {"enable_host_based_firewall": self.enabled, "default_rule": self.get_parameter('default_rule')}} - if self._rule_groups_valid: + if self._rule_groups_loaded: put_data['parameters']['rule_groups'] = [group._flatten() for group in self._rule_groups] else: put_data['parameters']['rule_groups'] = self.get_parameter('rule_groups') @@ -484,7 +476,7 @@ def _update_ruleconfig(self): raise ApiError("update of host-based firewall failed") self._info = success[0] self._rule_groups = [] - self._rule_groups_valid = False + self._rule_groups_loaded = False self._mark_changed(False) def _delete_ruleconfig(self): @@ -494,7 +486,7 @@ def _delete_ruleconfig(self): self._info = {"id": my_id} self._full_init = False # forcing _refresh() next time we read an attribute self._rule_groups = [] - self._rule_groups_valid = False + self._rule_groups_loaded = False self._mark_changed(False) @property @@ -548,15 +540,10 @@ def rule_groups(self): Returns: list[FirewallRuleGroup]: The list of rule groups. """ - if not self._rule_groups_valid: - rg_param = self.get_parameter("rule_groups") - if rg_param is not None: - self._rule_groups = [HostBasedFirewallRuleConfig.FirewallRuleGroup(self._cb, d) for d in rg_param] - for group in self._rule_groups: - group._init_parent(self) - else: - self._rule_groups = [] - self._rule_groups_valid = True + if not self._rule_groups_loaded: + self._rule_groups = [HostBasedFirewallRuleConfig.FirewallRuleGroup(self._cb, self, d) + for d in self.get_parameter("rule_groups", [])] + self._rule_groups_loaded = True return self._rule_groups def new_rule_group(self, name, description): @@ -570,8 +557,8 @@ def new_rule_group(self, name, description): Returns: FirewallRuleGroup: The new rule group object. Add it to this rule configuration with append_rule_group. """ - return HostBasedFirewallRuleConfig.FirewallRuleGroup(self._cb, {"name": name, "description": description, - "rules": []}) + return HostBasedFirewallRuleConfig.FirewallRuleGroup(self._cb, None, {"name": name, "description": description, + "rules": []}) def append_rule_group(self, rule_group): """ @@ -604,11 +591,11 @@ def new_rule(self, name, action, direction, protocol, remote_ip): raise ApiError(f"invalid rule direction: {direction}") if protocol not in ("TCP", "UDP"): raise ApiError(f"invalid rule protocol: {protocol}") - return HostBasedFirewallRuleConfig.FirewallRule(self._cb, {"action": action, "application_path": "*", - "direction": direction, "enabled": True, - "name": name, - "protocol": protocol, "local_ip_address": "*", - "local_port_ranges": "*", - "remote_ip_address": remote_ip, - "remote_port_ranges": "*", - "test_mode": False}) + return HostBasedFirewallRuleConfig.FirewallRule(self._cb, None, {"action": action, "application_path": "*", + "direction": direction, "enabled": True, + "name": name, + "protocol": protocol, "local_ip_address": "*", + "local_port_ranges": "*", + "remote_ip_address": remote_ip, + "remote_port_ranges": "*", + "test_mode": False}) From c3e1db16516f6c4afeade6a0b8f2ff021ab984f0 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Mon, 1 May 2023 16:36:29 -0600 Subject: [PATCH 12/76] split an overly long test, simplifying it, and eliminated _base_url() --- src/cbc_sdk/platform/policy_ruleconfigs.py | 60 +++++-------------- .../unit/platform/test_policy_ruleconfigs.py | 29 +++++---- 2 files changed, 32 insertions(+), 57 deletions(-) diff --git a/src/cbc_sdk/platform/policy_ruleconfigs.py b/src/cbc_sdk/platform/policy_ruleconfigs.py index 65f65ae50..574223ab4 100644 --- a/src/cbc_sdk/platform/policy_ruleconfigs.py +++ b/src/cbc_sdk/platform/policy_ruleconfigs.py @@ -33,6 +33,7 @@ class PolicyRuleConfig(MutableBaseModel): """ urlobject = "/policyservice/v1/orgs/{0}/policies" + urlobject_single = "/policyservice/v1/orgs/{0}/policies/{1}/rule_configs" primary_key = "id" swagger_meta_file = "platform/models/policy_ruleconfig.yaml" @@ -55,21 +56,6 @@ def __init__(self, cb, parent, model_unique_id=None, initial_data=None, force_in if model_unique_id is None: self.touch(True) - def _base_url(self): - """ - Calculates the base URL for these particular rule configs, including the org key and the parent policy ID. - - Returns: - str: The base URL for these particular rule configs. - - Raises: - InvalidObjectError: If the rule config object is unparented. - """ - if self._parent is None: - raise InvalidObjectError("no parent for rule config") - return PolicyRuleConfig.urlobject.format(self._cb.credentials.org_key) \ - + f"/{self._parent._model_unique_id}/rule_configs" - def _refresh(self): """ Refreshes the rule configuration object from the server. @@ -221,6 +207,7 @@ class CorePreventionRuleConfig(PolicyRuleConfig): permission. """ + urlobject_single = "/policyservice/v1/orgs/{0}/policies/{1}/rule_configs/core_prevention" 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): @@ -237,18 +224,6 @@ def __init__(self, cb, parent, model_unique_id=None, initial_data=None, force_in """ super(CorePreventionRuleConfig, self).__init__(cb, parent, model_unique_id, initial_data, force_init, full_doc) - def _base_url(self): - """ - Calculates the base URL for these particular rule configs, including the org key and the parent policy ID. - - Returns: - str: The base URL for these particular rule configs. - - Raises: - InvalidObjectError: If the rule config object is unparented. - """ - return super(CorePreventionRuleConfig, self)._base_url() + "/core_prevention" - def _refresh(self): """ Refreshes the rule configuration object from the server. @@ -262,7 +237,8 @@ def _refresh(self): Raises: InvalidObjectError: If the object is unparented or its ID is invalid. """ - return_data = self._cb.get_object(self._base_url()) + 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] @@ -273,12 +249,14 @@ 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(self._base_url(), body) + self._cb.put_object(url, body) def _delete_ruleconfig(self): """Perform the internal delete of the rule configuration object.""" - self._cb.delete_object(self._base_url() + f"/{self.id}") + url = self.urlobject_single.format(self._cb.credentials.org_key, self._parent._model_unique_id) + f"/{self.id}" + self._cb.delete_object(url) self._info["parameters"] = copy.deepcopy({"WindowsAssignmentMode": "BLOCK"}) # mirror server side def get_assignment_mode(self): @@ -304,6 +282,7 @@ def set_assignment_mode(self, mode): class HostBasedFirewallRuleConfig(PolicyRuleConfig): """Represents a host-based firewall rule configuration in the policy.""" + urlobject_single = "/policyservice/v1/orgs/{0}/policies/{1}/rule_configs/host_based_firewall" 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): @@ -425,18 +404,6 @@ def remove(self): self._parent._mark_changed() self._parent = None - def _base_url(self): - """ - Calculates the base URL for these particular rule configs, including the org key and the parent policy ID. - - Returns: - str: The base URL for these particular rule configs. - - Raises: - InvalidObjectError: If the rule config object is unparented. - """ - return super(HostBasedFirewallRuleConfig, self)._base_url() + "/host_based_firewall" - def _refresh(self): """ Refreshes the rule configuration object from the server. @@ -450,7 +417,8 @@ def _refresh(self): Raises: InvalidObjectError: If the object is unparented or its ID is invalid. """ - return_data = self._cb.get_object(self._base_url()) + 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] @@ -463,13 +431,14 @@ 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) put_data = {"id": self.id, "parameters": {"enable_host_based_firewall": self.enabled, "default_rule": self.get_parameter('default_rule')}} if self._rule_groups_loaded: put_data['parameters']['rule_groups'] = [group._flatten() for group in self._rule_groups] else: put_data['parameters']['rule_groups'] = self.get_parameter('rule_groups') - resp = self._cb.put_object(self._base_url(), [put_data]) + resp = self._cb.put_object(url, [put_data]) result = resp.json() success = [d for d in result.get("successful", []) if d.get("id", None) == self.id] if not success: @@ -482,7 +451,8 @@ def _update_ruleconfig(self): def _delete_ruleconfig(self): """Perform the internal delete of the rule configuration object.""" my_id = self.id - self._cb.delete_object(self._base_url() + f"/{my_id}") + url = self.urlobject_single.format(self._cb.credentials.org_key, self._parent._model_unique_id) + f"/{my_id}" + self._cb.delete_object(url) self._info = {"id": my_id} self._full_init = False # forcing _refresh() next time we read an attribute self._rule_groups = [] diff --git a/src/tests/unit/platform/test_policy_ruleconfigs.py b/src/tests/unit/platform/test_policy_ruleconfigs.py index 551cd893f..0385019ef 100644 --- a/src/tests/unit/platform/test_policy_ruleconfigs.py +++ b/src/tests/unit/platform/test_policy_ruleconfigs.py @@ -291,32 +291,37 @@ def on_delete(url, body): assert delete_called -def test_host_based_firewall_contents(cbcsdk_mock): +def test_host_based_firewall_contents(policy): """Tests the contents of the host-based firewall rule configuration, along with the refresh.""" - cbcsdk_mock.mock_request('GET', '/policyservice/v1/orgs/test/policies/65536/rule_configs/host_based_firewall', - HBFW_GET_RESULT) - api = cbcsdk_mock.api - policy = Policy(api, 65536, copy.deepcopy(FULL_POLICY_1), False, True) rule_config = policy.host_based_firewall_rule_config assert not rule_config.enabled assert rule_config.default_action == "ALLOW" groups = rule_config.rule_groups assert len(groups) == 1 assert groups[0].name == "Argon_firewall" - assert groups[0].description == "Whatever" rules = groups[0].rules_ assert len(rules) == 1 assert rules[0].action == "ALLOW" - assert rules[0].application_path == "*" assert rules[0].direction == "IN" - assert rules[0].enabled assert rules[0].local_ip_address == "1.2.3.4" - assert rules[0].local_port_ranges == "1234" assert rules[0].name == "my_first_rule" - assert rules[0].protocol == "TCP" assert rules[0].remote_ip_address == "5.6.7.8" - assert rules[0].remote_port_ranges == "5678" - assert not rules[0].test_mode + + +def test_host_based_firewall_refresh(cbcsdk_mock): + """Tests that refresh() restores values in the rule configuration.""" + cbcsdk_mock.mock_request('GET', '/policyservice/v1/orgs/test/policies/65536/rule_configs/host_based_firewall', + HBFW_GET_RESULT) + api = cbcsdk_mock.api + policy = Policy(api, 65536, copy.deepcopy(FULL_POLICY_1), False, True) + rule_config = policy.host_based_firewall_rule_config + rule_config.set_default_action("BLOCK") + groups = rule_config.rule_groups + assert len(groups) == 1 + rules = groups[0].rules_ + assert len(rules) == 1 + rules[0].local_ip_address = "127.0.0.1" + rules[0].remote_ip_address = "10.29.99.1" rule_config.refresh() assert not rule_config.enabled assert rule_config.default_action == "ALLOW" From 57ae9ed99dcee106c3db02dcfc46b67e8dd5d50e Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Wed, 3 May 2023 16:53:30 -0600 Subject: [PATCH 13/76] different implementation for tracking changes in FirewallRule/FirewallRuleGroup attributes --- src/cbc_sdk/base.py | 12 ++++++---- src/cbc_sdk/platform/policy_ruleconfigs.py | 28 +++++++++++++++++----- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/src/cbc_sdk/base.py b/src/cbc_sdk/base.py index 9f74b9037..3eeb48576 100644 --- a/src/cbc_sdk/base.py +++ b/src/cbc_sdk/base.py @@ -823,11 +823,14 @@ def __setattr__(self, attrname, val): else: object.__setattr__(self, attrname, val) - def _field_updated(self, attrname): - """Method called whenever a field is updated.""" - pass - def _set(self, attrname, new_value): + """ + Sets the value of an attribute on the object. + + Args: + attrname (str): Name of the attribute. + new_value (Any): Value of the attribute. + """ # ensure that we are operating on the full object first if not self._full_init and self._model_unique_id is not None: self.refresh() @@ -848,7 +851,6 @@ def _set(self, attrname, new_value): self._dirty_attributes[attrname] = self._info.get(attrname, None) # finally, make the change self._info[attrname] = new_value - self._field_updated(attrname) def refresh(self): """Reload this object from the server.""" diff --git a/src/cbc_sdk/platform/policy_ruleconfigs.py b/src/cbc_sdk/platform/policy_ruleconfigs.py index 574223ab4..260d003cb 100644 --- a/src/cbc_sdk/platform/policy_ruleconfigs.py +++ b/src/cbc_sdk/platform/policy_ruleconfigs.py @@ -320,9 +320,17 @@ def __init__(self, cb, parent, initial_data): self._rules = [HostBasedFirewallRuleConfig.FirewallRule(cb, parent, d) for d in initial_data.get("rules", [])] - def _field_updated(self, attrname): - """Method called whenever a field is updated.""" - if self._parent: + def _set(self, attrname, new_value): + """ + Sets the value of an attribute on the object. + + Args: + attrname (str): Name of the attribute. + new_value (Any): Value of the attribute. + """ + pristine = (attrname not in self._dirty_attributes) + super(HostBasedFirewallRuleConfig.FirewallRuleGroup, self)._set(attrname, new_value) + if self._parent and pristine and attrname in self._dirty_attributes: self._parent._mark_changed() def _flatten(self): @@ -381,9 +389,17 @@ def __init__(self, cb, parent, initial_data): super(HostBasedFirewallRuleConfig.FirewallRule, self).__init__(cb, None, initial_data, False, True) self._parent = parent - def _field_updated(self, attrname): - """Method called whenever a field is updated.""" - if self._parent: + def _set(self, attrname, new_value): + """ + Sets the value of an attribute on the object. + + Args: + attrname (str): Name of the attribute. + new_value (Any): Value of the attribute. + """ + pristine = (attrname not in self._dirty_attributes) + super(HostBasedFirewallRuleConfig.FirewallRule, self)._set(attrname, new_value) + if self._parent and pristine and attrname in self._dirty_attributes: self._parent._mark_changed() def _flatten(self): From 6e49b27927865e8bf8df1ad84a0ce4ed3a035304 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Fri, 5 May 2023 13:51:03 -0600 Subject: [PATCH 14/76] ditched _flatten, manipulated the _info directly in append/remove groups/rules --- src/cbc_sdk/platform/policies.py | 18 ++++++++ src/cbc_sdk/platform/policy_ruleconfigs.py | 42 ++++++------------- .../unit/platform/test_policy_ruleconfigs.py | 8 ++++ 3 files changed, 39 insertions(+), 29 deletions(-) diff --git a/src/cbc_sdk/platform/policies.py b/src/cbc_sdk/platform/policies.py index f870c4a7a..ce9f5eee5 100644 --- a/src/cbc_sdk/platform/policies.py +++ b/src/cbc_sdk/platform/policies.py @@ -624,6 +624,24 @@ def _refresh(self): self._object_rule_configs_need_load = True return rc + def is_dirty(self): + """ + Returns whether or not any fields of this object have been changed. + + Returns: + bool: True if any fields of this object have been changed, False if not. + """ + if super(Policy, self).is_dirty(): + return True + # we need to check the rules and rule configs as well + if not self._object_rules_need_load: + if any(rule.is_dirty() for rule in self._object_rules.values()): + return True + if not self._object_rule_configs_need_load: + if any(rule_config.is_dirty() for rule_config in self._object_rule_configs.values()): + return True + return False + @property def rules(self): """ diff --git a/src/cbc_sdk/platform/policy_ruleconfigs.py b/src/cbc_sdk/platform/policy_ruleconfigs.py index 260d003cb..7b776dba5 100644 --- a/src/cbc_sdk/platform/policy_ruleconfigs.py +++ b/src/cbc_sdk/platform/policy_ruleconfigs.py @@ -333,17 +333,6 @@ def _set(self, attrname, new_value): if self._parent and pristine and attrname in self._dirty_attributes: self._parent._mark_changed() - def _flatten(self): - """ - Turns this rule group into a dict for transferral to the server. - - Returns: - dict: The information defining the rule group and its constituent rules. - """ - rc = copy.deepcopy(self._info) - rc['rules'] = [rule._flatten() for rule in self._rules] - return rc - @property def rules_(self): """ @@ -363,15 +352,19 @@ def append_rule(self, rule): """ rule._parent = self._parent self._rules.append(rule) + self._info['rules'].append(rule._info) if self._parent: self._parent._mark_changed() def remove(self): """Removes this rule group from the rule configuration.""" - if self._parent and self in self._parent.rule_groups: - self._parent.rule_groups.remove(self) - self._parent._mark_changed() - self._parent = None + if self._parent: + location = [ndx for ndx, item in enumerate(self._parent.rule_groups) if item == self] + if location: + self._parent.rule_groups.remove(self) + del self._parent._info["parameters"]["rule_groups"][location[0]] + self._parent._mark_changed() + self._parent = None class FirewallRule(MutableBaseModel): """Represents a single firewall rule.""" @@ -402,21 +395,14 @@ def _set(self, attrname, new_value): if self._parent and pristine and attrname in self._dirty_attributes: self._parent._mark_changed() - def _flatten(self): - """ - Turns this rule into a dict for transferral to the server. - - Returns: - dict: The information defining the rule. - """ - return copy.deepcopy(self._info) - def remove(self): """Removes this rule from the rule group that contains it.""" if self._parent: group_list = [group for group in self._parent.rule_groups if self in group._rules] if group_list: + location = [ndx for ndx, item in enumerate(group_list[0]._rules) if item == self] group_list[0]._rules.remove(self) + del group_list[0]._info['rules'][location[0]] self._parent._mark_changed() self._parent = None @@ -449,11 +435,8 @@ 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) put_data = {"id": self.id, "parameters": {"enable_host_based_firewall": self.enabled, - "default_rule": self.get_parameter('default_rule')}} - if self._rule_groups_loaded: - put_data['parameters']['rule_groups'] = [group._flatten() for group in self._rule_groups] - else: - put_data['parameters']['rule_groups'] = self.get_parameter('rule_groups') + "default_rule": self.get_parameter('default_rule'), + "rule_groups": self.get_parameter('rule_groups')}} resp = self._cb.put_object(url, [put_data]) result = resp.json() success = [d for d in result.get("successful", []) if d.get("id", None) == self.id] @@ -554,6 +537,7 @@ def append_rule_group(self, rule_group): rule_group (FirewallRuleGroup): The rule group to be added. """ self.rule_groups.append(rule_group) + self._info['parameters']['rule_groups'].append(rule_group._info) rule_group._parent = self self._mark_changed() diff --git a/src/tests/unit/platform/test_policy_ruleconfigs.py b/src/tests/unit/platform/test_policy_ruleconfigs.py index 0385019ef..ac28f7416 100644 --- a/src/tests/unit/platform/test_policy_ruleconfigs.py +++ b/src/tests/unit/platform/test_policy_ruleconfigs.py @@ -422,6 +422,7 @@ def on_put(url, body, **kwargs): on_put) api = cbcsdk_mock.api policy = Policy(api, 65536, copy.deepcopy(FULL_POLICY_1), False, True) + assert not policy.is_dirty() rule_config = policy.host_based_firewall_rule_config groups = rule_config.rule_groups new_rule = rule_config.new_rule("DoomyDoomsOfDoom", "BLOCK", "BOTH", "TCP", "199.201.128.1") @@ -429,6 +430,7 @@ def on_put(url, body, **kwargs): new_rule.local_ip_address = "10.29.99.1" new_rule.application_path = "C:\\DOOM\\DOOM.EXE" groups[0].append_rule(new_rule) + assert policy.is_dirty() rule_config.save() assert put_called groups = rule_config.rule_groups @@ -476,6 +478,7 @@ def on_put(url, body, **kwargs): on_put) api = cbcsdk_mock.api policy = Policy(api, 65536, copy.deepcopy(FULL_POLICY_1), False, True) + assert not policy.is_dirty() rule_config = policy.host_based_firewall_rule_config new_group = rule_config.new_rule_group("DOOM_firewall", "No playing DOOM!") new_rule = rule_config.new_rule("DoomyDoomsOfDoom", "BLOCK", "BOTH", "TCP", "199.201.128.1") @@ -484,6 +487,7 @@ def on_put(url, body, **kwargs): new_rule.application_path = "C:\\DOOM\\DOOM.EXE" new_group.append_rule(new_rule) rule_config.append_rule_group(new_group) + assert policy.is_dirty() rule_config.save() assert put_called groups = rule_config.rule_groups @@ -516,12 +520,14 @@ def on_put(url, body, **kwargs): on_put) api = cbcsdk_mock.api policy = Policy(api, 1492, copy.deepcopy(FULL_POLICY_5), False, True) + assert not policy.is_dirty() rule_config = policy.host_based_firewall_rule_config result_groups = [group for group in rule_config.rule_groups if group.name == "Crapco_firewall"] assert len(result_groups) == 1 result_rules = [rule for rule in result_groups[0].rules_ if rule.name == "DoomyDoomsOfDoom"] assert len(result_rules) == 1 result_rules[0].remove() + assert policy.is_dirty() rule_config.save() assert put_called result_groups = [group for group in rule_config.rule_groups if group.name == "Crapco_firewall"] @@ -547,10 +553,12 @@ def on_put(url, body, **kwargs): on_put) api = cbcsdk_mock.api policy = Policy(api, 1492, copy.deepcopy(FULL_POLICY_5), False, True) + assert not policy.is_dirty() rule_config = policy.host_based_firewall_rule_config result_groups = [group for group in rule_config.rule_groups if group.name == "Isolate"] assert len(result_groups) == 1 result_groups[0].remove() + assert policy.is_dirty() rule_config.save() assert put_called result_groups = rule_config.rule_groups From 9021226566777aa02084196e3211faa0bc1bf66d Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Fri, 5 May 2023 14:06:31 -0600 Subject: [PATCH 15/76] allow optional values for new_rule to be added via kwargs --- src/cbc_sdk/platform/policy_ruleconfigs.py | 19 ++++++++++--------- .../unit/platform/test_policy_ruleconfigs.py | 14 ++++++-------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/src/cbc_sdk/platform/policy_ruleconfigs.py b/src/cbc_sdk/platform/policy_ruleconfigs.py index 7b776dba5..10aaa9f76 100644 --- a/src/cbc_sdk/platform/policy_ruleconfigs.py +++ b/src/cbc_sdk/platform/policy_ruleconfigs.py @@ -541,7 +541,7 @@ def append_rule_group(self, rule_group): rule_group._parent = self self._mark_changed() - def new_rule(self, name, action, direction, protocol, remote_ip): + def new_rule(self, name, action, direction, protocol, remote_ip, **kwargs): """ Creates a new FirewallRule object. @@ -551,6 +551,7 @@ def new_rule(self, name, action, direction, protocol, remote_ip): direction (str): The traffic direction this rule matches. Valid values are "IN," "OUT," and "BOTH." protocol (str): The network protocol this rule matches. Valid values are "TCP" and "UDP." remote_ip (str): The remote IP address this rule matches. + kwargs (dict): Additional parameters which may be added to the new rule. Returns: FirewallRule: The new firewall rule. Append it to a rule group using the group's append_rule method. @@ -561,11 +562,11 @@ def new_rule(self, name, action, direction, protocol, remote_ip): raise ApiError(f"invalid rule direction: {direction}") if protocol not in ("TCP", "UDP"): raise ApiError(f"invalid rule protocol: {protocol}") - return HostBasedFirewallRuleConfig.FirewallRule(self._cb, None, {"action": action, "application_path": "*", - "direction": direction, "enabled": True, - "name": name, - "protocol": protocol, "local_ip_address": "*", - "local_port_ranges": "*", - "remote_ip_address": remote_ip, - "remote_port_ranges": "*", - "test_mode": False}) + # specify defaults for optional params, overlay kwargs, then add in the required params + params = {"application_path": "*", "enabled": True, "local_ip_address": "*", "local_port_ranges": "*", + "remote_port_ranges": "*", "test_mode": False} + specified_params = {k: v for k, v in kwargs.items() if k in params.keys()} + params.update(specified_params) + params.update({"action": action, "direction": direction, "name": name, "protocol": protocol, + "remote_ip_address": remote_ip}) + return HostBasedFirewallRuleConfig.FirewallRule(self._cb, None, params) diff --git a/src/tests/unit/platform/test_policy_ruleconfigs.py b/src/tests/unit/platform/test_policy_ruleconfigs.py index ac28f7416..80c2dce1a 100644 --- a/src/tests/unit/platform/test_policy_ruleconfigs.py +++ b/src/tests/unit/platform/test_policy_ruleconfigs.py @@ -425,10 +425,9 @@ def on_put(url, body, **kwargs): assert not policy.is_dirty() rule_config = policy.host_based_firewall_rule_config groups = rule_config.rule_groups - new_rule = rule_config.new_rule("DoomyDoomsOfDoom", "BLOCK", "BOTH", "TCP", "199.201.128.1") - new_rule.remote_port_ranges = "666" - new_rule.local_ip_address = "10.29.99.1" - new_rule.application_path = "C:\\DOOM\\DOOM.EXE" + new_rule = rule_config.new_rule("DoomyDoomsOfDoom", "BLOCK", "BOTH", "TCP", "199.201.128.1", + remote_port_ranges="666", local_ip_address="10.29.99.1", + application_path="C:\\DOOM\\DOOM.EXE") groups[0].append_rule(new_rule) assert policy.is_dirty() rule_config.save() @@ -481,10 +480,9 @@ def on_put(url, body, **kwargs): assert not policy.is_dirty() rule_config = policy.host_based_firewall_rule_config new_group = rule_config.new_rule_group("DOOM_firewall", "No playing DOOM!") - new_rule = rule_config.new_rule("DoomyDoomsOfDoom", "BLOCK", "BOTH", "TCP", "199.201.128.1") - new_rule.remote_port_ranges = "666" - new_rule.local_ip_address = "10.29.99.1" - new_rule.application_path = "C:\\DOOM\\DOOM.EXE" + new_rule = rule_config.new_rule("DoomyDoomsOfDoom", "BLOCK", "BOTH", "TCP", "199.201.128.1", + remote_port_ranges="666", local_ip_address="10.29.99.1", + application_path="C:\\DOOM\\DOOM.EXE") new_group.append_rule(new_rule) rule_config.append_rule_group(new_group) assert policy.is_dirty() From c7f55d38fe9bb7a85c9b50be0325a31e095d88cb Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Fri, 5 May 2023 16:27:58 -0600 Subject: [PATCH 16/76] fixed location determination to use index() instead of list comprehension search --- src/cbc_sdk/platform/policy_ruleconfigs.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/cbc_sdk/platform/policy_ruleconfigs.py b/src/cbc_sdk/platform/policy_ruleconfigs.py index 10aaa9f76..ddba4373b 100644 --- a/src/cbc_sdk/platform/policy_ruleconfigs.py +++ b/src/cbc_sdk/platform/policy_ruleconfigs.py @@ -359,12 +359,13 @@ def append_rule(self, rule): def remove(self): """Removes this rule group from the rule configuration.""" if self._parent: - location = [ndx for ndx, item in enumerate(self._parent.rule_groups) if item == self] - if location: - self._parent.rule_groups.remove(self) - del self._parent._info["parameters"]["rule_groups"][location[0]] + try: + location = self._parent.rule_groups.index(self) + del self._parent._info["parameters"]["rule_groups"][location] self._parent._mark_changed() self._parent = None + except ValueError: + pass class FirewallRule(MutableBaseModel): """Represents a single firewall rule.""" @@ -400,9 +401,9 @@ def remove(self): if self._parent: group_list = [group for group in self._parent.rule_groups if self in group._rules] if group_list: - location = [ndx for ndx, item in enumerate(group_list[0]._rules) if item == self] + location = group_list[0]._rules.index(self) group_list[0]._rules.remove(self) - del group_list[0]._info['rules'][location[0]] + del group_list[0]._info['rules'][location] self._parent._mark_changed() self._parent = None From c0c8327dc831cd67bb653495af5e3a06a56d746e Mon Sep 17 00:00:00 2001 From: Emanuela Mitreva Date: Mon, 8 May 2023 18:36:12 +0300 Subject: [PATCH 17/76] Auth Events uat tests --- src/tests/uat/auth_events_uat.py | 190 +++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 src/tests/uat/auth_events_uat.py diff --git a/src/tests/uat/auth_events_uat.py b/src/tests/uat/auth_events_uat.py new file mode 100644 index 000000000..487637496 --- /dev/null +++ b/src/tests/uat/auth_events_uat.py @@ -0,0 +1,190 @@ +#!/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. + +""" +To execute, a profile must be provided using the standard CBC Credentials. + +Auth Events: +https://developer.carbonblack.com/reference/carbon-black-cloud/cb-threathunter/latest/auth-events-api +""" + +import sys +import time +import requests + +from cbc_sdk.helpers import build_cli_parser, get_cb_cloud_object +from cbc_sdk.enterprise_edr import AuthEvent, AuthEventFacet + +# ------------------------------ APIs ---------------------------------------------- + +# search job + grouped results +START_SEARCH_JOB = "{}api/investigate/v2/orgs/{}/auth_events/search_jobs" +# by default only 10 are returned +GET_SEARCH_RESULTS = "{}api/investigate/v2/orgs/{}/auth_events/search_jobs/{}/results?start=0&rows=500" +GET_GROUPED_RESULTS = "{}api/investigate/v2/orgs/{}/auth_events/search_jobs/{}/group_results" +QUERY = "auth_username:Administrator" + +# detail job +START_DETAILS_JOB = "{}api/investigate/v2/orgs/{}/auth_events/detail_jobs" +GET_DETAILS_RESULTS = "{}api/investigate/v2/orgs/{}/auth_events/detail_jobs/{}/results" + +# facet job +START_FACET_JOB = "{}api/investigate/v2/orgs/{}/auth_events/facet_jobs" +GET_FACET_RESULTS = "{}api/investigate/v2/orgs/{}/auth_events/facet_jobs/{}/results" + +# others +SEARCH_SUGGESTIONS = "{}api/investigate/v2/orgs/{}/auth_events/search_suggestions?suggest.q={}" +GET_DESCRIPTIONS = "{}api/investigate/v2/orgs/{}/auth_events/descriptions" + +# ------------------------------ Formatters ------------------------------------------ + +HEADERS = {"X-Auth-Token": "", "Content-Type": "application/json"} +ORG_KEY = "" +HOSTNAME = "" +DELIMITER = "-" +SYMBOLS = 70 +AUTH_EVENT_ID = 0 +SECTION_TITLES = ["Auth Events"] +TITLES = [ + "Get Search Suggestions", + "Get Auth Events Description", + "Get Search Results", + "Get Search Grouped Results", + "Get Details Results", + "Get Facet Data" +] + +# ------------------------------ Helper functions ------------------------------------- + + +def get_search_results(): + """Get search results - both groupped and not grouped""" + global AUTH_EVENT_ID + sdata = {"query": QUERY, "time_range": {"window": "-1d"}} + gdata = {"fields": ["*"], "range": {}, "rows": 50} + job_id = requests.post(START_SEARCH_JOB.format(HOSTNAME, ORG_KEY), headers=HEADERS, json=sdata).json()["job_id"] + time.sleep(2) + results = requests.get(GET_SEARCH_RESULTS.format(HOSTNAME, ORG_KEY, job_id), headers=HEADERS).json() + AUTH_EVENT_ID = results["results"][0]["event_id"] + gresults = requests.post( + GET_GROUPED_RESULTS.format(HOSTNAME, ORG_KEY, job_id), + headers=HEADERS, + json=gdata, + ).json() + return results["results"], gresults["group_results"] + + +def get_details_results(event_id): + """Get details results""" + ddata = {"event_ids": [event_id]} + job_id = requests.post(START_DETAILS_JOB.format(HOSTNAME, ORG_KEY), headers=HEADERS, json=ddata).json()["job_id"] + time.sleep(0.5) + results = requests.get(GET_DETAILS_RESULTS.format(HOSTNAME, ORG_KEY, job_id), headers=HEADERS) + return results.json()["results"][0] + + +def get_facet_results(): + """Get facet results""" + fdata = { + "query": QUERY, + "terms": {"fields": ["device_name"]}, + } + job_id = requests.post(START_FACET_JOB.format(HOSTNAME, ORG_KEY), headers=HEADERS, json=fdata).json()["job_id"] + time.sleep(0.5) + results = requests.get(GET_FACET_RESULTS.format(HOSTNAME, ORG_KEY, job_id), headers=HEADERS) + return results.json() + + +def get_search_suggestions(q="auth"): + """Get search suggestions""" + results = requests.get(SEARCH_SUGGESTIONS.format(HOSTNAME, ORG_KEY, q), headers=HEADERS) + return results.json()["suggestions"] + + +def get_descriptions(): + """Get descriptions""" + results = requests.get(GET_DESCRIPTIONS.format(HOSTNAME, ORG_KEY), headers=HEADERS) + return results.json() + + +# ------------------------------ Main ------------------------------------------------- + + +def main(): + """Script entry point""" + global ORG_KEY + global HOSTNAME + parser = build_cli_parser() + args = parser.parse_args() + print_detail = args.verbose + + if print_detail: + print(f"profile being used is {args.__dict__}") + + cb = get_cb_cloud_object(args) + HEADERS["X-Auth-Token"] = cb.credentials.token + ORG_KEY = cb.credentials.org_key + HOSTNAME = cb.credentials.url + + print() + print(f"{SECTION_TITLES[0]:^70}") + print(SYMBOLS * DELIMITER) + + api_result = get_search_suggestions() + sdk_result = AuthEvent.search_suggestions(cb, query="auth") + assert api_result == sdk_result, f"Test Failed Expected: {api_result} Actual: {sdk_result}" + print(TITLES[0] + "." * (SYMBOLS - len(TITLES[0]) - 2) + "OK") + + api_result = get_descriptions() + sdk_result = AuthEvent.get_auth_events_descriptions(cb) + assert api_result == sdk_result, f"Test Failed Expected: {api_result} Actual: {sdk_result}" + print(TITLES[1] + "." * (SYMBOLS - len(TITLES[1]) - 2) + "OK") + + # check get search job + sapi_result, gapi_result = get_search_results() + sdk_r = cb.select(AuthEvent).where(QUERY).set_time_range(window="-1d") + ssdk_result = [x._info for x in sdk_r] + assert sapi_result == ssdk_result, f"Test Failed Expected: {sapi_result} Actual: {ssdk_result}" + print(TITLES[2] + "." * (SYMBOLS - len(TITLES[2]) - 2) + "OK") + + # check get group results + sdk_result = [y._info for x in sdk_r.group_results("device_name") for y in x.auth_events] + api_result = [] + for group in gapi_result: + api_result.extend(group["results"]) + assert api_result == sdk_result, f"Test Failed Expected: {api_result} Actual: {sdk_result}" + print(TITLES[3] + "." * (SYMBOLS - len(TITLES[3]) - 2) + "OK") + auth_event = sdk_r[0] + # check get details job + api_result = get_details_results(auth_event.event_id) + sdk_result = auth_event.get_details()._info + assert api_result == sdk_result, f"Test Failed Expected: {api_result} Actual: {sdk_result}" + print(TITLES[4] + "." * (SYMBOLS - len(TITLES[4]) - 2) + "OK") + + # check get facet job + api_result = get_facet_results()["terms"] + xx = ( + cb.select(AuthEventFacet) + .where(QUERY) + .add_facet_field("device_name") + .results + ) + sdk_result = xx.terms + assert api_result == sdk_result, f"Test Failed Expected: {api_result} Actual: {sdk_result}" + print(TITLES[5] + "." * (SYMBOLS - len(TITLES[5]) - 2) + "OK") + + +if __name__ == "__main__": + try: + sys.exit(main()) + except KeyboardInterrupt: + print("\nInterrupted by user") From aed1c6495aed3dd4ebe7914494dd0f620d8cb660 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Mon, 8 May 2023 10:55:18 -0600 Subject: [PATCH 18/76] limit requests version to keep from requiring a urllib3 that conflicts with botocore/boto3 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1138b55d4..55425bf4e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # Package dependencies -requests +requests<=2.29.0 pyyaml python-dateutil schema From 4a35131d569577c89825a0bb2eee4fd44119995d Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Mon, 8 May 2023 11:06:45 -0600 Subject: [PATCH 19/76] include a .readthedocs.yaml config file to try and work around the urllib3 issue --- .readthedocs.yaml | 29 +++++++++++++++++++++++++++++ requirements.txt | 2 +- 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 .readthedocs.yaml diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 000000000..7706e525d --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,29 @@ +# .readthedocs.yaml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the version of Python and other tools you might need +build: + os: ubuntu-20.04 + tools: + python: "3.7" + # You can also specify other tool versions: + # nodejs: "19" + # rust: "1.64" + # golang: "1.19" + +# Build documentation in the docs/ directory with Sphinx +sphinx: + configuration: docs/conf.py + +# If using Sphinx, optionally build your docs in additional formats such as PDF +# formats: +# - pdf + +# Optionally declare the Python requirements required to build your docs +python: + install: + - requirements: docs/requirements.txt diff --git a/requirements.txt b/requirements.txt index 55425bf4e..1138b55d4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # Package dependencies -requests<=2.29.0 +requests pyyaml python-dateutil schema From 4636309954f44606d842a425eb5d808a93444a1c Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Mon, 8 May 2023 11:19:45 -0600 Subject: [PATCH 20/76] try #2 to see if it will work --- .readthedocs.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 7706e525d..d303c6b41 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -27,3 +27,8 @@ sphinx: python: install: - requirements: docs/requirements.txt + - method: setuptools + path: . + extra_requirements: + - docs + system_packages: true From d400ce58eaf9f272fd3d6c4d44bcd54806b7c11c Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Mon, 8 May 2023 11:21:30 -0600 Subject: [PATCH 21/76] removed bogus lines --- .readthedocs.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index d303c6b41..38a88f250 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -29,6 +29,4 @@ python: - requirements: docs/requirements.txt - method: setuptools path: . - extra_requirements: - - docs system_packages: true From dc23020ec3326f6bbbc0d818aaee605b12300176 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Mon, 8 May 2023 11:30:13 -0600 Subject: [PATCH 22/76] try pip again instead of setuptools --- .readthedocs.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 38a88f250..944e01029 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -27,6 +27,6 @@ sphinx: python: install: - requirements: docs/requirements.txt - - method: setuptools + - method: pip path: . system_packages: true From 8d9afe9ba336130bf5e70d2469219083d6947a03 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Mon, 8 May 2023 11:44:12 -0600 Subject: [PATCH 23/76] try again --- .readthedocs.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 944e01029..7706e525d 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -27,6 +27,3 @@ sphinx: python: install: - requirements: docs/requirements.txt - - method: pip - path: . - system_packages: true From 0cd4eeac331a564773cfdc321c984a1f83ce72d7 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Mon, 8 May 2023 11:49:13 -0600 Subject: [PATCH 24/76] added fail_on_warning to configuration --- .readthedocs.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 7706e525d..7310209a5 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -18,6 +18,7 @@ build: # Build documentation in the docs/ directory with Sphinx sphinx: configuration: docs/conf.py + fail_on_warning: true # If using Sphinx, optionally build your docs in additional formats such as PDF # formats: From 93c3a211868d9f9b4baeecbce7c8ef680db9b55e Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Mon, 8 May 2023 12:10:17 -0600 Subject: [PATCH 25/76] seeing if this works now --- .readthedocs.yaml | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 7310209a5..46094c556 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -7,9 +7,9 @@ version: 2 # Set the version of Python and other tools you might need build: - os: ubuntu-20.04 + os: ubuntu-22.04 tools: - python: "3.7" + python: "3.8" # You can also specify other tool versions: # nodejs: "19" # rust: "1.64" @@ -21,10 +21,15 @@ sphinx: fail_on_warning: true # If using Sphinx, optionally build your docs in additional formats such as PDF -# formats: -# - pdf +formats: + - pdf + - epub # Optionally declare the Python requirements required to build your docs python: install: - - requirements: docs/requirements.txt + - method: pip + path: . + requirements: docs/requirements.txt + - method: pip + path: package From 1c4e2ac7bef1b9fc63f20e56e0cfee9a92f043b2 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Mon, 8 May 2023 12:14:56 -0600 Subject: [PATCH 26/76] gain --- .readthedocs.yaml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 46094c556..09e43ed86 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -28,8 +28,7 @@ formats: # Optionally declare the Python requirements required to build your docs python: install: - - method: pip + - requirements: docs/requirements.txt + - requirements: requirements.txt + - method: setuptools path: . - requirements: docs/requirements.txt - - method: pip - path: package From 396effd8f7f536f62af761fc7a5755b105a32c1e Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Mon, 8 May 2023 12:24:05 -0600 Subject: [PATCH 27/76] turn off fail on warning because it looks like it's finding stuff now --- .readthedocs.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 09e43ed86..31cb715d1 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -18,7 +18,7 @@ build: # Build documentation in the docs/ directory with Sphinx sphinx: configuration: docs/conf.py - fail_on_warning: true +# fail_on_warning: true # If using Sphinx, optionally build your docs in additional formats such as PDF formats: From 9d1f723db955e139670940bb219805cc1ddbcccd Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Mon, 8 May 2023 12:38:19 -0600 Subject: [PATCH 28/76] meaningless change to force restart --- .readthedocs.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 31cb715d1..2559611fa 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -20,7 +20,7 @@ sphinx: configuration: docs/conf.py # fail_on_warning: true -# If using Sphinx, optionally build your docs in additional formats such as PDF +# If using Sphinx, optionally build your docs in additional formats, such as PDF formats: - pdf - epub From 4d803e84520d9fc50ecc2ea93dfd1a349b833ba6 Mon Sep 17 00:00:00 2001 From: Kylie Ebringer Date: Tue, 9 May 2023 16:00:12 -0600 Subject: [PATCH 29/76] Added example script for host-based firewall module. Fixed a typo in another script. Added linkage from rst guides and docs to example scripts --- docs/guides-and-resources.rst | 1 + docs/policy.rst | 18 +++ examples/platform/policy_core_prevention.py | 4 +- .../platform/policy_host_based_firewall.py | 152 ++++++++++++++++++ .../policy_service_crud_operations.py | 16 ++ 5 files changed, 189 insertions(+), 2 deletions(-) create mode 100644 docs/policy.rst create mode 100644 examples/platform/policy_host_based_firewall.py diff --git a/docs/guides-and-resources.rst b/docs/guides-and-resources.rst index 1af00deba..3518b42b5 100755 --- a/docs/guides-and-resources.rst +++ b/docs/guides-and-resources.rst @@ -31,6 +31,7 @@ Guides * :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 * :doc:`live-response` - Live Response allows security operators to collect information and take action on remote endpoints in real time. +* :doc:`policy` - Use policies to define and prioritize rules for how applications can behave on groups of assets * :doc:`recommendations` - Work with Endpoint Standard recommendations for reputation override. * :doc:`reputation-override` - Manage reputation overrides for known applications, IT tools or certs. * :doc:`unified-binary-store` - The unified binary store (UBS) is responsible for storing all binaries and corresponding metadata for those binaries. diff --git a/docs/policy.rst b/docs/policy.rst new file mode 100644 index 000000000..ddf52bba1 --- /dev/null +++ b/docs/policy.rst @@ -0,0 +1,18 @@ +Policy - Core Prevention and Host-Based Firewall Examples +========================================================= + + +A policy determines preventative behavior and establishes sensor settings. Each endpoint sensor or sensor group +is assigned a policy. + +Policies are a collection of prevention rules and behavioral settings that define how your sensor interacts and +prevents or allows behavior on your endpoint. Within Policies, you can create custom blocking rules, allow +applications, and modify the way your sensor communicates with the Carbon Black Cloud. + +Example scripts are available in the GitHub repository in examples/platform that demonstrate +* Basic Create, Read, Update, Delete and Export/Import operations for Prevention, Local Scan and Sensor rules + * policy_service_crud_operations.py +* Core Prevention policy rule operations + * policy_core_prevention.py +* Host-Based Firewall policy rule operations + * policy_host_based_firewall.py diff --git a/examples/platform/policy_core_prevention.py b/examples/platform/policy_core_prevention.py index c99fd20b9..f6ebbab15 100644 --- a/examples/platform/policy_core_prevention.py +++ b/examples/platform/policy_core_prevention.py @@ -10,7 +10,7 @@ # * WARRANTIES OR CONDITIONS OF MERCHANTABILITY, SATISFACTORY QUALITY, # * NON-INFRINGEMENT AND FITNESS FOR A PARTICULAR PURPOSE. -"""Example script which sends control messages to devices.""" +"""Example script which lists and updates core prevention settings in a policy.""" import sys from cbc_sdk.helpers import build_cli_parser, get_cb_cloud_object @@ -57,7 +57,7 @@ def set_core_prevention_status(policy, config_name, mode): def main(): - """Main function for Device Actions script.""" + """Main function for Core Prevention example script.""" parser = build_cli_parser("View or set core prevention settings on a policy") parser.add_argument("-p", "--policy", type=int, required=True, help="The ID of the policy to be manipulated") subparsers = parser.add_subparsers(dest="command", required=True) diff --git a/examples/platform/policy_host_based_firewall.py b/examples/platform/policy_host_based_firewall.py new file mode 100644 index 000000000..825a36767 --- /dev/null +++ b/examples/platform/policy_host_based_firewall.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python +# ******************************************************* +# Copyright (c) VMware, Inc. 2020-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. + +"""Example script which manipulates rules in the Host-Based Firewall component of policy. + +There are two methods that demonstrate interactions with the Host-Based Firewall Policy. +The first prints summary information about each policy, and optionally iterates through each core +prevention and host-based firewall rules. +This is an example of the command line to execute. --all_details True will include the rules. +> python examples/platform/policy_host_based_firewall.py --profile EXAMPLE_CREDENTIALS --all_details True + +The second method creates two new host-based firewall rules in a new rule group on an existing policy. +The name and description of the group are passed in on the command line, the rules are hard coded for +to make the example easier to read. +This is an example of the command line to execute where -p 12345678 specifies the policy to operate on +> python examples/platform/policy_host_based_firewall.py --profile EXAMPLE_CREDENTIALS -p 12345678 / + create_rule --rule_group_name sdk_test_two --rule_group_desc sdk_test_two_desc +""" + +import sys +from cbc_sdk.helpers import build_cli_parser, get_cb_cloud_object +from cbc_sdk.platform import Policy +from cbc_sdk.platform.policies import HostBasedFirewallRuleConfig + + +def list_policy_summaries(cb, args): + """List all policies and their rules.""" + # the cb.select(Policy) with no parameters will get all the policies for the organization + for p in cb.select(Policy): + print(u"Policy id {0}: {1} {2}".format(p.id, p.name, "({0})".format(p.description) if p.description else "")) + print("Rules:") + # the command line argument of "--all_details" is used here + if args.all_details: + for r in p.rules: + print(" {0}: {1} when {2} {3} is {4}".format(r.get("id"), r.get("action"), + r.get("application", {}).get("type"), + r.get("application", {}).get("value"), + r.get("operation"))) + print("Core Prevention") + if p.core_prevention_rule_configs is None: + print("No Core Prevention Rules") + else: + if args.all_details: + for cprc in p.core_prevention_rule_configs.values(): + print(" {0}".format(cprc.name)) + else: + print("Details not requested.") + + print("Host-Based Firewall") + if p.host_based_firewall_rule_config is None: + print("No Host-Based Firewall Rules") + else: + if args.all_details: + hbfwr = p.host_based_firewall_rule_config + print("Rule config name: {0}".format(hbfwr.name)) + if hbfwr.rule_groups is None: + print("No rule groups") + else: + for rg in hbfwr.rule_groups: + print("Rule Group Name: {0}".format(rg.name)) + if rg.rules is None: + print("No rules") + else: + for r in rg.rules: + print("rule: {0}".format(r)) + else: + print("Details not requested.") + + print("") + print("End of Policy Object") + print("") + + +def add_hbfw_rule(cb, args): + """Create a rule and add it to the rule group specified in the input args""" + # get the policy specified in the input parameters + policy = cb.select(Policy, args.policy) + print("id: {0}. name: {1}".format(policy.id, policy.name)) + # If the policy does not yet have a host-based firewall section then create it + if policy.host_based_firewall_rule_config is None: + policy.host_based_firewall_rule_config = HostBasedFirewallRuleConfig(cb, policy) + + # get the rule config in a variable to work with + rc = policy.host_based_firewall_rule_config + # create a new rule group + rg = rc.new_rule_group(args.rule_group_name, args.rule_group_desc) + # add the rule group to the rule config + rc.append_rule_group(rg) # to do remove this + + # create two rules + # name = SDK Example Rule One, action = ALLOW, direction = IN, protocol = TCP, remote ip address = 15.16.17.18 + r1 = rc.new_rule("SDK Example Rule One", "ALLOW", "IN", "TCP", "15.16.17.18") + # then set the rest of the fields in the rule + r1.application_path = "C:\\sdk\\example\\allow\\rule\\path" + r1.enabled = False + r1.local_ip_address = "11.12.13.14" + r1.local_port_ranges = "1313" + r1.remote_port_ranges = "2121" + # append the rule to the group + rg.append_rule(r1) # TO DO - remove this + # create the second rule + r2 = rc.new_rule("The second SDK Example Rule", "BLOCK", "OUT", "UDP", "5.6.7.8") + # then set the rest of the fields in the rule + r2.application_path = "C:\\another\\sdk\\example\\path" + r2.enabled = True + r2.local_ip_address = "1.2.3.4" + r2.local_port_ranges = "3131" + r2.remote_port_ranges = "1212" + # append the rule to the group + rg.append_rule(r2) # TO DO - remove this + # To do - either remove rc.save() if the policy.save() does the rules too or document that rule config must be saved + rc.save() + policy.save() + print(rc) + + +def main(): + """Main function for Policy - Host-Based Firewall script.""" + parser = build_cli_parser("View or set host based firewall rules on a policy") + parser.add_argument("-p", "--policy", type=int, required=False, help="The ID of the policy to be manipulated") + subparsers = parser.add_subparsers(dest="command", required=True) + + policy_summaries = subparsers.add_parser("list_policies", help="List summary information about each policy") + policy_summaries.add_argument("--all_details", help="Print core prevention and host-based firewall rules") + + create_rule = subparsers.add_parser("create_rule", help="Create a new Host-Based Firewall rule") + create_rule.add_argument("--rule_group_name", help="The name of the rule group to hold the new rule") + create_rule.add_argument("--rule_group_desc", help="The description of the new rule group") + + args = parser.parse_args() + cb = get_cb_cloud_object(args) + + if args.command == "list_policies": + list_policy_summaries(cb, args) + elif args.command == "create_rule": + add_hbfw_rule(cb, args) + else: + raise NotImplementedError("Unknown command") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/examples/platform/policy_service_crud_operations.py b/examples/platform/policy_service_crud_operations.py index 28ac29f08..63a6ddf70 100755 --- a/examples/platform/policy_service_crud_operations.py +++ b/examples/platform/policy_service_crud_operations.py @@ -190,6 +190,15 @@ def build_minimal_policy(cb, parser, args): print("Added policy. New policy ID is {0}".format(policy.id)) +def get_core_prevention_rule_configs(cb, parser, args): + """Get the core prevention rules""" + # cp_rule_configs = list(cb.select(CorePreventionRuleConfig, args.id)) + policy = cb.select(Policy, args.id) + cp_rule_configs = policy.core_prevention_rule_configs_list + for rc in cp_rule_configs: + print(rc) + + def main(): """Main function for the Policy Operations script. @@ -250,6 +259,11 @@ def main(): build_minimal_policy_command.add_argument("-p", "--prioritylevel", help="Priority level (HIGH, MEDIUM, LOW)", default="LOW") + print_core_prevention_rule_configs_command = \ + commands.add_parser("print_core_prevention", + help="Print all core prevention rule configs in specified policy") + print_core_prevention_rule_configs_command.add_argument("-i", "--id", type=int, help="ID of policy") + args = parser.parse_args() cb = get_cb_cloud_object(args) @@ -269,6 +283,8 @@ def main(): return replace_rule(cb, parser, args) elif args.command_name == "build-minimal-policy": return build_minimal_policy(cb, parser, args) + elif args.command_name == "print_core_prevention": + return get_core_prevention_rule_configs(cb, parser, args) if __name__ == "__main__": From 8069ac67d031df2cbe567c491769805ffd983277 Mon Sep 17 00:00:00 2001 From: Emanuela Mitreva Date: Wed, 10 May 2023 17:02:46 +0300 Subject: [PATCH 30/76] Improvements --- src/tests/uat/auth_events_uat.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/tests/uat/auth_events_uat.py b/src/tests/uat/auth_events_uat.py index 487637496..26857048a 100644 --- a/src/tests/uat/auth_events_uat.py +++ b/src/tests/uat/auth_events_uat.py @@ -60,7 +60,7 @@ "Get Search Results", "Get Search Grouped Results", "Get Details Results", - "Get Facet Data" + "Get Facet Data", ] # ------------------------------ Helper functions ------------------------------------- @@ -167,17 +167,13 @@ def main(): # check get details job api_result = get_details_results(auth_event.event_id) sdk_result = auth_event.get_details()._info - assert api_result == sdk_result, f"Test Failed Expected: {api_result} Actual: {sdk_result}" + sdk_result2 = cb.select(AuthEvent, auth_event.event_id).get_details() + assert api_result == sdk_result == sdk_result2, f"Test Failed Expected: {api_result} Actual: {sdk_result}" print(TITLES[4] + "." * (SYMBOLS - len(TITLES[4]) - 2) + "OK") # check get facet job api_result = get_facet_results()["terms"] - xx = ( - cb.select(AuthEventFacet) - .where(QUERY) - .add_facet_field("device_name") - .results - ) + xx = cb.select(AuthEventFacet).where(QUERY).add_facet_field("device_name").results sdk_result = xx.terms assert api_result == sdk_result, f"Test Failed Expected: {api_result} Actual: {sdk_result}" print(TITLES[5] + "." * (SYMBOLS - len(TITLES[5]) - 2) + "OK") From 6c456f55d3d98cf9c6a074a15639c1ff84557a15 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Mon, 15 May 2023 09:57:48 -0600 Subject: [PATCH 31/76] merged "new" and "append" functionality and placed "append_rule" on rule group, fixes CBAPI-4721 and CBAPI-4722 --- src/cbc_sdk/platform/policy_ruleconfigs.py | 81 ++++++++----------- .../unit/platform/test_policy_ruleconfigs.py | 26 +++--- 2 files changed, 44 insertions(+), 63 deletions(-) diff --git a/src/cbc_sdk/platform/policy_ruleconfigs.py b/src/cbc_sdk/platform/policy_ruleconfigs.py index ddba4373b..1505fc8a2 100644 --- a/src/cbc_sdk/platform/policy_ruleconfigs.py +++ b/src/cbc_sdk/platform/policy_ruleconfigs.py @@ -343,18 +343,40 @@ def rules_(self): """ return self._rules - def append_rule(self, rule): + def append_rule(self, name, action, direction, protocol, remote_ip, **kwargs): """ - Appends a new rule to this rule group. + Creates a new FirewallRule object and appends it to this rule group. Args: - rule (HostBasedFirewallRuleConfig.FirewallRule): The new rule. + name (str): The name for the new rule. + action (str): The action to be taken by this rule. Valid values are "ALLOW," "BLOCK," and "BLOCK_ALERT." + direction (str): The traffic direction this rule matches. Valid values are "IN," "OUT," and "BOTH." + protocol (str): The network protocol this rule matches. Valid values are "TCP" and "UDP." + remote_ip (str): The remote IP address this rule matches. + kwargs (dict): Additional parameters which may be added to the new rule. + + Returns: + FirewallRule: The new rule object. """ - rule._parent = self._parent + if action not in ("ALLOW", "BLOCK", "BLOCK_ALERT"): + raise ApiError(f"invalid rule action: {action}") + if direction not in ("IN", "OUT", "BOTH"): + raise ApiError(f"invalid rule direction: {direction}") + if protocol not in ("TCP", "UDP"): + raise ApiError(f"invalid rule protocol: {protocol}") + # specify defaults for optional params, overlay kwargs, then add in the required params + params = {"application_path": "*", "enabled": True, "local_ip_address": "*", "local_port_ranges": "*", + "remote_port_ranges": "*", "test_mode": False} + specified_params = {k: v for k, v in kwargs.items() if k in params.keys()} + params.update(specified_params) + params.update({"action": action, "direction": direction, "name": name, "protocol": protocol, + "remote_ip_address": remote_ip}) + rule = HostBasedFirewallRuleConfig.FirewallRule(self._cb, self._parent, params) self._rules.append(rule) self._info['rules'].append(rule._info) if self._parent: self._parent._mark_changed() + return rule def remove(self): """Removes this rule group from the rule configuration.""" @@ -516,58 +538,21 @@ def rule_groups(self): self._rule_groups_loaded = True return self._rule_groups - def new_rule_group(self, name, description): + def append_rule_group(self, name, description): """ - Creates a new FirewallRuleGroup object. + Creates a new FirewallRuleGroup object and appends it to the list of rule groups in the rule configuration. Args: name (str): The name of the new rule group. description (str): The description of the new rule group. Returns: - FirewallRuleGroup: The new rule group object. Add it to this rule configuration with append_rule_group. - """ - return HostBasedFirewallRuleConfig.FirewallRuleGroup(self._cb, None, {"name": name, "description": description, - "rules": []}) - - def append_rule_group(self, rule_group): - """ - Appends a rule group to the list of rule groups in the rule configuration. - - Args: - rule_group (FirewallRuleGroup): The rule group to be added. + FirewallRuleGroup: The newly added rule group. """ + rule_group = HostBasedFirewallRuleConfig.FirewallRuleGroup(self._cb, self, + {"name": name, "description": description, + "rules": []}) self.rule_groups.append(rule_group) self._info['parameters']['rule_groups'].append(rule_group._info) - rule_group._parent = self self._mark_changed() - - def new_rule(self, name, action, direction, protocol, remote_ip, **kwargs): - """ - Creates a new FirewallRule object. - - Args: - name (str): The name for the new rule. - action (str): The action to be taken by this rule. Valid values are "ALLOW," "BLOCK," and "BLOCK_ALERT." - direction (str): The traffic direction this rule matches. Valid values are "IN," "OUT," and "BOTH." - protocol (str): The network protocol this rule matches. Valid values are "TCP" and "UDP." - remote_ip (str): The remote IP address this rule matches. - kwargs (dict): Additional parameters which may be added to the new rule. - - Returns: - FirewallRule: The new firewall rule. Append it to a rule group using the group's append_rule method. - """ - if action not in ("ALLOW", "BLOCK", "BLOCK_ALERT"): - raise ApiError(f"invalid rule action: {action}") - if direction not in ("IN", "OUT", "BOTH"): - raise ApiError(f"invalid rule direction: {direction}") - if protocol not in ("TCP", "UDP"): - raise ApiError(f"invalid rule protocol: {protocol}") - # specify defaults for optional params, overlay kwargs, then add in the required params - params = {"application_path": "*", "enabled": True, "local_ip_address": "*", "local_port_ranges": "*", - "remote_port_ranges": "*", "test_mode": False} - specified_params = {k: v for k, v in kwargs.items() if k in params.keys()} - params.update(specified_params) - params.update({"action": action, "direction": direction, "name": name, "protocol": protocol, - "remote_ip_address": remote_ip}) - return HostBasedFirewallRuleConfig.FirewallRule(self._cb, None, params) + return rule_group diff --git a/src/tests/unit/platform/test_policy_ruleconfigs.py b/src/tests/unit/platform/test_policy_ruleconfigs.py index 80c2dce1a..7a1c62bf3 100644 --- a/src/tests/unit/platform/test_policy_ruleconfigs.py +++ b/src/tests/unit/platform/test_policy_ruleconfigs.py @@ -425,10 +425,8 @@ def on_put(url, body, **kwargs): assert not policy.is_dirty() rule_config = policy.host_based_firewall_rule_config groups = rule_config.rule_groups - new_rule = rule_config.new_rule("DoomyDoomsOfDoom", "BLOCK", "BOTH", "TCP", "199.201.128.1", - remote_port_ranges="666", local_ip_address="10.29.99.1", - application_path="C:\\DOOM\\DOOM.EXE") - groups[0].append_rule(new_rule) + groups[0].append_rule("DoomyDoomsOfDoom", "BLOCK", "BOTH", "TCP", "199.201.128.1", remote_port_ranges="666", + local_ip_address="10.29.99.1", application_path="C:\\DOOM\\DOOM.EXE") assert policy.is_dirty() rule_config.save() assert put_called @@ -450,15 +448,16 @@ def on_put(url, body, **kwargs): assert not rules[1].test_mode -def test_new_rule_parameter_errors(cb, policy): - """Tests the parameter check errors on the new_rule() method.""" +def test_append_rule_parameter_errors(cb, policy): + """Tests the parameter check errors on the append_rule() method.""" rule_config = policy.host_based_firewall_rule_config + groups = rule_config.rule_groups with pytest.raises(ApiError): - rule_config.new_rule("DoomyDoomsOfDoom", "NOTEXIST", "BOTH", "TCP", "199.201.128.1") + groups[0].append_rule("DoomyDoomsOfDoom", "NOTEXIST", "BOTH", "TCP", "199.201.128.1") with pytest.raises(ApiError): - rule_config.new_rule("DoomyDoomsOfDoom", "BLOCK", "NOTEXIST", "TCP", "199.201.128.1") + groups[0].append_rule("DoomyDoomsOfDoom", "BLOCK", "NOTEXIST", "TCP", "199.201.128.1") with pytest.raises(ApiError): - rule_config.new_rule("DoomyDoomsOfDoom", "BLOCK", "BOTH", "NOTEXIST", "199.201.128.1") + groups[0].append_rule("DoomyDoomsOfDoom", "BLOCK", "BOTH", "NOTEXIST", "199.201.128.1") def test_modify_add_rule_group_to_host_based_firewall(cbcsdk_mock): @@ -479,12 +478,9 @@ def on_put(url, body, **kwargs): policy = Policy(api, 65536, copy.deepcopy(FULL_POLICY_1), False, True) assert not policy.is_dirty() rule_config = policy.host_based_firewall_rule_config - new_group = rule_config.new_rule_group("DOOM_firewall", "No playing DOOM!") - new_rule = rule_config.new_rule("DoomyDoomsOfDoom", "BLOCK", "BOTH", "TCP", "199.201.128.1", - remote_port_ranges="666", local_ip_address="10.29.99.1", - application_path="C:\\DOOM\\DOOM.EXE") - new_group.append_rule(new_rule) - rule_config.append_rule_group(new_group) + new_group = rule_config.append_rule_group("DOOM_firewall", "No playing DOOM!") + new_group.append_rule("DoomyDoomsOfDoom", "BLOCK", "BOTH", "TCP", "199.201.128.1", remote_port_ranges="666", + local_ip_address="10.29.99.1", application_path="C:\\DOOM\\DOOM.EXE") assert policy.is_dirty() rule_config.save() assert put_called From 2175f3f5a075be3726326ba1dee52b7d2b3374a1 Mon Sep 17 00:00:00 2001 From: Kylie Ebringer Date: Mon, 15 May 2023 14:36:57 -0600 Subject: [PATCH 32/76] Updated to use input() and updated append() methods. --- .../platform/policy_host_based_firewall.py | 140 +++++++++++++----- 1 file changed, 100 insertions(+), 40 deletions(-) diff --git a/examples/platform/policy_host_based_firewall.py b/examples/platform/policy_host_based_firewall.py index 825a36767..c8042a745 100644 --- a/examples/platform/policy_host_based_firewall.py +++ b/examples/platform/policy_host_based_firewall.py @@ -18,12 +18,10 @@ This is an example of the command line to execute. --all_details True will include the rules. > python examples/platform/policy_host_based_firewall.py --profile EXAMPLE_CREDENTIALS --all_details True -The second method creates two new host-based firewall rules in a new rule group on an existing policy. -The name and description of the group are passed in on the command line, the rules are hard coded for -to make the example easier to read. -This is an example of the command line to execute where -p 12345678 specifies the policy to operate on -> python examples/platform/policy_host_based_firewall.py --profile EXAMPLE_CREDENTIALS -p 12345678 / - create_rule --rule_group_name sdk_test_two --rule_group_desc sdk_test_two_desc +The second method creates new host-based firewall rules in a new rule group on an existing policy. +Command line prompts ask for required info and defaults are provided. +This is an example of the command line to execute. The script will prompt on the command line for Policy Id. +> python examples/platform/policy_host_based_firewall.py --profile EXAMPLE_CREDENTIALS create_rule """ import sys @@ -80,11 +78,12 @@ def list_policy_summaries(cb, args): print("") -def add_hbfw_rule(cb, args): +def add_hbfw_rule(cb): """Create a rule and add it to the rule group specified in the input args""" - # get the policy specified in the input parameters - policy = cb.select(Policy, args.policy) - print("id: {0}. name: {1}".format(policy.id, policy.name)) + # prompt the user for a policy Id + user_input = input("Enter the policy Id to add a rule group and rules to") + policy = cb.select(Policy, user_input) + print("Using policy id: {0}. name: {1}".format(policy.id, policy.name)) # If the policy does not yet have a host-based firewall section then create it if policy.host_based_firewall_rule_config is None: policy.host_based_firewall_rule_config = HostBasedFirewallRuleConfig(cb, policy) @@ -92,31 +91,95 @@ def add_hbfw_rule(cb, args): # get the rule config in a variable to work with rc = policy.host_based_firewall_rule_config # create a new rule group - rg = rc.new_rule_group(args.rule_group_name, args.rule_group_desc) - # add the rule group to the rule config - rc.append_rule_group(rg) # to do remove this - - # create two rules - # name = SDK Example Rule One, action = ALLOW, direction = IN, protocol = TCP, remote ip address = 15.16.17.18 - r1 = rc.new_rule("SDK Example Rule One", "ALLOW", "IN", "TCP", "15.16.17.18") - # then set the rest of the fields in the rule - r1.application_path = "C:\\sdk\\example\\allow\\rule\\path" - r1.enabled = False - r1.local_ip_address = "11.12.13.14" - r1.local_port_ranges = "1313" - r1.remote_port_ranges = "2121" - # append the rule to the group - rg.append_rule(r1) # TO DO - remove this - # create the second rule - r2 = rc.new_rule("The second SDK Example Rule", "BLOCK", "OUT", "UDP", "5.6.7.8") - # then set the rest of the fields in the rule - r2.application_path = "C:\\another\\sdk\\example\\path" - r2.enabled = True - r2.local_ip_address = "1.2.3.4" - r2.local_port_ranges = "3131" - r2.remote_port_ranges = "1212" - # append the rule to the group - rg.append_rule(r2) # TO DO - remove this + rule_group_name = "Demo Rule Group" + rule_group_desc = "Description of Demo Rule Group" + user_input = input("Creating a rule group. Enter a name for the rule group or press Enter to use the default of {}" + .format(rule_group_name)) + if user_input: + rule_group_name = user_input + + user_input = input("Enter a description for the rule group press Enter to use the default of {}" + .format(rule_group_desc)) + if user_input: + rule_group_desc = user_input + + rg = rc.append_rule_group(rule_group_name, rule_group_desc) + + # prompt the user to enter rule config or use defaults + # Set default values + rule_name = "SDK Example Rule" + rule_action = "ALLOW" + direction = "IN" + protocol = "TCP" + application_path = "C:\\sdk\\example\\allow\\rule\\path" + enabled = False + remote_ip_address = "15.16.17.18" + local_ip_address = "11.12.13.14" + local_port_ranges = "1313" + remote_port_ranges = "2121" + create_rule = True + + while create_rule: + # prompt user to enter values + user_input = input("Enter Rule Name or press Enter to use the default of {}".format(rule_name)) + if user_input: + rule_name = user_input + + user_input = input("Enter Rule Action (ALLOW or BLOCK) or press Enter to use the default of {}" + .format(rule_action)) + if user_input: + rule_action = user_input.upper() + + user_input = input("Set Direction (IN or OUT)or press Enter to use the default of {}".format(direction)) + if user_input: + direction = user_input.upper() + + user_input = input("Set Protocol or press Enter to use the default of {}".format(protocol)) + if user_input: + protocol = user_input.upper() + + user_input = input("Enter the application path or press Enter use default of {}".format(application_path)) + if user_input: + application_path = user_input + + user_input = input("Enter T for the rule to be enabled or press Enter to use the default of disabled") + if user_input: + enabled = True + + user_input = input("Enter the Remote IP Address or press Enter to use the default of {}" + .format(remote_ip_address)) + if user_input: + remote_ip_address = user_input + + user_input = input("Enter the Local IP Address or press Enter to use the default of {}" + .format(local_ip_address)) + if user_input: + local_ip_address = user_input + + user_input = input("Enter the Remote port range or press Enter to use the default of {}" + .format(remote_port_ranges)) + if user_input: + remote_port_ranges = user_input + + user_input = input("Enter the Local port range or press Enter to use the default of {}" + .format(local_port_ranges)) + if user_input: + local_port_ranges = user_input + + rule = rg.append_rule(rule_name, rule_action, direction, protocol, remote_ip_address) + # then set the rest of the fields in the rule + rule.application_path = application_path + rule.enabled = enabled + rule.local_ip_address = local_ip_address + rule.local_port_ranges = local_port_ranges + rule.remote_port_ranges = remote_port_ranges + + create_rule = False + user_input = input("Enter Y to create another rule. Press Enter to finish and save.") + if user_input: + if user_input == "Y": + create_rule = True + # To do - either remove rc.save() if the policy.save() does the rules too or document that rule config must be saved rc.save() policy.save() @@ -126,15 +189,12 @@ def add_hbfw_rule(cb, args): def main(): """Main function for Policy - Host-Based Firewall script.""" parser = build_cli_parser("View or set host based firewall rules on a policy") - parser.add_argument("-p", "--policy", type=int, required=False, help="The ID of the policy to be manipulated") subparsers = parser.add_subparsers(dest="command", required=True) policy_summaries = subparsers.add_parser("list_policies", help="List summary information about each policy") policy_summaries.add_argument("--all_details", help="Print core prevention and host-based firewall rules") - create_rule = subparsers.add_parser("create_rule", help="Create a new Host-Based Firewall rule") - create_rule.add_argument("--rule_group_name", help="The name of the rule group to hold the new rule") - create_rule.add_argument("--rule_group_desc", help="The description of the new rule group") + subparsers.add_parser("create_rule", help="Create a new Host-Based Firewall rule") args = parser.parse_args() cb = get_cb_cloud_object(args) @@ -142,7 +202,7 @@ def main(): if args.command == "list_policies": list_policy_summaries(cb, args) elif args.command == "create_rule": - add_hbfw_rule(cb, args) + add_hbfw_rule(cb) else: raise NotImplementedError("Unknown command") return 0 From 4e75c065234a18714f2dcf713951d5c728aa2ce3 Mon Sep 17 00:00:00 2001 From: Kylie Ebringer Date: Tue, 16 May 2023 08:47:30 -0600 Subject: [PATCH 33/76] Fixed bug where defaults were overwritten and added comment about known rulesets issue --- .../platform/policy_host_based_firewall.py | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/examples/platform/policy_host_based_firewall.py b/examples/platform/policy_host_based_firewall.py index c8042a745..037f0e643 100644 --- a/examples/platform/policy_host_based_firewall.py +++ b/examples/platform/policy_host_based_firewall.py @@ -104,22 +104,21 @@ def add_hbfw_rule(cb): rule_group_desc = user_input rg = rc.append_rule_group(rule_group_name, rule_group_desc) - - # prompt the user to enter rule config or use defaults - # Set default values - rule_name = "SDK Example Rule" - rule_action = "ALLOW" - direction = "IN" - protocol = "TCP" - application_path = "C:\\sdk\\example\\allow\\rule\\path" - enabled = False - remote_ip_address = "15.16.17.18" - local_ip_address = "11.12.13.14" - local_port_ranges = "1313" - remote_port_ranges = "2121" create_rule = True - while create_rule: + # prompt the user to enter rule config or use defaults + # Set default values + rule_name = "SDK Example Rule" + rule_action = "ALLOW" + direction = "IN" + protocol = "TCP" + application_path = "C:\\sdk\\example\\allow\\rule\\path" + enabled = False + remote_ip_address = "15.16.17.18" + local_ip_address = "11.12.13.14" + local_port_ranges = "1313" + remote_port_ranges = "2121" + # prompt user to enter values user_input = input("Enter Rule Name or press Enter to use the default of {}".format(rule_name)) if user_input: @@ -180,8 +179,10 @@ def add_hbfw_rule(cb): if user_input == "Y": create_rule = True - # To do - either remove rc.save() if the policy.save() does the rules too or document that rule config must be saved + # There is a known issue in Carbon Black Cloud that requires the rule_configs to be saved explicitly. + # There is no adverse impact to performing this call but in the future will be un-necessary rc.save() + # save the policy and all child elements. policy.save() print(rc) From cd636200462f2766dc7ddbb8386a08c7273792aa Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Tue, 16 May 2023 11:48:50 -0600 Subject: [PATCH 34/76] fix adding a new rule group to a HBFW config with no existing rule groups (fixes CBAPI-4730) --- src/cbc_sdk/platform/policy_ruleconfigs.py | 5 +- .../platform/mock_policy_ruleconfigs.py | 108 ++++++++++++++++++ .../unit/platform/test_policy_ruleconfigs.py | 43 +++++++ 3 files changed, 155 insertions(+), 1 deletion(-) diff --git a/src/cbc_sdk/platform/policy_ruleconfigs.py b/src/cbc_sdk/platform/policy_ruleconfigs.py index 1505fc8a2..f75c0790b 100644 --- a/src/cbc_sdk/platform/policy_ruleconfigs.py +++ b/src/cbc_sdk/platform/policy_ruleconfigs.py @@ -553,6 +553,9 @@ def append_rule_group(self, name, description): {"name": name, "description": description, "rules": []}) self.rule_groups.append(rule_group) - self._info['parameters']['rule_groups'].append(rule_group._info) + if 'rule_groups' in self._info['parameters']: + self._info['parameters']['rule_groups'].append(rule_group._info) + else: + self._info['parameters']['rule_groups'] = [rule_group._info] self._mark_changed() return rule_group diff --git a/src/tests/unit/fixtures/platform/mock_policy_ruleconfigs.py b/src/tests/unit/fixtures/platform/mock_policy_ruleconfigs.py index b6bf8779c..dd570f990 100644 --- a/src/tests/unit/fixtures/platform/mock_policy_ruleconfigs.py +++ b/src/tests/unit/fixtures/platform/mock_policy_ruleconfigs.py @@ -572,6 +572,114 @@ "failed": [] } +HBFW_ADD_RULE_GROUP_EMPTY_PUT_REQUEST = [ + { + "id": "df181779-f623-415d-879e-91c40246535d", + "parameters": { + "rule_groups": [ + { + "description": "No playing DOOM!", + "name": "DOOM_firewall", + "rules": [ + { + "action": "BLOCK", + "application_path": "C:\\DOOM\\DOOM.EXE", + "direction": "BOTH", + "enabled": True, + "local_ip_address": "10.29.99.1", + "local_port_ranges": "*", + "name": "DoomyDoomsOfDoom", + "protocol": "TCP", + "remote_ip_address": "199.201.128.1", + "remote_port_ranges": "666", + "test_mode": False + } + ] + } + ], + "default_rule": { + "action": "ALLOW", + "default_rule_access_check_guid": "08dc129b-ab72-4ed7-8282-8db7f62bc7e8", + "default_rule_inbound_event_check_guid": "40dd836c-e676-4e3b-b98b-c870c4b6faa7", + "default_rule_outbound_event_check_guid": "94283d79-c2d1-472c-b303-77a0fb387bcc" + }, + "enable_host_based_firewall": False + } + } +] + +HBFW_ADD_RULE_GROUP_EMPTY_PUT_RESPONSE = { + "successful": [ + { + "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. [...].", + "inherited_from": "", + "category": "host_based_firewall", + "parameters": { + "rulesets": [ + { + "description": "No playing DOOM!", + "name": "DOOM_firewall", + "rules": [ + { + "action": "BLOCK", + "application_path": "C:\\DOOM\\DOOM.EXE", + "direction": "BOTH", + "enabled": True, + "local_ip_address": "10.29.99.1", + "local_port_ranges": "*", + "name": "DoomyDoomsOfDoom", + "protocol": "TCP", + "remote_ip_address": "199.201.128.1", + "remote_port_ranges": "666", + "rule_access_check_guid": "6d36954a-a944-4944-ae94-df6f94b877b8", + "rule_inbound_event_check_guid": "8a39c00b-f907-4085-929f-f2e98e8b7b87", + "rule_outbound_event_check_guid": "7e7a9761-4187-4065-8ae1-b5161fae75a2", + "test_mode": False + } + ], + "ruleset_id": "0c0ce332-6f81-43d9-ad9b-875e82eb53f9" + } + ], + "rule_groups": [ + { + "description": "No playing DOOM!", + "name": "DOOM_firewall", + "rules": [ + { + "action": "BLOCK", + "application_path": "C:\\DOOM\\DOOM.EXE", + "direction": "BOTH", + "enabled": True, + "local_ip_address": "10.29.99.1", + "local_port_ranges": "*", + "name": "DoomyDoomsOfDoom", + "protocol": "TCP", + "remote_ip_address": "199.201.128.1", + "remote_port_ranges": "666", + "rule_access_check_guid": "6d36954a-a944-4944-ae94-df6f94b877b8", + "rule_inbound_event_check_guid": "8a39c00b-f907-4085-929f-f2e98e8b7b87", + "rule_outbound_event_check_guid": "7e7a9761-4187-4065-8ae1-b5161fae75a2", + "test_mode": False + } + ], + "ruleset_id": "0c0ce332-6f81-43d9-ad9b-875e82eb53f9" + } + ], + "default_rule": { + "action": "ALLOW", + "default_rule_access_check_guid": "08dc129b-ab72-4ed7-8282-8db7f62bc7e8", + "default_rule_inbound_event_check_guid": "40dd836c-e676-4e3b-b98b-c870c4b6faa7", + "default_rule_outbound_event_check_guid": "94283d79-c2d1-472c-b303-77a0fb387bcc" + }, + "enable_host_based_firewall": False + } + } + ], + "failed": [] +} + HBFW_REMOVE_RULE_PUT_REQUEST = [ { "id": "df181779-f623-415d-879e-91c40246535d", diff --git a/src/tests/unit/platform/test_policy_ruleconfigs.py b/src/tests/unit/platform/test_policy_ruleconfigs.py index 7a1c62bf3..5fdcb18e2 100644 --- a/src/tests/unit/platform/test_policy_ruleconfigs.py +++ b/src/tests/unit/platform/test_policy_ruleconfigs.py @@ -29,6 +29,8 @@ HBFW_ADD_RULE_PUT_RESPONSE, HBFW_ADD_RULE_GROUP_PUT_REQUEST, HBFW_ADD_RULE_GROUP_PUT_RESPONSE, + HBFW_ADD_RULE_GROUP_EMPTY_PUT_REQUEST, + HBFW_ADD_RULE_GROUP_EMPTY_PUT_RESPONSE, HBFW_REMOVE_RULE_PUT_REQUEST, HBFW_REMOVE_RULE_PUT_RESPONSE, HBFW_REMOVE_RULE_GROUP_PUT_REQUEST, @@ -498,6 +500,47 @@ def on_put(url, body, **kwargs): assert rules[0].name == "DoomyDoomsOfDoom" +def test_modify_add_rule_group_to_host_based_firewall_when_empty(cbcsdk_mock): + """Tests modifying an empty host-based firewall rule configuration by adding a rule group.""" + put_called = False + + def on_put(url, body, **kwargs): + nonlocal put_called + assert body == HBFW_ADD_RULE_GROUP_EMPTY_PUT_REQUEST + put_called = True + return copy.deepcopy(HBFW_ADD_RULE_GROUP_EMPTY_PUT_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/host_based_firewall', + on_put) + api = cbcsdk_mock.api + # remove all rule groups from HBFW rule config + policy_data = copy.deepcopy(FULL_POLICY_1) + hbfw = [ruleconfig for ruleconfig in policy_data.get('rule_configs', []) + if ruleconfig['category'] == 'host_based_firewall'] + assert len(hbfw) == 1 + params = hbfw[0]['parameters'] + if 'rule_groups' in params: + del params['rule_groups'] + policy = Policy(api, 65536, policy_data, False, True) + assert not policy.is_dirty() + rule_config = policy.host_based_firewall_rule_config + new_group = rule_config.append_rule_group("DOOM_firewall", "No playing DOOM!") + new_group.append_rule("DoomyDoomsOfDoom", "BLOCK", "BOTH", "TCP", "199.201.128.1", remote_port_ranges="666", + local_ip_address="10.29.99.1", application_path="C:\\DOOM\\DOOM.EXE") + assert policy.is_dirty() + rule_config.save() + assert put_called + groups = rule_config.rule_groups + assert len(groups) == 1 + assert groups[0].name == "DOOM_firewall" + assert groups[0].description == "No playing DOOM!" + rules = groups[0].rules_ + assert len(rules) == 1 + assert rules[0].name == "DoomyDoomsOfDoom" + + def test_modify_remove_rule_from_host_based_firewall(cbcsdk_mock): """Tests modifying a host-based firewall rule configuration by removing a rule.""" put_called = False From 7e245fd876bffe307aed7e83abdae6877974d90e Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Wed, 17 May 2023 12:19:56 -0600 Subject: [PATCH 35/76] upgraded pymox to 1.0.0 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1138b55d4..e7eb59dc2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ boto3 # Dev dependencies pytest==7.2.1 -pymox==0.7.8 +pymox==1.0.0 coverage==6.5.0 coveralls==3.3.1 flake8==5.0.4 From 4ef90507711cd56770d59f264637a106d8e39a10 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Wed, 17 May 2023 12:26:02 -0600 Subject: [PATCH 36/76] update the README as well --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c899fb553..f706328fb 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ At least one Carbon Black Cloud product is required to use this SDK: If developing the SDK, you also need: - pytest==5.4.2 -- pymox==0.7.8 +- pymox==1.0.0 - coverage==5.1 - coveralls==2.0.0 - flake8==3.8.1 From c04902944962fa3692f507fc0ae2824bf1a18a64 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Wed, 17 May 2023 15:29:48 -0600 Subject: [PATCH 37/76] don't forget setup.py (thanks Alex) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ab211d729..a7ddd56bb 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ extras_require = { "test": [ 'pytest==7.2.1', - 'pymox==0.7.8', + 'pymox==1.0.0', 'coverage==6.5.0', 'coveralls==3.3.1', 'flake8==5.0.4', From f05da4a08e4be53ef4ee672457d4ced33eb88d03 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Thu, 27 Apr 2023 14:59:04 -0600 Subject: [PATCH 38/76] first try at adding a Python 3.11 step --- codeship-services.yml | 4 ++++ codeship-steps.yml | 3 +++ docker/python3.11/Dockerfile | 7 +++++++ 3 files changed, 14 insertions(+) create mode 100644 docker/python3.11/Dockerfile diff --git a/codeship-services.yml b/codeship-services.yml index 3f4daaf37..b4df07b69 100644 --- a/codeship-services.yml +++ b/codeship-services.yml @@ -16,6 +16,10 @@ testingpython310: build: dockerfile: ./docker/python3.10/Dockerfile +testingpython311: + build: + dockerfile: ./docker/python3.11/Dockerfile + testingrhel: build: dockerfile: ./docker/rhel/Dockerfile diff --git a/codeship-steps.yml b/codeship-steps.yml index 5aadccfba..ccab7349d 100644 --- a/codeship-steps.yml +++ b/codeship-steps.yml @@ -17,6 +17,9 @@ - name: testing python 3.10 service: testingpython310 command: pytest + - name: testing python 3.11 + service: testingpython311 + command: pytest - name: testing red hat service: testingrhel command: pytest diff --git a/docker/python3.11/Dockerfile b/docker/python3.11/Dockerfile new file mode 100644 index 000000000..1f571053d --- /dev/null +++ b/docker/python3.11/Dockerfile @@ -0,0 +1,7 @@ +from python:3.11 +MAINTAINER cb-developer-network@vmware.com + +COPY . /app +WORKDIR /app + +RUN pip3 install -r requirements.txt From 889bd623a9f876a93c4c61a645dd8c851b693656 Mon Sep 17 00:00:00 2001 From: Alex Van Brunt Date: Mon, 22 May 2023 09:12:55 -0600 Subject: [PATCH 39/76] Change Sphinx to build dirhtml --- .readthedocs.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 2559611fa..92ee7b134 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -18,6 +18,7 @@ build: # Build documentation in the docs/ directory with Sphinx sphinx: configuration: docs/conf.py + builder: dirhtml # fail_on_warning: true # If using Sphinx, optionally build your docs in additional formats, such as PDF From 56fd25c5443f5c2d5b262b4346b3e137502df024 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Fri, 19 May 2023 15:03:12 -0600 Subject: [PATCH 40/76] added two new functions + tests for one of them --- src/cbc_sdk/platform/policy_ruleconfigs.py | 54 +++++++++++++ .../platform/mock_policy_ruleconfigs.py | 77 +++++++++++++++++++ .../unit/platform/test_policy_ruleconfigs.py | 36 ++++++++- 3 files changed, 166 insertions(+), 1 deletion(-) diff --git a/src/cbc_sdk/platform/policy_ruleconfigs.py b/src/cbc_sdk/platform/policy_ruleconfigs.py index f75c0790b..b4d963ab7 100644 --- a/src/cbc_sdk/platform/policy_ruleconfigs.py +++ b/src/cbc_sdk/platform/policy_ruleconfigs.py @@ -559,3 +559,57 @@ def append_rule_group(self, name, description): self._info['parameters']['rule_groups'] = [rule_group._info] self._mark_changed() return rule_group + + def copy_rules_to(self, *args): + """ + Copies the parameters for host-based firewall rule configurations to another policy or policies. + + Required Permissions: + org.firewall.rules(UPDATE) + + Args: + args (list[Any]): References to policies to copy to. May be Policy objects, integers, or other. + + Returns: + dict: Result structure from copy operation. + + Raises: + ApiError: If the parameters could not be converted to policy IDs. + """ + from cbc_sdk.platform.policies import Policy + target_ids = [] + for arg in args: + if isinstance(arg, Policy): + target_ids.append(arg.id) + elif isinstance(arg, int): + target_ids.append(arg) + else: + try: + target_ids.append(int(str(arg))) + except ValueError: + raise ApiError(f"invalid policy ID or reference: {arg}") + if not target_ids: + raise ApiError("at least one policy ID or reference must be specified") + url = self.urlobject_single.format(self._cb.credentials.org_key, self._parent._model_unique_id) + "/_copy" + body = {"target_policy_ids": target_ids, "parameters": {"rule_groups": self.get_parameter("rule_groups", [])}} + result = self._cb.put_object(url, body) + return result.json() + + def export_rules(self, format="json"): + """ + Exports the rules from this host-based firewall rule configuration. + + Required Permissions: + org.firewall.rules(READ) + + Args: + format (str): The format to return the rule data in. Valid values are "csv" and "json" (the default). + + Returns: + str: The exported rule configuration data. + """ + if format not in ("csv", "json"): + raise ApiError(f"Invalid format: {format}") + url = self.urlobject_single.format(self._cb.credentials.org_key, self._parent._model_unique_id)\ + + "/rules/_export" + return self._cb.get_raw_data(url, {"format": format}) diff --git a/src/tests/unit/fixtures/platform/mock_policy_ruleconfigs.py b/src/tests/unit/fixtures/platform/mock_policy_ruleconfigs.py index dd570f990..7a75efee4 100644 --- a/src/tests/unit/fixtures/platform/mock_policy_ruleconfigs.py +++ b/src/tests/unit/fixtures/platform/mock_policy_ruleconfigs.py @@ -1020,3 +1020,80 @@ ], "failed": [] } + +HBFW_COPY_RULES_PUT_REQUEST = { + "target_policy_ids": [601, 65536, 344], + "parameters": { + "rule_groups": [ + { + "description": "Whatever", + "name": "Crapco_firewall", + "rules": [ + { + "action": "ALLOW", + "application_path": "*", + "direction": "IN", + "enabled": True, + "local_ip_address": "1.2.3.4", + "local_port_ranges": "1234", + "name": "my_first_rule", + "protocol": "TCP", + "remote_ip_address": "5.6.7.8", + "remote_port_ranges": "5678", + "rule_access_check_guid": "935477b8-997a-4476-8160-9179840d9892", + "rule_inbound_event_check_guid": "203d0685-04a6-49d8-bd9b-20ddda2c6c73", + "rule_outbound_event_check_guid": "16b8a622-a6d0-4873-8197-2974295c0f47", + "test_mode": False + }, + { + "action": "BLOCK", + "application_path": "C:\\DOOM\\DOOM.EXE", + "direction": "BOTH", + "enabled": True, + "local_ip_address": "10.29.99.1", + "local_port_ranges": "*", + "name": "DoomyDoomsOfDoom", + "protocol": "TCP", + "remote_ip_address": "199.201.128.1", + "remote_port_ranges": "666", + "rule_access_check_guid": "28acfcac-7891-423d-9e99-d887aa4662fc", + "rule_inbound_event_check_guid": "01e26bc9-7729-4c0d-a550-f63a865b8c9f", + "rule_outbound_event_check_guid": "b9b625eb-1599-4f7d-b852-0f12db6c5a19", + "test_mode": False + } + ], + "ruleset_id": "fa3f7254-6d50-4ebf-aca6-d617bcd644b9" + }, + { + "description": "IRC is a sewer", + "name": "Isolate", + "rules": [ + { + "action": "BLOCK_ALERT", + "application_path": "*", + "direction": "BOTH", + "enabled": True, + "local_ip_address": "10.29.99.1", + "local_port_ranges": "*", + "name": "BlockIRC", + "protocol": "TCP", + "remote_ip_address": "26.2.0.74", + "remote_port_ranges": "6667", + "rule_access_check_guid": "b1454c18-f08c-419a-9b57-186c25aa6c9d", + "rule_inbound_event_check_guid": "b80e9216-5f9f-4e9a-9bcb-79a5af78d976", + "rule_outbound_event_check_guid": "765cdf79-4ff9-419c-9775-abb18e6f6518", + "test_mode": False + } + ], + "ruleset_id": "cc7b30e8-b0e5-4253-96e9-93d345fbe642" + } + ] + } +} + +HBFW_COPY_RULES_PUT_RESPONSE = { + "failed_policy_ids": [344], + "num_applied": 3, + "message": "This is a message", + "success": True +} diff --git a/src/tests/unit/platform/test_policy_ruleconfigs.py b/src/tests/unit/platform/test_policy_ruleconfigs.py index 5fdcb18e2..073938c91 100644 --- a/src/tests/unit/platform/test_policy_ruleconfigs.py +++ b/src/tests/unit/platform/test_policy_ruleconfigs.py @@ -34,7 +34,9 @@ HBFW_REMOVE_RULE_PUT_REQUEST, HBFW_REMOVE_RULE_PUT_RESPONSE, HBFW_REMOVE_RULE_GROUP_PUT_REQUEST, - HBFW_REMOVE_RULE_GROUP_PUT_RESPONSE) + HBFW_REMOVE_RULE_GROUP_PUT_RESPONSE, + HBFW_COPY_RULES_PUT_REQUEST, + HBFW_COPY_RULES_PUT_RESPONSE) logging.basicConfig(format='%(asctime)s %(levelname)s:%(message)s', level=logging.DEBUG, filename='log.txt') @@ -601,3 +603,35 @@ def on_put(url, body, **kwargs): result_groups = rule_config.rule_groups assert len(result_groups) == 1 assert result_groups[0].name == "Crapco_firewall" + + +def test_copy_hbfw_rules(cbcsdk_mock): + """Tests the copy_rules_to function.""" + put_called = False + + def on_put(url, body, **kwargs): + nonlocal put_called + assert body == HBFW_COPY_RULES_PUT_REQUEST + put_called = True + return copy.deepcopy(HBFW_COPY_RULES_PUT_RESPONSE) + + cbcsdk_mock.mock_request('PUT', '/policyservice/v1/orgs/test/policies/1492/rule_configs/host_based_firewall/_copy', + on_put) + api = cbcsdk_mock.api + policy = Policy(api, 1492, copy.deepcopy(FULL_POLICY_5), False, True) + target_policy = Policy(api, 65536, copy.deepcopy(FULL_POLICY_1), False, True) + result = policy.host_based_firewall_rule_config.copy_rules_to(601, target_policy, "344") + assert put_called + assert result['success'] + assert result['failed_policy_ids'] == [344] + assert result['num_applied'] == 3 + + +def test_copy_hbfw_rules_error_conditions(cb): + """Tests the error conditions in the copy_rules_to function.""" + policy = Policy(cb, 1492, copy.deepcopy(FULL_POLICY_5), False, True) + hbfw = policy.host_based_firewall_rule_config + with pytest.raises(ApiError): + hbfw.copy_rules_to() + with pytest.raises(ApiError): + hbfw.copy_rules_to(16, "Bogus", 3) From 3632c5740a944d23b3f61aff1152330078a31877 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Mon, 22 May 2023 15:13:57 -0600 Subject: [PATCH 41/76] code now passes unit tests --- src/cbc_sdk/platform/policy_ruleconfigs.py | 5 +- src/tests/unit/fixtures/CBCSDKMock.py | 2 +- .../platform/mock_policy_ruleconfigs.py | 77 +++++++++++++++++++ src/tests/unit/platform/test_devicev6_api.py | 2 +- .../unit/platform/test_policy_ruleconfigs.py | 30 +++++++- 5 files changed, 112 insertions(+), 4 deletions(-) diff --git a/src/cbc_sdk/platform/policy_ruleconfigs.py b/src/cbc_sdk/platform/policy_ruleconfigs.py index b4d963ab7..7d684f08d 100644 --- a/src/cbc_sdk/platform/policy_ruleconfigs.py +++ b/src/cbc_sdk/platform/policy_ruleconfigs.py @@ -612,4 +612,7 @@ def export_rules(self, format="json"): raise ApiError(f"Invalid format: {format}") url = self.urlobject_single.format(self._cb.credentials.org_key, self._parent._model_unique_id)\ + "/rules/_export" - return self._cb.get_raw_data(url, {"format": format}) + if format == "json": + return self._cb.get_object(url, {"format": format}) + else: + return self._cb.get_raw_data(url, {"format": format}) diff --git a/src/tests/unit/fixtures/CBCSDKMock.py b/src/tests/unit/fixtures/CBCSDKMock.py index 3477b387e..adbcc6ffc 100644 --- a/src/tests/unit/fixtures/CBCSDKMock.py +++ b/src/tests/unit/fixtures/CBCSDKMock.py @@ -155,7 +155,7 @@ def _get_raw_data(url, query_params=None, default=None, **kwargs): matched = self.match_key(self.get_mock_key("RAW_GET", url)) if matched: if callable(self.mocks[matched]): - return self.mocks[matched](url, query_params, **kwargs) + return self.mocks[matched](url, query_params, default) elif isinstance(self.mocks[matched], Exception): raise self.mocks[matched] else: diff --git a/src/tests/unit/fixtures/platform/mock_policy_ruleconfigs.py b/src/tests/unit/fixtures/platform/mock_policy_ruleconfigs.py index 7a75efee4..3813744f3 100644 --- a/src/tests/unit/fixtures/platform/mock_policy_ruleconfigs.py +++ b/src/tests/unit/fixtures/platform/mock_policy_ruleconfigs.py @@ -1097,3 +1097,80 @@ "message": "This is a message", "success": True } + +HBFW_EXPORT_RULE_CONFIGS_RESPONSE = [ + { + "policy_name": "Crapco", + "rule_group_name": "Crapco_firewall", + "rule_group_description": "Whatever", + "rule_group_rank": "1", + "rule_group_enabled": "true", + "rule_rank": "1", + "rule_enabled": True, + "action": "ALLOW", + "application_path": "*", + "direction": "IN", + "local_ip": "1.2.3.4", + "local_port": "1234", + "remote_ip": "5.6.7.8", + "remote_port": "5678", + "protocol": "TCP" + }, + { + "policy_name": "Crapco", + "rule_group_name": "Crapco_firewall", + "rule_group_description": "Whatever", + "rule_group_rank": "1", + "rule_group_enabled": "true", + "rule_rank": "2", + "rule_enabled": True, + "action": "BLOCK", + "application_path": "C:\\DOOM\\DOOM.EXE", + "direction": "BOTH", + "local_ip": "10.29.99.1", + "local_port": "*", + "remote_ip": "199.201.128.1", + "remote_port": "666", + "protocol": "TCP" + }, + { + "policy_name": "Crapco", + "rule_group_name": "Isolate", + "rule_group_description": "IRC is a sewer", + "rule_group_rank": "2", + "rule_group_enabled": "true", + "rule_rank": "1", + "rule_enabled": True, + "action": "BLOCK_ALERT", + "application_path": "*", + "direction": "BOTH", + "local_ip": "10.29.99.1", + "local_port": "*", + "remote_ip": "26.2.0.74", + "remote_port": "6667", + "protocol": "TCP" + }, + { + "policy_name": "Crapco", + "rule_group_rank": "3", + "rule_group_enabled": "true", + "rule_rank": "1", + "rule_enabled": True, + "action": "ALLOW", + "application_path": "*", + "direction": "BOTH", + "local_ip": "*", + "local_port": "*", + "remote_ip": "*", + "remote_port": "*", + "protocol": "ANY" + } +] + +HBFW_EXPORT_RULE_CONFIGS_RESPONSE_CSV = \ +"""Policy Name,Rule Group Name,Rule Group Description,Rule Group Rank,Rule Group Enabled,Rule Rank,Rule Enabled,... +Crapco,Crapco_firewall,Whatever,1,true,1,true,ALLOW,*,IN,1.2.3.4,1234,5.6.7.8,5678,TCP +Crapco,Crapco_firewall,Whatever,1,true,2,true,BLOCK,C:\\DOOM\\DOOM.EXE,BOTH,10.29.99.1,*,199.201.128.1,666,TCP +Crapco,Isolate,IRC is a sewer,2,true,1,true,BLOCK_ALERT,*,BOTH,10.29.99.1,*,26.2.0.74,6667,TCP +Crapco,,,3,true,1,true,ALLOW,*,BOTH,*,*,*,*,ANY +""" diff --git a/src/tests/unit/platform/test_devicev6_api.py b/src/tests/unit/platform/test_devicev6_api.py index 80fda77ce..1fb5fb6de 100755 --- a/src/tests/unit/platform/test_devicev6_api.py +++ b/src/tests/unit/platform/test_devicev6_api.py @@ -265,7 +265,7 @@ def test_facet_generated_queries(cb, init_facet, desired_criteria): def test_query_device_download(cbcsdk_mock): """Test downloading the results of a device query as CSV.""" - def on_download(url, query_params, **kwargs): + def on_download(url, query_params, default): assert query_params == {"status": "ALL", "ad_group_id": "14,25", "policy_id": "8675309", "target_priority": "HIGH", "query_string": "foobar", "sort_field": "name", "sort_order": "DESC"} diff --git a/src/tests/unit/platform/test_policy_ruleconfigs.py b/src/tests/unit/platform/test_policy_ruleconfigs.py index 073938c91..eebe88098 100644 --- a/src/tests/unit/platform/test_policy_ruleconfigs.py +++ b/src/tests/unit/platform/test_policy_ruleconfigs.py @@ -36,7 +36,9 @@ HBFW_REMOVE_RULE_GROUP_PUT_REQUEST, HBFW_REMOVE_RULE_GROUP_PUT_RESPONSE, HBFW_COPY_RULES_PUT_REQUEST, - HBFW_COPY_RULES_PUT_RESPONSE) + HBFW_COPY_RULES_PUT_RESPONSE, + HBFW_EXPORT_RULE_CONFIGS_RESPONSE, + HBFW_EXPORT_RULE_CONFIGS_RESPONSE_CSV) logging.basicConfig(format='%(asctime)s %(levelname)s:%(message)s', level=logging.DEBUG, filename='log.txt') @@ -635,3 +637,29 @@ def test_copy_hbfw_rules_error_conditions(cb): hbfw.copy_rules_to() with pytest.raises(ApiError): hbfw.copy_rules_to(16, "Bogus", 3) + + +def test_export_hbfw_rules(cbcsdk_mock): + """Tests the export_rules function with JSON output.""" + cbcsdk_mock.mock_request('GET', '/policyservice/v1/orgs/test/policies/1492/rule_configs/host_based_firewall' + '/rules/_export?format=json', HBFW_EXPORT_RULE_CONFIGS_RESPONSE) + api = cbcsdk_mock.api + policy = Policy(api, 1492, copy.deepcopy(FULL_POLICY_5), False, True) + output = policy.host_based_firewall_rule_config.export_rules('json') + assert len(output) == 4 + assert all(rule['policy_name'] == 'Crapco' for rule in output) + assert all(rule['rule_enabled'] for rule in output) + + +def test_export_hbfw_rules_as_csv(cbcsdk_mock): + """Tests the export_rules function with CSV output.""" + def on_get(url, params, default): + assert params['format'] == 'csv' + return HBFW_EXPORT_RULE_CONFIGS_RESPONSE_CSV + + cbcsdk_mock.mock_request('RAW_GET', '/policyservice/v1/orgs/test/policies/1492/rule_configs/host_based_firewall' + '/rules/_export', on_get) + api = cbcsdk_mock.api + policy = Policy(api, 1492, copy.deepcopy(FULL_POLICY_5), False, True) + output = policy.host_based_firewall_rule_config.export_rules('csv') + assert output == HBFW_EXPORT_RULE_CONFIGS_RESPONSE_CSV From 3cdb3c463556c59ded5d50e6d7dabb33d5fa0b89 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Mon, 22 May 2023 15:25:16 -0600 Subject: [PATCH 42/76] one more test added to cover everything, and deflake8'd --- .../unit/fixtures/platform/mock_policy_ruleconfigs.py | 3 +-- src/tests/unit/platform/test_policy_ruleconfigs.py | 7 +++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/tests/unit/fixtures/platform/mock_policy_ruleconfigs.py b/src/tests/unit/fixtures/platform/mock_policy_ruleconfigs.py index 3813744f3..9342c0672 100644 --- a/src/tests/unit/fixtures/platform/mock_policy_ruleconfigs.py +++ b/src/tests/unit/fixtures/platform/mock_policy_ruleconfigs.py @@ -1167,8 +1167,7 @@ } ] -HBFW_EXPORT_RULE_CONFIGS_RESPONSE_CSV = \ -"""Policy Name,Rule Group Name,Rule Group Description,Rule Group Rank,Rule Group Enabled,Rule Rank,Rule Enabled,... +HBFW_EXPORT_RULE_CONFIGS_RESPONSE_CSV = """Policy Name,Rule Group Name,Rule Group Description,Rule Group Rank,... Crapco,Crapco_firewall,Whatever,1,true,1,true,ALLOW,*,IN,1.2.3.4,1234,5.6.7.8,5678,TCP Crapco,Crapco_firewall,Whatever,1,true,2,true,BLOCK,C:\\DOOM\\DOOM.EXE,BOTH,10.29.99.1,*,199.201.128.1,666,TCP Crapco,Isolate,IRC is a sewer,2,true,1,true,BLOCK_ALERT,*,BOTH,10.29.99.1,*,26.2.0.74,6667,TCP diff --git a/src/tests/unit/platform/test_policy_ruleconfigs.py b/src/tests/unit/platform/test_policy_ruleconfigs.py index eebe88098..e8ab70efa 100644 --- a/src/tests/unit/platform/test_policy_ruleconfigs.py +++ b/src/tests/unit/platform/test_policy_ruleconfigs.py @@ -663,3 +663,10 @@ def on_get(url, params, default): policy = Policy(api, 1492, copy.deepcopy(FULL_POLICY_5), False, True) output = policy.host_based_firewall_rule_config.export_rules('csv') assert output == HBFW_EXPORT_RULE_CONFIGS_RESPONSE_CSV + + +def test_export_hbfw_rules_bad_format(cb): + """Tests what happens when we give export_rules a bad format.""" + policy = Policy(cb, 1492, copy.deepcopy(FULL_POLICY_5), False, True) + with pytest.raises(ApiError): + policy.host_based_firewall_rule_config.export_rules('mp3') From c0a902f493e983da0a58a6ce879e194dc1ea604d Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Tue, 23 May 2023 16:04:09 -0600 Subject: [PATCH 43/76] remaned copy_rules_to() to copy_rules() --- src/cbc_sdk/platform/policy_ruleconfigs.py | 2 +- src/tests/unit/platform/test_policy_ruleconfigs.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/cbc_sdk/platform/policy_ruleconfigs.py b/src/cbc_sdk/platform/policy_ruleconfigs.py index 7d684f08d..a7f5ecc3b 100644 --- a/src/cbc_sdk/platform/policy_ruleconfigs.py +++ b/src/cbc_sdk/platform/policy_ruleconfigs.py @@ -560,7 +560,7 @@ def append_rule_group(self, name, description): self._mark_changed() return rule_group - def copy_rules_to(self, *args): + def copy_rules(self, *args): """ Copies the parameters for host-based firewall rule configurations to another policy or policies. diff --git a/src/tests/unit/platform/test_policy_ruleconfigs.py b/src/tests/unit/platform/test_policy_ruleconfigs.py index e8ab70efa..629e2139c 100644 --- a/src/tests/unit/platform/test_policy_ruleconfigs.py +++ b/src/tests/unit/platform/test_policy_ruleconfigs.py @@ -622,7 +622,7 @@ def on_put(url, body, **kwargs): api = cbcsdk_mock.api policy = Policy(api, 1492, copy.deepcopy(FULL_POLICY_5), False, True) target_policy = Policy(api, 65536, copy.deepcopy(FULL_POLICY_1), False, True) - result = policy.host_based_firewall_rule_config.copy_rules_to(601, target_policy, "344") + result = policy.host_based_firewall_rule_config.copy_rules(601, target_policy, "344") assert put_called assert result['success'] assert result['failed_policy_ids'] == [344] @@ -634,9 +634,9 @@ def test_copy_hbfw_rules_error_conditions(cb): policy = Policy(cb, 1492, copy.deepcopy(FULL_POLICY_5), False, True) hbfw = policy.host_based_firewall_rule_config with pytest.raises(ApiError): - hbfw.copy_rules_to() + hbfw.copy_rules() with pytest.raises(ApiError): - hbfw.copy_rules_to(16, "Bogus", 3) + hbfw.copy_rules(16, "Bogus", 3) def test_export_hbfw_rules(cbcsdk_mock): From 381ae8573e652b260fd837fcbea73eee55198efa Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Wed, 24 May 2023 10:20:48 -0600 Subject: [PATCH 44/76] doc modification to copy_rules --- src/cbc_sdk/platform/policy_ruleconfigs.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/cbc_sdk/platform/policy_ruleconfigs.py b/src/cbc_sdk/platform/policy_ruleconfigs.py index a7f5ecc3b..849b1f5c0 100644 --- a/src/cbc_sdk/platform/policy_ruleconfigs.py +++ b/src/cbc_sdk/platform/policy_ruleconfigs.py @@ -568,7 +568,8 @@ def copy_rules(self, *args): org.firewall.rules(UPDATE) Args: - args (list[Any]): References to policies to copy to. May be Policy objects, integers, or other. + args (list[Any]): References to policies to copy to. May be Policy objects, integers, or + string representations of integers. Returns: dict: Result structure from copy operation. From e635502ffd1ab2e08f058499c386e6c1fdf77186 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Wed, 24 May 2023 11:30:37 -0600 Subject: [PATCH 45/76] fixed URL and method for copy_to --- src/cbc_sdk/platform/policy_ruleconfigs.py | 5 +++-- src/tests/unit/platform/test_policy_ruleconfigs.py | 14 +++++++------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/cbc_sdk/platform/policy_ruleconfigs.py b/src/cbc_sdk/platform/policy_ruleconfigs.py index 849b1f5c0..a9cfd3761 100644 --- a/src/cbc_sdk/platform/policy_ruleconfigs.py +++ b/src/cbc_sdk/platform/policy_ruleconfigs.py @@ -282,6 +282,7 @@ def set_assignment_mode(self, mode): class HostBasedFirewallRuleConfig(PolicyRuleConfig): """Represents a host-based firewall rule configuration in the policy.""" + urlobject = "/policyservice/v1/orgs/{0}/policies" urlobject_single = "/policyservice/v1/orgs/{0}/policies/{1}/rule_configs/host_based_firewall" swagger_meta_file = "platform/models/policy_ruleconfig.yaml" @@ -591,9 +592,9 @@ def copy_rules(self, *args): raise ApiError(f"invalid policy ID or reference: {arg}") if not target_ids: raise ApiError("at least one policy ID or reference must be specified") - url = self.urlobject_single.format(self._cb.credentials.org_key, self._parent._model_unique_id) + "/_copy" + url = self.urlobject.format(self._cb.credentials.org_key) + "/rule_configs/host_based_firewall/_copy" body = {"target_policy_ids": target_ids, "parameters": {"rule_groups": self.get_parameter("rule_groups", [])}} - result = self._cb.put_object(url, body) + result = self._cb.post_object(url, body) return result.json() def export_rules(self, format="json"): diff --git a/src/tests/unit/platform/test_policy_ruleconfigs.py b/src/tests/unit/platform/test_policy_ruleconfigs.py index 629e2139c..356381b14 100644 --- a/src/tests/unit/platform/test_policy_ruleconfigs.py +++ b/src/tests/unit/platform/test_policy_ruleconfigs.py @@ -609,21 +609,21 @@ def on_put(url, body, **kwargs): def test_copy_hbfw_rules(cbcsdk_mock): """Tests the copy_rules_to function.""" - put_called = False + post_called = False - def on_put(url, body, **kwargs): - nonlocal put_called + def on_post(url, body, **kwargs): + nonlocal post_called assert body == HBFW_COPY_RULES_PUT_REQUEST - put_called = True + post_called = True return copy.deepcopy(HBFW_COPY_RULES_PUT_RESPONSE) - cbcsdk_mock.mock_request('PUT', '/policyservice/v1/orgs/test/policies/1492/rule_configs/host_based_firewall/_copy', - on_put) + cbcsdk_mock.mock_request('POST', '/policyservice/v1/orgs/test/policies/rule_configs/host_based_firewall/_copy', + on_post) api = cbcsdk_mock.api policy = Policy(api, 1492, copy.deepcopy(FULL_POLICY_5), False, True) target_policy = Policy(api, 65536, copy.deepcopy(FULL_POLICY_1), False, True) result = policy.host_based_firewall_rule_config.copy_rules(601, target_policy, "344") - assert put_called + assert post_called assert result['success'] assert result['failed_policy_ids'] == [344] assert result['num_applied'] == 3 From 72b3d1884396ea69aebd17e1b3478550b693ac08 Mon Sep 17 00:00:00 2001 From: Jasmine Clark <89797061+jclark-vmware@users.noreply.github.com> Date: Mon, 5 Jun 2023 14:03:23 -0400 Subject: [PATCH 46/76] reformatting sdk docs --- docs/cbc_sdk.audit_remediation.rst | 4 +- docs/cbc_sdk.cache.rst | 2 +- docs/cbc_sdk.credential_providers.rst | 12 +- docs/cbc_sdk.endpoint_standard.rst | 14 +- docs/cbc_sdk.enterprise_edr.rst | 4 +- docs/cbc_sdk.platform.rst | 14 +- docs/cbc_sdk.rst | 4 +- docs/cbc_sdk.workload.rst | 4 +- docs/concepts.rst | 123 +++++++++--------- docs/getting-started.rst | 6 + docs/{guides-and-resources.rst => guides.rst} | 17 +-- docs/index.rst | 28 +++- docs/resources.rst | 30 +++++ 13 files changed, 151 insertions(+), 111 deletions(-) rename docs/{guides-and-resources.rst => guides.rst} (80%) create mode 100755 docs/resources.rst diff --git a/docs/cbc_sdk.audit_remediation.rst b/docs/cbc_sdk.audit_remediation.rst index 9a8d33e97..a2eb38e39 100644 --- a/docs/cbc_sdk.audit_remediation.rst +++ b/docs/cbc_sdk.audit_remediation.rst @@ -1,5 +1,5 @@ -Audit and Remediation -===================== +Audit and Remediation Package +=================================== Submodules ---------- diff --git a/docs/cbc_sdk.cache.rst b/docs/cbc_sdk.cache.rst index ff42f53f9..dcfe5f2d7 100644 --- a/docs/cbc_sdk.cache.rst +++ b/docs/cbc_sdk.cache.rst @@ -1,4 +1,4 @@ -cbc\_sdk.cache package +Cache Package ====================== Submodules diff --git a/docs/cbc_sdk.credential_providers.rst b/docs/cbc_sdk.credential_providers.rst index 9df90f9e3..7df2a4d01 100644 --- a/docs/cbc_sdk.credential_providers.rst +++ b/docs/cbc_sdk.credential_providers.rst @@ -1,9 +1,17 @@ -Credential Providers -==================== +Credential Providers Package +====================================== Submodules ---------- +cbc\_sdk.credential\_providers.aws\_sm\_credential\_provider module +------------------------------------------------------------------- + +.. automodule:: cbc_sdk.credential_providers.aws_sm_credential_provider + :members: + :undoc-members: + :show-inheritance: + cbc\_sdk.credential\_providers.default module --------------------------------------------- diff --git a/docs/cbc_sdk.endpoint_standard.rst b/docs/cbc_sdk.endpoint_standard.rst index 0102f08ff..4995aca86 100644 --- a/docs/cbc_sdk.endpoint_standard.rst +++ b/docs/cbc_sdk.endpoint_standard.rst @@ -1,15 +1,5 @@ -Endpoint Standard -================= - -Decommissioned Functionality ----------------------------- - -The Endpoint Standard events (``cbc_sdk.endpoint_standard.Event``) have been decommissioned and should no longer be -used. Any attempt to use them will raise a ``FunctionalityDecommissioned`` exception. Please use -``cbc_sdk.endpoint_standard.EnrichedEvent`` instead. Refer to -`this migration guide -`_ -on the Carbon Black Developer Network Community for more information. +Endpoint Standard Package +=================================== Submodules ---------- diff --git a/docs/cbc_sdk.enterprise_edr.rst b/docs/cbc_sdk.enterprise_edr.rst index daa95d77f..8010cfe4e 100644 --- a/docs/cbc_sdk.enterprise_edr.rst +++ b/docs/cbc_sdk.enterprise_edr.rst @@ -1,5 +1,5 @@ -Enterprise EDR -============== +Enterprise EDR Package +================================ Submodules ---------- diff --git a/docs/cbc_sdk.platform.rst b/docs/cbc_sdk.platform.rst index 779720adb..57cff17a7 100644 --- a/docs/cbc_sdk.platform.rst +++ b/docs/cbc_sdk.platform.rst @@ -1,5 +1,5 @@ -Platform -======== +Platform Package +========================= Submodules ---------- @@ -52,8 +52,8 @@ cbc\_sdk.platform.jobs module :undoc-members: :show-inheritance: -cbc\_sdk.platform.network_threat_metadata module ------------------------------------------------- +cbc\_sdk.platform.network\_threat\_metadata module +-------------------------------------------------- .. automodule:: cbc_sdk.platform.network_threat_metadata :members: @@ -69,15 +69,15 @@ cbc\_sdk.platform.observations module :show-inheritance: cbc\_sdk.platform.policies module ----------------------------------- +--------------------------------- .. automodule:: cbc_sdk.platform.policies :members: :undoc-members: :show-inheritance: -cbc\_sdk.platform.policy_ruleconfigs module -------------------------------------------- +cbc\_sdk.platform.policy\_ruleconfigs module +-------------------------------------------- .. automodule:: cbc_sdk.platform.policy_ruleconfigs :members: diff --git a/docs/cbc_sdk.rst b/docs/cbc_sdk.rst index 93195737e..ecea29bb2 100644 --- a/docs/cbc_sdk.rst +++ b/docs/cbc_sdk.rst @@ -1,5 +1,5 @@ -CBC SDK -======= +CBC SDK Package +================ Subpackages ----------- diff --git a/docs/cbc_sdk.workload.rst b/docs/cbc_sdk.workload.rst index b9ca8793d..697390a05 100644 --- a/docs/cbc_sdk.workload.rst +++ b/docs/cbc_sdk.workload.rst @@ -1,11 +1,11 @@ -Workload +Workload Package ========================= Submodules ---------- cbc\_sdk.workload.nsx\_remediation module ------------------------------------------- +----------------------------------------- .. automodule:: cbc_sdk.workload.nsx_remediation :members: diff --git a/docs/concepts.rst b/docs/concepts.rst index c9e94dd02..d1089cf1b 100644 --- a/docs/concepts.rst +++ b/docs/concepts.rst @@ -1,68 +1,6 @@ Concepts ================================ -Live Response with Platform Devices ---------------------------------------------- -As of version 1.3.0 Live Response has been changed to support CUSTOM type API Keys which enables -the platform Device model and Live Response session to be used with a single API key. Ensure your -API key has the ``Device READ`` permission along with the desired :doc:`live-response` permissions - -:: - - # Device information is accessible with Platform Devices - >>> from cbc_sdk import CBCloudAPI - >>> from cbc_sdk.platform import Device - >>> api = CBCloudAPI(profile='platform') - >>> platform_devices = api.select(Device).set_os(["WINDOWS", "LINUX"]) - >>> for device in platform_devices: - ... print( - f''' - Device ID: {device.id} - Device Name: {device.name} - - ''') - Device ID: 1234 - Device Name: Win10x64 - - Device ID: 5678 - Device Name: UbuntuDev - - - # Live Response is accessible with Platform Devices - >>> from cbc_sdk import CBCloudAPI - >>> from cbc_sdk.platform import Device - >>> api = CBCloudAPI(profile='platform') - >>> platform_device = api.select(Device, 1234) - >>> platform_device.lr_session() - url: /appservices/v6/orgs/{org_key}/liveresponse/sessions/428:1234 -> status: PENDING - [...] - -For more examples on Live Response, check :doc:`live-response` - -USB Devices -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Note that ``USBDevice`` is distinct from either the Platform API ``Device`` or the Endpoint Standard ``Device``. Access -to USB devices is through the Endpoint Standard package ``from cbc_sdk.endpoint_standard import USBDevice``. - -:: - - # USB device information is accessible with Endpoint Standard - >>> from cbc_sdk import CBCloudAPI - >>> from cbc_sdk.endpoint_standard import USBDevice - >>> api = CBCloudAPI(profile='endpoint_standard') - >>> usb_devices = api.select(USBDevice).set_statuses(['APPROVED']) - >>> for usb in usb_devices: - ... print(f''' - ... USB Device ID: {usb.id} - ... USB Device: {usb.vendor_name} {usb.product_name} - ... ''') - USB Device ID: 774 - USB Device: SanDisk Ultra - - USB Device ID: 778 - USB Device: SanDisk Cruzer Mini - Queries ---------------------------------------- @@ -514,6 +452,67 @@ Get details for all events per alert Type: NETWORK Alert Id: ['BE084638'] +Live Response with Platform Devices +--------------------------------------------- +As of version 1.3.0 Live Response has been changed to support CUSTOM type API Keys which enables +the platform Device model and Live Response session to be used with a single API key. Ensure your +API key has the ``Device READ`` permission along with the desired :doc:`live-response` permissions + +:: + + # Device information is accessible with Platform Devices + >>> from cbc_sdk import CBCloudAPI + >>> from cbc_sdk.platform import Device + >>> api = CBCloudAPI(profile='platform') + >>> platform_devices = api.select(Device).set_os(["WINDOWS", "LINUX"]) + >>> for device in platform_devices: + ... print( + f''' + Device ID: {device.id} + Device Name: {device.name} + + ''') + Device ID: 1234 + Device Name: Win10x64 + + Device ID: 5678 + Device Name: UbuntuDev + + + # Live Response is accessible with Platform Devices + >>> from cbc_sdk import CBCloudAPI + >>> from cbc_sdk.platform import Device + >>> api = CBCloudAPI(profile='platform') + >>> platform_device = api.select(Device, 1234) + >>> platform_device.lr_session() + url: /appservices/v6/orgs/{org_key}/liveresponse/sessions/428:1234 -> status: PENDING + [...] + +For more examples on Live Response, check :doc:`live-response` + +USB Devices +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Note that ``USBDevice`` is distinct from either the Platform API ``Device`` or the Endpoint Standard ``Device``. Access +to USB devices is through the Endpoint Standard package ``from cbc_sdk.endpoint_standard import USBDevice``. + +:: + + # USB device information is accessible with Endpoint Standard + >>> from cbc_sdk import CBCloudAPI + >>> from cbc_sdk.endpoint_standard import USBDevice + >>> api = CBCloudAPI(profile='endpoint_standard') + >>> usb_devices = api.select(USBDevice).set_statuses(['APPROVED']) + >>> for usb in usb_devices: + ... print(f''' + ... USB Device ID: {usb.id} + ... USB Device: {usb.vendor_name} {usb.product_name} + ... ''') + USB Device ID: 774 + USB Device: SanDisk Ultra + + USB Device ID: 778 + USB Device: SanDisk Cruzer Mini Static Methods -------------- diff --git a/docs/getting-started.rst b/docs/getting-started.rst index d932f0b59..361e7cd5a 100644 --- a/docs/getting-started.rst +++ b/docs/getting-started.rst @@ -152,3 +152,9 @@ how you could identify yourself: integration_name=MyScript/0.9.0 See the :doc:`authentication` documentation for more information about credentials. + +Next Steps +---------- + + - :doc:`concepts`: General information about retrieving data from your Carbon Black Cloud instance + - :doc:`guides`: Information and Examples related to specific actions you want to take on your Carbon Black Cloud data \ No newline at end of file diff --git a/docs/guides-and-resources.rst b/docs/guides.rst similarity index 80% rename from docs/guides-and-resources.rst rename to docs/guides.rst index 1af00deba..486507f1d 100755 --- a/docs/guides-and-resources.rst +++ b/docs/guides.rst @@ -1,5 +1,5 @@ -Guides and Resources -==================== +Guides +====== Here we've listed a collection of tutorials, recorded demonstrations and other resources we think will be useful to get the most out of the Carbon Black Cloud Python SDK. @@ -16,13 +16,6 @@ In general, and unless otherwise indicated, these guides are directed at those t Certain guides may be more geared towards audiences with more experience with the Carbon Black Cloud, such as administrators. -Recordings ----------- - -Demonstrations are found on our `YouTube channel `_. - -A recent highlight shows how to schedule Audit and Remediation Tasks. - Guides ------ @@ -38,9 +31,3 @@ Guides * :doc:`vulnerabilities` - View asset (Endpoint or Workload) vulnerabilities to increase security visibility. * :doc:`watchlists-feeds-reports` - Work with Enterprise EDR watchlists, feeds, reports, and Indicators of Compromise (IOCs). * :doc:`workload` - Advanced protection purpose-built for securing modern workloads to reduce the attack surface and strengthen security posture. - -Examples --------- - -The `GitHub repository `_ also has -some example scripts which will help you get started using the SDK. diff --git a/docs/index.rst b/docs/index.rst index d436357b4..b8486e972 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -66,11 +66,28 @@ Get started with Carbon Black Cloud Python SDK here. For detailed information on authentication Getting Started concepts - guides-and-resources + resources porting-guide - logging - os_functional_testing - changelog + +Guides +------ + +.. toctree:: + :caption: Guides + :maxdepth: 2 + + alerts + device-control + differential-analysis + live-query + live-response + recommendations + reputation-override + unified-binary-store + users-grants + vulnerabilities + watchlists-feeds-reports + workload` Full SDK Documentation ---------------------- @@ -89,6 +106,9 @@ See detailed information on the objects and methods exposed by the Carbon Black cbc_sdk.platform cbc_sdk.workload cbc_sdk + logging + os_functional_testing + changelog exceptions Indices and tables diff --git a/docs/resources.rst b/docs/resources.rst new file mode 100755 index 000000000..76db5198c --- /dev/null +++ b/docs/resources.rst @@ -0,0 +1,30 @@ +Resources +==================== + +Here you can find examples, recorded demonstrations, and other resources we think will be useful +to get the most out of the Carbon Black Cloud Python SDK. + +Audience for These Resources +------------------------- + +In general, and unless otherwise indicated, these guides are directed at those that: + +- Have a working knowledge of Python. +- Have a basic understanding of what the Carbon Black Cloud does, and its basic terminology such as events, alerts, + and watchlists. + +Certain guides may be more geared towards audiences with more experience with the Carbon Black Cloud, such as +administrators. + +Examples +-------- + +The `GitHub repository `_ also has +some example scripts which will help you get started using the SDK. + +Recordings +---------- + +Demonstrations are found on our `YouTube channel `_. + +A recent highlight shows how to schedule Audit and Remediation Tasks. \ No newline at end of file From b4eaf5ecb8cffbe6952af78c025da95b5b62b08d Mon Sep 17 00:00:00 2001 From: Jasmine Clark <89797061+jclark-vmware@users.noreply.github.com> Date: Fri, 9 Jun 2023 10:01:00 -0400 Subject: [PATCH 47/76] reformatting sdk docs --- docs/getting-started.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting-started.rst b/docs/getting-started.rst index 361e7cd5a..49e7d53a0 100644 --- a/docs/getting-started.rst +++ b/docs/getting-started.rst @@ -156,5 +156,5 @@ See the :doc:`authentication` documentation for more information about credentia Next Steps ---------- - - :doc:`concepts`: General information about retrieving data from your Carbon Black Cloud instance + - :doc:`concepts`: General information about using the Carbon Black Cloud SDK - :doc:`guides`: Information and Examples related to specific actions you want to take on your Carbon Black Cloud data \ No newline at end of file From a597535518e241389858fa291018c24f58a14738 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Tue, 23 May 2023 15:56:26 -0600 Subject: [PATCH 48/76] added support for URI to ServerError and ClientError --- src/cbc_sdk/errors.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/cbc_sdk/errors.py b/src/cbc_sdk/errors.py index dc312374a..06015905c 100644 --- a/src/cbc_sdk/errors.py +++ b/src/cbc_sdk/errors.py @@ -44,20 +44,22 @@ def __str__(self): class ClientError(ApiError): """A ClientError is raised when an HTTP 4xx error code is returned from the Carbon Black server.""" - def __init__(self, error_code, message, result=None, original_exception=None): + def __init__(self, error_code, message, **kwargs): """ Initialize the ClientError. Args: error_code (int): The error code that was received from the server. message (str): The actual error message. - result (object): The result of the operation from the server. - original_exception (Exception): The exception that caused this one to be raised. + kwargs (dict): Additional arguments, which may include 'result' (server operation result), + 'original_exception' (exception causing this one to be raised), and 'uri' (URI being accessed + when this error was raised). """ - super(ClientError, self).__init__(message=message, original_exception=original_exception) + super(ClientError, self).__init__(message=message, original_exception=kwargs.get('original_exception', None)) self.error_code = error_code - self.result = result + self.result = kwargs.get('result', None) + self.uri = kwargs.get('uri', None) def __str__(self): """ @@ -109,20 +111,22 @@ def __str__(self): class ServerError(ApiError): """A ServerError is raised when an HTTP 5xx error code is returned from the Carbon Black server.""" - def __init__(self, error_code, message, result=None, original_exception=None): + def __init__(self, error_code, message, **kwargs): """ Initialize the ServerError. Args: error_code (int): The error code that was received from the server. message (str): The actual error message. - result (object): The result of the operation from the server. - original_exception (Exception): The exception that caused this one to be raised. + kwargs (dict): Additional arguments, which may include 'result' (server operation result), + 'original_exception' (exception causing this one to be raised), and 'uri' (URI being accessed + when this error was raised). """ - super(ServerError, self).__init__(message=message, original_exception=original_exception) + super(ServerError, self).__init__(message=message, original_exception=kwargs.get('original_exception', None)) self.error_code = error_code - self.result = result + self.result = kwargs.get('result', None) + self.uri = kwargs.get('uri', None) def __str__(self): """ From 41427cf50a91e51a381021e6399e00547eadc8de Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Wed, 24 May 2023 15:19:49 -0600 Subject: [PATCH 49/76] made sure to set the uri= parameter on ServerError and ClientError --- src/cbc_sdk/audit_remediation/base.py | 6 ++-- src/cbc_sdk/base.py | 5 +-- src/cbc_sdk/connection.py | 34 +++++-------------- .../endpoint_standard/usb_device_control.py | 3 +- src/cbc_sdk/platform/devices.py | 3 +- src/cbc_sdk/platform/policies.py | 8 +++-- src/cbc_sdk/platform/reputation.py | 7 ++-- src/cbc_sdk/platform/users.py | 2 +- src/cbc_sdk/rest_api.py | 3 +- src/cbc_sdk/workload/nsx_remediation.py | 2 +- 10 files changed, 33 insertions(+), 40 deletions(-) diff --git a/src/cbc_sdk/audit_remediation/base.py b/src/cbc_sdk/audit_remediation/base.py index 0c115d39f..176f74d29 100644 --- a/src/cbc_sdk/audit_remediation/base.py +++ b/src/cbc_sdk/audit_remediation/base.py @@ -132,7 +132,8 @@ def stop(self): self._last_refresh_time = time.time() return True except Exception: - raise ServerError(result.status_code, "Cannot parse response as JSON: {0:s}".format(result.content)) + raise ServerError(result.status_code, "Cannot parse response as JSON: {0:s}".format(result.content), + uri=url) return False def delete(self): @@ -591,7 +592,8 @@ def stop(self): self._last_refresh_time = time.time() return True except Exception: - raise ServerError(result.status_code, "Cannot parse response as JSON: {0:s}".format(result.content)) + raise ServerError(result.status_code, "Cannot parse response as JSON: {0:s}".format(result.content), + uri=url) return False def query_runs(self): diff --git a/src/cbc_sdk/base.py b/src/cbc_sdk/base.py index 3eeb48576..dfe71f703 100644 --- a/src/cbc_sdk/base.py +++ b/src/cbc_sdk/base.py @@ -910,7 +910,7 @@ def _refresh_if_needed(self, request_ret): message = request_ret.text raise ServerError(request_ret.status_code, message, - result="Did not update {} record.".format(self.__class__.__name__)) + result="Did not update {} record.".format(self.__class__.__name__), uri=None) else: try: message = request_ret.json() @@ -920,7 +920,8 @@ def _refresh_if_needed(self, request_ret): if post_result and post_result != "success": raise ServerError(request_ret.status_code, post_result, - result="Did not update {0:s} record.".format(self.__class__.__name__)) + result="Did not update {0:s} record.".format(self.__class__.__name__), + uri=None) else: refresh_required = True else: diff --git a/src/cbc_sdk/connection.py b/src/cbc_sdk/connection.py index 5e4b45b5e..5b15a538e 100644 --- a/src/cbc_sdk/connection.py +++ b/src/cbc_sdk/connection.py @@ -321,7 +321,7 @@ def http_request(self, method, url, **kwargs): original_exception=e) else: if r.status_code >= 500: - raise ServerError(error_code=r.status_code, message=r.text) + raise ServerError(error_code=r.status_code, message=r.text, uri=uri) elif r.status_code == 404: raise ObjectNotFoundError(uri=uri, message=r.text) elif r.status_code == 401: @@ -329,7 +329,7 @@ def http_request(self, method, url, **kwargs): elif r.status_code == 400 and try_json(r).get('reason') == 'query_malformed_syntax': raise QuerySyntaxError(uri=uri, message=r.text) elif r.status_code >= 400: - raise ClientError(error_code=r.status_code, message=r.text) + raise ClientError(error_code=r.status_code, message=r.text, uri=uri) return r def get(self, url, **kwargs): @@ -436,25 +436,6 @@ def __init__(self, *args, **kwargs): pool_maxsize=pool_maxsize, pool_block=pool_block) - def raise_unless_json(self, ret, expected): - """ - Raise a ServerError unless we got back an HTTP 200 response with JSON containing all the expected values. - - Args: - ret (object): Return value to be checked. - expected (dict): Expected keys and values that need to be found in the JSON response. - - Raises: - ServerError: If the HTTP response is anything but 200, or if the expected values are not found. - """ - if ret.status_code == 200: - message = ret.json() - for k, v in iter(expected.items()): - if k not in message or message[k] != v: - raise ServerError(ret.status_code, message) - else: - raise ServerError(ret.status_code, "{0}".format(ret.content), ) - def get_object(self, uri, query_parameters=None, default=None): """ Submit a GET request to the server and parse the result as JSON before returning. @@ -472,12 +453,14 @@ def get_object(self, uri, query_parameters=None, default=None): try: return result.json() except Exception: - raise ServerError(result.status_code, "Cannot parse response as JSON: {0:s}".format(result.content)) + raise ServerError(result.status_code, "Cannot parse response as JSON: {0:s}".format(result.content), + uri=uri) elif result.status_code == 204: # empty response return default else: - raise ServerError(error_code=result.status_code, message="Unknown error: {0}".format(result.content)) + raise ServerError(error_code=result.status_code, message="Unknown error: {0}".format(result.content), + uri=uri) def get_raw_data(self, uri, query_parameters=None, default=None, **kwargs): """ @@ -500,7 +483,8 @@ def get_raw_data(self, uri, query_parameters=None, default=None, **kwargs): # empty response return default else: - raise ServerError(error_code=result.status_code, message="Unknown error: {0}".format(result.content)) + raise ServerError(error_code=result.status_code, message="Unknown error: {0}".format(result.content), + uri=uri) def api_json_request(self, method, uri, **kwargs): """ @@ -536,7 +520,7 @@ def api_json_request(self, method, uri, **kwargs): return result if "errorMessage" in resp: - raise ServerError(error_code=result.status_code, message=resp["errorMessage"]) + raise ServerError(error_code=result.status_code, message=resp["errorMessage"], uri=uri) return result diff --git a/src/cbc_sdk/endpoint_standard/usb_device_control.py b/src/cbc_sdk/endpoint_standard/usb_device_control.py index f6f94aed3..10c0b3913 100755 --- a/src/cbc_sdk/endpoint_standard/usb_device_control.py +++ b/src/cbc_sdk/endpoint_standard/usb_device_control.py @@ -245,7 +245,8 @@ def delete(self): message = json.loads(ret.text)[0] except Exception: message = ret.text - raise ServerError(ret.status_code, message, result="Did not delete {0:s}.".format(str(self))) + raise ServerError(ret.status_code, message, result="Did not delete {0:s}.".format(str(self)), + uri=self._build_api_request_uri()) @classmethod def create(cls, cb, policy_id): diff --git a/src/cbc_sdk/platform/devices.py b/src/cbc_sdk/platform/devices.py index c87d0e362..86ac2d22f 100644 --- a/src/cbc_sdk/platform/devices.py +++ b/src/cbc_sdk/platform/devices.py @@ -217,7 +217,8 @@ def vulnerability_refresh(self): elif resp.status_code == 204: return None else: - raise ServerError(error_code=resp.status_code, message="Device action error: {0}".format(resp.content)) + raise ServerError(error_code=resp.status_code, message="Device action error: {0}".format(resp.content), + uri=url) def get_vulnerability_summary(self, category=None): """ diff --git a/src/cbc_sdk/platform/policies.py b/src/cbc_sdk/platform/policies.py index ce9f5eee5..57086f7cd 100644 --- a/src/cbc_sdk/platform/policies.py +++ b/src/cbc_sdk/platform/policies.py @@ -1232,15 +1232,17 @@ def _update_object(self): new_object_info = copy.deepcopy(self._info) if "id" in new_object_info: del new_object_info["id"] - ret = self._cb.post_object(self._parent._build_api_request_uri() + "/rules", new_object_info) + url = self._parent._build_api_request_uri() + "/rules" + ret = self._cb.post_object(url, new_object_info) else: - ret = self._cb.put_object(self._parent._build_api_request_uri() + f"/rules/{self.id}", self._info) + url = self._parent._build_api_request_uri() + f"/rules/{self.id}" + ret = self._cb.put_object(url, self._info) if ret.status_code not in range(200, 300): try: message = json.loads(ret.text)[0] except Exception: message = ret.text - raise ServerError(ret.status_code, message, result="Unable to update policy rule") + raise ServerError(ret.status_code, message, result="Unable to update policy rule", uri=url) self._info = json.loads(ret.text) self._full_init = True self._parent._on_updated_rule(self) diff --git a/src/cbc_sdk/platform/reputation.py b/src/cbc_sdk/platform/reputation.py index ba049cc3a..4177e98cd 100644 --- a/src/cbc_sdk/platform/reputation.py +++ b/src/cbc_sdk/platform/reputation.py @@ -86,7 +86,7 @@ def delete(self): message = json.loads(resp.text)[0] except Exception: message = resp.text - raise ServerError(resp.status_code, message, result="Did not delete {0:s}.".format(str(self))) + raise ServerError(resp.status_code, message, result="Did not delete {0:s}.".format(str(self)), uri=None) self._is_deleted = True @classmethod @@ -132,13 +132,14 @@ def bulk_delete(cls, cb, overrides): ] """ - resp = cb.post_object(cls.urlobject.format(cb.credentials.org_key) + "/_delete", overrides) + url = cls.urlobject.format(cb.credentials.org_key) + "/_delete" + resp = cb.post_object(url, overrides) if resp.status_code not in (200, 204): try: message = json.loads(resp.text)[0] except Exception: message = resp.text - raise ServerError(resp.status_code, message, result="Did not delete overrides.") + raise ServerError(resp.status_code, message, result="Did not delete overrides.", uri=url) return resp.json() diff --git a/src/cbc_sdk/platform/users.py b/src/cbc_sdk/platform/users.py index 883cb8d47..9e5673f64 100644 --- a/src/cbc_sdk/platform/users.py +++ b/src/cbc_sdk/platform/users.py @@ -209,7 +209,7 @@ def _create_user(cls, cb, user_data): resp = result.json() if resp['registration_status'] != 'SUCCESS': raise ServerError(500, f"registration return was unsuccessful: {resp['registration_status']} - " - f"{resp['message']}") + f"{resp['message']}", uri=url) # N.B.: new user is not "findable" until activated and initial password set def _refresh(self): diff --git a/src/cbc_sdk/rest_api.py b/src/cbc_sdk/rest_api.py index 326d64372..c168fe00d 100644 --- a/src/cbc_sdk/rest_api.py +++ b/src/cbc_sdk/rest_api.py @@ -188,7 +188,8 @@ def _raw_device_action(self, request): elif resp.status_code == 204: return None else: - raise ServerError(error_code=resp.status_code, message="Device action error: {0}".format(resp.content)) + raise ServerError(error_code=resp.status_code, message="Device action error: {0}".format(resp.content), + uri=url) def _device_action(self, device_ids, action_type, options=None): """ diff --git a/src/cbc_sdk/workload/nsx_remediation.py b/src/cbc_sdk/workload/nsx_remediation.py index 98a65950d..c492b4219 100644 --- a/src/cbc_sdk/workload/nsx_remediation.py +++ b/src/cbc_sdk/workload/nsx_remediation.py @@ -74,7 +74,7 @@ def start_request(cls, cb, device_ids, tag, set_tag=True): response = cb.post_object(url, body=request_body) if response.status_code != 201: raise ServerError(response.status_code, - f"could not start remediation request, error code {response.status_code}") + f"could not start remediation request, error code {response.status_code}", uri=url) job_ids = response.json().get('job_ids', []) if job_ids: return NSXRemediationJob(cb, job_ids) From 4873ab059e80f616383ac5bef3726cfafb4d2ae7 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Wed, 24 May 2023 15:26:02 -0600 Subject: [PATCH 50/76] Took out a test that tested a piece of dead code that was removed --- src/tests/unit/test_base_api.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/tests/unit/test_base_api.py b/src/tests/unit/test_base_api.py index 5508db0b3..a1a734105 100755 --- a/src/tests/unit/test_base_api.py +++ b/src/tests/unit/test_base_api.py @@ -170,19 +170,6 @@ def test_BaseAPI_generate_user_agent(integration, expected_line): assert sut.session.token_header['User-Agent'] == expected_line -@pytest.mark.parametrize("response, expected, scode", [ - (StubResponse({'color': 'green'}, 400), {}, 400), - (StubResponse({'color': 'green'}), {'color': 'blue'}, 200), - (StubResponse({'color': 'green'}), {'color': 'green', 'mode': 3}, 200) -]) -def test_BaseAPI_raise_unless_json_raises(response, expected, scode): - """Test the "raise" cases of raise_unless_json.""" - sut = BaseAPI(url='https://example.com', token='ABCDEFGH', org_key='A1B2C3D4') - with pytest.raises(ServerError) as excinfo: - sut.raise_unless_json(response, expected) - assert excinfo.value.error_code == scode - - @pytest.mark.parametrize("expath, response, params, default, expected", [ ('/path', StubResponse({'a': 1, 'b': 2}), None, {'a': 8, 'b': 9}, {'a': 1, 'b': 2}), ('/path', StubResponse({'a': 1, 'b': 2}), [('x', 1), ('y', 2)], {'a': 8, 'b': 9}, {'a': 1, 'b': 2}), From 7cbe080d445fb597a3b5ecc5e8b8499aca5a9119 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Thu, 25 May 2023 16:42:09 -0600 Subject: [PATCH 51/76] added test of URI field in exceptions --- src/tests/unit/test_connection.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tests/unit/test_connection.py b/src/tests/unit/test_connection.py index 8678e03af..e5f8ed60e 100755 --- a/src/tests/unit/test_connection.py +++ b/src/tests/unit/test_connection.py @@ -143,6 +143,7 @@ def test_http_request_error_code_cases(mox, response, exception_caught, prefix): with pytest.raises(exception_caught) as excinfo: conn.http_request('get', '/path') assert excinfo.value.message.startswith(prefix) + assert excinfo.value.uri == 'https://example.com/path' mox.VerifyAll() From 977b1de892b220d4cecf07a99de90409a167b423 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Fri, 2 Jun 2023 13:54:21 -0600 Subject: [PATCH 52/76] added URI (if present) to ServerError messages, and flipped "message" and "result" parameters to ServerError where it made sense to do so --- src/cbc_sdk/base.py | 12 ++++++------ src/cbc_sdk/endpoint_standard/usb_device_control.py | 2 +- src/cbc_sdk/errors.py | 4 +++- src/cbc_sdk/platform/policies.py | 2 +- src/cbc_sdk/platform/reputation.py | 4 ++-- src/cbc_sdk/rest_api.py | 4 ++-- 6 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/cbc_sdk/base.py b/src/cbc_sdk/base.py index dfe71f703..25bd971ca 100644 --- a/src/cbc_sdk/base.py +++ b/src/cbc_sdk/base.py @@ -909,8 +909,8 @@ def _refresh_if_needed(self, request_ret): except Exception: message = request_ret.text - raise ServerError(request_ret.status_code, message, - result="Did not update {} record.".format(self.__class__.__name__), uri=None) + raise ServerError(request_ret.status_code, f"Did not update {self.__class__.__name__} record.", + result=message, uri=None) else: try: message = request_ret.json() @@ -919,9 +919,9 @@ def _refresh_if_needed(self, request_ret): post_result = message.get("result", None) if post_result and post_result != "success": - raise ServerError(request_ret.status_code, post_result, - result="Did not update {0:s} record.".format(self.__class__.__name__), - uri=None) + raise ServerError(request_ret.status_code, + f"Did not update {self.__class__.__name__} record.", + result=post_result, uri=None) else: refresh_required = True else: @@ -980,7 +980,7 @@ def _delete_object(self): message = json.loads(ret.text)[0] except Exception: message = ret.text - raise ServerError(ret.status_code, message, result="Did not delete {0:s}.".format(str(self))) + raise ServerError(ret.status_code, f"Did not delete {str(self)}.", result=message, uri=None) def validate(self): """ diff --git a/src/cbc_sdk/endpoint_standard/usb_device_control.py b/src/cbc_sdk/endpoint_standard/usb_device_control.py index 10c0b3913..168befccb 100755 --- a/src/cbc_sdk/endpoint_standard/usb_device_control.py +++ b/src/cbc_sdk/endpoint_standard/usb_device_control.py @@ -245,7 +245,7 @@ def delete(self): message = json.loads(ret.text)[0] except Exception: message = ret.text - raise ServerError(ret.status_code, message, result="Did not delete {0:s}.".format(str(self)), + raise ServerError(ret.status_code, f"Did not delete {str(self)}.", result=message, uri=self._build_api_request_uri()) @classmethod diff --git a/src/cbc_sdk/errors.py b/src/cbc_sdk/errors.py index 06015905c..3400acf96 100644 --- a/src/cbc_sdk/errors.py +++ b/src/cbc_sdk/errors.py @@ -142,7 +142,9 @@ def __str__(self): msg += " (No further information provided)" if self.result: - msg += ". {}".format(self.result) + msg += f". {self.result}" + if self.uri: + msg += f" <{self.uri}>" return msg diff --git a/src/cbc_sdk/platform/policies.py b/src/cbc_sdk/platform/policies.py index 57086f7cd..c18cb1351 100644 --- a/src/cbc_sdk/platform/policies.py +++ b/src/cbc_sdk/platform/policies.py @@ -1242,7 +1242,7 @@ def _update_object(self): message = json.loads(ret.text)[0] except Exception: message = ret.text - raise ServerError(ret.status_code, message, result="Unable to update policy rule", uri=url) + raise ServerError(ret.status_code, "Unable to update policy rule", result=message, uri=url) self._info = json.loads(ret.text) self._full_init = True self._parent._on_updated_rule(self) diff --git a/src/cbc_sdk/platform/reputation.py b/src/cbc_sdk/platform/reputation.py index 4177e98cd..9ea3271a9 100644 --- a/src/cbc_sdk/platform/reputation.py +++ b/src/cbc_sdk/platform/reputation.py @@ -86,7 +86,7 @@ def delete(self): message = json.loads(resp.text)[0] except Exception: message = resp.text - raise ServerError(resp.status_code, message, result="Did not delete {0:s}.".format(str(self)), uri=None) + raise ServerError(resp.status_code, f"Did not delete {str(self)}.", result=message, uri=None) self._is_deleted = True @classmethod @@ -139,7 +139,7 @@ def bulk_delete(cls, cb, overrides): message = json.loads(resp.text)[0] except Exception: message = resp.text - raise ServerError(resp.status_code, message, result="Did not delete overrides.", uri=url) + raise ServerError(resp.status_code, "Did not delete overrides.", result=message, uri=url) return resp.json() diff --git a/src/cbc_sdk/rest_api.py b/src/cbc_sdk/rest_api.py index c168fe00d..c006cab1f 100644 --- a/src/cbc_sdk/rest_api.py +++ b/src/cbc_sdk/rest_api.py @@ -510,5 +510,5 @@ def get_policy_ruleconfig_parameter_schema(self, ruleconfig_id): url = f"/policyservice/v1/orgs/{self.credentials.org_key}/rule_configs/{ruleconfig_id}/parameters/schema" try: return self.get_object(url) - except ServerError: - raise InvalidObjectError(f"invalid rule config ID {ruleconfig_id}") + except ServerError as e: + raise InvalidObjectError(f"invalid rule config ID {ruleconfig_id}", original_exception=e) From 0729dae0bc5a4bf4de58c75ba51694e162449c3f Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Fri, 2 Jun 2023 14:12:43 -0600 Subject: [PATCH 53/76] renamed some variable references because Alex thought they needed it --- src/cbc_sdk/base.py | 22 +++++++++---------- .../endpoint_standard/usb_device_control.py | 6 ++--- src/cbc_sdk/platform/policies.py | 6 ++--- src/cbc_sdk/platform/reputation.py | 12 +++++----- 4 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/cbc_sdk/base.py b/src/cbc_sdk/base.py index 25bd971ca..d2e3c9d47 100644 --- a/src/cbc_sdk/base.py +++ b/src/cbc_sdk/base.py @@ -905,18 +905,18 @@ def _refresh_if_needed(self, request_ret): if request_ret.status_code not in range(200, 300): try: - message = json.loads(request_ret.text)[0] + result = json.loads(request_ret.text)[0] except Exception: - message = request_ret.text + result = request_ret.text raise ServerError(request_ret.status_code, f"Did not update {self.__class__.__name__} record.", - result=message, uri=None) + result=result, uri=None) else: try: - message = request_ret.json() - log.debug("Received response: %s" % message) - if list(message.keys()) == ["result"]: - post_result = message.get("result", None) + result = request_ret.json() + log.debug("Received response: %s" % result) + if list(result.keys()) == ["result"]: + post_result = result.get("result", None) if post_result and post_result != "success": raise ServerError(request_ret.status_code, @@ -926,7 +926,7 @@ def _refresh_if_needed(self, request_ret): refresh_required = True else: self._info = json.loads(request_ret.text) - if message.keys() == ["id"]: + if result.keys() == ["id"]: # if all we got back was an ID, try refreshing to get the entire record. log.debug("Only received an ID back from the server, forcing a refresh") refresh_required = True @@ -977,10 +977,10 @@ def _delete_object(self): if ret.status_code not in (200, 204): try: - message = json.loads(ret.text)[0] + result = json.loads(ret.text)[0] except Exception: - message = ret.text - raise ServerError(ret.status_code, f"Did not delete {str(self)}.", result=message, uri=None) + result = ret.text + raise ServerError(ret.status_code, f"Did not delete {str(self)}.", result=result, uri=None) def validate(self): """ diff --git a/src/cbc_sdk/endpoint_standard/usb_device_control.py b/src/cbc_sdk/endpoint_standard/usb_device_control.py index 168befccb..0cb98292a 100755 --- a/src/cbc_sdk/endpoint_standard/usb_device_control.py +++ b/src/cbc_sdk/endpoint_standard/usb_device_control.py @@ -242,10 +242,10 @@ def delete(self): if ret.status_code not in (200, 204): try: - message = json.loads(ret.text)[0] + result = json.loads(ret.text)[0] except Exception: - message = ret.text - raise ServerError(ret.status_code, f"Did not delete {str(self)}.", result=message, + result = ret.text + raise ServerError(ret.status_code, f"Did not delete {str(self)}.", result=result, uri=self._build_api_request_uri()) @classmethod diff --git a/src/cbc_sdk/platform/policies.py b/src/cbc_sdk/platform/policies.py index c18cb1351..fe0368cdb 100644 --- a/src/cbc_sdk/platform/policies.py +++ b/src/cbc_sdk/platform/policies.py @@ -1239,10 +1239,10 @@ def _update_object(self): ret = self._cb.put_object(url, self._info) if ret.status_code not in range(200, 300): try: - message = json.loads(ret.text)[0] + result = json.loads(ret.text)[0] except Exception: - message = ret.text - raise ServerError(ret.status_code, "Unable to update policy rule", result=message, uri=url) + result = ret.text + raise ServerError(ret.status_code, "Unable to update policy rule", result=result, uri=url) self._info = json.loads(ret.text) self._full_init = True self._parent._on_updated_rule(self) diff --git a/src/cbc_sdk/platform/reputation.py b/src/cbc_sdk/platform/reputation.py index 9ea3271a9..372fef70f 100644 --- a/src/cbc_sdk/platform/reputation.py +++ b/src/cbc_sdk/platform/reputation.py @@ -83,10 +83,10 @@ def delete(self): resp = self._cb.delete_object(self.urlobject_single.format(self._cb.credentials.org_key, self._model_unique_id)) if resp.status_code not in (200, 204): try: - message = json.loads(resp.text)[0] + result = json.loads(resp.text)[0] except Exception: - message = resp.text - raise ServerError(resp.status_code, f"Did not delete {str(self)}.", result=message, uri=None) + result = resp.text + raise ServerError(resp.status_code, f"Did not delete {str(self)}.", result=result, uri=None) self._is_deleted = True @classmethod @@ -136,10 +136,10 @@ def bulk_delete(cls, cb, overrides): resp = cb.post_object(url, overrides) if resp.status_code not in (200, 204): try: - message = json.loads(resp.text)[0] + result = json.loads(resp.text)[0] except Exception: - message = resp.text - raise ServerError(resp.status_code, "Did not delete overrides.", result=message, uri=url) + result = resp.text + raise ServerError(resp.status_code, "Did not delete overrides.", result=result, uri=url) return resp.json() From a66901e4422e8812a58fc48629fa3a6a419b14bf Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Mon, 5 Jun 2023 11:42:14 -0600 Subject: [PATCH 54/76] added a suggestion on URL handling in _refresh_if_needed() --- src/cbc_sdk/base.py | 4 ++-- src/tests/unit/base/test_base_models.py | 8 +++++--- src/tests/unit/fixtures/CBCSDKMock.py | 4 +++- src/tests/unit/fixtures/stubresponse.py | 3 ++- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/cbc_sdk/base.py b/src/cbc_sdk/base.py index d2e3c9d47..58577ab44 100644 --- a/src/cbc_sdk/base.py +++ b/src/cbc_sdk/base.py @@ -910,7 +910,7 @@ def _refresh_if_needed(self, request_ret): result = request_ret.text raise ServerError(request_ret.status_code, f"Did not update {self.__class__.__name__} record.", - result=result, uri=None) + result=result, uri=request_ret.url) else: try: result = request_ret.json() @@ -921,7 +921,7 @@ def _refresh_if_needed(self, request_ret): if post_result and post_result != "success": raise ServerError(request_ret.status_code, f"Did not update {self.__class__.__name__} record.", - result=post_result, uri=None) + result=post_result, uri=request_ret.url) else: refresh_required = True else: diff --git a/src/tests/unit/base/test_base_models.py b/src/tests/unit/base/test_base_models.py index 872b4f27d..2e85f21c3 100644 --- a/src/tests/unit/base/test_base_models.py +++ b/src/tests/unit/base/test_base_models.py @@ -352,7 +352,8 @@ def test_delete_mbm(cbcsdk_mock): STUBOBJECT_GET_RESP_2) newStub = StubObject(api, 30243) delete_resp = cbcsdk_mock.StubResponse(contents={"success": False}, scode=403, - text="Failed to delete for some reason") + text="Failed to delete for some reason", + url="/testing_only/v1/stubobjects/30243") cbcsdk_mock.mock_request("DELETE", "/testing_only/v1/stubobjects/30243", delete_resp) with pytest.raises(ServerError): newStub.delete() @@ -368,12 +369,13 @@ def test_refresh_if_needed_mbm(cbcsdk_mock): mutableBaseModelStub = StubObject(api, 30242) # 200 status code - refresh_resp_200 = cbcsdk_mock.StubResponse(STUBOBJECT_GET_RESP_1, 200) + refresh_resp_200 = cbcsdk_mock.StubResponse(STUBOBJECT_GET_RESP_1, 200, url="/testing_only/v1/stubobjects/30242") model_id = mutableBaseModelStub._refresh_if_needed(refresh_resp_200) assert model_id == 30242 # 404 status code - refresh_resp_404 = cbcsdk_mock.StubResponse({}, 404, "Object not found text") + refresh_resp_404 = cbcsdk_mock.StubResponse({}, 404, "Object not found text", + url="/testing_only/v1/stubobjects/30242") with pytest.raises(ServerError): model_id = mutableBaseModelStub._refresh_if_needed(refresh_resp_404) assert model_id == 12345 diff --git a/src/tests/unit/fixtures/CBCSDKMock.py b/src/tests/unit/fixtures/CBCSDKMock.py index adbcc6ffc..449b2e656 100644 --- a/src/tests/unit/fixtures/CBCSDKMock.py +++ b/src/tests/unit/fixtures/CBCSDKMock.py @@ -48,12 +48,13 @@ def __init__(self, monkeypatch, api): class StubResponse(object): """Stubbed response to object to support json function similar to requests package""" - def __init__(self, contents, scode=200, text="", json_parsable=True): + def __init__(self, contents, scode=200, text="", json_parsable=True, url=None): """Init default properties""" if isinstance(contents, CBCSDKMock.StubResponse): self.content = contents.content self.status_code = contents.status_code self.text = contents.text + self.url = contents.url self._json_parsable = contents._json_parsable else: self.content = contents @@ -62,6 +63,7 @@ def __init__(self, contents, scode=200, text="", json_parsable=True): self.text = json.dumps(contents) else: self.text = text + self.url = url self._json_parsable = json_parsable def json(self): diff --git a/src/tests/unit/fixtures/stubresponse.py b/src/tests/unit/fixtures/stubresponse.py index e34c4750d..3a82fcd27 100755 --- a/src/tests/unit/fixtures/stubresponse.py +++ b/src/tests/unit/fixtures/stubresponse.py @@ -13,10 +13,11 @@ def total_seconds(self): class StubResponse(object): """Stub for an HTTP response object.""" - def __init__(self, contents, scode=200, text=None): + def __init__(self, contents, scode=200, text=None, url=None): """Initialize the StubResponse object.""" self._contents = contents self.status_code = scode + self.url = url self.text = text or json.dumps(contents) self.content = self.text self.elapsed = StubElapsed() From dd9a41a0199fbb5c76366e96601ab1a0f70661c4 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Wed, 14 Jun 2023 08:25:48 -0600 Subject: [PATCH 55/76] changed format of ServerError at Robert's and Alex's suggestions --- src/cbc_sdk/errors.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/cbc_sdk/errors.py b/src/cbc_sdk/errors.py index 3400acf96..d5b9d2812 100644 --- a/src/cbc_sdk/errors.py +++ b/src/cbc_sdk/errors.py @@ -136,16 +136,19 @@ def __str__(self): str: String equivalent of the exception. """ msg = "Received error code {0:d} from API".format(self.error_code) - if self.message: - msg += ": {0:s}".format(self.message) - else: - msg += " (No further information provided)" - - if self.result: - msg += f". {self.result}" if self.uri: msg += f" <{self.uri}>" - return msg + details = "" + if self.message: + details += f" ({self.message})" + if self.result: + fmt_result = str(self.result) + if len(fmt_result) > 100: + fmt_result = f"{fmt_result[0:100]} [...]" + details += f" ({fmt_result})" + if not details: + details = " (No further information provided)" + return msg + details class ObjectNotFoundError(ApiError): From 63888ad4f45190f3c793ad4062b5c6f08bf94545 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Wed, 14 Jun 2023 09:52:12 -0600 Subject: [PATCH 56/76] took out truncation from ServerError as Alex says it's not needed --- src/cbc_sdk/errors.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/cbc_sdk/errors.py b/src/cbc_sdk/errors.py index d5b9d2812..47a613e83 100644 --- a/src/cbc_sdk/errors.py +++ b/src/cbc_sdk/errors.py @@ -142,10 +142,7 @@ def __str__(self): if self.message: details += f" ({self.message})" if self.result: - fmt_result = str(self.result) - if len(fmt_result) > 100: - fmt_result = f"{fmt_result[0:100]} [...]" - details += f" ({fmt_result})" + details += f" ({self.result})" if not details: details = " (No further information provided)" return msg + details From b7c72ded5782a514bd29658582440bdfbc69619e Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Mon, 12 Jun 2023 14:27:28 -0600 Subject: [PATCH 57/76] added partial tests - coverage now 94% --- src/cbc_sdk/platform/policies.py | 16 +++- src/cbc_sdk/platform/policy_ruleconfigs.py | 78 +++++++++++++++++++ .../unit/fixtures/platform/mock_policies.py | 39 ++++++++++ .../platform/mock_policy_ruleconfigs.py | 15 ++++ .../unit/platform/test_policy_ruleconfigs.py | 22 +++++- 5 files changed, 166 insertions(+), 4 deletions(-) diff --git a/src/cbc_sdk/platform/policies.py b/src/cbc_sdk/platform/policies.py index fe0368cdb..c2a0fea42 100644 --- a/src/cbc_sdk/platform/policies.py +++ b/src/cbc_sdk/platform/policies.py @@ -16,13 +16,15 @@ import json from types import MappingProxyType from cbc_sdk.base import MutableBaseModel, BaseQuery, IterableQueryMixin, AsyncQueryMixin -from cbc_sdk.platform.policy_ruleconfigs import PolicyRuleConfig, CorePreventionRuleConfig, HostBasedFirewallRuleConfig +from cbc_sdk.platform.policy_ruleconfigs import (PolicyRuleConfig, CorePreventionRuleConfig, + HostBasedFirewallRuleConfig, DataCollectionRuleConfig) from cbc_sdk.errors import ApiError, ServerError, InvalidObjectError SPECIFIC_RULECONFIGS = MappingProxyType({ "core_prevention": CorePreventionRuleConfig, - "host_based_firewall": HostBasedFirewallRuleConfig + "host_based_firewall": HostBasedFirewallRuleConfig, + "data_collection": DataCollectionRuleConfig }) @@ -732,6 +734,16 @@ def host_based_firewall_rule_config(self): raise InvalidObjectError("found multiple host-based firewall rule configurations") return tmp[0] + @property + def data_collection_rule_configs_list(self): + """ + Returns a list of data collection rule configuration objects for this Policy. + + Returns: + list: A list of DataCollectionRuleConfig objects. + """ + return [rconf for rconf in self.object_rule_configs.values() if isinstance(rconf, DataCollectionRuleConfig)] + def valid_rule_configs(self): """ Returns a dictionary identifying all valid rule configurations for this policy. diff --git a/src/cbc_sdk/platform/policy_ruleconfigs.py b/src/cbc_sdk/platform/policy_ruleconfigs.py index a9cfd3761..6a17bf74e 100644 --- a/src/cbc_sdk/platform/policy_ruleconfigs.py +++ b/src/cbc_sdk/platform/policy_ruleconfigs.py @@ -124,6 +124,19 @@ def is_dirty(self): """ return self._params_changed or super(PolicyRuleConfig, self).is_dirty() + @property + def parameter_names(self): + """ + Returns a list of parameter names in this rule configuration. + + Returns: + list[str]: A list of parameter names in this rule configuration. + """ + if 'parameters' not in self._info: + self.refresh() + params = self._info['parameters'] + return list(params.keys()) + def get_parameter(self, name, default_value=None): """ Returns a parameter value from the rule configuration. @@ -618,3 +631,68 @@ def export_rules(self, format="json"): return self._cb.get_object(url, {"format": format}) else: return self._cb.get_raw_data(url, {"format": format}) + + +class DataCollectionRuleConfig(PolicyRuleConfig): + """ + Represents a data collection 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 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) + permission. + """ + urlobject_single = "/policyservice/v1/orgs/{0}/policies/{1}/rule_configs/data_collection" + 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 CorePreventionRuleConfig 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(DataCollectionRuleConfig, 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, "parameters": self.parameters}] + 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) + f"/{self.id}" + self._cb.delete_object(url) diff --git a/src/tests/unit/fixtures/platform/mock_policies.py b/src/tests/unit/fixtures/platform/mock_policies.py index 2fd28056d..b189706ae 100644 --- a/src/tests/unit/fixtures/platform/mock_policies.py +++ b/src/tests/unit/fixtures/platform/mock_policies.py @@ -190,6 +190,16 @@ } ], "rule_configs": [ + { + "id": "91c919da-fb90-4e63-9eac-506255b0a0d0", + "name": "Authentication Events", + "description": "Authentication Events", + "inherited_from": "", + "category": "data_collection", + "parameters": { + "enable_auth_events": True + } + }, { "id": "1f8a5e4b-34f2-4d31-9f8f-87c56facaec8", "name": "Advanced Scripting Prevention", @@ -1635,6 +1645,16 @@ "quarantine": True }, "rule_configs": [ + { + "id": "91c919da-fb90-4e63-9eac-506255b0a0d0", + "name": "Authentication Events", + "description": "Authentication Events", + "inherited_from": "", + "category": "data_collection", + "parameters": { + "enable_auth_events": True + } + }, { "id": "1f8a5e4b-34f2-4d31-9f8f-87c56facaec8", "name": "Advanced Scripting Prevention", @@ -1706,6 +1726,15 @@ POLICY_CONFIG_PRESENTATION = { "configs": [ + { + "id": "91c919da-fb90-4e63-9eac-506255b0a0d0", + "name": "Authentication Events", + "description": "Authentication Events", + "presentation": { + "category": "data_collection" + }, + "parameters": [] + }, { "id": "1f8a5e4b-34f2-4d31-9f8f-87c56facaec8", "name": "Advanced Scripting Prevention", @@ -2219,6 +2248,16 @@ } ], "rule_configs": [ + { + "id": "91c919da-fb90-4e63-9eac-506255b0a0d0", + "name": "Authentication Events", + "description": "Authentication Events", + "inherited_from": "", + "category": "data_collection", + "parameters": { + "enable_auth_events": True + } + }, { "id": "ac67fa14-f6be-4df9-93f2-6de0dbd96061", "name": "Credential Theft", diff --git a/src/tests/unit/fixtures/platform/mock_policy_ruleconfigs.py b/src/tests/unit/fixtures/platform/mock_policy_ruleconfigs.py index 9342c0672..834be7495 100644 --- a/src/tests/unit/fixtures/platform/mock_policy_ruleconfigs.py +++ b/src/tests/unit/fixtures/platform/mock_policy_ruleconfigs.py @@ -1173,3 +1173,18 @@ Crapco,Isolate,IRC is a sewer,2,true,1,true,BLOCK_ALERT,*,BOTH,10.29.99.1,*,26.2.0.74,6667,TCP Crapco,,,3,true,1,true,ALLOW,*,BOTH,*,*,*,*,ANY """ + +DATA_COLLECTION_RETURNS = { + "results": [ + { + "id": "91c919da-fb90-4e63-9eac-506255b0a0d0", + "name": "Authentication Events", + "description": "Authentication Events", + "inherited_from": "", + "category": "data_collection", + "parameters": { + "enable_auth_events": True + } + } + ] +} \ No newline at end of file diff --git a/src/tests/unit/platform/test_policy_ruleconfigs.py b/src/tests/unit/platform/test_policy_ruleconfigs.py index 356381b14..53d94ca05 100644 --- a/src/tests/unit/platform/test_policy_ruleconfigs.py +++ b/src/tests/unit/platform/test_policy_ruleconfigs.py @@ -17,7 +17,8 @@ from contextlib import ExitStack as does_not_raise from cbc_sdk.rest_api import CBCloudAPI from cbc_sdk.platform import Policy, PolicyRuleConfig -from cbc_sdk.platform.policy_ruleconfigs import CorePreventionRuleConfig +from cbc_sdk.platform.policy_ruleconfigs import (CorePreventionRuleConfig, HostBasedFirewallRuleConfig, + DataCollectionRuleConfig) 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, @@ -38,7 +39,8 @@ HBFW_COPY_RULES_PUT_REQUEST, HBFW_COPY_RULES_PUT_RESPONSE, HBFW_EXPORT_RULE_CONFIGS_RESPONSE, - HBFW_EXPORT_RULE_CONFIGS_RESPONSE_CSV) + HBFW_EXPORT_RULE_CONFIGS_RESPONSE_CSV, + DATA_COLLECTION_RETURNS) logging.basicConfig(format='%(asctime)s %(levelname)s:%(message)s', level=logging.DEBUG, filename='log.txt') @@ -104,6 +106,7 @@ def param_schema(uri, query_params, default): rule_config = Policy._create_rule_config(api, None, initial_data) with handler as h: rule_config.validate() + assert rule_config.parameter_names == list(initial_data['parameters'].keys()) if message is not None: assert h.value.args[0] == message @@ -157,6 +160,7 @@ def test_rule_config_refresh(cbcsdk_mock, policy): assert rule_config.name == old_name assert rule_config.category == old_category assert rule_config.parameters == old_parameters + assert rule_config.parameter_names == list(old_parameters.keys()) def test_rule_config_add_base_not_implemented(cbcsdk_mock): @@ -209,8 +213,14 @@ def test_rule_config_initialization_matches_categories(policy): for cfg in policy.object_rule_configs.values(): if cfg.category == "core_prevention": assert isinstance(cfg, CorePreventionRuleConfig) + elif cfg.category == "host_based_firewall": + assert isinstance(cfg, HostBasedFirewallRuleConfig) + elif cfg.category == "data_collection": + assert isinstance(cfg, DataCollectionRuleConfig) else: assert not isinstance(cfg, CorePreventionRuleConfig) + assert not isinstance(cfg, HostBasedFirewallRuleConfig) + assert not isinstance(cfg, DataCollectionRuleConfig) def test_core_prevention_refresh(cbcsdk_mock, policy): @@ -670,3 +680,11 @@ def test_export_hbfw_rules_bad_format(cb): policy = Policy(cb, 1492, copy.deepcopy(FULL_POLICY_5), False, True) with pytest.raises(ApiError): policy.host_based_firewall_rule_config.export_rules('mp3') + + +def test_data_collection_refresh(cbcsdk_mock, policy): + """Tests the refresh operation for a CorePreventionRuleConfig.""" + cbcsdk_mock.mock_request('GET', '/policyservice/v1/orgs/test/policies/65536/rule_configs/data_collection', + DATA_COLLECTION_RETURNS) + for rule_config in policy.data_collection_rule_configs_list: + rule_config.refresh() From b7cc69e7e1319587bf7dddedfe1d1296b72b25c5 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Mon, 12 Jun 2023 16:02:52 -0600 Subject: [PATCH 58/76] test coverage now 96%, within acceptable limits --- src/cbc_sdk/platform/policies.py | 12 ++++ .../platform/mock_policy_ruleconfigs.py | 25 +++++++ .../unit/platform/test_policy_ruleconfigs.py | 68 ++++++++++++++++++- 3 files changed, 103 insertions(+), 2 deletions(-) diff --git a/src/cbc_sdk/platform/policies.py b/src/cbc_sdk/platform/policies.py index c2a0fea42..cbbc308a1 100644 --- a/src/cbc_sdk/platform/policies.py +++ b/src/cbc_sdk/platform/policies.py @@ -734,6 +734,18 @@ def host_based_firewall_rule_config(self): raise InvalidObjectError("found multiple host-based firewall rule configurations") return tmp[0] + @property + def data_collection_rule_configs(self): + """ + Returns a dictionary of data collection rule configuration IDs and objects for this Policy. + + Returns: + dict: A dictionary with data collection rule configuration IDs as keys and DataCollectionRuleConfig objects + as values. + """ + return {key: rconf for (key, rconf) in self.object_rule_configs.items() + if isinstance(rconf, DataCollectionRuleConfig)} + @property def data_collection_rule_configs_list(self): """ diff --git a/src/tests/unit/fixtures/platform/mock_policy_ruleconfigs.py b/src/tests/unit/fixtures/platform/mock_policy_ruleconfigs.py index 834be7495..e40308a22 100644 --- a/src/tests/unit/fixtures/platform/mock_policy_ruleconfigs.py +++ b/src/tests/unit/fixtures/platform/mock_policy_ruleconfigs.py @@ -1187,4 +1187,29 @@ } } ] +} + +DATA_COLLECTION_UPDATE_1 = [ + { + "id": "91c919da-fb90-4e63-9eac-506255b0a0d0", + "parameters": { + "enable_auth_events": False + } + } +] + +DATA_COLLECTION_UPDATE_RETURNS_1 = { + "successful": [ + { + "id": "91c919da-fb90-4e63-9eac-506255b0a0d0", + "name": "Authentication Events", + "description": "Authentication Events", + "inherited_from": "", + "category": "data_collection", + "parameters": { + "enable_auth_events": False + } + } + ], + "failed": [] } \ No newline at end of file diff --git a/src/tests/unit/platform/test_policy_ruleconfigs.py b/src/tests/unit/platform/test_policy_ruleconfigs.py index 53d94ca05..9639fe3fa 100644 --- a/src/tests/unit/platform/test_policy_ruleconfigs.py +++ b/src/tests/unit/platform/test_policy_ruleconfigs.py @@ -40,7 +40,8 @@ HBFW_COPY_RULES_PUT_RESPONSE, HBFW_EXPORT_RULE_CONFIGS_RESPONSE, HBFW_EXPORT_RULE_CONFIGS_RESPONSE_CSV, - DATA_COLLECTION_RETURNS) + DATA_COLLECTION_RETURNS, DATA_COLLECTION_UPDATE_1, + DATA_COLLECTION_UPDATE_RETURNS_1) logging.basicConfig(format='%(asctime)s %(levelname)s:%(message)s', level=logging.DEBUG, filename='log.txt') @@ -683,8 +684,71 @@ def test_export_hbfw_rules_bad_format(cb): def test_data_collection_refresh(cbcsdk_mock, policy): - """Tests the refresh operation for a CorePreventionRuleConfig.""" + """Tests the refresh operation for a DataCollectionRuleConfig.""" cbcsdk_mock.mock_request('GET', '/policyservice/v1/orgs/test/policies/65536/rule_configs/data_collection', DATA_COLLECTION_RETURNS) for rule_config in policy.data_collection_rule_configs_list: rule_config.refresh() + + +def test_data_collection_update_and_save(cbcsdk_mock, policy): + """Tests updating the data collection data and saving it.""" + put_called = False + + def on_put(url, body, **kwargs): + nonlocal put_called + assert body == DATA_COLLECTION_UPDATE_1 + put_called = True + return copy.deepcopy(DATA_COLLECTION_UPDATE_RETURNS_1) + + 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/data_collection', on_put) + rule_config = policy.data_collection_rule_configs['91c919da-fb90-4e63-9eac-506255b0a0d0'] + assert rule_config.name == 'Authentication Events' + assert rule_config.get_parameter('enable_auth_events') is True + rule_config.set_parameter('enable_auth_events', False) + rule_config.save() + assert put_called + + +def test_data_collection_update_via_replace(cbcsdk_mock, policy): + """Tests updating the data collection data and saving it via replace_rule_config.""" + put_called = False + + def on_put(url, body, **kwargs): + nonlocal put_called + assert body == DATA_COLLECTION_UPDATE_1 + put_called = True + return copy.deepcopy(DATA_COLLECTION_UPDATE_RETURNS_1) + + 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/data_collection', on_put) + rule_config = policy.data_collection_rule_configs['91c919da-fb90-4e63-9eac-506255b0a0d0'] + assert rule_config.name == 'Authentication Events' + assert rule_config.get_parameter('enable_auth_events') is True + new_data = copy.deepcopy(rule_config._info) + new_data["parameters"]["enable_auth_events"] = False + policy.replace_rule_config('91c919da-fb90-4e63-9eac-506255b0a0d0', new_data) + assert put_called + assert rule_config.get_parameter('enable_auth_events') is False + + +def test_data_collection_delete(cbcsdk_mock, policy): + """Tests delete of a data collection data item.""" + 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/data_collection' + '/91c919da-fb90-4e63-9eac-506255b0a0d0', on_delete) + rule_config = policy.data_collection_rule_configs['91c919da-fb90-4e63-9eac-506255b0a0d0'] + assert rule_config.name == 'Authentication Events' + rule_config.delete() + assert delete_called From 3909988ed38bb77ae5072b8b809731f96e1e1e48 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Tue, 13 Jun 2023 11:28:24 -0600 Subject: [PATCH 59/76] deflake8'd --- src/tests/unit/fixtures/platform/mock_policy_ruleconfigs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/unit/fixtures/platform/mock_policy_ruleconfigs.py b/src/tests/unit/fixtures/platform/mock_policy_ruleconfigs.py index e40308a22..f55d63eee 100644 --- a/src/tests/unit/fixtures/platform/mock_policy_ruleconfigs.py +++ b/src/tests/unit/fixtures/platform/mock_policy_ruleconfigs.py @@ -1212,4 +1212,4 @@ } ], "failed": [] -} \ No newline at end of file +} From 3d0497b261bc40432502a7be61a5563fb73483ed Mon Sep 17 00:00:00 2001 From: Kylie Ebringer Date: Tue, 20 Jun 2023 09:36:13 -0600 Subject: [PATCH 60/76] fixed typo on workload guide --- docs/workload.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/workload.rst b/docs/workload.rst index 51b249e7b..72909a61c 100644 --- a/docs/workload.rst +++ b/docs/workload.rst @@ -72,7 +72,7 @@ Example (vSphere workloads):: >>> cbc = CBCloudAPI() >>> query = cbc.select(VCenterComputeResource).set_os_type(['WINDOWS']).set_cluster_name(['example-cluster-name']) >>> for result in list(query): - ... print(results) + ... print(result) Example Output:: @@ -136,7 +136,7 @@ Example (AWS workloads):: >>> query = cbc.select(AWSComputeResource).set_region(['us-west-1']) >>> results = list(query) >>> for result in results: - ... print(results) + ... print(result) Example Output:: From 2a3ab84a242143e2a1b35263555f2d2db87aa7eb Mon Sep 17 00:00:00 2001 From: Jasmine Clark <89797061+jclark-vmware@users.noreply.github.com> Date: Tue, 20 Jun 2023 11:56:58 -0400 Subject: [PATCH 61/76] reformatting sdk docs --- docs/index.rst | 3 ++- docs/policy.rst | 3 +-- docs/vulnerabilities.rst | 2 +- docs/workload.rst | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index b8486e972..e2b72d356 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -81,13 +81,14 @@ Guides differential-analysis live-query live-response + policy recommendations reputation-override unified-binary-store users-grants vulnerabilities watchlists-feeds-reports - workload` + workload Full SDK Documentation ---------------------- diff --git a/docs/policy.rst b/docs/policy.rst index ddf52bba1..fe1752662 100644 --- a/docs/policy.rst +++ b/docs/policy.rst @@ -1,7 +1,6 @@ -Policy - Core Prevention and Host-Based Firewall Examples +Policy ========================================================= - A policy determines preventative behavior and establishes sensor settings. Each endpoint sensor or sensor group is assigned a policy. diff --git a/docs/vulnerabilities.rst b/docs/vulnerabilities.rst index 21c8cf215..eb82f7cba 100644 --- a/docs/vulnerabilities.rst +++ b/docs/vulnerabilities.rst @@ -1,4 +1,4 @@ -Managing Vulnerabilities +Vulnerabilities ======================== The Vulnerability Assessment API allows users to view asset (Endpoint or Workload) vulnerabilities, diff --git a/docs/workload.rst b/docs/workload.rst index 51b249e7b..618122a4d 100644 --- a/docs/workload.rst +++ b/docs/workload.rst @@ -1,4 +1,4 @@ -VM Workloads Search Guide and Examples +Workloads ====================================== These APIs allow you to visualize the inventory of compute resources available under either vSphere From 7f96b47637752d0b7273b64bac008a0dcbb57484 Mon Sep 17 00:00:00 2001 From: Kylie Ebringer Date: Tue, 20 Jun 2023 17:45:53 -0600 Subject: [PATCH 62/76] fixed formating of policy guide. --- docs/policy.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/policy.rst b/docs/policy.rst index ddf52bba1..98b1e7a38 100644 --- a/docs/policy.rst +++ b/docs/policy.rst @@ -10,9 +10,15 @@ prevents or allows behavior on your endpoint. Within Policies, you can create cu applications, and modify the way your sensor communicates with the Carbon Black Cloud. Example scripts are available in the GitHub repository in examples/platform that demonstrate + * Basic Create, Read, Update, Delete and Export/Import operations for Prevention, Local Scan and Sensor rules + * policy_service_crud_operations.py + * Core Prevention policy rule operations + * policy_core_prevention.py + * Host-Based Firewall policy rule operations + * policy_host_based_firewall.py From 3890c18e099b2cc6a92bd00258272c4b6c506df0 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Thu, 15 Jun 2023 15:46:55 -0600 Subject: [PATCH 63/76] change IOC link validation to be either a URL or a simple domain name --- src/cbc_sdk/enterprise_edr/threat_intelligence.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cbc_sdk/enterprise_edr/threat_intelligence.py b/src/cbc_sdk/enterprise_edr/threat_intelligence.py index d36afb977..0236f4a15 100644 --- a/src/cbc_sdk/enterprise_edr/threat_intelligence.py +++ b/src/cbc_sdk/enterprise_edr/threat_intelligence.py @@ -1725,8 +1725,8 @@ def validate(self): """ super(IOC_V2, self).validate() - if self.link and not validators.domain(self.link): - raise InvalidObjectError("link should be a valid domain URL (FQDN)") + if self.link and not (validators.url(self.link) or validators.domain(self.link)): + raise InvalidObjectError(f"link should be a valid URL or domain: {self.link}") @property def ignored(self): From 09565fcc571dcc30814c29d6192d528889460a2c Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Thu, 15 Jun 2023 16:12:36 -0600 Subject: [PATCH 64/76] a large number of tests of link validation for IOCs (15 right now) --- .../test_enterprise_edr_threatintel.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/tests/unit/enterprise_edr/test_enterprise_edr_threatintel.py b/src/tests/unit/enterprise_edr/test_enterprise_edr_threatintel.py index 1a91574d3..8d46f5f6f 100644 --- a/src/tests/unit/enterprise_edr/test_enterprise_edr_threatintel.py +++ b/src/tests/unit/enterprise_edr/test_enterprise_edr_threatintel.py @@ -418,6 +418,30 @@ def test_create_regex_ioc(cb): assert re.fullmatch(GUID_PATTERN, ioc._info['id']) +@pytest.mark.parametrize("linkvalue, expectation", [ + ('', does_not_raise()), + ('silcom.com', does_not_raise()), + ('silcom.com.', pytest.raises(InvalidObjectError)), + ('silcom. com', pytest.raises(InvalidObjectError)), + ('bloom-beacon.mit.edu', does_not_raise()), + ('bloom-beacon.mit.edu/', pytest.raises(InvalidObjectError)), + ('199.201.128.1', pytest.raises(InvalidObjectError)), + ('simplename', pytest.raises(InvalidObjectError)), + ('erbosoft.com/git', pytest.raises(InvalidObjectError)), + ('http://erbosoft.com/git', does_not_raise()), + ('https://erbosoft.com/git', does_not_raise()), + ('bogusschema://erbosoft.com', pytest.raises(InvalidObjectError)), + ('ftp://26.2.0.74', does_not_raise()), + ('https://midwinter.com/lurk/lurker.html', does_not_raise()), + ('//servername/share', pytest.raises(InvalidObjectError)) +]) +def test_ioc_link_validation(cb, linkvalue, expectation): + ioc = IOC_V2.create_equality(cb, None, "process_name", "Alpha") + ioc.link = linkvalue + with expectation: + ioc.validate() + + def test_ioc_read_ignored(cbcsdk_mock): """Tests reading the ignore status of an IOC.""" cbcsdk_mock.mock_request("GET", "/threathunter/watchlistmgr/v3/orgs/test/reports/a1b2/iocs/foo/ignore", From d127cae34f387ea83c2f4af7bde3a9fec3da127a Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Fri, 16 Jun 2023 10:55:36 -0600 Subject: [PATCH 65/76] Added IP address validation and made report link validation match IOC --- src/cbc_sdk/enterprise_edr/threat_intelligence.py | 6 +++--- .../unit/enterprise_edr/test_enterprise_edr_threatintel.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/cbc_sdk/enterprise_edr/threat_intelligence.py b/src/cbc_sdk/enterprise_edr/threat_intelligence.py index 0236f4a15..8dc713be9 100644 --- a/src/cbc_sdk/enterprise_edr/threat_intelligence.py +++ b/src/cbc_sdk/enterprise_edr/threat_intelligence.py @@ -1181,8 +1181,8 @@ def validate(self): """ super(Report, self).validate() - if self.link and not validators.url(self.link): - raise InvalidObjectError("link should be a valid URL") + if self.link and not (validators.ipv4(self.link) or validators.url(self.link) or validators.domain(self.link)): + raise InvalidObjectError(f"link should be a valid URL or domain: {self.link}") if self.iocs_v2: [ioc.validate() for ioc in self._iocs_v2] @@ -1725,7 +1725,7 @@ def validate(self): """ super(IOC_V2, self).validate() - if self.link and not (validators.url(self.link) or validators.domain(self.link)): + if self.link and not (validators.ipv4(self.link) or validators.url(self.link) or validators.domain(self.link)): raise InvalidObjectError(f"link should be a valid URL or domain: {self.link}") @property diff --git a/src/tests/unit/enterprise_edr/test_enterprise_edr_threatintel.py b/src/tests/unit/enterprise_edr/test_enterprise_edr_threatintel.py index 8d46f5f6f..e2a9aa95d 100644 --- a/src/tests/unit/enterprise_edr/test_enterprise_edr_threatintel.py +++ b/src/tests/unit/enterprise_edr/test_enterprise_edr_threatintel.py @@ -425,7 +425,7 @@ def test_create_regex_ioc(cb): ('silcom. com', pytest.raises(InvalidObjectError)), ('bloom-beacon.mit.edu', does_not_raise()), ('bloom-beacon.mit.edu/', pytest.raises(InvalidObjectError)), - ('199.201.128.1', pytest.raises(InvalidObjectError)), + ('199.201.128.1', does_not_raise()), ('simplename', pytest.raises(InvalidObjectError)), ('erbosoft.com/git', pytest.raises(InvalidObjectError)), ('http://erbosoft.com/git', does_not_raise()), From baaa7d39acaba58a1688df730ddcda0ddc6ff711 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Tue, 20 Jun 2023 12:42:10 -0600 Subject: [PATCH 66/76] added test of report link validation (same as test of IOC link validation) --- .../test_enterprise_edr_threatintel.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/tests/unit/enterprise_edr/test_enterprise_edr_threatintel.py b/src/tests/unit/enterprise_edr/test_enterprise_edr_threatintel.py index e2a9aa95d..1132c31b8 100644 --- a/src/tests/unit/enterprise_edr/test_enterprise_edr_threatintel.py +++ b/src/tests/unit/enterprise_edr/test_enterprise_edr_threatintel.py @@ -533,6 +533,32 @@ def on_post(url, body, **kwargs): assert report._info['id'] == "AaBbCcDdEeFfGg" +@pytest.mark.parametrize("linkvalue, expectation", [ + ('', does_not_raise()), + ('silcom.com', does_not_raise()), + ('silcom.com.', pytest.raises(InvalidObjectError)), + ('silcom. com', pytest.raises(InvalidObjectError)), + ('bloom-beacon.mit.edu', does_not_raise()), + ('bloom-beacon.mit.edu/', pytest.raises(InvalidObjectError)), + ('199.201.128.1', does_not_raise()), + ('simplename', pytest.raises(InvalidObjectError)), + ('erbosoft.com/git', pytest.raises(InvalidObjectError)), + ('http://erbosoft.com/git', does_not_raise()), + ('https://erbosoft.com/git', does_not_raise()), + ('bogusschema://erbosoft.com', pytest.raises(InvalidObjectError)), + ('ftp://26.2.0.74', does_not_raise()), + ('https://midwinter.com/lurk/lurker.html', does_not_raise()), + ('//servername/share', pytest.raises(InvalidObjectError)) +]) +def test_report_link_validation(cb, linkvalue, expectation): + builder = Report.create(cb, "NotReal", "Not real description", 2) + builder.set_title("ReportTitle").set_description("The report description").set_timestamp(1234567890) + builder.set_severity(5).set_link(linkvalue).add_tag("Alpha").add_tag("Bravo") + report = builder.build() + with expectation: + report.validate() + + @pytest.mark.parametrize("init_data, feed, watchlist, do_request, url_id, expectation, result", [ (REPORT_INIT, None, True, True, "69e2a8d0-bc36-4970-9834-8687efe1aff7", does_not_raise(), True), (REPORT_INIT, None, False, False, "", pytest.raises(InvalidObjectError), True), From 316469838dab6ef903929b13ccd3e2cc3904bfaf Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Wed, 21 Jun 2023 15:54:59 -0600 Subject: [PATCH 67/76] added docstrings for added tests --- .../unit/enterprise_edr/test_enterprise_edr_threatintel.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/tests/unit/enterprise_edr/test_enterprise_edr_threatintel.py b/src/tests/unit/enterprise_edr/test_enterprise_edr_threatintel.py index 1132c31b8..736de7512 100644 --- a/src/tests/unit/enterprise_edr/test_enterprise_edr_threatintel.py +++ b/src/tests/unit/enterprise_edr/test_enterprise_edr_threatintel.py @@ -436,6 +436,7 @@ def test_create_regex_ioc(cb): ('//servername/share', pytest.raises(InvalidObjectError)) ]) def test_ioc_link_validation(cb, linkvalue, expectation): + """Tests validation of the link field in IOCs.""" ioc = IOC_V2.create_equality(cb, None, "process_name", "Alpha") ioc.link = linkvalue with expectation: @@ -551,6 +552,7 @@ def on_post(url, body, **kwargs): ('//servername/share', pytest.raises(InvalidObjectError)) ]) def test_report_link_validation(cb, linkvalue, expectation): + """Tests validation of the link field in reports.""" builder = Report.create(cb, "NotReal", "Not real description", 2) builder.set_title("ReportTitle").set_description("The report description").set_timestamp(1234567890) builder.set_severity(5).set_link(linkvalue).add_tag("Alpha").add_tag("Bravo") From 02a218d3b86136ce24167d30894edf84b1a20fc1 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Fri, 23 Jun 2023 10:42:26 -0600 Subject: [PATCH 68/76] all updates except for changelog --- README.md | 4 ++-- VERSION | 2 +- docs/conf.py | 2 +- src/cbc_sdk/__init__.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index f706328fb..865d6be08 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # VMware Carbon Black Cloud Python SDK -**Latest Version:** 1.4.2 +**Latest Version:** 1.4.3
-**Release Date:** March 22, 2023 +**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 9df886c42..428b770e3 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.4.2 +1.4.3 diff --git a/docs/conf.py b/docs/conf.py index 7dc18811d..b024d3366 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -23,7 +23,7 @@ author = 'Developer Relations' # The full version, including alpha/beta/rc tags -release = '1.4.2' +release = '1.4.3' # -- General configuration --------------------------------------------------- diff --git a/src/cbc_sdk/__init__.py b/src/cbc_sdk/__init__.py index caab01932..b60942812 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-2023 VMware Carbon Black' -__version__ = '1.4.2' +__version__ = '1.4.3' from .rest_api import CBCloudAPI from .cache import lru From e8bf4b4cabbc85266b03a0ab3fc1e8b66a687650 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Fri, 23 Jun 2023 11:06:24 -0600 Subject: [PATCH 69/76] added changelog --- docs/changelog.rst | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 9f1a4cc06..323534eca 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,5 +1,35 @@ Changelog ================================ +CBC SDK 1.4.3 - Released (TBD) +--------------------------------------- + +New Features: + +* Policy Rule Configurations - support for additional rule configuration types: + + * Host-Based Firewall - addresses the protection of assets based on rules governing network and application behavior. + * Data Collection - control over what data is uploaded to the Carbon Black Cloud. + +Updates: + +* Added an example script for manipulating core prevention rule configuration status on a policy. +* Changed ``pymox`` dependency to the latest version, which eliminates warning messages on unit test and provides + compatibility with Python 3.11 and later. +* Added specific testing support for Python 3.11. +* Added additional UAT tests for authentication events. +* Many exception classes now carry a ``uri`` field which holds the URI of the API being accessed that caused the + exception to be raised. + +Bug Fixes: + +* Fixed link validation for reports and IOCs to accept IPv4 addresses, domain names, or URIs. + +Documentation: + +* Documentation has been reorganized for ease of reference; guides have been added to the main menu, the menu has been + reordered, and various modules have been renamed. +* Fixed typo in workload guide. + CBC SDK 1.4.2 - Released March 22, 2023 --------------------------------------- From ca30dd24ee0c0c65fba667b407bebec56ea09c6c Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Fri, 23 Jun 2023 11:19:41 -0600 Subject: [PATCH 70/76] added more description to data collection --- docs/changelog.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 323534eca..d083c615d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -8,7 +8,8 @@ New Features: * Policy Rule Configurations - support for additional rule configuration types: * Host-Based Firewall - addresses the protection of assets based on rules governing network and application behavior. - * Data Collection - control over what data is uploaded to the Carbon Black Cloud. + * Data Collection - control over what data is uploaded to the Carbon Black Cloud. Specifically, can enable or + disable auth events collection. Updates: From 7b1d3df0d08c96de009fa81ac74eac8f7cdde9b1 Mon Sep 17 00:00:00 2001 From: Kylie Ebringer Date: Fri, 23 Jun 2023 16:48:17 -0600 Subject: [PATCH 71/76] Added example for Auth Events enable and disable Removed work around for earlier backend policy bug --- examples/platform/policy_data_collection.py | 132 ++++++++++++++++++ .../platform/policy_host_based_firewall.py | 2 +- 2 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 examples/platform/policy_data_collection.py diff --git a/examples/platform/policy_data_collection.py b/examples/platform/policy_data_collection.py new file mode 100644 index 000000000..463847663 --- /dev/null +++ b/examples/platform/policy_data_collection.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python +# ******************************************************* +# Copyright (c) VMware, Inc. 2020-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. + +"""Example script which manipulates rules in the Data Collection component of policy. + +There are three methods that demonstrate interactions with the Data Collection Policy. +The first prints summary information about each policy, and iterates through each data_collection rule +This is an example of the command line to execute. +> python examples/platform/policy_data_collection.py --profile EXAMPLE_CREDENTIALS list_policies + +The second method enables Auth Event collection (a type of data collection rule) on an existing policy. +Command line prompts ask for required info and defaults are provided. +This is an example of the command line to execute. The script will prompt on the command line for Policy Id. +> python examples/platform/policy_data_collection.py --profile EXAMPLE_CREDENTIALS enable_auth_event_rule + +To disable an Auth Event policy, +> python examples/platform/policy_data_collection.py --profile EXAMPLE_CREDENTIALS disable_auth_event_rule + +The API specification for underlying APIs is available on the Developer Network: +https://developer.carbonblack.com/reference/carbon-black-cloud/platform/latest/policy-service/#rule-config---data-collection +""" + +import sys +from cbc_sdk.helpers import build_cli_parser, get_cb_cloud_object +from cbc_sdk.platform import Policy + + +def list_policy_summaries(cb, args): + """List all policies and their rules.""" + # the cb.select(Policy) with no parameters will get all the policies for the organization + for p in cb.select(Policy): + print(u"Policy id {0}: {1} {2}".format(p.id, p.name, "({0})".format(p.description) if p.description else "")) + print("Data Collection Rules:") + if p.data_collection_rule_configs is None: + print("No Data Collection Rules") + elif len(p.data_collection_rule_configs_list) == 0: + print("No Data Collection Rules") + else: + for dcrc in p.data_collection_rule_configs_list: + print(dcrc) + print("the schema for this data collection") + print(p.get_ruleconfig_parameter_schema(dcrc.id)) + + print("") + print("End of Policy Object") + print("") + + +def enable_auth_event_rule(cb): + """Enable the rule to collect Auth Events from the sensor""" + # prompt the user for a policy Id + user_input = input("Enter the policy Id on which to enable collection") + policy = cb.select(Policy, user_input) + print("Using policy id: {0}. name: {1}".format(policy.id, policy.name)) + # If the policy does not have a data collection section then fail + if len(policy.data_collection_rule_configs_list) == 0: + print("No data collection elements available to enable") + exit() + + auth_event_rule_config = None + for dcrc in policy.data_collection_rule_configs_list: + if dcrc.name == 'Authentication Events': + dcrc.set_parameter('enable_auth_events', True) + dcrc.save() + auth_event_rule_config = dcrc + + if auth_event_rule_config is not None: + print(auth_event_rule_config) + else: + print("Auth Event Rule Config Element Not Found") + + +def disable_auth_event_rule(cb): + """Disable the rule to collect Auth Events from the sensor""" + # prompt the user for a policy Id + user_input = input("Enter the policy Id on which to disable collection") + policy = cb.select(Policy, user_input) + print("Using policy id: {0}. name: {1}".format(policy.id, policy.name)) + # If the policy does not have a data collection section then fail + if len(policy.data_collection_rule_configs_list) == 0: + print("No data collection elements available to enable") + exit() + + auth_event_rule_config = None + for dcrc in policy.data_collection_rule_configs_list: + if dcrc.name == 'Authentication Events': + dcrc.set_parameter('enable_auth_events', False) + dcrc.save() + auth_event_rule_config = dcrc + + if auth_event_rule_config is not None: + print(auth_event_rule_config) + else: + print("Auth Event Rule Config Element Not Found") + + +def main(): + """Main function for Policy - Host-Based Firewall script.""" + parser = build_cli_parser("View or set host based firewall rules on a policy") + subparsers = parser.add_subparsers(dest="command", required=True) + + subparsers.add_parser("list_policies", help="List summary information about each policy") + + subparsers.add_parser("enable_auth_event_rule", help="Enable the data collection rule to get Auth Events") + + subparsers.add_parser("disable_auth_event_rule", help="Disable the data collection rule to get Auth Events") + + args = parser.parse_args() + cb = get_cb_cloud_object(args) + + if args.command == "list_policies": + list_policy_summaries(cb, args) + elif args.command == "enable_auth_event_rule": + enable_auth_event_rule(cb) + elif args.command == "disable_auth_event_rule": + disable_auth_event_rule(cb) + else: + raise NotImplementedError("Unknown command") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/examples/platform/policy_host_based_firewall.py b/examples/platform/policy_host_based_firewall.py index 037f0e643..10bcd6404 100644 --- a/examples/platform/policy_host_based_firewall.py +++ b/examples/platform/policy_host_based_firewall.py @@ -181,7 +181,7 @@ def add_hbfw_rule(cb): # There is a known issue in Carbon Black Cloud that requires the rule_configs to be saved explicitly. # There is no adverse impact to performing this call but in the future will be un-necessary - rc.save() + # rc.save() # save the policy and all child elements. policy.save() print(rc) From bee82188e84ce2792ec94880be4a29410d99ba74 Mon Sep 17 00:00:00 2001 From: Kylie Ebringer Date: Fri, 23 Jun 2023 16:54:29 -0600 Subject: [PATCH 72/76] Added data_collection example to policy guide page. --- docs/policy.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/policy.rst b/docs/policy.rst index 8e26e8909..e926e699f 100644 --- a/docs/policy.rst +++ b/docs/policy.rst @@ -21,3 +21,9 @@ Example scripts are available in the GitHub repository in examples/platform that * Host-Based Firewall policy rule operations * policy_host_based_firewall.py + +* Data Collection policy rule operations + + * Demonstrates how to enable and disable Auth Event collection. + + * policy_data_collection.py From 1ba83b0ff387c1b86cfef84d680958085ebc995b Mon Sep 17 00:00:00 2001 From: Kylie Ebringer Date: Mon, 26 Jun 2023 09:19:17 -0600 Subject: [PATCH 73/76] Merged enable and disable methods per review suggestion. --- examples/platform/policy_data_collection.py | 43 ++++++--------------- 1 file changed, 12 insertions(+), 31 deletions(-) diff --git a/examples/platform/policy_data_collection.py b/examples/platform/policy_data_collection.py index 463847663..e6de2b993 100644 --- a/examples/platform/policy_data_collection.py +++ b/examples/platform/policy_data_collection.py @@ -34,7 +34,7 @@ from cbc_sdk.platform import Policy -def list_policy_summaries(cb, args): +def list_policy_summaries(cb): """List all policies and their rules.""" # the cb.select(Policy) with no parameters will get all the policies for the organization for p in cb.select(Policy): @@ -55,10 +55,10 @@ def list_policy_summaries(cb, args): print("") -def enable_auth_event_rule(cb): +def set_auth_event_rule(cb, enable_auth_events): """Enable the rule to collect Auth Events from the sensor""" # prompt the user for a policy Id - user_input = input("Enter the policy Id on which to enable collection") + user_input = input("Enter the policy Id on which to change auth event collection setting") policy = cb.select(Policy, user_input) print("Using policy id: {0}. name: {1}".format(policy.id, policy.name)) # If the policy does not have a data collection section then fail @@ -69,31 +69,12 @@ def enable_auth_event_rule(cb): auth_event_rule_config = None for dcrc in policy.data_collection_rule_configs_list: if dcrc.name == 'Authentication Events': - dcrc.set_parameter('enable_auth_events', True) - dcrc.save() - auth_event_rule_config = dcrc - - if auth_event_rule_config is not None: - print(auth_event_rule_config) - else: - print("Auth Event Rule Config Element Not Found") - - -def disable_auth_event_rule(cb): - """Disable the rule to collect Auth Events from the sensor""" - # prompt the user for a policy Id - user_input = input("Enter the policy Id on which to disable collection") - policy = cb.select(Policy, user_input) - print("Using policy id: {0}. name: {1}".format(policy.id, policy.name)) - # If the policy does not have a data collection section then fail - if len(policy.data_collection_rule_configs_list) == 0: - print("No data collection elements available to enable") - exit() - - auth_event_rule_config = None - for dcrc in policy.data_collection_rule_configs_list: - if dcrc.name == 'Authentication Events': - dcrc.set_parameter('enable_auth_events', False) + if enable_auth_events: + dcrc.set_parameter('enable_auth_events', True) + print("Auth Event Collection Enabled") + else: + dcrc.set_parameter('enable_auth_events', False) + print("Auth Event Collection Disabled") dcrc.save() auth_event_rule_config = dcrc @@ -118,11 +99,11 @@ def main(): cb = get_cb_cloud_object(args) if args.command == "list_policies": - list_policy_summaries(cb, args) + list_policy_summaries(cb) elif args.command == "enable_auth_event_rule": - enable_auth_event_rule(cb) + set_auth_event_rule(cb, True) elif args.command == "disable_auth_event_rule": - disable_auth_event_rule(cb) + set_auth_event_rule(cb, False) else: raise NotImplementedError("Unknown command") return 0 From 9b2ef8acf8d9c0c9ed6411504b2bf0cd4956f402 Mon Sep 17 00:00:00 2001 From: Kylie Ebringer Date: Mon, 26 Jun 2023 09:41:24 -0600 Subject: [PATCH 74/76] streamlining enable/disable logic --- examples/platform/policy_data_collection.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/examples/platform/policy_data_collection.py b/examples/platform/policy_data_collection.py index e6de2b993..a284eec26 100644 --- a/examples/platform/policy_data_collection.py +++ b/examples/platform/policy_data_collection.py @@ -69,12 +69,8 @@ def set_auth_event_rule(cb, enable_auth_events): auth_event_rule_config = None for dcrc in policy.data_collection_rule_configs_list: if dcrc.name == 'Authentication Events': - if enable_auth_events: - dcrc.set_parameter('enable_auth_events', True) - print("Auth Event Collection Enabled") - else: - dcrc.set_parameter('enable_auth_events', False) - print("Auth Event Collection Disabled") + dcrc.set_parameter('enable_auth_events', enable_auth_events) + print(f"Auth Event Collection {'Enabled' if enable_auth_events else 'Disabled'}") dcrc.save() auth_event_rule_config = dcrc From 609976fe60d59d9dadf4aed9077239d9a4b7fb5a Mon Sep 17 00:00:00 2001 From: Kylie Ebringer Date: Mon, 26 Jun 2023 10:53:28 -0600 Subject: [PATCH 75/76] added data_collection example to changelog --- docs/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index d083c615d..ff0c1c469 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -13,7 +13,7 @@ New Features: Updates: -* Added an example script for manipulating core prevention rule configuration status on a policy. +* Added an example script for manipulating core prevention rule configuration and data collection status on a policy. * Changed ``pymox`` dependency to the latest version, which eliminates warning messages on unit test and provides compatibility with Python 3.11 and later. * Added specific testing support for Python 3.11. From b12c87665242bb88d5f4c578183f19b312883e2c Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Mon, 26 Jun 2023 11:52:19 -0600 Subject: [PATCH 76/76] set release date --- README.md | 2 +- docs/changelog.rst | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 865d6be08..aba864fca 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ **Latest Version:** 1.4.3
-**Release Date:** TBD +**Release Date:** June 26, 2023 [![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 ff0c1c469..1eed5f34c 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,7 +1,7 @@ Changelog ================================ -CBC SDK 1.4.3 - Released (TBD) ---------------------------------------- +CBC SDK 1.4.3 - Released June 26, 2023 +-------------------------------------- New Features: