From e1a5e6a97ee052af0d18a0b667455b7fe1acd9fb Mon Sep 17 00:00:00 2001
From: Amy Bowersox <abowersox@carbonblack.com>
Date: Tue, 17 Sep 2019 12:08:39 -0600
Subject: [PATCH 1/2] Amy's experiment in moving modules between
 directories/packages

---
 src/cbapi/psc/models.py              | 168 ++++++++++++++++++++
 src/cbapi/psc/models/deviceInfo.yaml | 221 +++++++++++++++++++++++++++
 src/cbapi/psc/models/policyInfo.yaml |  25 +++
 test/cbapi/test_model_conflict.py    |  21 +++
 4 files changed, 435 insertions(+)
 create mode 100755 src/cbapi/psc/models.py
 create mode 100755 src/cbapi/psc/models/deviceInfo.yaml
 create mode 100755 src/cbapi/psc/models/policyInfo.yaml
 create mode 100755 test/cbapi/test_model_conflict.py

diff --git a/src/cbapi/psc/models.py b/src/cbapi/psc/models.py
new file mode 100755
index 00000000..094f7b02
--- /dev/null
+++ b/src/cbapi/psc/models.py
@@ -0,0 +1,168 @@
+from cbapi.models import MutableBaseModel, CreatableModelMixin, NewBaseModel
+
+from copy import deepcopy
+import logging
+import json
+
+from cbapi.errors import ServerError
+
+log = logging.getLogger(__name__)
+
+
+class DefenseMutableModel(MutableBaseModel):
+    _change_object_http_method = "PATCH"
+    _change_object_key_name = None
+
+    def __init__(self, cb, model_unique_id=None, initial_data=None, force_init=False, full_doc=False):
+        super(DefenseMutableModel, self).__init__(cb, model_unique_id=model_unique_id, initial_data=initial_data,
+                                                  force_init=force_init, full_doc=full_doc)
+        if not self._change_object_key_name:
+            self._change_object_key_name = self.primary_key
+
+    def _parse(self, obj):
+        if type(obj) == dict and self.info_key in obj:
+            return obj[self.info_key]
+
+    def _update_object(self):
+        if self._change_object_http_method != "PATCH":
+            return self._update_entire_object()
+        else:
+            return self._patch_object()
+
+    def _update_entire_object(self):
+        if self.__class__.primary_key in self._dirty_attributes.keys() or self._model_unique_id is None:
+            new_object_info = deepcopy(self._info)
+            try:
+                if not self._new_object_needs_primary_key:
+                    del(new_object_info[self.__class__.primary_key])
+            except Exception:
+                pass
+            log.debug("Creating a new {0:s} object".format(self.__class__.__name__))
+            ret = self._cb.api_json_request(self.__class__._new_object_http_method, self.urlobject,
+                                            data={self.info_key: new_object_info})
+        else:
+            log.debug("Updating {0:s} with unique ID {1:s}".format(self.__class__.__name__, str(self._model_unique_id)))
+            ret = self._cb.api_json_request(self.__class__._change_object_http_method,
+                                            self._build_api_request_uri(), data={self.info_key: self._info})
+
+        return self._refresh_if_needed(ret)
+
+    def _patch_object(self):
+        if self.__class__.primary_key in self._dirty_attributes.keys() or self._model_unique_id is None:
+            log.debug("Creating a new {0:s} object".format(self.__class__.__name__))
+            ret = self._cb.api_json_request(self.__class__._new_object_http_method, self.urlobject,
+                                            data=self._info)
+        else:
+            updates = {}
+            for k in self._dirty_attributes.keys():
+                updates[k] = self._info[k]
+            log.debug("Updating {0:s} with unique ID {1:s}".format(self.__class__.__name__, str(self._model_unique_id)))
+            ret = self._cb.api_json_request(self.__class__._change_object_http_method,
+                                            self._build_api_request_uri(), data=updates)
+
+        return self._refresh_if_needed(ret)
+
+    def _refresh_if_needed(self, request_ret):
+        refresh_required = True
+
+        if request_ret.status_code not in range(200, 300):
+            try:
+                message = json.loads(request_ret.text)[0]
+            except:
+                message = request_ret.text
+
+            raise ServerError(request_ret.status_code, message,
+                              result="Did not update {} record.".format(self.__class__.__name__))
+        else:
+            try:
+                message = request_ret.json()
+                log.debug("Received response: %s" % message)
+                if not isinstance(message, dict):
+                    raise ServerError(request_ret.status_code, message,
+                                      result="Unknown error updating {0:s} record.".format(self.__class__.__name__))
+                else:
+                    if message.get("success", False):
+                        if isinstance(message.get(self.info_key, None), dict):
+                            self._info = message.get(self.info_key)
+                            self._full_init = True
+                            refresh_required = False
+                        else:
+                            if self._change_object_key_name in message.keys():
+                                # 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")
+                                self._info[self.primary_key] = message[self._change_object_key_name]
+                                refresh_required = True
+                    else:
+                        # "success" is False
+                        raise ServerError(request_ret.status_code, message.get("message", ""),
+                                          result="Did not update {0:s} record.".format(self.__class__.__name__))
+            except:
+                pass
+
+        self._dirty_attributes = {}
+        if refresh_required:
+            self.refresh()
+        return self._model_unique_id
+
+
+class Device(DefenseMutableModel):
+    urlobject = "/integrationServices/v3/device"
+    primary_key = "deviceId"
+    info_key = "deviceInfo"
+    swagger_meta_file = "psc/models/deviceInfo.yaml"
+
+    def __init__(self, cb, model_unique_id, initial_data=None):
+        super(Device, self).__init__(cb, model_unique_id, initial_data)
+
+    def lr_session(self):
+        """
+        Retrieve a Live Response session object for this Device.
+
+        :return: Live Response session object
+        :rtype: :py:class:`cbapi.defense.cblr.LiveResponseSession`
+        :raises ApiError: if there is an error establishing a Live Response session for this Device
+
+        """
+        return self._cb._request_lr_session(self._model_unique_id)
+
+
+class Event(NewBaseModel):
+    urlobject = "/integrationServices/v3/event"
+    primary_key = "eventId"
+    info_key = "eventInfo"
+
+    def _parse(self, obj):
+        if type(obj) == dict and self.info_key in obj:
+            return obj[self.info_key]
+
+    def __init__(self, cb, model_unique_id, initial_data=None):
+        super(Event, self).__init__(cb, model_unique_id, initial_data)
+
+
+class Policy(DefenseMutableModel, CreatableModelMixin):
+    urlobject = "/integrationServices/v3/policy"
+    info_key = "policyInfo"
+    swagger_meta_file = "psc/models/policyInfo.yaml"
+    _change_object_http_method = "PUT"
+    _change_object_key_name = "policyId"
+
+    @property
+    def rules(self):
+        return dict([(r.get("id"), r) for r in self.policy.get("rules", [])])
+
+    def add_rule(self, new_rule):
+        self._cb.post_object("{0}/rule".format(self._build_api_request_uri()), {"ruleInfo": new_rule})
+        self.refresh()
+
+    def delete_rule(self, rule_id):
+        self._cb.delete_object("{0}/rule/{1}".format(self._build_api_request_uri(), rule_id))
+        self.refresh()
+
+    def replace_rule(self, rule_id, new_rule):
+        self._cb.put_object("{0}/rule/{1}".format(self._build_api_request_uri(), rule_id),
+                            {"ruleInfo": new_rule})
+        self.refresh()
+        
+    def do_funky_things(self):
+        pass
+    
\ No newline at end of file
diff --git a/src/cbapi/psc/models/deviceInfo.yaml b/src/cbapi/psc/models/deviceInfo.yaml
new file mode 100755
index 00000000..7d7106a7
--- /dev/null
+++ b/src/cbapi/psc/models/deviceInfo.yaml
@@ -0,0 +1,221 @@
+type: object
+properties:
+  osVersion:
+    type: string
+  activationCode:
+    type: string
+  organizationId:
+    type: integer
+    format: int64
+  deviceId:
+    type: integer
+    format: int64
+  deviceSessionId:
+    type: integer
+    format: int64
+  deviceOwnerId:
+    type: integer
+    format: int64
+  deviceGuid:
+    type: string
+    format: uuid
+  email:
+    type: string
+    format: email
+  assignedToId:
+    type: integer
+    format: int64
+  assignedToName:
+    type: string
+  deviceType:
+    type: string
+    x-nullable: true
+    enum:
+      - "MAC"
+      - "WINDOWS"
+  firstName:
+    type: string
+  lastName:
+    type: string
+  middleName:
+    type: string
+  createTime:
+    type: integer
+    format: epoch-ms-date-time
+  policyId:
+    type: integer
+    format: int64
+  policyName:
+    type: string
+  quarantined:
+    type: boolean
+  targetPriorityType:
+    type: string
+    x-nullable: true
+    enum:
+    - "HIGH"
+    - "LOW"
+    - "MEDIUM"
+    - "MISSION_CRITICAL"
+  lastVirusActivityTime:
+    type: integer
+    format: epoch-ms-date-time
+  firstVirusActivityTime:
+    type: integer
+    format: epoch-ms-date-time
+  activationCodeExpiryTime:
+    type: integer
+    format: epoch-ms-date-time
+  organizationName:
+    type: string
+  sensorVersion:
+    type: string
+  registeredTime:
+    type: integer
+    format: epoch-ms-date-time
+  lastContact:
+    type: integer
+    format: epoch-ms-date-time
+  lastReportedTime:
+    type: integer
+    format: epoch-ms-date-time
+  windowsPlatform:
+    type: string
+    x-nullable: true
+    enum:
+      - "CLIENT_X64"
+      - "CLIENT_X86"
+      - "SERVER_X6"
+      - "SERVER_X86"
+  vdiBaseDevice:
+    type: integer
+    format: int64
+  avStatus:
+    type: array
+    items:
+      type: string
+      x-nullable: true
+      enum:
+        - "AV_ACTIVE"
+        - "AV_BYPASS"
+        - "AV_DEREGISTERED"
+        - "AV_NOT_REGISTERED"
+        - "AV_REGISTERED"
+        - "FULLY_DISABLED"
+        - "FULLY_ENABLED"
+        - "INSTALLED"
+        - "INSTALLED_SERVER"
+        - "NOT_INSTALLED"
+        - "ONACCESS_SCAN_DISABLED"
+        - "ONDEMAND_SCAN_DISABLED"
+        - "ONDEMOND_SCAN_DISABLED"
+        - "PRODUCT_UPDATE_DISABLED"
+        - "SIGNATURE_UPDATE_DISABLED"
+        - "UNINSTALLED"
+        - "UNINSTALLED_SERVER"
+  deregisteredTime:
+    type: integer
+    format: epoch-ms-date-time
+  sensorStates:
+    type: array
+    items:
+      type: string
+      x-nullable: true
+      enum:
+        - "ACTIVE"
+        - "CSR_ACTION"
+        - "DB_CORRUPTION_DETECTED"
+        - "DRIVER_INIT_ERROR"
+        - "LOOP_DETECTED"
+        - "PANICS_DETECTED"
+        - "REMGR_INIT_ERROR"
+        - "REPUX_ACTION"
+        - "SENSOR_MAINTENANCE"
+        - "SENSOR_RESET_IN_PROGRESS"
+        - "SENSOR_SHUTDOWN"
+        - "SENSOR_UNREGISTERED"
+        - "SENSOR_UPGRADE_IN_PROGRESS"
+        - "UNSUPPORTED_OS"
+        - "WATCHDOG"
+  messages:
+    type: array
+    items:
+      type: object
+      properties:
+        message:
+          type: string
+        time:
+          type: integer
+          format: epoch-ms-date-time
+  rootedBySensor:
+    type: boolean
+  rootedBySensorTime:
+    type: integer
+    format: epoch-ms-date-time
+  lastInternalIpAddress:
+    type: string
+  lastExternalIpAddress:
+    type: string
+  lastLocation:
+    type: string
+    x-nullable: true
+    enum:
+      - "OFFSITE"
+      - "ONSITE"
+      - "UNKNOWN"
+  avUpdateServers:
+    type: array
+    items:
+      type: string
+  passiveMode:
+    type: boolean
+  lastResetTime:
+    type: integer
+    format: epoch-ms-date-time
+  lastShutdownTime:
+    type: integer
+    format: epoch-ms-date-time
+  scanStatus:
+    type: string
+  scanLastActionTime:
+    type: integer
+    format: epoch-ms-date-time
+  scanLastCompleteTime:
+    type: integer
+    format: epoch-ms-date-time
+  linuxKernelVersion:
+    type: string
+  avEngine:
+    type: string
+  avLastScanTime:
+    type: integer
+    format: epoch-ms-date-time
+  rootedByAnalytics:
+    type: boolean
+  rootedByAnalyticsTime:
+    type: integer
+    format: epoch-ms-date-time
+  testId:
+    type: integer
+  avMaster:
+    type: boolean
+  uninstalledTime:
+    type: integer
+    format: epoch-ms-date-time
+  name:
+    type: string
+  status:
+    type: string
+    x-nullable: true
+    enum:
+      - "ACTIVE"
+      - "ALL"
+      - "BYPASS"
+      - "BYPASS_ON"
+      - "DEREGISTERED"
+      - "ERROR"
+      - "INACTIVE"
+      - "PENDING"
+      - "QUARANTINE"
+      - "REGISTERED"
+      - "UNINSTALLED"
diff --git a/src/cbapi/psc/models/policyInfo.yaml b/src/cbapi/psc/models/policyInfo.yaml
new file mode 100755
index 00000000..b13dc207
--- /dev/null
+++ b/src/cbapi/psc/models/policyInfo.yaml
@@ -0,0 +1,25 @@
+type: object
+required:
+- description
+- name
+- policy
+- priorityLevel
+- version
+properties:
+  description:
+    type: string
+  id:
+    type: integer
+  latestRevision:
+    type: integer
+  name:
+    type: string
+  policy:
+    type: object
+  priorityLevel:
+    type: string
+  systemPolicy:
+    type: boolean
+  version:
+    type: integer
+
diff --git a/test/cbapi/test_model_conflict.py b/test/cbapi/test_model_conflict.py
new file mode 100755
index 00000000..e5870878
--- /dev/null
+++ b/test/cbapi/test_model_conflict.py
@@ -0,0 +1,21 @@
+import pytest
+from cbapi.psc.models import Policy
+from cbapi.psc.defense.models import Policy as PolicyOld
+from cbapi.example_helpers import get_cb_defense_object
+
+class TempArgs:
+    pass
+
+def test_model_conflict():
+    args = TempArgs()
+    args.verbose = False
+    args.cburl = "https://localhost.example.com"
+    args.apitoken = "foo"
+    args.no_ssl_verify = True
+    apiobj = get_cb_defense_object(args)
+    mod1 = Policy(apiobj)
+    mod2 = PolicyOld(apiobj)
+    mod1.do_funky_things()
+    with pytest.raises(AttributeError):
+        mod2.do_funky_things()
+    
\ No newline at end of file

From c1b241d320f9a140df5c22415255c4ff3d6218f9 Mon Sep 17 00:00:00 2001
From: Amy Bowersox <abowersox@carbonblack.com>
Date: Mon, 23 Sep 2019 10:19:18 -0600
Subject: [PATCH 2/2] something minor

---
 test/cbapi/test_model_conflict.py | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/test/cbapi/test_model_conflict.py b/test/cbapi/test_model_conflict.py
index e5870878..54995194 100755
--- a/test/cbapi/test_model_conflict.py
+++ b/test/cbapi/test_model_conflict.py
@@ -7,12 +7,14 @@ class TempArgs:
     pass
 
 def test_model_conflict():
+    # this is setup temporary stuff
     args = TempArgs()
     args.verbose = False
     args.cburl = "https://localhost.example.com"
     args.apitoken = "foo"
     args.no_ssl_verify = True
     apiobj = get_cb_defense_object(args)
+    # this is the actual test
     mod1 = Policy(apiobj)
     mod2 = PolicyOld(apiobj)
     mod1.do_funky_things()
