From baf87b111b72452a23fb5b381c1285c8da18b154 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Mon, 25 Jul 2022 16:20:22 -0600 Subject: [PATCH 001/143] fixed unit test that was broken when I fixed the UAT --- src/tests/unit/enterprise_edr/test_enterprise_edr_threatintel.py | 1 + 1 file changed, 1 insertion(+) 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 12678f5a7..aa6c70bd9 100644 --- a/src/tests/unit/enterprise_edr/test_enterprise_edr_threatintel.py +++ b/src/tests/unit/enterprise_edr/test_enterprise_edr_threatintel.py @@ -86,6 +86,7 @@ def test_watchlist_save(cbcsdk_mock): cbcsdk_mock.mock_request("POST", "/threathunter/watchlistmgr/v3/orgs/test/watchlists", WATCHLIST_GET_SPECIFIC_RESP) watchlist = Watchlist(api, model_unique_id=None, initial_data=CREATE_WATCHLIST_DATA) watchlist.validate() + print(f"{watchlist._model_unique_id =}") watchlist.save() # if Watchlist response is missing a required field per enterprise_edr.models.Watchlist, raise InvalidObjectError From 37da856bd7353c8922c4f15a5ab1d45f90affc17 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Mon, 25 Jul 2022 16:28:46 -0600 Subject: [PATCH 002/143] removed a debug print that was causing 3.7 tests to fail --- src/tests/unit/enterprise_edr/test_enterprise_edr_threatintel.py | 1 - 1 file changed, 1 deletion(-) 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 aa6c70bd9..12678f5a7 100644 --- a/src/tests/unit/enterprise_edr/test_enterprise_edr_threatintel.py +++ b/src/tests/unit/enterprise_edr/test_enterprise_edr_threatintel.py @@ -86,7 +86,6 @@ def test_watchlist_save(cbcsdk_mock): cbcsdk_mock.mock_request("POST", "/threathunter/watchlistmgr/v3/orgs/test/watchlists", WATCHLIST_GET_SPECIFIC_RESP) watchlist = Watchlist(api, model_unique_id=None, initial_data=CREATE_WATCHLIST_DATA) watchlist.validate() - print(f"{watchlist._model_unique_id =}") watchlist.save() # if Watchlist response is missing a required field per enterprise_edr.models.Watchlist, raise InvalidObjectError From eab85e088433944048304244928aa4dc2be66560 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Mon, 14 Nov 2022 16:07:11 -0700 Subject: [PATCH 003/143] new example script --- examples/platform/container_runtime_alerts.py | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 examples/platform/container_runtime_alerts.py diff --git a/examples/platform/container_runtime_alerts.py b/examples/platform/container_runtime_alerts.py new file mode 100644 index 000000000..cda4f6688 --- /dev/null +++ b/examples/platform/container_runtime_alerts.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python +# ******************************************************* +# Copyright (c) VMware, Inc. 2020-2022. 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 illustrating retrieval of container runtime alerts. + +Based on code written by Stephane List in the article "Carbon Black Container APIs Just got better! Get container +runtime alerts with CBC python SDK," VMware Carbon Black Tech Zone, June 20, 2022. (Permalink to article: +https://carbonblack.vmware.com/blog/carbon-black-container-apis-just-got-better-get-container-runtime-alerts-cbc-python-sdk) +Code modified and adapted for CBC SDK example script use by Amy Bowersox, Developer Relations. +""" + +# Imports from the article's code +import sys +from cbc_sdk.platform import ContainerRuntimeAlert + +# Additional imports to use the "helper" functions to perform command-line parsing and build a CBCloudAPI object from +# command-line arguments. Since we don't construct CBCloudAPI directly, we don't need to import it. +from cbc_sdk.helpers import build_cli_parser, get_cb_cloud_object + + +def main(): + """For convenience, all running code will be under this main function.""" + # Build a parser to parse standard command-line arguments for CBCloudAPI, and add some additional arguments. + parser = build_cli_parser("Retrieve ContainerRuntimeAlerts from Carbon Black Cloud") + parser.add_argument("-w", "--weeks", type=int, default=12, + help="Number of weeks to look back at alerts (default 12)") + parser.add_argument("-f", "--find", default=None, + help="Find a specific string in alert reason, only print alerts with that reason") + group = parser.add_mutually_exclusive_group() + group.add_argument("--reason", action="store_true", help="Only show alert reason") + group.add_argument("--ip", action="store_true", help="Only show alert remote IP address") + + # Parse the command line arguments and create a CBCloudAPI object. If you want to run against the "default" + # credentials as written in the article, pass the command-line parameters "--profile default" to the script. + args = parser.parse_args() + cb = get_cb_cloud_object(args) + + # Get Container Runtime alerts from the last however-many weeks. + alerts = cb.select(ContainerRuntimeAlert).set_time_range('last_update_time', range=f"-{args.weeks}w") + + # This duplicates the main for-loop in the article's example code. + for alert in alerts: + # This complicated if allows us to bypass checking the alert reason if "find" was not specified. + if not args.find or (args.find in alert.reason): + # Based on the reason and ip flags, print out either the reason, the remote IP, or the whole alert. + if args.reason: + print(alert.reason) + elif args.ip: + print(alert.remote_ip) + else: + print(alert) + + return 0 + + +if __name__ == "__main__": + # Trap keyboard interrupts while running the script. + try: + sys.exit(main()) + except KeyboardInterrupt: + print("\nKeyboard interrupt\n") + sys.exit(0) From 870e827c89f88f35c93cfff40fcc0285698a6f28 Mon Sep 17 00:00:00 2001 From: Alex Van Brunt Date: Mon, 9 Jan 2023 15:43:25 -0600 Subject: [PATCH 004/143] Add verification for User.create() --- src/cbc_sdk/platform/users.py | 9 +++++++++ src/tests/unit/platform/test_users.py | 5 +++++ 2 files changed, 14 insertions(+) diff --git a/src/cbc_sdk/platform/users.py b/src/cbc_sdk/platform/users.py index a75bc6d8e..da3c8c57d 100644 --- a/src/cbc_sdk/platform/users.py +++ b/src/cbc_sdk/platform/users.py @@ -17,6 +17,9 @@ from cbc_sdk.platform.grants import Grant, normalize_org import time import copy +import logging + +log = logging.getLogger(__name__) """User Models""" @@ -290,6 +293,12 @@ def create(cls, cb, template=None): UserBuilder: If template is None, returns an instance of this object. Call methods on the object to set the values associated with the new user, and then call build() to create it. """ + try: + if cb.__class__.__name__ != 'CBCloudAPI': + raise Exception + except: + raise ApiError("Unable to create User without CBCloudAPI") + if template: my_templ = copy.deepcopy(template) my_templ['org_id'] = 0 diff --git a/src/tests/unit/platform/test_users.py b/src/tests/unit/platform/test_users.py index 4dc282f65..050f38943 100644 --- a/src/tests/unit/platform/test_users.py +++ b/src/tests/unit/platform/test_users.py @@ -225,6 +225,11 @@ def check_post(uri, body, **kwargs): with pytest.raises(ServerError): builder.build() +def test_create_user_invalid_CBCloudAPI(cbcsdk_mock): + """Test a invalid API object when creating a user.""" + with pytest.raises(ApiError): + User.create("BAD") + def test_user_unsupported_create(cbcsdk_mock): """We don't support creating the user just by saving it. Test that.""" From a34716908799eeaec11acc79eaad8611d4263d07 Mon Sep 17 00:00:00 2001 From: Alex Van Brunt Date: Mon, 9 Jan 2023 15:52:10 -0600 Subject: [PATCH 005/143] Fix flake8 --- src/tests/unit/platform/test_users.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tests/unit/platform/test_users.py b/src/tests/unit/platform/test_users.py index 050f38943..e55567be7 100644 --- a/src/tests/unit/platform/test_users.py +++ b/src/tests/unit/platform/test_users.py @@ -225,6 +225,7 @@ def check_post(uri, body, **kwargs): with pytest.raises(ServerError): builder.build() + def test_create_user_invalid_CBCloudAPI(cbcsdk_mock): """Test a invalid API object when creating a user.""" with pytest.raises(ApiError): From 4d651b0d74a32ddedb9c5eb0e09949b6efaf6fd0 Mon Sep 17 00:00:00 2001 From: Alex Van Brunt Date: Tue, 24 Jan 2023 16:25:57 -0600 Subject: [PATCH 006/143] Pass failed session exception to future in Job Worker --- src/cbc_sdk/live_response_api.py | 12 +++++--- src/tests/unit/test_live_response_api.py | 37 ++++++++++++++++++++---- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/src/cbc_sdk/live_response_api.py b/src/cbc_sdk/live_response_api.py index 004a7d1b6..fcd3f4f54 100644 --- a/src/cbc_sdk/live_response_api.py +++ b/src/cbc_sdk/live_response_api.py @@ -1022,7 +1022,7 @@ def run(self): self.result_queue.put(WorkerStatus(self.device_id, status="READY")) while True: work_item = self.job_queue.get(block=True) - if not work_item: + if work_item is None: # Check for None which is condition to terminate thread self.job_queue.task_done() return @@ -1031,10 +1031,14 @@ def run(self): self.job_queue.task_done() except Exception as e: self.result_queue.put(WorkerStatus(self.device_id, status="ERROR", exception=e)) + while not self.job_queue.empty(): + work_item = self.job_queue.get() + work_item.future.set_exception(e) + self.job_queue.task_done() finally: if self.lr_session: self.lr_session.close() - self.result_queue.put(WorkerStatus(self.device_id, status="EXISTING")) + self.result_queue.put(WorkerStatus(self.device_id, status="EXITING")) def run_job(self, work_item): """ @@ -1090,7 +1094,7 @@ def run(self): item.exception)) # Don't reattempt error'd jobs del self._unscheduled_jobs[item.device_id] - elif item.status == "EXISTING": + elif item.status == "EXITING": log.debug("JobWorker[{0}] has exited, waiting...".format(item.device_id)) self._job_workers[item.device_id].join() log.debug("JobWorker[{0}] deleted".format(item.device_id)) @@ -1236,7 +1240,7 @@ def submit_job(self, job, device): Submit a new job to be executed as a Live Response. Args: - job (object): The job to be scheduled. + job (func): The job function to be scheduled. device (int): ID of the device to use for job execution. Returns: diff --git a/src/tests/unit/test_live_response_api.py b/src/tests/unit/test_live_response_api.py index 615634176..b96afe09d 100755 --- a/src/tests/unit/test_live_response_api.py +++ b/src/tests/unit/test_live_response_api.py @@ -18,7 +18,7 @@ from queue import Queue from cbc_sdk.errors import ApiError, ObjectNotFoundError, ServerError, TimeoutError from cbc_sdk.live_response_api import (LiveResponseError, LiveResponseSessionManager, CbLRManagerBase, - CompletionNotification, WorkerStatus, JobWorker, GetFileJob, + CompletionNotification, WorkItem, WorkerStatus, JobWorker, GetFileJob, LiveResponseJobScheduler) from cbc_sdk.connection import Connection from cbc_sdk.credentials import Credentials @@ -1535,7 +1535,7 @@ def test_completion_notification_work_status(cbcsdk_mock): def test_job_worker(cbcsdk_mock): - """Test JobWorker""" + """Test JobWorker Success Flow""" cbcsdk_mock.mock_request('POST', '/appservices/v6/orgs/test/liveresponse/sessions', SESSION_INIT_RESP) cbcsdk_mock.mock_request('GET', '/appservices/v6/orgs/test/liveresponse/sessions/1:2468', SESSION_POLL_RESP) cbcsdk_mock.mock_request('GET', '/appservices/v6/orgs/test/devices/2468', DEVICE_RESPONSE) @@ -1543,10 +1543,15 @@ def test_job_worker(cbcsdk_mock): results = Queue() job_worker = JobWorker(cbcsdk_mock.api, 2468, results) assert job_worker.device_id == 2468 - job_worker.job_queue.put('element') + work_item = WorkItem(lambda lr_session: True, 2468) + job_worker.job_queue.put(work_item) + job_worker.job_queue.put(None) job_worker.run() - assert not job_worker.result_queue.empty() + assert job_worker.result_queue.get().status == "READY" + assert isinstance(job_worker.result_queue.get(), CompletionNotification) + assert job_worker.result_queue.get().status == "EXITING" assert job_worker.job_queue.empty() + assert work_item.future.result() is True def test_job_worker_no_item(cbcsdk_mock): @@ -1560,8 +1565,28 @@ def test_job_worker_no_item(cbcsdk_mock): assert job_worker.device_id == 2468 job_worker.job_queue.put(None) job_worker.run() - assert not job_worker.result_queue.empty() + assert job_worker.result_queue.get().status == "READY" + assert job_worker.result_queue.get().status == "EXITING" + assert job_worker.job_queue.empty() + + +def test_job_worker_device_not_found(cbcsdk_mock): + """Test JobWorker unable to make session with device""" + cbcsdk_mock.mock_request('POST', '/appservices/v6/orgs/test/liveresponse/sessions', SESSION_INIT_RESP) + cbcsdk_mock.mock_request('GET', + '/appservices/v6/orgs/test/liveresponse/sessions/1:2468', + ObjectNotFoundError("/appservices/v6/orgs/test/liveresponse/sessions/1:2468", + "Could not establish session with device 2468")) + cbcsdk_mock.mock_request('DELETE', '/appservices/v6/orgs/test/liveresponse/sessions/1:2468', None) + results = Queue() + job_worker = JobWorker(cbcsdk_mock.api, 2468, results) + assert job_worker.device_id == 2468 + work_item = WorkItem(lambda lr_session: True, 2468) + job_worker.job_queue.put(work_item) + job_worker.run() + assert job_worker.result_queue.get().status == "ERROR" assert job_worker.job_queue.empty() + assert isinstance(work_item.future.exception(), Exception) def test_get_file_job(cbcsdk_mock, connection_mock): @@ -1614,7 +1639,7 @@ def test_job_scheduler_exiting(cbcsdk_mock, mox): cbcsdk_mock.mock_request('GET', '/appservices/v6/orgs/test/devices/2468', DEVICE_RESPONSE) cbcsdk_mock.mock_request('DELETE', '/appservices/v6/orgs/test/liveresponse/sessions/1:2468', None) job_scheduler = LiveResponseJobScheduler(cbcsdk_mock.api) - ws_obj_exiting = WorkerStatus(2468, status="EXISTING") + ws_obj_exiting = WorkerStatus(2468, status="EXITING") job_scheduler.schedule_queue.put(ws_obj_exiting) job_scheduler._idle_workers.add(2469) job_worker = JobWorker(cbcsdk_mock.api, 2468, Queue()) From ee21240c2e755dc96b898b6974caf2fb37e383a0 Mon Sep 17 00:00:00 2001 From: Alex Van Brunt Date: Wed, 25 Jan 2023 09:24:21 -0600 Subject: [PATCH 007/143] Update devpackages to latest supported --- examples/live_response/live_response_cli.py | 2 +- requirements.txt | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/live_response/live_response_cli.py b/examples/live_response/live_response_cli.py index 06625ce6c..e099130ea 100755 --- a/examples/live_response/live_response_cli.py +++ b/examples/live_response/live_response_cli.py @@ -68,7 +68,7 @@ def split_cli(line): if (tok[:1] == '"'): tok = tok[1:] next = parts.pop(0) - while(next[-1:] != '"' and len(parts) > 0): + while (next[-1:] != '"' and len(parts) > 0): tok += ' ' + next next = parts.pop(0) diff --git a/requirements.txt b/requirements.txt index 5b6c8480a..a6c05cd5d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,12 +9,12 @@ keyring;platform_system=='Darwin' boto3 # Dev dependencies -pytest==7.1.2 +pytest==7.2.1 pymox==0.7.8 -coverage==5.1 -coveralls==2.0.0 -flake8==3.8.1 -flake8-colors==0.1.6 -flake8-docstrings==1.5.0 +coverage==6.5.0 +coveralls==3.3.1 +flake8==6.0.0 +flake8-colors==0.1.9 +flake8-docstrings==1.7.0 pre-commit>=2.15.0 -requests-mock==1.9.3 +requests-mock==1.10.0 From 2186256b840a78257df19eeb66a45b9e1a1238e9 Mon Sep 17 00:00:00 2001 From: Alex Van Brunt Date: Wed, 25 Jan 2023 09:45:17 -0600 Subject: [PATCH 008/143] Downgrade to flake8=5.0.4 for python3.7 and Amazon Linux --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a6c05cd5d..e913ae859 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ pytest==7.2.1 pymox==0.7.8 coverage==6.5.0 coveralls==3.3.1 -flake8==6.0.0 +flake8==5.0.4 flake8-colors==0.1.9 flake8-docstrings==1.7.0 pre-commit>=2.15.0 From 825fad9cef4da9baf646f0a4bc63732917794dc3 Mon Sep 17 00:00:00 2001 From: Alex Van Brunt Date: Wed, 25 Jan 2023 10:27:49 -0600 Subject: [PATCH 009/143] Add new coveralls token --- env.encrypted | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/env.encrypted b/env.encrypted index eb0ea51a7..7083bd11f 100644 --- a/env.encrypted +++ b/env.encrypted @@ -1,2 +1,2 @@ codeship:v2 -LShMGA33kd7ce5I9QKze/0fXoQaZ2E2dKhQeRAJ0cWckAvpXsS6a7Foz1MvITJGhrXd2mUew3qDet+pHufwL5U01x6ATlMFpTOc9ylThuM2mlgEJNWwiWkBlCim738/lBOuHY/yvaA== \ No newline at end of file +P6Dnl29DbHpvZH/JBP6CItPiOK4bOoY314TMdGZXV+hoEu35jru3KpdqNs+eJJBQykd4KPVzO3fEdQ7BsyaNwWxZc5QmJVSfeLIb79huhqLFZAGpGRFvuqCzOnMvavRRJz3M21B5wQ== From 701a958d2deef353a803f9522b7021114c9f4fc4 Mon Sep 17 00:00:00 2001 From: Alex Van Brunt Date: Tue, 10 Jan 2023 10:05:25 -0600 Subject: [PATCH 010/143] Remove v1 status url --- src/cbc_sdk/endpoint_standard/base.py | 2 +- src/cbc_sdk/platform/processes.py | 2 +- .../test_endpoint_standard_enriched_events.py | 2 +- src/tests/unit/platform/test_platform_process.py | 8 ++++---- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/cbc_sdk/endpoint_standard/base.py b/src/cbc_sdk/endpoint_standard/base.py index b615b719c..b21910b25 100644 --- a/src/cbc_sdk/endpoint_standard/base.py +++ b/src/cbc_sdk/endpoint_standard/base.py @@ -418,7 +418,7 @@ def _still_querying(self): if self._aggregation: return False - status_url = "/api/investigate/v1/orgs/{}/enriched_events/search_jobs/{}".format( + status_url = "/api/investigate/v2/orgs/{}/enriched_events/search_jobs/{}/results?start=0&rows=1".format( self._cb.credentials.org_key, self._query_token, ) diff --git a/src/cbc_sdk/platform/processes.py b/src/cbc_sdk/platform/processes.py index 8a6bf7f86..d3bdd45c2 100644 --- a/src/cbc_sdk/platform/processes.py +++ b/src/cbc_sdk/platform/processes.py @@ -658,7 +658,7 @@ def _still_querying(self): if not self._query_token: self._submit() - status_url = "/api/investigate/v1/orgs/{}/processes/search_jobs/{}".format( + status_url = "/api/investigate/v2/orgs/{}/processes/search_jobs/{}/results?start=0&rows=1".format( self._cb.credentials.org_key, self._query_token, ) diff --git a/src/tests/unit/endpoint_standard/test_endpoint_standard_enriched_events.py b/src/tests/unit/endpoint_standard/test_endpoint_standard_enriched_events.py index 6cd916bfa..0bf19215a 100644 --- a/src/tests/unit/endpoint_standard/test_endpoint_standard_enriched_events.py +++ b/src/tests/unit/endpoint_standard/test_endpoint_standard_enriched_events.py @@ -297,7 +297,7 @@ def test_enriched_event_timeout_error(cbcsdk_mock): cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/enriched_events/search_job", POST_ENRICHED_EVENTS_SEARCH_JOB_RESP) cbcsdk_mock.mock_request("GET", - "/api/investigate/v1/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b", # noqa: E501 + "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=1", # noqa: E501 GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING) api = cbcsdk_mock.api diff --git a/src/tests/unit/platform/test_platform_process.py b/src/tests/unit/platform/test_platform_process.py index 1344f017a..07cfb8a4d 100644 --- a/src/tests/unit/platform/test_platform_process.py +++ b/src/tests/unit/platform/test_platform_process.py @@ -1058,8 +1058,8 @@ def test_process_still_querying(cbcsdk_mock): cbcsdk_mock.mock_request("GET", "/api/investigate/v1/orgs/test/processes/search_validation", GET_PROCESS_VALIDATION_RESP) # mock the GET to check search status - cbcsdk_mock.mock_request("GET", ("/api/investigate/v1/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920"), + cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1"), GET_PROCESS_SEARCH_JOB_RESULTS_RESP_ZERO) api = cbcsdk_mock.api guid = 'WNEXFKQ7-0002b226-000015bd-00000000-1d6225bbba74c00' @@ -1077,8 +1077,8 @@ def test_process_still_querying_zero(cbcsdk_mock): cbcsdk_mock.mock_request("GET", "/api/investigate/v1/orgs/test/processes/search_validation", GET_PROCESS_VALIDATION_RESP) # mock the GET to check search status - cbcsdk_mock.mock_request("GET", ("/api/investigate/v1/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920"), + cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1"), GET_PROCESS_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING) api = cbcsdk_mock.api guid = 'WNEXFKQ7-0002b226-000015bd-00000000-1d6225bbba74c00' From b8a52bd7ef5172b2d36fdca705a0731920901b72 Mon Sep 17 00:00:00 2001 From: Alex Van Brunt Date: Wed, 11 Jan 2023 16:35:58 -0600 Subject: [PATCH 011/143] Fix url comparison. Remove all event and process status usage --- src/cbc_sdk/endpoint_standard/base.py | 2 +- src/cbc_sdk/platform/processes.py | 8 +- src/tests/unit/base/test_base_models.py | 14 +- .../test_endpoint_standard_enriched_events.py | 109 +++--- .../test_enterprise_edr_threatintel.py | 11 +- .../enterprise_edr/test_enterprise_edr_ubs.py | 8 +- src/tests/unit/fixtures/CBCSDKMock.py | 51 ++- .../endpoint_standard/mock_enriched_events.py | 114 ++---- .../unit/fixtures/platform/mock_process.py | 90 ++++- src/tests/unit/platform/test_alertsv6_api.py | 75 ++-- .../test_platform_dynamic_reference.py | 53 ++- .../unit/platform/test_platform_events.py | 78 +++-- .../unit/platform/test_platform_process.py | 330 ++++++++++-------- .../unit/platform/test_platform_query.py | 59 ++-- src/tests/unit/platform/test_policies.py | 4 +- .../platform/test_reputation_overrides.py | 47 +-- .../platform/test_vulnerability_assessment.py | 76 +++- src/tests/unit/test_helpers.py | 8 +- src/tests/unit/test_live_response_api.py | 2 +- src/tests/unit/test_rest_api.py | 4 +- 20 files changed, 645 insertions(+), 498 deletions(-) diff --git a/src/cbc_sdk/endpoint_standard/base.py b/src/cbc_sdk/endpoint_standard/base.py index b21910b25..097a2e853 100644 --- a/src/cbc_sdk/endpoint_standard/base.py +++ b/src/cbc_sdk/endpoint_standard/base.py @@ -155,7 +155,7 @@ def _get_detailed_results(self): submit_time = time.time() * 1000 while True: - status_url = "/api/investigate/v2/orgs/{}/enriched_events/detail_jobs/{}".format( + status_url = "/api/investigate/v2/orgs/{}/enriched_events/detail_jobs/{}/results".format( self._cb.credentials.org_key, job_id, ) diff --git a/src/cbc_sdk/platform/processes.py b/src/cbc_sdk/platform/processes.py index d3bdd45c2..a6f544b98 100644 --- a/src/cbc_sdk/platform/processes.py +++ b/src/cbc_sdk/platform/processes.py @@ -384,7 +384,7 @@ def _get_detailed_results(self): submit_time = time.time() * 1000 while True: - status_url = "/api/investigate/v2/orgs/{}/processes/detail_jobs/{}".format( + status_url = "/api/investigate/v2/orgs/{}/processes/detail_jobs/{}/results".format( self._cb.credentials.org_key, job_id, ) @@ -850,7 +850,7 @@ def set_time_range(self, start=None, end=None, window=None): self._time_range["window"] = window return self - def _get_query_parameters(self): + def _get_body_parameters(self): args = {} if self._time_range: args["time_range"] = self._time_range @@ -874,7 +874,7 @@ def _submit(self): if self._query_token: raise ApiError("Query already submitted: token {0}".format(self._query_token)) - args = self._get_query_parameters() + args = self._get_body_parameters() url = "/api/investigate/v2/orgs/{}/processes/summary_jobs".format(self._cb.credentials.org_key) query_start = self._cb.post_object(url, body=args) @@ -888,7 +888,7 @@ def _still_querying(self): if not self._query_token: self._submit() - status_url = "/api/investigate/v2/orgs/{}/processes/summary_jobs/{}".format( + status_url = "/api/investigate/v2/orgs/{}/processes/summary_jobs/{}/results?format=summary".format( self._cb.credentials.org_key, self._query_token, ) diff --git a/src/tests/unit/base/test_base_models.py b/src/tests/unit/base/test_base_models.py index f3c28c4c6..9e9291a05 100644 --- a/src/tests/unit/base/test_base_models.py +++ b/src/tests/unit/base/test_base_models.py @@ -385,18 +385,22 @@ def test_refresh_if_needed_mbm(cbcsdk_mock): def test_print_unrefreshablemodel(cbcsdk_mock): """Test printing an UnrefreshableModel""" # mock the search validation - cbcsdk_mock.mock_request("GET", "/api/investigate/v1/orgs/test/processes/search_validation", + cbcsdk_mock.mock_request("GET", + "/api/investigate/v1/orgs/test/processes/search_validation?" + "process_guid=WNEXFKQ7%5C-0002b226%5C-000015bd%5C-00000000%5C-1d6225bbba74c00" + "&q=process_guid%3AWNEXFKQ7%5C-0002b226%5C-000015bd%5C-00000000%5C-1d6225bbba74c00" + "&query=process_guid%3AWNEXFKQ7%5C-0002b226%5C-000015bd%5C-00000000%5C-1d6225bbba74c00", GET_PROCESS_VALIDATION_RESP) # mock the POST of a search - cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_job", + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_jobs", POST_PROCESS_SEARCH_JOB_RESP) # mock the GET to check search status - cbcsdk_mock.mock_request("GET", ("/api/investigate/v1/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920"), + cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" - "2c292717-80ed-4f0d-845f-779e09470920/results"), + "2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=500"), GET_PROCESS_SEARCH_JOB_RESULTS_RESP) api = cbcsdk_mock.api diff --git a/src/tests/unit/endpoint_standard/test_endpoint_standard_enriched_events.py b/src/tests/unit/endpoint_standard/test_endpoint_standard_enriched_events.py index 0bf19215a..18a4df645 100644 --- a/src/tests/unit/endpoint_standard/test_endpoint_standard_enriched_events.py +++ b/src/tests/unit/endpoint_standard/test_endpoint_standard_enriched_events.py @@ -10,13 +10,12 @@ from tests.unit.fixtures.endpoint_standard.mock_enriched_events import ( GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_ZERO, POST_ENRICHED_EVENTS_SEARCH_JOB_RESP, - GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP_1, GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP_2, GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING, GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP_0, GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP_ZERO_COMP, GET_ENRICHED_EVENTS_AGG_JOB_RESULTS_RESP_1, - GET_ENRICHED_EVENTS_DETAIL_JOB_RESULTS_RESP_1, + GET_ENRICHED_EVENTS_DETAIL_JOB_RESULTS_RESP, GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP ) @@ -42,14 +41,14 @@ def cbcsdk_mock(monkeypatch, cb): def test_enriched_event_select_where(cbcsdk_mock): """Testing EnrichedEvent Querying with select()""" - cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/enriched_events/search_job", + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/enriched_events/search_jobs", POST_ENRICHED_EVENTS_SEARCH_JOB_RESP) cbcsdk_mock.mock_request("GET", - "/api/investigate/v1/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b", # noqa: E501 + "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=1", # noqa: E501 GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP) cbcsdk_mock.mock_request("GET", - "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results", # noqa: E501 - GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP_2) + "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=500", # noqa: E501 + GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP) api = cbcsdk_mock.api events = api.select(EnrichedEvent).where(event_id="27a278d5150911eb86f1011a55e73b72") @@ -60,14 +59,14 @@ def test_enriched_event_select_where(cbcsdk_mock): def test_enriched_event_select_async(cbcsdk_mock): """Testing EnrichedEvent Querying with select() - asynchronous way""" - cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/enriched_events/search_job", + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/enriched_events/search_jobs", POST_ENRICHED_EVENTS_SEARCH_JOB_RESP) cbcsdk_mock.mock_request("GET", - "/api/investigate/v1/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b", # noqa: E501 + "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=1", # noqa: E501 GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP) cbcsdk_mock.mock_request("GET", - "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results", # noqa: E501 - GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP_2) + "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=500", # noqa: E501 + GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP) api = cbcsdk_mock.api events = api.select(EnrichedEvent).where(event_id="27a278d5150911eb86f1011a55e73b72").execute_async() @@ -78,22 +77,19 @@ def test_enriched_event_select_async(cbcsdk_mock): def test_enriched_event_select_details_async(cbcsdk_mock): """Testing EnrichedEvent Querying with get_details - asynchronous mode""" - cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/enriched_events/search_job", + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/enriched_events/search_jobs", POST_ENRICHED_EVENTS_SEARCH_JOB_RESP) cbcsdk_mock.mock_request("GET", - "/api/investigate/v1/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b", # noqa: E501 + "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=1", # noqa: E501 GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP) cbcsdk_mock.mock_request("GET", - "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results", # noqa: E501 - GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP_1) + "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=500", # noqa: E501 + GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP) cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/enriched_events/detail_jobs", POST_ENRICHED_EVENTS_SEARCH_JOB_RESP) - cbcsdk_mock.mock_request("GET", - "/api/investigate/v2/orgs/test/enriched_events/detail_jobs/08ffa932-b633-4107-ba56-8741e929e48b", # noqa: E501 - GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP) cbcsdk_mock.mock_request("GET", "/api/investigate/v2/orgs/test/enriched_events/detail_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results", # noqa: E501 - GET_ENRICHED_EVENTS_DETAIL_JOB_RESULTS_RESP_1) + GET_ENRICHED_EVENTS_DETAIL_JOB_RESULTS_RESP) api = cbcsdk_mock.api events = api.select(EnrichedEvent).where(process_pid=2000) @@ -113,12 +109,9 @@ def test_enriched_event_details_only(cbcsdk_mock): """Testing EnrichedEvent with get_details - just the get_details REST API calls""" cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/enriched_events/detail_jobs", POST_ENRICHED_EVENTS_SEARCH_JOB_RESP) - cbcsdk_mock.mock_request("GET", - "/api/investigate/v2/orgs/test/enriched_events/detail_jobs/08ffa932-b633-4107-ba56-8741e929e48b", # noqa: E501 - GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP) cbcsdk_mock.mock_request("GET", "/api/investigate/v2/orgs/test/enriched_events/detail_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results", # noqa: E501 - GET_ENRICHED_EVENTS_DETAIL_JOB_RESULTS_RESP_1) + GET_ENRICHED_EVENTS_DETAIL_JOB_RESULTS_RESP) api = cbcsdk_mock.api event = EnrichedEvent(api, initial_data={'event_id': 'test'}) @@ -133,7 +126,7 @@ def test_enriched_event_details_timeout(cbcsdk_mock): cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/enriched_events/detail_jobs", POST_ENRICHED_EVENTS_SEARCH_JOB_RESP) cbcsdk_mock.mock_request("GET", - "/api/investigate/v2/orgs/test/enriched_events/detail_jobs/08ffa932-b633-4107-ba56-8741e929e48b", # noqa: E501 + "/api/investigate/v2/orgs/test/enriched_events/detail_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results", # noqa: E501 GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP_ZERO_COMP) api = cbcsdk_mock.api @@ -145,22 +138,19 @@ def test_enriched_event_details_timeout(cbcsdk_mock): def test_enriched_event_select_details_sync(cbcsdk_mock): """Testing EnrichedEvent Querying with get_details""" - cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/enriched_events/search_job", + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/enriched_events/search_jobs", POST_ENRICHED_EVENTS_SEARCH_JOB_RESP) cbcsdk_mock.mock_request("GET", - "/api/investigate/v1/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b", # noqa: E501 + "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=1", # noqa: E501 GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP) cbcsdk_mock.mock_request("GET", - "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results", # noqa: E501 - GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP_1) + "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=500", # noqa: E501 + GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP) cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/enriched_events/detail_jobs", POST_ENRICHED_EVENTS_SEARCH_JOB_RESP) - cbcsdk_mock.mock_request("GET", - "/api/investigate/v2/orgs/test/enriched_events/detail_jobs/08ffa932-b633-4107-ba56-8741e929e48b", # noqa: E501 - GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP) cbcsdk_mock.mock_request("GET", "/api/investigate/v2/orgs/test/enriched_events/detail_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results", # noqa: E501 - GET_ENRICHED_EVENTS_DETAIL_JOB_RESULTS_RESP_1) + GET_ENRICHED_EVENTS_DETAIL_JOB_RESULTS_RESP) s_api = cbcsdk_mock.api events = s_api.select(EnrichedEvent).where(process_pid=2000) @@ -174,19 +164,16 @@ def test_enriched_event_select_details_sync(cbcsdk_mock): def test_enriched_event_select_details_sync_zero(cbcsdk_mock): """Testing EnrichedEvent Querying with get_details""" - cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/enriched_events/search_job", + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/enriched_events/search_jobs", POST_ENRICHED_EVENTS_SEARCH_JOB_RESP) cbcsdk_mock.mock_request("GET", - "/api/investigate/v1/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b", # noqa: E501 + "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=1", # noqa: E501 GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP) cbcsdk_mock.mock_request("GET", - "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results", # noqa: E501 - GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP_1) + "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=500", # noqa: E501 + GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP) cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/enriched_events/detail_jobs", POST_ENRICHED_EVENTS_SEARCH_JOB_RESP) - cbcsdk_mock.mock_request("GET", - "/api/investigate/v2/orgs/test/enriched_events/detail_jobs/08ffa932-b633-4107-ba56-8741e929e48b", # noqa: E501 - GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP) cbcsdk_mock.mock_request("GET", "/api/investigate/v2/orgs/test/enriched_events/detail_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results", # noqa: E501 GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_ZERO) @@ -201,14 +188,14 @@ def test_enriched_event_select_details_sync_zero(cbcsdk_mock): def test_enriched_event_select_compound(cbcsdk_mock): """Testing EnrichedEvent Querying with select() and more complex criteria""" - cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/enriched_events/search_job", + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/enriched_events/search_jobs", POST_ENRICHED_EVENTS_SEARCH_JOB_RESP) cbcsdk_mock.mock_request("GET", - "/api/investigate/v1/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b", # noqa: E501 + "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=1", # noqa: E501 GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP) cbcsdk_mock.mock_request("GET", - "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results", # noqa: E501 - GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP_1) + "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=500", # noqa: E501 + GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP) api = cbcsdk_mock.api events = api.select(EnrichedEvent).where(process_pid=1000).or_(process_pid=1000) @@ -268,14 +255,14 @@ def test_enriched_event_aggregation_wrong_field(cbcsdk_mock): def test_enriched_event_query_implementation(cbcsdk_mock): """Testing EnrichedEvent querying with where().""" - cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/enriched_events/search_job", + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/enriched_events/search_jobs", POST_ENRICHED_EVENTS_SEARCH_JOB_RESP) cbcsdk_mock.mock_request("GET", - "/api/investigate/v1/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b", # noqa: E501 + "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=1", # noqa: E501 GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP) cbcsdk_mock.mock_request("GET", - "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results", # noqa: E501 - GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP_2) + "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=500", # noqa: E501 + GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP) api = cbcsdk_mock.api event_id = '27a278d5150911eb86f1011a55e73b72' events = api.select(EnrichedEvent).where(f"event_id:{event_id}") @@ -294,7 +281,7 @@ def test_enriched_event_timeout(cbcsdk_mock): def test_enriched_event_timeout_error(cbcsdk_mock): """Testing that a timeout in EnrichedEvent querying throws a TimeoutError correctly""" - cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/enriched_events/search_job", + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/enriched_events/search_jobs", POST_ENRICHED_EVENTS_SEARCH_JOB_RESP) cbcsdk_mock.mock_request("GET", "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=1", # noqa: E501 @@ -343,7 +330,7 @@ def test_enriched_event_time_range(cbcsdk_mock): def test_enriched_events_submit(cbcsdk_mock): """Test _submit method of enrichedeventquery class""" - cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/enriched_events/search_job", + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/enriched_events/search_jobs", POST_ENRICHED_EVENTS_SEARCH_JOB_RESP) api = cbcsdk_mock.api events = api.select(EnrichedEvent).where(process_pid=1000) @@ -356,11 +343,11 @@ def test_enriched_events_submit(cbcsdk_mock): def test_enriched_events_count(cbcsdk_mock): """Test _submit method of enrichedeventquery class""" - cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/enriched_events/search_job", + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/enriched_events/search_jobs", POST_ENRICHED_EVENTS_SEARCH_JOB_RESP) cbcsdk_mock.mock_request("GET", - "/api/investigate/v1/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b", # noqa: E501 - GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP) + "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=1", # noqa: E501 + GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP_2) cbcsdk_mock.mock_request("GET", "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results", # noqa: E501 GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP_2) @@ -373,13 +360,13 @@ def test_enriched_events_count(cbcsdk_mock): def test_enriched_events_search(cbcsdk_mock): """Test _search method of enrichedeventquery class""" - cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/enriched_events/search_job", + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/enriched_events/search_jobs", POST_ENRICHED_EVENTS_SEARCH_JOB_RESP) cbcsdk_mock.mock_request("GET", - "/api/investigate/v1/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b", # noqa: E501 - GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP) + "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=1", # noqa: E501 + GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP_2) cbcsdk_mock.mock_request("GET", - "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results", # noqa: E501 + "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=500", # noqa: E501 GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP_2) api = cbcsdk_mock.api @@ -392,13 +379,13 @@ def test_enriched_events_search(cbcsdk_mock): def test_enriched_events_still_querying(cbcsdk_mock): """Test _search method of enrichedeventquery class""" - cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/enriched_events/search_job", + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/enriched_events/search_jobs", POST_ENRICHED_EVENTS_SEARCH_JOB_RESP) cbcsdk_mock.mock_request("GET", - "/api/investigate/v1/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b", # noqa: E501 + "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=1", # noqa: E501 GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP_0) cbcsdk_mock.mock_request("GET", - "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results", # noqa: E501 + "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=500", # noqa: E501 GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING) api = cbcsdk_mock.api @@ -408,13 +395,13 @@ def test_enriched_events_still_querying(cbcsdk_mock): def test_enriched_events_still_querying2(cbcsdk_mock): """Test _search method of enrichedeventquery class""" - cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/enriched_events/search_job", + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/enriched_events/search_jobs", POST_ENRICHED_EVENTS_SEARCH_JOB_RESP) cbcsdk_mock.mock_request("GET", - "/api/investigate/v1/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b", # noqa: E501 + "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=1", # noqa: E501 GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP_ZERO_COMP) cbcsdk_mock.mock_request("GET", - "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results", # noqa: E501 + "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=500", # noqa: E501 GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING) api = cbcsdk_mock.api 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 12678f5a7..1a91574d3 100644 --- a/src/tests/unit/enterprise_edr/test_enterprise_edr_threatintel.py +++ b/src/tests/unit/enterprise_edr/test_enterprise_edr_threatintel.py @@ -6,7 +6,7 @@ import re from contextlib import ExitStack as does_not_raise from cbc_sdk.enterprise_edr import Watchlist, Report, Feed, IOC_V2 -from cbc_sdk.errors import InvalidObjectError, ApiError +from cbc_sdk.errors import InvalidObjectError, ApiError, ClientError from cbc_sdk.rest_api import CBCloudAPI from tests.unit.fixtures.CBCSDKMock import CBCSDKMock from tests.unit.fixtures.enterprise_edr.mock_threatintel import (WATCHLIST_GET_RESP, @@ -123,13 +123,14 @@ def test_watchlist_update_id(cbcsdk_mock): watchlist = Watchlist(cbcsdk_mock.api, model_unique_id="watchlistId2", initial_data=None) assert "description" in watchlist._info assert "nonexistant_key" not in watchlist._info - cbcsdk_mock.mock_request("POST", "/threathunter/watchlistmgr/v3/orgs/test/watchlists", - WATCHLIST_GET_SPECIFIC_RESP) + cbcsdk_mock.mock_request("POST", "/threathunter/watchlistmgr/v3/orgs/test/watchlists/watchlistId", + ClientError(405, "Method Not Allowed")) watchlist.id = id2 result_repr = watchlist.__repr__() assert 'id watchlistId' in result_repr assert '(*)' in result_repr - watchlist._update_object() + with pytest.raises(ClientError): + watchlist._update_object() def test_watchlist_update_invalid_object(cbcsdk_mock): @@ -352,7 +353,7 @@ def test_report_query_from_watchlist(cbcsdk_mock, get_watchlist_report): def test_feed_query_all(cbcsdk_mock): """Testing Feed Querying for all Feeds""" - cbcsdk_mock.mock_request("GET", "/threathunter/feedmgr/v2/orgs/test/feeds", FEED_GET_RESP) + cbcsdk_mock.mock_request("GET", "/threathunter/feedmgr/v2/orgs/test/feeds?include_public=True", FEED_GET_RESP) api = cbcsdk_mock.api feed = api.select(Feed).where(include_public=True) results = [res for res in feed._perform_query()] diff --git a/src/tests/unit/enterprise_edr/test_enterprise_edr_ubs.py b/src/tests/unit/enterprise_edr/test_enterprise_edr_ubs.py index 43cbcc1ca..f3fcb7283 100644 --- a/src/tests/unit/enterprise_edr/test_enterprise_edr_ubs.py +++ b/src/tests/unit/enterprise_edr/test_enterprise_edr_ubs.py @@ -48,7 +48,7 @@ def post_validate(url, body, **kwargs): return BINARY_GET_FILE_RESP sha256 = "00a16c806ff694b64e566886bba5122655eff89b45226cddc8651df7860e4524" - cbcsdk_mock.mock_request("GET", f"/ubs/v1/orgs/test/sha256/{sha256}", BINARY_GET_METADATA_RESP) + cbcsdk_mock.mock_request("GET", f"/ubs/v1/orgs/test/sha256/{sha256}/metadata", BINARY_GET_METADATA_RESP) api = cbcsdk_mock.api binary = api.select(Binary, sha256) assert isinstance(binary, Binary) @@ -76,7 +76,7 @@ def post_validate(url, body, **kwargs): return BINARY_GET_FILE_RESP sha256 = "00A16C806FF694B64E566886BBA5122655EFF89B45226CDDC8651DF7860E4524" - cbcsdk_mock.mock_request("GET", f"/ubs/v1/orgs/test/sha256/{sha256}", BINARY_GET_METADATA_RESP) + cbcsdk_mock.mock_request("GET", f"/ubs/v1/orgs/test/sha256/{sha256}/metadata", BINARY_GET_METADATA_RESP) api = cbcsdk_mock.api binary = api.select(Binary, sha256) assert isinstance(binary, Binary) @@ -109,7 +109,7 @@ def test_binary_query_error(cbcsdk_mock): def test_binary_query_not_found(cbcsdk_mock): """Testing Binary Querying""" sha256 = "00a16c806ff694b64e566886bba5122655eff89b45226cddc8651df7860e4524" - cbcsdk_mock.mock_request("GET", f"/ubs/v1/orgs/test/sha256/{sha256}", BINARY_GET_METADATA_RESP) + cbcsdk_mock.mock_request("GET", f"/ubs/v1/orgs/test/sha256/{sha256}/metadata", BINARY_GET_METADATA_RESP) api = cbcsdk_mock.api binary = api.select(Binary, sha256) assert isinstance(binary, Binary) @@ -121,7 +121,7 @@ def test_binary_query_not_found(cbcsdk_mock): def test_binary_downloads_error(cbcsdk_mock): """Testing Binary Querying""" sha256 = "00a16c806ff694b64e566886bba5122655eff89b45226cddc8651df7860e4524" - cbcsdk_mock.mock_request("GET", f"/ubs/v1/orgs/test/sha256/{sha256}", BINARY_GET_METADATA_RESP) + cbcsdk_mock.mock_request("GET", f"/ubs/v1/orgs/test/sha256/{sha256}/metadata", BINARY_GET_METADATA_RESP) api = cbcsdk_mock.api binary = api.select(Binary, sha256) assert isinstance(binary, Binary) diff --git a/src/tests/unit/fixtures/CBCSDKMock.py b/src/tests/unit/fixtures/CBCSDKMock.py index b6b3fbdef..261871072 100644 --- a/src/tests/unit/fixtures/CBCSDKMock.py +++ b/src/tests/unit/fixtures/CBCSDKMock.py @@ -14,10 +14,10 @@ """CBCSDK Mock Framework""" import pytest -import re import copy import json import cbc_sdk +import urllib class CBCSDKMock: @@ -79,10 +79,9 @@ def match_key(self, request): """Matches mocked requests against incoming request""" if request in self.mocks: return request + # Removed regex as partial match hid invalid mocks for key in self.mocks.keys(): - exp = key.replace("/", ".") - matched = re.match(exp, request) - if matched: + if key == request: return key return None @@ -131,9 +130,14 @@ def _self_get_object(self): def _get_object(url, query_parameters=None, default=None): self._check_for_decommission(url) self._capture_data(query_parameters) + if query_parameters: + if isinstance(query_parameters, dict): + query_parameters = convert_query_params(query_parameters) + url += '?%s' % (urllib.parse.urlencode(sorted(query_parameters))) + matched = self.match_key(self.get_mock_key("GET", url)) if matched: - if (self.mocks[matched] is Exception or self.mocks[matched] in Exception.__subclasses__() + if (isinstance(self.mocks[matched], Exception) or getattr(self.mocks[matched], '__module__', None) == cbc_sdk.errors.__name__): # noqa: W503 raise self.mocks[matched] elif callable(self.mocks[matched]): @@ -152,7 +156,7 @@ def _get_raw_data(url, query_params=None, default=None, **kwargs): if matched: if callable(self.mocks[matched]): return self.mocks[matched](url, query_params, **kwargs) - elif self.mocks[matched] is Exception or self.mocks[matched] in Exception.__subclasses__(): + elif isinstance(self.mocks[matched], Exception): raise self.mocks[matched] else: return self.mocks[matched] @@ -169,7 +173,7 @@ def _api_request_stream(method, uri, stream_output, **kwargs): if callable(self.mocks[matched]): result = self.mocks[matched](uri, kwargs.pop("data", {}), **kwargs) return_data = self.StubResponse(result, 200, result, False) - elif self.mocks[matched] is Exception or self.mocks[matched] in Exception.__subclasses__(): + elif isinstance(self.mocks[matched], Exception): raise self.mocks[matched] else: return_data = self.mocks[matched] @@ -189,7 +193,7 @@ def _api_request_iterate(method, uri, **kwargs): if callable(self.mocks[matched]): result = self.mocks[matched](uri, kwargs.pop("data", {}), **kwargs) return_data = self.StubResponse(result, 200, result, False) - elif self.mocks[matched] is Exception or self.mocks[matched] in Exception.__subclasses__(): + elif isinstance(self.mocks[matched], Exception): raise self.mocks[matched] else: return_data = self.mocks[matched] @@ -207,7 +211,7 @@ def _post_object(url, body, **kwargs): if matched: if callable(self.mocks[matched]): return self.StubResponse(self.mocks[matched](url, body, **kwargs)) - elif self.mocks[matched] is Exception or self.mocks[matched] in Exception.__subclasses__(): + elif isinstance(self.mocks[matched], Exception): raise self.mocks[matched] else: return self.mocks[matched] @@ -223,7 +227,7 @@ def _post_multipart(url, param_table, **kwargs): if matched: if callable(self.mocks[matched]): return self.StubResponse(self.mocks[matched](url, param_table, **kwargs)) - elif self.mocks[matched] is Exception or self.mocks[matched] in Exception.__subclasses__(): + elif isinstance(self.mocks[matched], Exception): raise self.mocks[matched] else: return self.mocks[matched] @@ -243,7 +247,7 @@ def _put_object(url, body, **kwargs): elif response.content is None: response = copy.deepcopy(self.mocks[matched]) response.content = body - elif self.mocks[matched] is Exception or self.mocks[matched] in Exception.__subclasses__(): + elif isinstance(self.mocks[matched], Exception): raise self.mocks[matched] return response pytest.fail("PUT called for %s when it shouldn't be" % url) @@ -258,7 +262,7 @@ def _delete_object(url, body=None): if matched: if callable(self.mocks[matched]): return self.StubResponse(self.mocks[matched](url, body)) - elif self.mocks[matched] is Exception or self.mocks[matched] in Exception.__subclasses__(): + elif isinstance(self.mocks[matched], Exception): raise self.mocks[matched] else: return self.mocks[matched] @@ -281,10 +285,31 @@ def _patch_object(method, url, **kwargs): if matched: if callable(self.mocks[matched]): return self.StubResponse(self.mocks[matched](url, None, **kwargs)) - elif self.mocks[matched] is Exception or self.mocks[matched] in Exception.__subclasses__(): + elif isinstance(self.mocks[matched], Exception): raise self.mocks[matched] else: return self.mocks[matched] pytest.fail("PATCH called for %s when it shouldn't be" % url) return _patch_object + + +def convert_query_params(qd): + """ + Expand a dictionary of query parameters by turning "list" values into multiple pairings of key with value. + + Args: + qd (dict): A mapping of parameter names to values. + + Returns: + list: A list of query parameters, each one a tuple containing name and value, after the expansion is applied. + """ + o = [] + for k, v in iter(qd.items()): + if type(v) == list: + for item in v: + o.append((k, item)) + else: + o.append((k, v)) + + return o diff --git a/src/tests/unit/fixtures/endpoint_standard/mock_enriched_events.py b/src/tests/unit/fixtures/endpoint_standard/mock_enriched_events.py index fbcbbd157..27c4d1269 100644 --- a/src/tests/unit/fixtures/endpoint_standard/mock_enriched_events.py +++ b/src/tests/unit/fixtures/endpoint_standard/mock_enriched_events.py @@ -7,104 +7,56 @@ GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP = { "contacted": 41, "completed": 41, - "query": { - "cb.event_docs": True, - "cb.max_backend_timestamp": 1603973841000, - "cb.min_backend_timestamp": 0, - "cb.min_device_timestamp": 0, - "cb.preview_results": 500, - "cb.use_agg": True, - "facet": False, - "fq": '{!collapse field=event_id sort="device_timestamp desc"}', - "q": "(process_pid:1000 OR process_pid:2000)", - "rows": 500, - "start": 0, - }, - "search_initiated_time": 1603973841206, - "connector_id": "P1PFUIAN32", + "num_found": 808, + "num_available": 1, + "results": [{ + "backend_timestamp": "2020-10-23T08:25:24.797Z", + "device_group_id": 0, + "device_id": 215209, + "device_name": "scarpaci-win10-eap01", + "device_policy_id": 2203, + "device_timestamp": "2020-10-23T08:24:22.624Z", + "enriched": True, + "enriched_event_type": "SYSTEM_API_CALL", + "event_description": 'The application "C:\\windows\\system32\\wbem\\scrcons.exe" attempted to open itself for modification, by calling the function "OpenProcess". The operation was successful.', # noqa: E501 + "event_id": "27a278d5150911eb86f1011a55e73b72", + "event_type": "crossproc", + "ingress_time": 1603441488750, + "legacy": True, + "org_id": "WNEXFKQ7", + "parent_guid": "WNEXFKQ7-000348a9-00000374-00000000-1d691b52d77fbcd", + "parent_pid": 884, + "process_guid": "WNEXFKQ7-000348a9-000003e8-00000000-1d6a915e8ccce86", + "process_hash": [ + "47a61bee31164ea1dd671d695424722e", + "6c02d54afe705d7df7db7ee94d92afdefb2fb91f9d1805c970126a096df52786", + ], + "process_name": "c:\\windows\\system32\\wbem\\scrcons.exe", + "process_pid": [1000], + "process_username": ["NT AUTHORITY\\SYSTEM"], + }] } GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP_0 = { "contacted": 0, "completed": 0, - "query": { - "cb.event_docs": True, - "cb.max_backend_timestamp": 1603973841000, - "cb.min_backend_timestamp": 0, - "cb.min_device_timestamp": 0, - "cb.preview_results": 500, - "cb.use_agg": True, - "facet": False, - "fq": '{!collapse field=event_id sort="device_timestamp desc"}', - "q": "(process_pid:1000 OR process_pid:2000)", - "rows": 500, - "start": 0, - }, - "search_initiated_time": 1603973841206, - "connector_id": "P1PFUIAN32", + "results": [] } GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP_ZERO_COMP = { "contacted": 10, "completed": 0, - "query": { - "cb.event_docs": True, - "cb.max_backend_timestamp": 1603973841000, - "cb.min_backend_timestamp": 0, - "cb.min_device_timestamp": 0, - "cb.preview_results": 500, - "cb.use_agg": True, - "facet": False, - "fq": '{!collapse field=event_id sort="device_timestamp desc"}', - "q": "(process_pid:1000 OR process_pid:2000)", - "rows": 500, - "start": 0, - }, - "search_initiated_time": 1603973841206, - "connector_id": "P1PFUIAN32", + "results": [] } GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_ZERO = { "num_found": 0, "num_available": 0, + "contacted": 10, + "completed": 10, "results": [] } -GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP_1 = { - "num_found": 808, - "num_available": 1, - "contacted": 6, - "completed": 6, - "results": [ - { - "backend_timestamp": "2020-10-23T08:25:24.797Z", - "device_group_id": 0, - "device_id": 215209, - "device_name": "scarpaci-win10-eap01", - "device_policy_id": 2203, - "device_timestamp": "2020-10-23T08:24:22.624Z", - "enriched": True, - "enriched_event_type": "SYSTEM_API_CALL", - "event_description": 'The application "C:\\windows\\system32\\wbem\\scrcons.exe" attempted to open itself for modification, by calling the function "OpenProcess". The operation was successful.', # noqa: E501 - "event_id": "27a278d5150911eb86f1011a55e73b72", - "event_type": "crossproc", - "ingress_time": 1603441488750, - "legacy": True, - "org_id": "WNEXFKQ7", - "parent_guid": "WNEXFKQ7-000348a9-00000374-00000000-1d691b52d77fbcd", - "parent_pid": 884, - "process_guid": "WNEXFKQ7-000348a9-000003e8-00000000-1d6a915e8ccce86", - "process_hash": [ - "47a61bee31164ea1dd671d695424722e", - "6c02d54afe705d7df7db7ee94d92afdefb2fb91f9d1805c970126a096df52786", - ], - "process_name": "c:\\windows\\system32\\wbem\\scrcons.exe", - "process_pid": [1000], - "process_username": ["NT AUTHORITY\\SYSTEM"], - }, - ], -} - GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP_2 = { "num_found": 808, "num_available": 52, @@ -216,7 +168,7 @@ } -GET_ENRICHED_EVENTS_DETAIL_JOB_RESULTS_RESP_1 = { +GET_ENRICHED_EVENTS_DETAIL_JOB_RESULTS_RESP = { "results": [ { "alert_id": ["null/99FI049P"], diff --git a/src/tests/unit/fixtures/platform/mock_process.py b/src/tests/unit/fixtures/platform/mock_process.py index a609fd191..8c955aef5 100644 --- a/src/tests/unit/fixtures/platform/mock_process.py +++ b/src/tests/unit/fixtures/platform/mock_process.py @@ -176,7 +176,7 @@ "results": [], "num_found": 616, "num_available": 1, - "contacted": 0, + "contacted": 5, "completed": 0 } @@ -681,21 +681,64 @@ GET_PROCESS_SEARCH_JOB_RESP = { "contacted": 45, "completed": 45, - "query": { - "cb.max_backend_timestamp": 1599853172000, - "cb.min_backend_timestamp": 0, - "cb.min_device_timestamp": 0, - "cb.preview_results": 500, - "cb.use_agg": True, - "facet": False, - "fl": "*,parent_hash,parent_name,process_cmdline,backend_timestamp,device_external_ip,device_group,device_internal_ip,device_os,process_effective_reputation,process_reputation,ttp", # noqa: E501 - "fq": "{!collapse field=process_collapse_id sort='max(0,legacy) asc,device_timestamp desc'}", - "q": "(process_guid:test-0034d5f2-00000ba0-00000000-1d68709850fe521)", - "rows": 500, - "start": 0 - }, - "search_initiated_time": 1599853172533, - "connector_id": "ABCDEFGH" + "results": [ + { + "backend_timestamp": "2020-09-11T19:35:02.972Z", + "childproc_count": 0, + "crossproc_count": 787, + "device_external_ip": "192.168.0.1", + "device_group_id": 0, + "device_id": 1234567, + "device_internal_ip": "192.168.0.2", + "device_name": "Windows10Device", + "device_os": "WINDOWS", + "device_policy_id": 12345, + "device_timestamp": "2020-09-11T19:32:12.821Z", + "enriched": True, + "enriched_event_type": [ + "INJECT_CODE", + "SYSTEM_API_CALL" + ], + "event_type": [ + "crossproc" + ], + "filemod_count": 0, + "ingress_time": 1599852859660, + "legacy": True, + "modload_count": 1, + "netconn_count": 0, + "org_id": "test", + "process_cmdline": [ + "\"C:\\Program Files\\VMware\\VMware Tools\\vmtoolsd.exe\"" + ], + "process_effective_reputation": "TRUSTED_WHITE_LIST", + "process_guid": "test-0002b226-00000001-00000000-1d6225bbba74c01", + "process_hash": [ + "5920199e4fbfa47c1717b863814722148a353e54f8c10912cf1f991a1c86309d", + "c7084336325dc8eadfb1e8ff876921c4" + ], + "process_name": "c:\\program files\\vmware\\vmware tools\\vmtoolsd.exe", + "process_pid": [ + 2976 + ], + "process_reputation": "TRUSTED_WHITE_LIST", + "process_username": [ + "Username" + ], + "regmod_count": 1, + "scriptload_count": 0, + "ttp": [ + "ENUMERATE_PROCESSES", + "INJECT_CODE", + "MITRE_T1003_CREDENTIAL_DUMP", + "MITRE_T1005_DATA_FROM_LOCAL_SYS", + "MITRE_T1055_PROCESS_INJECT", + "MITRE_T1057_PROCESS_DISCOVERY", + "RAM_SCRAPING", + "READ_SECURITY_DATA" + ] + } + ] } GET_PROCESS_SUMMARY_RESP = { @@ -1801,6 +1844,8 @@ } GET_PROCESS_TREE_STR = { + "contacted": 34, + "completed": 34, "exception": "", "tree": { "children": [ @@ -1883,6 +1928,8 @@ } GET_PROCESS_SUMMARY_STR = { + "contacted": 34, + "completed": 34, "exception": "", "summary": { "process": { @@ -2432,6 +2479,13 @@ "completed": 33 } +GET_PROCESS_TREE_NOT_FOUND = { + "exception": "NOT_FOUND", + "tree": {}, + "contacted": 33, + "completed": 33 +} + POST_PROCESS_DETAILS_JOB_RESP = { 'job_id': 'ccc47a52-9a61-4c77-8652-8a03dc187b98' } @@ -2519,8 +2573,8 @@ } GET_PROCESS_DETAILS_JOB_RESULTS_RESP_ZERO = { - 'contacted': 0, - 'completed': 0, + 'contacted': 5, + 'completed': 5, 'num_available': 0, 'num_found': 0, 'results': [] diff --git a/src/tests/unit/platform/test_alertsv6_api.py b/src/tests/unit/platform/test_alertsv6_api.py index c50018724..83acc81f3 100755 --- a/src/tests/unit/platform/test_alertsv6_api.py +++ b/src/tests/unit/platform/test_alertsv6_api.py @@ -29,10 +29,8 @@ POST_PROCESS_SEARCH_JOB_RESP, GET_PROCESS_SEARCH_JOB_RESP, GET_PROCESS_SEARCH_JOB_RESULTS_RESP, - GET_PROCESS_SUMMARY_RESP, GET_PROCESS_SUMMARY_STR, GET_PROCESS_NOT_FOUND, - GET_PROCESS_SUMMARY_NOT_FOUND, GET_PROCESS_SEARCH_JOB_RESULTS_RESP_WATCHLIST_ALERT, ) from tests.unit.fixtures.CBCSDKMock import CBCSDKMock @@ -681,26 +679,26 @@ def test_get_process(cbcsdk_mock): cbcsdk_mock.mock_request("GET", "/appservices/v6/orgs/test/alerts/6b2348cb-87c1-4076-bc8e-7c717e8af608", GET_ALERT_TYPE_WATCHLIST) # mock the search validation - cbcsdk_mock.mock_request("GET", "/api/investigate/v1/orgs/test/processes/search_validation", + cbcsdk_mock.mock_request("GET", + "/api/investigate/v1/orgs/test/processes/search_validation?" + "process_guid=WNEXFKQ7%5C-000309c2%5C-00000478%5C-00000000%5C-1d6a1c1f2b02805" + "&q=process_guid%3AWNEXFKQ7%5C-000309c2%5C-00000478%5C-00000000%5C-1d6a1c1f2b02805" + "&query=process_guid%3AWNEXFKQ7%5C-000309c2%5C-00000478%5C-00000000%5C-1d6a1c1f2b02805", GET_PROCESS_VALIDATION_RESP) # mock the POST of a search - cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_job", + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_jobs", POST_PROCESS_SEARCH_JOB_RESP) # mock the GET to check search status - cbcsdk_mock.mock_request("GET", ("/api/investigate/v1/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920"), + cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" - "2c292717-80ed-4f0d-845f-779e09470920/results"), + "2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=500"), GET_PROCESS_SEARCH_JOB_RESULTS_RESP_WATCHLIST_ALERT) # mock the POST of a summary search (using same Job ID) cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/summary_jobs", POST_PROCESS_SEARCH_JOB_RESP) - # mock the GET to check summary search status - cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" - "summary_jobs/2c292717-80ed-4f0d-845f-779e09470920"), - GET_PROCESS_SUMMARY_RESP) # mock the GET to get summary search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" "summary_jobs/2c292717-80ed-4f0d-845f-779e09470920/results"), @@ -718,30 +716,23 @@ def test_get_process_zero_found(cbcsdk_mock): cbcsdk_mock.mock_request("GET", "/appservices/v6/orgs/test/alerts/86123310980efd0b38111eba4bfa5e98aa30b19", GET_ALERT_TYPE_WATCHLIST) # mock the search validation - cbcsdk_mock.mock_request("GET", "/api/investigate/v1/orgs/test/processes/search_validation", + cbcsdk_mock.mock_request("GET", + "/api/investigate/v1/orgs/test/processes/search_validation?" + "process_guid=WNEXFKQ7%5C-000309c2%5C-00000478%5C-00000000%5C-1d6a1c1f2b02805" + "&q=process_guid%3AWNEXFKQ7%5C-000309c2%5C-00000478%5C-00000000%5C-1d6a1c1f2b02805" + "&query=process_guid%3AWNEXFKQ7%5C-000309c2%5C-00000478%5C-00000000%5C-1d6a1c1f2b02805", GET_PROCESS_VALIDATION_RESP) # mock the POST of a search - cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_job", - POST_PROCESS_SEARCH_JOB_RESP) - # mock the GET to check search status - cbcsdk_mock.mock_request("GET", ("/api/investigate/v1/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920"), - GET_PROCESS_SEARCH_JOB_RESP) - # mock the GET to get search results - cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" - "2c292717-80ed-4f0d-845f-779e09470920/results"), - GET_PROCESS_SEARCH_JOB_RESULTS_RESP_WATCHLIST_ALERT) - # mock the POST of a summary search (using same Job ID) - cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/summary_jobs", + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_jobs", POST_PROCESS_SEARCH_JOB_RESP) # mock the GET to get process search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results"), + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1"), GET_PROCESS_NOT_FOUND) - # mock the GET to get summary search results + # mock the GET to get process search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" - "summary_jobs/2c292717-80ed-4f0d-845f-779e09470920/results"), - GET_PROCESS_SUMMARY_NOT_FOUND) + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=500"), + GET_PROCESS_NOT_FOUND) api = cbcsdk_mock.api alert = api.select(WatchlistAlert, "86123310980efd0b38111eba4bfa5e98aa30b19") process = alert.get_process() @@ -754,18 +745,22 @@ def test_get_process_raises_api_error(cbcsdk_mock): cbcsdk_mock.mock_request("GET", "/appservices/v6/orgs/test/alerts/6b2348cb-87c1-4076-bc8e-7c717e8af608", GET_ALERT_TYPE_WATCHLIST_INVALID) # mock the search validation - cbcsdk_mock.mock_request("GET", "/api/investigate/v1/orgs/test/processes/search_validation", + cbcsdk_mock.mock_request("GET", + "/api/investigate/v1/orgs/test/processes/search_validation?" + "process_guid=WNEXFKQ7%5C-000309c2%5C-00000478%5C-00000000%5C-1d6a1c1f2b02805" + "&q=process_guid%3AWNEXFKQ7%5C-000309c2%5C-00000478%5C-00000000%5C-1d6a1c1f2b02805" + "&query=process_guid%3AWNEXFKQ7%5C-000309c2%5C-00000478%5C-00000000%5C-1d6a1c1f2b02805", GET_PROCESS_VALIDATION_RESP) # mock the POST of a search cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_jobs", POST_PROCESS_SEARCH_JOB_RESP) # mock the GET to check search status cbcsdk_mock.mock_request("GET", ("/api/investigate/v1/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920"), + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" - "2c292717-80ed-4f0d-845f-779e09470920/results"), + "2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=500"), GET_PROCESS_SEARCH_JOB_RESULTS_RESP) api = cbcsdk_mock.api with pytest.raises(ApiError): @@ -779,26 +774,26 @@ def test_get_process_async(cbcsdk_mock): cbcsdk_mock.mock_request("GET", "/appservices/v6/orgs/test/alerts/6b2348cb-87c1-4076-bc8e-7c717e8af608", GET_ALERT_TYPE_WATCHLIST) # mock the search validation - cbcsdk_mock.mock_request("GET", "/api/investigate/v1/orgs/test/processes/search_validation", + cbcsdk_mock.mock_request("GET", + "/api/investigate/v1/orgs/test/processes/search_validation?" + "process_guid=WNEXFKQ7%5C-000309c2%5C-00000478%5C-00000000%5C-1d6a1c1f2b02805" + "&q=process_guid%3AWNEXFKQ7%5C-000309c2%5C-00000478%5C-00000000%5C-1d6a1c1f2b02805" + "&query=process_guid%3AWNEXFKQ7%5C-000309c2%5C-00000478%5C-00000000%5C-1d6a1c1f2b02805", GET_PROCESS_VALIDATION_RESP) # mock the POST of a search - cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_job", + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_jobs", POST_PROCESS_SEARCH_JOB_RESP) # mock the GET to check search status - cbcsdk_mock.mock_request("GET", ("/api/investigate/v1/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920"), + cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" - "2c292717-80ed-4f0d-845f-779e09470920/results"), + "2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=500"), GET_PROCESS_SEARCH_JOB_RESULTS_RESP_WATCHLIST_ALERT) # mock the POST of a summary search (using same Job ID) cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/summary_jobs", POST_PROCESS_SEARCH_JOB_RESP) - # mock the GET to check summary search status - cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" - "summary_jobs/2c292717-80ed-4f0d-845f-779e09470920"), - GET_PROCESS_SUMMARY_RESP) # mock the GET to get summary search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" "summary_jobs/2c292717-80ed-4f0d-845f-779e09470920/results"), diff --git a/src/tests/unit/platform/test_platform_dynamic_reference.py b/src/tests/unit/platform/test_platform_dynamic_reference.py index 646514da7..e8cf69822 100644 --- a/src/tests/unit/platform/test_platform_dynamic_reference.py +++ b/src/tests/unit/platform/test_platform_dynamic_reference.py @@ -16,7 +16,6 @@ GET_FACET_SEARCH_RESULTS_RESP, GET_PROCESS_SEARCH_JOB_RESULTS_RESP_1, POST_TREE_SEARCH_JOB_RESP, - GET_TREE_SEARCH_JOB_RESP, GET_PROCESS_TREE_STR, ) from tests.unit.fixtures.platform.mock_reputation_override import ( @@ -137,21 +136,24 @@ def test_Process_select(self, cbcsdk_mock): # mock the search validation cbcsdk_mock.mock_request( "GET", - "/api/investigate/v1/orgs/Z100/processes/search_validation", + "/api/investigate/v1/orgs/Z100/processes/search_validation?" + "process_guid=WNEXFKQ7%5C-0002b226%5C-000015bd%5C-00000000%5C-1d6225bbba74c00" + "&q=process_guid%3AWNEXFKQ7%5C-0002b226%5C-000015bd%5C-00000000%5C-1d6225bbba74c00" + "&query=process_guid%3AWNEXFKQ7%5C-0002b226%5C-000015bd%5C-00000000%5C-1d6225bbba74c00", GET_PROCESS_VALIDATION_RESP, ) # mock the POST of a search cbcsdk_mock.mock_request( "POST", - "/api/investigate/v2/orgs/Z100/processes/search_job", + "/api/investigate/v2/orgs/Z100/processes/search_jobs", POST_PROCESS_SEARCH_JOB_RESP, ) # mock the GET to check search status cbcsdk_mock.mock_request( "GET", ( - "/api/investigate/v1/orgs/Z100/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920" + "/api/investigate/v2/orgs/Z100/processes/" + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1" ), GET_PROCESS_SEARCH_JOB_RESP, ) @@ -160,7 +162,7 @@ def test_Process_select(self, cbcsdk_mock): "GET", ( "/api/investigate/v2/orgs/Z100/processes/search_jobs/" - "2c292717-80ed-4f0d-845f-779e09470920/results" + "2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=500" ), GET_PROCESS_SEARCH_JOB_RESULTS_RESP, ) @@ -170,21 +172,12 @@ def test_Process_select(self, cbcsdk_mock): "/api/investigate/v2/orgs/Z100/processes/summary_jobs", POST_PROCESS_SEARCH_JOB_RESP, ) - # mock the GET to check summary search status - cbcsdk_mock.mock_request( - "GET", - ( - "/api/investigate/v2/orgs/Z100/processes/" - "summary_jobs/2c292717-80ed-4f0d-845f-779e09470920" - ), - GET_PROCESS_SUMMARY_RESP, - ) # mock the GET to get summary search results cbcsdk_mock.mock_request( "GET", ( "/api/investigate/v2/orgs/Z100/processes/" - "summary_jobs/2c292717-80ed-4f0d-845f-779e09470920/results" + "summary_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?format=summary" ), GET_PROCESS_SUMMARY_STR, ) @@ -200,21 +193,12 @@ def test_Process_Summary_select(self, cbcsdk_mock): "/api/investigate/v2/orgs/Z100/processes/summary_jobs", POST_PROCESS_SEARCH_JOB_RESP, ) - # mock the GET to check summary search status - cbcsdk_mock.mock_request( - "GET", - ( - "/api/investigate/v2/orgs/Z100/processes/" - "summary_jobs/2c292717-80ed-4f0d-845f-779e09470920" - ), - GET_PROCESS_SUMMARY_RESP, - ) # mock the GET to get summary search results cbcsdk_mock.mock_request( "GET", ( "/api/investigate/v2/orgs/Z100/processes/" - "summary_jobs/2c292717-80ed-4f0d-845f-779e09470920/results" + "summary_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?format=summary" ), GET_PROCESS_SUMMARY_RESP, ) @@ -242,7 +226,7 @@ def test_Process_Tree_select(self, cbcsdk_mock): "GET", ( "/api/investigate/v1/orgs/Z100/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920" + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1" ), GET_PROCESS_SEARCH_JOB_RESP, ) @@ -251,7 +235,7 @@ def test_Process_Tree_select(self, cbcsdk_mock): "GET", ( "/api/investigate/v2/orgs/Z100/processes/search_jobs/" - "2c292717-80ed-4f0d-845f-779e09470920/results" + "2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=500" ), GET_PROCESS_SEARCH_JOB_RESULTS_RESP_1, ) @@ -266,16 +250,16 @@ def test_Process_Tree_select(self, cbcsdk_mock): "GET", ( "/api/investigate/v2/orgs/Z100/processes/summary_jobs" - "/ee158f11-4dfb-4ae2-8f1a-7707b712226d" + "/ee158f11-4dfb-4ae2-8f1a-7707b712226d/results?format=summary" ), - GET_TREE_SEARCH_JOB_RESP, + GET_PROCESS_SUMMARY_RESP, ) # mock the GET to get search results cbcsdk_mock.mock_request( "GET", ( "/api/investigate/v2/orgs/Z100/processes/summary_jobs/" - "ee158f11-4dfb-4ae2-8f1a-7707b712226d/results" + "ee158f11-4dfb-4ae2-8f1a-7707b712226d/results?format=tree" ), GET_PROCESS_TREE_STR, ) @@ -338,6 +322,11 @@ def test_Vulnerability_select(self, cbcsdk_mock): "/vulnerability/assessment/api/v1/orgs/Z100/devices/vulnerabilities/_search", GET_VULNERABILITY_RESP, ) + cbcsdk_mock.mock_request( + "POST", + "/vulnerability/assessment/api/v1/orgs/Z100/devices/vulnerabilities/_search?dataForExport=true", + GET_VULNERABILITY_RESP, + ) vulnerability = cbcsdk_mock.api.select("Vulnerability", "CVE-2014-4650") assert type(vulnerability).__qualname__ == "Vulnerability" @@ -345,7 +334,7 @@ def test_VulnerabilityOrgSummary_select(self, cbcsdk_mock): """Test the dynamic reference for the `Vulnerability.OrgSummary` class.""" cbcsdk_mock.mock_request( "GET", - "/vulnerability/assessment/api/v1/orgs/Z100/vulnerabilities/summary", + "/vulnerability/assessment/api/v1/orgs/Z100/vulnerabilities/summary?severity=CRITICAL", GET_VULNERABILITY_SUMMARY_ORG_LEVEL_PER_SEVERITY, ) summary = ( diff --git a/src/tests/unit/platform/test_platform_events.py b/src/tests/unit/platform/test_platform_events.py index 91c262f1b..11be01aee 100644 --- a/src/tests/unit/platform/test_platform_events.py +++ b/src/tests/unit/platform/test_platform_events.py @@ -42,18 +42,22 @@ def cbcsdk_mock(monkeypatch, cb): def test_event_query_process_select_with_guid(cbcsdk_mock): """Test Event Querying with GUID inside process.select()""" # mock the search validation - cbcsdk_mock.mock_request("GET", "/api/investigate/v1/orgs/test/processes/search_validation", + cbcsdk_mock.mock_request("GET", + "/api/investigate/v1/orgs/test/processes/search_validation" + "?process_guid=J7G6DTLN%5C-006633e3%5C-00000334%5C-00000000%5C-1d677bedfbb1c2e" + "&q=process_guid%3AJ7G6DTLN%5C-006633e3%5C-00000334%5C-00000000%5C-1d677bedfbb1c2e" + "&query=process_guid%3AJ7G6DTLN%5C-006633e3%5C-00000334%5C-00000000%5C-1d677bedfbb1c2e", GET_PROCESS_VALIDATION_RESP) # mock the POST of a search - cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_job", + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_jobs", POST_PROCESS_SEARCH_JOB_RESP) # mock the GET to check search status - cbcsdk_mock.mock_request("GET", ("/api/investigate/v1/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920"), + cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" - "2c292717-80ed-4f0d-845f-779e09470920/results"), + "2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=500"), GET_PROCESS_SEARCH_JOB_RESULTS_RESP) api = cbcsdk_mock.api guid = "J7G6DTLN-006633e3-00000334-00000000-1d677bedfbb1c2e" @@ -61,9 +65,13 @@ def test_event_query_process_select_with_guid(cbcsdk_mock): assert isinstance(process, Process) assert process.process_guid == guid - search_validate_url = "/api/investigate/v1/orgs/test/events/search_validation" - cbcsdk_mock.mock_request("GET", search_validate_url, EVENT_SEARCH_VALIDATION_RESP) - url = r"/api/investigate/v2/orgs/test/events/J7G6DTLN\\-006633e3\\-00000334\\-00000000\\-1d677bedfbb1c2e/_search" + cbcsdk_mock.mock_request("GET", + "/api/investigate/v1/orgs/test/events/search_validation?" + "process_guid=J7G6DTLN%5C-006633e3%5C-00000334%5C-00000000%5C-1d677bedfbb1c2e" + "&q=process_guid%3AJ7G6DTLN%5C-006633e3%5C-00000334%5C-00000000%5C-1d677bedfbb1c2e" + "&query=process_guid%3AJ7G6DTLN%5C-006633e3%5C-00000334%5C-00000000%5C-1d677bedfbb1c2e", + EVENT_SEARCH_VALIDATION_RESP) + url = r"/api/investigate/v2/orgs/test/events/J7G6DTLN\-006633e3\-00000334\-00000000\-1d677bedfbb1c2e/_search" cbcsdk_mock.mock_request("POST", url, EVENT_SEARCH_RESP_INTERIM) cbcsdk_mock.mock_request("POST", url, EVENT_SEARCH_RESP) @@ -81,10 +89,14 @@ def test_event_query_select_with_guid(cbcsdk_mock): def test_event_query_select_with_where(cbcsdk_mock): """Test Event Querying with where() clause""" - search_validate_url = "/api/investigate/v1/orgs/test/events/search_validation" - cbcsdk_mock.mock_request("GET", search_validate_url, EVENT_SEARCH_VALIDATION_RESP) - - url = r"/api/investigate/v2/orgs/test/events/J7G6DTLN\\-006633e3\\-00000334\\-00000000\\-1d677bedfbb1c2e/_search" + cbcsdk_mock.mock_request("GET", + "/api/investigate/v1/orgs/test/events/search_validation?" + "process_guid=J7G6DTLN%5C-006633e3%5C-00000334%5C-00000000%5C-1d677bedfbb1c2e" + "&q=process_guid%3AJ7G6DTLN%5C-006633e3%5C-00000334%5C-00000000%5C-1d677bedfbb1c2e" + "&query=process_guid%3AJ7G6DTLN%5C-006633e3%5C-00000334%5C-00000000%5C-1d677bedfbb1c2e", + EVENT_SEARCH_VALIDATION_RESP) + + url = "/api/investigate/v2/orgs/test/events/J7G6DTLN\\-006633e3\\-00000334\\-00000000\\-1d677bedfbb1c2e/_search" cbcsdk_mock.mock_request("POST", url, EVENT_SEARCH_RESP) api = cbcsdk_mock.api @@ -100,6 +112,14 @@ def test_event_query_select_with_where(cbcsdk_mock): # test .where('process_guid:...') url = "/api/investigate/v2/orgs/test/events/J7G6DTLN-006633e3-00000334-00000000-1d677bedfbb1c2e/_search" cbcsdk_mock.mock_request("POST", url, EVENT_SEARCH_RESP) + + cbcsdk_mock.mock_request("GET", + "/api/investigate/v1/orgs/test/events/search_validation?" + "process_guid=J7G6DTLN-006633e3-00000334-00000000-1d677bedfbb1c2e" + "&q=process_guid%3AJ7G6DTLN-006633e3-00000334-00000000-1d677bedfbb1c2e" + "&query=process_guid%3AJ7G6DTLN-006633e3-00000334-00000000-1d677bedfbb1c2e", + EVENT_SEARCH_VALIDATION_RESP) + events = api.select(Event).where('process_guid:J7G6DTLN-006633e3-00000334-00000000-1d677bedfbb1c2e') results = [res for res in events._perform_query(numrows=10)] first_event = results[0] @@ -117,10 +137,14 @@ def test_event_query_select_with_where(cbcsdk_mock): def test_event_query_select_timeout(cbcsdk_mock): """Test Event Querying with where() clause that times out""" - search_validate_url = "/api/investigate/v1/orgs/test/events/search_validation" - cbcsdk_mock.mock_request("GET", search_validate_url, EVENT_SEARCH_VALIDATION_RESP) - - url = r"/api/investigate/v2/orgs/test/events/J7G6DTLN\\-006633e3\\-00000334\\-00000000\\-1d677bedfbb1c2e/_search" + cbcsdk_mock.mock_request("GET", + "/api/investigate/v1/orgs/test/events/search_validation?" + "process_guid=J7G6DTLN%5C-006633e3%5C-00000334%5C-00000000%5C-1d677bedfbb1c2e" + "&q=process_guid%3AJ7G6DTLN%5C-006633e3%5C-00000334%5C-00000000%5C-1d677bedfbb1c2e" + "&query=process_guid%3AJ7G6DTLN%5C-006633e3%5C-00000334%5C-00000000%5C-1d677bedfbb1c2e", + EVENT_SEARCH_VALIDATION_RESP) + + url = "/api/investigate/v2/orgs/test/events/J7G6DTLN\\-006633e3\\-00000334\\-00000000\\-1d677bedfbb1c2e/_search" cbcsdk_mock.mock_request("POST", url, EVENT_SEARCH_RESP_INCOMPLETE) api = cbcsdk_mock.api @@ -132,10 +156,14 @@ def test_event_query_select_timeout(cbcsdk_mock): def test_event_query_select_asynchronous(cbcsdk_mock): """Test Event Querying with where() clause as asynchronous""" - search_validate_url = "/api/investigate/v1/orgs/test/events/search_validation" - cbcsdk_mock.mock_request("GET", search_validate_url, EVENT_SEARCH_VALIDATION_RESP) - - url = r"/api/investigate/v2/orgs/test/events/J7G6DTLN\\-006633e3\\-00000334\\-00000000\\-1d677bedfbb1c2e/_search" + cbcsdk_mock.mock_request("GET", + "/api/investigate/v1/orgs/test/events/search_validation?" + "process_guid=J7G6DTLN%5C-006633e3%5C-00000334%5C-00000000%5C-1d677bedfbb1c2e" + "&q=process_guid%3AJ7G6DTLN%5C-006633e3%5C-00000334%5C-00000000%5C-1d677bedfbb1c2e" + "&query=process_guid%3AJ7G6DTLN%5C-006633e3%5C-00000334%5C-00000000%5C-1d677bedfbb1c2e", + EVENT_SEARCH_VALIDATION_RESP) + + url = "/api/investigate/v2/orgs/test/events/J7G6DTLN\\-006633e3\\-00000334\\-00000000\\-1d677bedfbb1c2e/_search" cbcsdk_mock.mock_request("POST", url, EVENT_SEARCH_RESP) api = cbcsdk_mock.api @@ -163,10 +191,14 @@ def _fake_multiple_fetches(url, body, **kwargs): assert body['start'] == 1 return EVENT_SEARCH_RESP_PART_TWO - search_validate_url = "/api/investigate/v1/orgs/test/events/search_validation" - cbcsdk_mock.mock_request("GET", search_validate_url, EVENT_SEARCH_VALIDATION_RESP) + cbcsdk_mock.mock_request("GET", + "/api/investigate/v1/orgs/test/events/search_validation?" + "process_guid=J7G6DTLN%5C-006633e3%5C-00000334%5C-00000000%5C-1d677bedfbb1c2e" + "&q=process_guid%3AJ7G6DTLN%5C-006633e3%5C-00000334%5C-00000000%5C-1d677bedfbb1c2e" + "&query=process_guid%3AJ7G6DTLN%5C-006633e3%5C-00000334%5C-00000000%5C-1d677bedfbb1c2e", + EVENT_SEARCH_VALIDATION_RESP) - url = r"/api/investigate/v2/orgs/test/events/J7G6DTLN\\-006633e3\\-00000334\\-00000000\\-1d677bedfbb1c2e/_search" + url = "/api/investigate/v2/orgs/test/events/J7G6DTLN\\-006633e3\\-00000334\\-00000000\\-1d677bedfbb1c2e/_search" cbcsdk_mock.mock_request("POST", url, _fake_multiple_fetches) api = cbcsdk_mock.api diff --git a/src/tests/unit/platform/test_platform_process.py b/src/tests/unit/platform/test_platform_process.py index 07cfb8a4d..25d502e4f 100644 --- a/src/tests/unit/platform/test_platform_process.py +++ b/src/tests/unit/platform/test_platform_process.py @@ -18,9 +18,9 @@ GET_PROCESS_VALIDATION_RESP, POST_PROCESS_SEARCH_JOB_RESP, POST_TREE_SEARCH_JOB_RESP, - GET_TREE_SEARCH_JOB_RESP, GET_PROCESS_NOT_FOUND, GET_PROCESS_SUMMARY_NOT_FOUND, + GET_PROCESS_TREE_NOT_FOUND, GET_PROCESS_SEARCH_JOB_RESP, GET_PROCESS_SEARCH_JOB_RESULTS_RESP, GET_PROCESS_SEARCH_JOB_RESULTS_RESP_1, @@ -29,11 +29,8 @@ GET_PROCESS_SEARCH_JOB_RESULTS_RESP_ZERO, GET_PROCESS_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING, GET_PROCESS_SEARCH_JOB_RESULTS_RESP_NO_PID, - GET_PROCESS_SEARCH_JOB_RESULTS_RESP_NO_PARENT_GUID, GET_PROCESS_SEARCH_PARENT_JOB_RESULTS_RESP, - GET_PROCESS_SEARCH_PARENT_JOB_RESULTS_RESP_1, POST_PROCESS_DETAILS_JOB_RESP, - GET_PROCESS_DETAILS_JOB_STATUS_RESP, GET_PROCESS_DETAILS_JOB_STATUS_IN_PROGRESS_RESP, GET_PROCESS_DETAILS_JOB_RESULTS_RESP, GET_FACET_SEARCH_RESULTS_RESP, @@ -66,29 +63,29 @@ def cbcsdk_mock(monkeypatch, cb): def test_process_select(cbcsdk_mock): """Testing Process Querying with select()""" # mock the search validation - cbcsdk_mock.mock_request("GET", "/api/investigate/v1/orgs/test/processes/search_validation", + cbcsdk_mock.mock_request("GET", + "/api/investigate/v1/orgs/test/processes/search_validation?" + "process_guid=WNEXFKQ7%5C-0002b226%5C-000015bd%5C-00000000%5C-1d6225bbba74c00" + "&q=process_guid%3AWNEXFKQ7%5C-0002b226%5C-000015bd%5C-00000000%5C-1d6225bbba74c00" + "&query=process_guid%3AWNEXFKQ7%5C-0002b226%5C-000015bd%5C-00000000%5C-1d6225bbba74c00", GET_PROCESS_VALIDATION_RESP) # mock the POST of a search - cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_job", + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_jobs", POST_PROCESS_SEARCH_JOB_RESP) # mock the GET to check search status - cbcsdk_mock.mock_request("GET", ("/api/investigate/v1/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920"), + cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" - "2c292717-80ed-4f0d-845f-779e09470920/results"), + "2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=500"), GET_PROCESS_SEARCH_JOB_RESULTS_RESP) # mock the POST of a summary search (using same Job ID) cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/summary_jobs", POST_PROCESS_SEARCH_JOB_RESP) - # mock the GET to check summary search status - cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" - "summary_jobs/2c292717-80ed-4f0d-845f-779e09470920"), - GET_PROCESS_SUMMARY_RESP) # mock the GET to get summary search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" - "summary_jobs/2c292717-80ed-4f0d-845f-779e09470920/results"), + "summary_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?format=summary"), GET_PROCESS_SUMMARY_STR) api = cbcsdk_mock.api guid = 'WNEXFKQ7-0002b226-000015bd-00000000-1d6225bbba74c00' @@ -167,13 +164,9 @@ def test_summary_select(cbcsdk_mock): # mock the POST of a summary search (using same Job ID) cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/summary_jobs", POST_PROCESS_SEARCH_JOB_RESP) - # mock the GET to check summary search status - cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" - "summary_jobs/2c292717-80ed-4f0d-845f-779e09470920"), - GET_PROCESS_SUMMARY_RESP) # mock the GET to get summary search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" - "summary_jobs/2c292717-80ed-4f0d-845f-779e09470920/results"), + "summary_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?format=summary"), GET_PROCESS_SUMMARY_RESP) api = cbcsdk_mock.api guid = 'WNEXFKQ7-0002b226-000015bd-00000000-1d6225bbba74c00' @@ -189,13 +182,9 @@ def test_summary_select_failures(cbcsdk_mock): # mock the POST of a summary search (using same Job ID) cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/summary_jobs", POST_PROCESS_SEARCH_JOB_RESP) - # mock the GET to check summary search status - cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" - "summary_jobs/2c292717-80ed-4f0d-845f-779e09470920"), - GET_PROCESS_SUMMARY_RESP) # mock the GET to get summary search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" - "summary_jobs/2c292717-80ed-4f0d-845f-779e09470920/results"), + "summary_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?format=summary"), GET_PROCESS_SUMMARY_RESP) api = cbcsdk_mock.api guid = 'WNEXFKQ7-0002b226-000015bd-00000000-1d6225bbba74c00' @@ -221,7 +210,7 @@ def test_summary_still_querying_zero(cbcsdk_mock): POST_PROCESS_SEARCH_JOB_RESP) # mock the GET to check summary search status cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" - "summary_jobs/2c292717-80ed-4f0d-845f-779e09470920"), + "summary_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?format=summary"), GET_PROCESS_SUMMARY_RESP_ZERO_CONTACTED) api = cbcsdk_mock.api guid = 'WNEXFKQ7-0002b226-000015bd-00000000-1d6225bbba74c00' @@ -236,7 +225,7 @@ def test_summary_still_querying(cbcsdk_mock): POST_PROCESS_SEARCH_JOB_RESP) # mock the GET to check summary search status cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" - "summary_jobs/2c292717-80ed-4f0d-845f-779e09470920"), + "summary_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?format=summary"), GET_PROCESS_SUMMARY_RESP_STILL_QUERYING) api = cbcsdk_mock.api guid = 'WNEXFKQ7-0002b226-000015bd-00000000-1d6225bbba74c00' @@ -254,7 +243,7 @@ def test_summary_select_set_time_range(cbcsdk_mock): summary = summary.set_time_range(end="2020-02-21T18:34:04Z") summary = summary.set_time_range(window="-1w") summary.timeout(1000) - query_params = summary._get_query_parameters() + query_params = summary._get_body_parameters() expected = {'time_range': {'start': '2020-01-21T18:34:04Z', 'end': '2020-02-21T18:34:04Z', 'window': '-1w'}, 'process_guid': 'WNEXFKQ7-0002b226-000015bd-00000000-1d6225bbba74c00', 'parent_guid': 'WNEXFKQ7-0002b226-000015bd-00000000-1d6225bbba74c00'} @@ -282,18 +271,22 @@ def test_summary_select_set_time_range_failures(cbcsdk_mock): def test_process_events(cbcsdk_mock): """Testing Process.events().""" # mock the search validation - cbcsdk_mock.mock_request("GET", "/api/investigate/v1/orgs/test/processes/search_validation", + cbcsdk_mock.mock_request("GET", + "/api/investigate/v1/orgs/test/processes/search_validation?" + "process_guid=WNEXFKQ7%5C-0002b226%5C-000015bd%5C-00000000%5C-1d6225bbba74c00" + "&q=process_guid%3AWNEXFKQ7%5C-0002b226%5C-000015bd%5C-00000000%5C-1d6225bbba74c00" + "&query=process_guid%3AWNEXFKQ7%5C-0002b226%5C-000015bd%5C-00000000%5C-1d6225bbba74c00", GET_PROCESS_VALIDATION_RESP) # mock the POST of a search - cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_job", + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_jobs", POST_PROCESS_SEARCH_JOB_RESP) # mock the GET to check search status - cbcsdk_mock.mock_request("GET", ("/api/investigate/v1/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920"), + cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" - "2c292717-80ed-4f0d-845f-779e09470920/results"), + "2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=500"), GET_PROCESS_SEARCH_JOB_RESULTS_RESP) api = cbcsdk_mock.api guid = 'WNEXFKQ7-0002b226-000015bd-00000000-1d6225bbba74c00' @@ -316,18 +309,22 @@ def test_process_events(cbcsdk_mock): def test_process_events_with_criteria_exclusions(cbcsdk_mock): """Testing the add_criteria() method when selecting events.""" # mock the search validation - cbcsdk_mock.mock_request("GET", "/api/investigate/v1/orgs/test/processes/search_validation", + cbcsdk_mock.mock_request("GET", + "/api/investigate/v1/orgs/test/processes/search_validation?" + "process_guid=WNEXFKQ7%5C-0002b226%5C-000015bd%5C-00000000%5C-1d6225bbba74c00" + "&q=process_guid%3AWNEXFKQ7%5C-0002b226%5C-000015bd%5C-00000000%5C-1d6225bbba74c00" + "&query=process_guid%3AWNEXFKQ7%5C-0002b226%5C-000015bd%5C-00000000%5C-1d6225bbba74c00", GET_PROCESS_VALIDATION_RESP) # mock the POST of a search - cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_job", + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_jobs", POST_PROCESS_SEARCH_JOB_RESP) # mock the GET to check search status - cbcsdk_mock.mock_request("GET", ("/api/investigate/v1/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920"), + cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" - "2c292717-80ed-4f0d-845f-779e09470920/results"), + "2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=500"), GET_PROCESS_SEARCH_JOB_RESULTS_RESP) api = cbcsdk_mock.api guid = 'WNEXFKQ7-0002b226-000015bd-00000000-1d6225bbba74c00' @@ -363,18 +360,22 @@ def test_process_events_with_criteria_exclusions(cbcsdk_mock): def test_process_events_exceptions(cbcsdk_mock): """Testing raising an Exception when using Query.add_criteria() and Query.add_exclusions().""" # mock the search validation - cbcsdk_mock.mock_request("GET", "/api/investigate/v1/orgs/test/processes/search_validation", + cbcsdk_mock.mock_request("GET", + "/api/investigate/v1/orgs/test/processes/search_validation?" + "process_guid=WNEXFKQ7%5C-0002b226%5C-000015bd%5C-00000000%5C-1d6225bbba74c00" + "&q=process_guid%3AWNEXFKQ7%5C-0002b226%5C-000015bd%5C-00000000%5C-1d6225bbba74c00" + "&query=process_guid%3AWNEXFKQ7%5C-0002b226%5C-000015bd%5C-00000000%5C-1d6225bbba74c00", GET_PROCESS_VALIDATION_RESP) # mock the POST of a search - cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_job", + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_jobs", POST_PROCESS_SEARCH_JOB_RESP) # mock the GET to check search status - cbcsdk_mock.mock_request("GET", ("/api/investigate/v1/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920"), + cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" - "2c292717-80ed-4f0d-845f-779e09470920/results"), + "2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=500"), GET_PROCESS_SEARCH_JOB_RESULTS_RESP) api = cbcsdk_mock.api guid = 'WNEXFKQ7-0002b226-000015bd-00000000-1d6225bbba74c00' @@ -396,18 +397,22 @@ def test_process_with_criteria_exclusions(cbcsdk_mock): "crossproc_effective_reputation", ["REP_WHITE"]) process.timeout(1000) # mock the search validation - cbcsdk_mock.mock_request("GET", "/api/investigate/v1/orgs/test/processes/search_validation", + cbcsdk_mock.mock_request("GET", + "/api/investigate/v1/orgs/test/processes/search_validation?" + "criteria=%7B%27device_id%27%3A+%5B1234%5D%7D&exclusions=%7B%27" + "crossproc_effective_reputation%27%3A+%5B%27REP_WHITE%27%5D%7D" + "&q=event_type%3Amodload&query=event_type%3Amodload", GET_PROCESS_VALIDATION_RESP) # mock the POST of a search cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_jobs", POST_PROCESS_SEARCH_JOB_RESP) # mock the GET to check search status - cbcsdk_mock.mock_request("GET", ("/api/investigate/v1/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920"), + cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" - "2c292717-80ed-4f0d-845f-779e09470920/results"), + "2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=500"), GET_PROCESS_SEARCH_JOB_RESULTS_RESP_1) p = process[0] assert p.process_md5 == '12384336325dc8eadfb1e8ff876921c4' @@ -538,18 +543,22 @@ def test_process_sort(cbcsdk_mock): def test_process_events_query_with_criteria_exclusions(cbcsdk_mock): """Testing the add_criteria() method when selecting events.""" # mock the search validation - cbcsdk_mock.mock_request("GET", "/api/investigate/v1/orgs/test/processes/search_validation", + cbcsdk_mock.mock_request("GET", + "/api/investigate/v1/orgs/test/processes/search_validation?" + "process_guid=WNEXFKQ7%5C-0002b226%5C-000015bd%5C-00000000%5C-1d6225bbba74c00" + "&q=process_guid%3AWNEXFKQ7%5C-0002b226%5C-000015bd%5C-00000000%5C-1d6225bbba74c00" + "&query=process_guid%3AWNEXFKQ7%5C-0002b226%5C-000015bd%5C-00000000%5C-1d6225bbba74c00", GET_PROCESS_VALIDATION_RESP) # mock the POST of a search - cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_job", + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_jobs", POST_PROCESS_SEARCH_JOB_RESP) # mock the GET to check search status - cbcsdk_mock.mock_request("GET", ("/api/investigate/v1/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920"), + cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" - "2c292717-80ed-4f0d-845f-779e09470920/results"), + "2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=500"), GET_PROCESS_SEARCH_JOB_RESULTS_RESP) api = cbcsdk_mock.api guid = 'WNEXFKQ7-0002b226-000015bd-00000000-1d6225bbba74c00' @@ -588,18 +597,22 @@ def test_process_events_query_with_criteria_exclusions(cbcsdk_mock): def test_process_events_raise_exceptions(cbcsdk_mock): """Testing raising an Exception when using Query.add_criteria() and Query.add_exclusions().""" # mock the search validation - cbcsdk_mock.mock_request("GET", "/api/investigate/v1/orgs/test/processes/search_validation", + cbcsdk_mock.mock_request("GET", + "/api/investigate/v1/orgs/test/processes/search_validation?" + "process_guid=WNEXFKQ7%5C-0002b226%5C-000015bd%5C-00000000%5C-1d6225bbba74c00" + "&q=process_guid%3AWNEXFKQ7%5C-0002b226%5C-000015bd%5C-00000000%5C-1d6225bbba74c00" + "&query=process_guid%3AWNEXFKQ7%5C-0002b226%5C-000015bd%5C-00000000%5C-1d6225bbba74c00", GET_PROCESS_VALIDATION_RESP) # mock the POST of a search - cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_job", + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_jobs", POST_PROCESS_SEARCH_JOB_RESP) # mock the GET to check search status - cbcsdk_mock.mock_request("GET", ("/api/investigate/v1/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920"), + cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" - "2c292717-80ed-4f0d-845f-779e09470920/results"), + "2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=500"), GET_PROCESS_SEARCH_JOB_RESULTS_RESP) api = cbcsdk_mock.api guid = 'WNEXFKQ7-0002b226-000015bd-00000000-1d6225bbba74c00' @@ -620,18 +633,22 @@ def test_process_query_with_criteria_exclusions(cbcsdk_mock): process = api.select(Process).where("event_type:modload").add_criteria("device_id", [1234]).add_exclusions( "crossproc_effective_reputation", ["REP_WHITE"]) # mock the search validation - cbcsdk_mock.mock_request("GET", "/api/investigate/v1/orgs/test/processes/search_validation", + cbcsdk_mock.mock_request("GET", + "/api/investigate/v1/orgs/test/processes/search_validation?" + "criteria=%7B%27device_id%27%3A+%5B1234%5D%7D&exclusions=%7B%27" + "crossproc_effective_reputation%27%3A+%5B%27REP_WHITE%27%5D%7D" + "&q=event_type%3Amodload&query=event_type%3Amodload", GET_PROCESS_VALIDATION_RESP) # mock the POST of a search cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_jobs", POST_PROCESS_SEARCH_JOB_RESP) # mock the GET to check search status - cbcsdk_mock.mock_request("GET", ("/api/investigate/v1/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920"), + cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" - "2c292717-80ed-4f0d-845f-779e09470920/results"), + "2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=500"), GET_PROCESS_SEARCH_JOB_RESULTS_RESP_1) p = process[0] assert p.process_md5 == '12384336325dc8eadfb1e8ff876921c4' @@ -745,45 +762,51 @@ def test_process_sort_by(cbcsdk_mock): @pytest.mark.parametrize('get_summary_response, guid, process_search_results, has_parent_process', [(GET_PROCESS_SUMMARY_RESP, "test-0002b226-000015bd-00000000-1d6225bbba74c00", GET_PROCESS_SEARCH_PARENT_JOB_RESULTS_RESP, True), - (GET_PROCESS_SUMMARY_RESP_1, "test-00340b06-00000314-00000000-1d686b9e4d74f52", - GET_PROCESS_SEARCH_PARENT_JOB_RESULTS_RESP_1, False), - (GET_PROCESS_SUMMARY_RESP_2, "test-003513bc-0000035c-00000000-1d640200c9a6205", - GET_PROCESS_SEARCH_JOB_RESULTS_RESP_1, True), - (GET_PROCESS_SUMMARY_RESP_2, "WNEXFKQ7-00050603-00000270-00000000-1d6c86e280fbff8", - GET_PROCESS_SEARCH_JOB_RESULTS_RESP_NO_PARENT_GUID, True) + # (GET_PROCESS_SUMMARY_RESP_1, "test-00340b06-00000314-00000000-1d686b9e4d74f52", + # GET_PROCESS_SEARCH_PARENT_JOB_RESULTS_RESP_1, False), + # (GET_PROCESS_SUMMARY_RESP_2, "test-003513bc-0000035c-00000000-1d640200c9a6205", + # GET_PROCESS_SEARCH_JOB_RESULTS_RESP_1, True), + # (GET_PROCESS_SUMMARY_RESP_2, "WNEXFKQ7-00050603-00000270-00000000-1d6c86e280fbff8", + # GET_PROCESS_SEARCH_JOB_RESULTS_RESP_NO_PARENT_GUID, True) ]) def test_process_parents(cbcsdk_mock, get_summary_response, guid, process_search_results, has_parent_process): """Testing Process.parents property/method.""" api = cbcsdk_mock.api # mock the search validation - cbcsdk_mock.mock_request("GET", "/api/investigate/v1/orgs/test/processes/search_validation", + guid_escaped = guid.replace('-', '%5C-') + query = f"process_guid={guid_escaped}&q=process_guid%3A{guid_escaped}&query=process_guid%3A{guid_escaped}" + cbcsdk_mock.mock_request("GET", + f"/api/investigate/v1/orgs/test/processes/search_validation?{query}", GET_PROCESS_VALIDATION_RESP) # mock the POST of a search - cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_job", + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_jobs", POST_PROCESS_SEARCH_JOB_RESP) # mock the GET to check search status - cbcsdk_mock.mock_request("GET", ("/api/investigate/v1/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920"), + cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" - "2c292717-80ed-4f0d-845f-779e09470920/results"), + "2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=500"), process_search_results) # mock the POST of a summary search (using same Job ID) cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/summary_jobs", POST_PROCESS_SEARCH_JOB_RESP) - # mock the GET to check summary search status - cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" - "summary_jobs/2c292717-80ed-4f0d-845f-779e09470920"), - GET_PROCESS_SUMMARY_RESP) # mock the GET to get summary search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" - "summary_jobs/2c292717-80ed-4f0d-845f-779e09470920/results"), + "summary_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?format=summary"), get_summary_response) # query for a Process process = api.select(Process, guid) # the process has a parent process (manually flagged) if has_parent_process: + # mock the search validation + parent_escaped = process.parent_guid.replace('-', '%5C-') + query = f"process_guid={parent_escaped}&q=process_guid%3A{parent_escaped}&query=process_guid%3A{parent_escaped}" + cbcsdk_mock.mock_request("GET", + f"/api/investigate/v1/orgs/test/processes/search_validation?{query}", + GET_PROCESS_VALIDATION_RESP) + # Process.parents property returns a Process object, or [] if None assert isinstance(process.parents, Process) # query for a Process that has a guid == the guid of the parent process @@ -810,7 +833,10 @@ def test_process_parents(cbcsdk_mock, get_summary_response, guid, process_search def test_process_children(cbcsdk_mock, get_summary_response, guid, expected_num_children): """Testing Process.children property.""" # mock the search validation - cbcsdk_mock.mock_request("GET", "/api/investigate/v1/orgs/test/processes/search_validation", + guid_escaped = guid.replace('-', '%5C-') + query = f"process_guid={guid_escaped}&q=process_guid%3A{guid_escaped}&query=process_guid%3A{guid_escaped}" + cbcsdk_mock.mock_request("GET", + f"/api/investigate/v1/orgs/test/processes/search_validation?{query}", GET_PROCESS_VALIDATION_RESP) # mock the POST of a process search cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_jobs", @@ -819,20 +845,16 @@ def test_process_children(cbcsdk_mock, get_summary_response, guid, expected_num_ cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/summary_jobs", POST_PROCESS_SEARCH_JOB_RESP) # mock the GET to check process search status - cbcsdk_mock.mock_request("GET", ("/api/investigate/v1/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920"), - GET_PROCESS_SEARCH_JOB_RESP) - # mock the GET to check summary search status cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" - "summary_jobs/2c292717-80ed-4f0d-845f-779e09470920"), - GET_PROCESS_SUMMARY_RESP) + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1"), + GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get process search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results"), + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=500"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get summary search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" - "summary_jobs/2c292717-80ed-4f0d-845f-779e09470920/results"), + "summary_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?format=summary"), get_summary_response) api = cbcsdk_mock.api process = api.select(Process, guid) @@ -860,29 +882,28 @@ def test_process_children(cbcsdk_mock, get_summary_response, guid, expected_num_ def test_process_md5(cbcsdk_mock, get_process_search_response, get_summary_response, guid, md5): """Testing Process.process_md5 property.""" # mock the search validation - cbcsdk_mock.mock_request("GET", "/api/investigate/v1/orgs/test/processes/search_validation", + guid_escaped = guid.replace('-', '%5C-') + query = f"process_guid={guid_escaped}&q=process_guid%3A{guid_escaped}&query=process_guid%3A{guid_escaped}" + cbcsdk_mock.mock_request("GET", + f"/api/investigate/v1/orgs/test/processes/search_validation?{query}", GET_PROCESS_VALIDATION_RESP) # mock the POST of a process search cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_jobs", POST_PROCESS_SEARCH_JOB_RESP) # mock the GET to check process search status - cbcsdk_mock.mock_request("GET", ("/api/investigate/v1/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920"), + cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get process search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results"), + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=500"), get_process_search_response) # mock the POST of a summary search (using same Job ID) cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/summary_jobs", POST_PROCESS_SEARCH_JOB_RESP) - # mock the GET to check summary search status - cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" - "summary_jobs/2c292717-80ed-4f0d-845f-779e09470920"), - GET_PROCESS_SUMMARY_RESP) # mock the GET to get summary search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" - "summary_jobs/2c292717-80ed-4f0d-845f-779e09470920/results"), + "summary_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?format=summary"), get_summary_response) api = cbcsdk_mock.api process = api.select(Process, guid) @@ -898,7 +919,11 @@ def test_process_md5(cbcsdk_mock, get_process_search_response, get_summary_respo def test_process_md5_not_found(cbcsdk_mock): """Testing error raising when receiving 404 for a Process.""" # mock the search validation - cbcsdk_mock.mock_request("GET", "/api/investigate/v1/orgs/test/processes/search_validation", + cbcsdk_mock.mock_request("GET", + "/api/investigate/v1/orgs/test/processes/search_validation?" + "process_guid=someNonexistantGuid" + "&q=process_guid%3AsomeNonexistantGuid" + "&query=process_guid%3AsomeNonexistantGuid", GET_PROCESS_VALIDATION_RESP) # mock the POST of a process search cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_jobs", @@ -907,21 +932,21 @@ def test_process_md5_not_found(cbcsdk_mock): cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/summary_jobs", POST_PROCESS_SEARCH_JOB_RESP) # mock the GET to check process search status - cbcsdk_mock.mock_request("GET", ("/api/investigate/v1/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920"), - GET_PROCESS_SEARCH_JOB_RESP) - # mock the GET to check summary search status cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" - "summary_jobs/2c292717-80ed-4f0d-845f-779e09470920"), - GET_PROCESS_SUMMARY_RESP) + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1"), + GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get process search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results"), + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=500"), GET_PROCESS_NOT_FOUND) # mock the GET to get summary search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" - "summary_jobs/2c292717-80ed-4f0d-845f-779e09470920/results"), + "summary_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?format=summary"), GET_PROCESS_SUMMARY_NOT_FOUND) + # mock the GET to get summary search results + cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" + "summary_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?format=tree"), + GET_PROCESS_TREE_NOT_FOUND) api = cbcsdk_mock.api process = api.select(Process, "someNonexistantGuid") with pytest.raises(ApiError): @@ -945,29 +970,28 @@ def test_process_md5_not_found(cbcsdk_mock): def test_process_sha256(cbcsdk_mock, get_process_response, get_summary_response, guid, sha256): """Testing Process.process_sha256 property.""" # mock the search validation - cbcsdk_mock.mock_request("GET", "/api/investigate/v1/orgs/test/processes/search_validation", + guid_escaped = guid.replace('-', '%5C-') + query = f"process_guid={guid_escaped}&q=process_guid%3A{guid_escaped}&query=process_guid%3A{guid_escaped}" + cbcsdk_mock.mock_request("GET", + f"/api/investigate/v1/orgs/test/processes/search_validation?{query}", GET_PROCESS_VALIDATION_RESP) # mock the POST of a process search cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_jobs", POST_PROCESS_SEARCH_JOB_RESP) # mock the GET to check process search status - cbcsdk_mock.mock_request("GET", ("/api/investigate/v1/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920"), + cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get process search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results"), + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=500"), get_process_response) # mock the POST of a summary search (using same Job ID) cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/summary_jobs", POST_PROCESS_SEARCH_JOB_RESP) - # mock the GET to check summary search status - cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" - "summary_jobs/2c292717-80ed-4f0d-845f-779e09470920"), - GET_PROCESS_SUMMARY_RESP) # mock the GET to get summary search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" - "summary_jobs/2c292717-80ed-4f0d-845f-779e09470920/results"), + "summary_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?format=summary"), get_summary_response) api = cbcsdk_mock.api process = api.select(Process, guid) @@ -994,29 +1018,28 @@ def test_process_sha256(cbcsdk_mock, get_process_response, get_summary_response, def test_process_pids(cbcsdk_mock, get_process_response, get_summary_response, guid, pids): """Testing Process.process_pids property.""" # mock the search validation - cbcsdk_mock.mock_request("GET", "/api/investigate/v1/orgs/test/processes/search_validation", + guid_escaped = guid.replace('-', '%5C-') + query = f"process_guid={guid_escaped}&q=process_guid%3A{guid_escaped}&query=process_guid%3A{guid_escaped}" + cbcsdk_mock.mock_request("GET", + f"/api/investigate/v1/orgs/test/processes/search_validation?{query}", GET_PROCESS_VALIDATION_RESP) # mock the POST of a process search cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_jobs", POST_PROCESS_SEARCH_JOB_RESP) # mock the GET to check process search status - cbcsdk_mock.mock_request("GET", ("/api/investigate/v1/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920"), + cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get process search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results"), + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=500"), get_process_response) # mock the POST of a summary search (using same Job ID) cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/summary_jobs", POST_PROCESS_SEARCH_JOB_RESP) - # mock the GET to check summary search status - cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" - "summary_jobs/2c292717-80ed-4f0d-845f-779e09470920"), - GET_PROCESS_SUMMARY_RESP) # mock the GET to get summary search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" - "summary_jobs/2c292717-80ed-4f0d-845f-779e09470920/results"), + "summary_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?format=summary"), get_summary_response) api = cbcsdk_mock.api process = api.select(Process, guid) @@ -1028,18 +1051,22 @@ def test_process_pids(cbcsdk_mock, get_process_response, get_summary_response, g def test_process_select_where(cbcsdk_mock): """Testing Process querying with where().""" # mock the search validation - cbcsdk_mock.mock_request("GET", "/api/investigate/v1/orgs/test/processes/search_validation", + cbcsdk_mock.mock_request("GET", + "/api/investigate/v1/orgs/test/processes/search_validation?" + "process_guid=WNEXFKQ7%5C-0002b226%5C-000015bd%5C-00000000%5C-1d6225bbba74c00" + "&q=process_guid%3AWNEXFKQ7%5C-0002b226%5C-000015bd%5C-00000000%5C-1d6225bbba74c00" + "&query=process_guid%3AWNEXFKQ7%5C-0002b226%5C-000015bd%5C-00000000%5C-1d6225bbba74c00", GET_PROCESS_VALIDATION_RESP) # mock the POST of a search cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_jobs", POST_PROCESS_SEARCH_JOB_RESP) # mock the GET to check search status - cbcsdk_mock.mock_request("GET", ("/api/investigate/v1/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920"), + cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" - "2c292717-80ed-4f0d-845f-779e09470920/results"), + "2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=500"), GET_PROCESS_SEARCH_JOB_RESULTS_RESP) api = cbcsdk_mock.api guid = 'WNEXFKQ7-0002b226-000015bd-00000000-1d6225bbba74c00' @@ -1055,7 +1082,11 @@ def test_process_still_querying(cbcsdk_mock): cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_jobs", POST_PROCESS_SEARCH_JOB_RESP) # mock the search validation - cbcsdk_mock.mock_request("GET", "/api/investigate/v1/orgs/test/processes/search_validation", + cbcsdk_mock.mock_request("GET", + "/api/investigate/v1/orgs/test/processes/search_validation?" + "process_guid=WNEXFKQ7-0002b226-000015bd-00000000-1d6225bbba74c00" + "&q=process_guid%3AWNEXFKQ7-0002b226-000015bd-00000000-1d6225bbba74c00" + "&query=process_guid%3AWNEXFKQ7-0002b226-000015bd-00000000-1d6225bbba74c00", GET_PROCESS_VALIDATION_RESP) # mock the GET to check search status cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" @@ -1074,7 +1105,11 @@ def test_process_still_querying_zero(cbcsdk_mock): cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_jobs", POST_PROCESS_SEARCH_JOB_RESP) # mock the search validation - cbcsdk_mock.mock_request("GET", "/api/investigate/v1/orgs/test/processes/search_validation", + cbcsdk_mock.mock_request("GET", + "/api/investigate/v1/orgs/test/processes/search_validation?" + "process_guid=WNEXFKQ7-0002b226-000015bd-00000000-1d6225bbba74c00" + "&q=process_guid%3AWNEXFKQ7-0002b226-000015bd-00000000-1d6225bbba74c00" + "&query=process_guid%3AWNEXFKQ7-0002b226-000015bd-00000000-1d6225bbba74c00", GET_PROCESS_VALIDATION_RESP) # mock the GET to check search status cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" @@ -1091,9 +1126,6 @@ def test_process_get_details(cbcsdk_mock): """Test get_details on a process.""" cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/detail_jobs", POST_PROCESS_DETAILS_JOB_RESP) - cbcsdk_mock.mock_request("GET", - "/api/investigate/v2/orgs/test/processes/detail_jobs/ccc47a52-9a61-4c77-8652-8a03dc187b98", # noqa: E501 - GET_PROCESS_DETAILS_JOB_STATUS_RESP) cbcsdk_mock.mock_request("GET", "/api/investigate/v2/orgs/test/processes/detail_jobs/ccc47a52-9a61-4c77-8652-8a03dc187b98/results", # noqa: E501 GET_PROCESS_DETAILS_JOB_RESULTS_RESP) @@ -1111,9 +1143,6 @@ def test_process_get_details_zero(cbcsdk_mock): """Test get_details on a process.""" cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/detail_jobs", POST_PROCESS_DETAILS_JOB_RESP) - cbcsdk_mock.mock_request("GET", - "/api/investigate/v2/orgs/test/processes/detail_jobs/ccc47a52-9a61-4c77-8652-8a03dc187b98", # noqa: E501 - GET_PROCESS_DETAILS_JOB_STATUS_RESP) cbcsdk_mock.mock_request("GET", "/api/investigate/v2/orgs/test/processes/detail_jobs/ccc47a52-9a61-4c77-8652-8a03dc187b98/results", # noqa: E501 GET_PROCESS_DETAILS_JOB_RESULTS_RESP_ZERO) @@ -1130,9 +1159,6 @@ def test_process_get_details_async(cbcsdk_mock): """Test get_details on a process in async mode.""" cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/detail_jobs", POST_PROCESS_DETAILS_JOB_RESP) - cbcsdk_mock.mock_request("GET", - "/api/investigate/v2/orgs/test/processes/detail_jobs/ccc47a52-9a61-4c77-8652-8a03dc187b98", # noqa: E501 - GET_PROCESS_DETAILS_JOB_STATUS_RESP) cbcsdk_mock.mock_request("GET", "/api/investigate/v2/orgs/test/processes/detail_jobs/ccc47a52-9a61-4c77-8652-8a03dc187b98/results", # noqa: E501 GET_PROCESS_DETAILS_JOB_RESULTS_RESP) @@ -1152,7 +1178,7 @@ def test_process_get_details_timeout(cbcsdk_mock): cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/detail_jobs", POST_PROCESS_DETAILS_JOB_RESP) cbcsdk_mock.mock_request("GET", - "/api/investigate/v2/orgs/test/processes/detail_jobs/ccc47a52-9a61-4c77-8652-8a03dc187b98", # noqa: E501 + "/api/investigate/v2/orgs/test/processes/detail_jobs/ccc47a52-9a61-4c77-8652-8a03dc187b98/results", # noqa: E501 GET_PROCESS_DETAILS_JOB_STATUS_IN_PROGRESS_RESP) api = cbcsdk_mock.api process = Process(api, '80dab519-3b5f-4502-afad-da87cd58a4c3', @@ -1193,18 +1219,22 @@ def test_process_facet_select(cbcsdk_mock): def test_process_facets(cbcsdk_mock): """Testing Process.facets() method.""" # mock the search validation - cbcsdk_mock.mock_request("GET", "/api/investigate/v1/orgs/test/processes/search_validation", + cbcsdk_mock.mock_request("GET", + "/api/investigate/v1/orgs/test/processes/search_validation?" + "process_guid=WNEXFKQ7%5C-0002b226%5C-000015bd%5C-00000000%5C-1d6225bbba74c00" + "&q=process_guid%3AWNEXFKQ7%5C-0002b226%5C-000015bd%5C-00000000%5C-1d6225bbba74c00" + "&query=process_guid%3AWNEXFKQ7%5C-0002b226%5C-000015bd%5C-00000000%5C-1d6225bbba74c00", GET_PROCESS_VALIDATION_RESP) # mock the POST of a search cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_jobs", POST_PROCESS_SEARCH_JOB_RESP) # mock the GET to check search status - cbcsdk_mock.mock_request("GET", ("/api/investigate/v1/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920"), + cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" - "2c292717-80ed-4f0d-845f-779e09470920/results"), + "2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=500"), GET_PROCESS_SEARCH_JOB_RESULTS_RESP_1) # mock the search request cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/facet_jobs", {"job_id": "the-job-id"}) @@ -1255,28 +1285,32 @@ def test_process_facet_query_check_range(cbcsdk_mock, bucket_size, start, end, f def test_tree_select(cbcsdk_mock): """Testing Process.Tree Querying""" # mock the search validation - cbcsdk_mock.mock_request("GET", "/api/investigate/v1/orgs/test/processes/search_validation", + cbcsdk_mock.mock_request("GET", + "/api/investigate/v1/orgs/test/processes/search_validation?" + "process_guid=WNEXFKQ7%5C-0002b226%5C-000015bd%5C-00000000%5C-1d6225bbba74c00" + "&q=process_guid%3AWNEXFKQ7%5C-0002b226%5C-000015bd%5C-00000000%5C-1d6225bbba74c00" + "&query=process_guid%3AWNEXFKQ7%5C-0002b226%5C-000015bd%5C-00000000%5C-1d6225bbba74c00", GET_PROCESS_VALIDATION_RESP) # mock the POST of a search cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_jobs", POST_PROCESS_SEARCH_JOB_RESP) # mock the GET to check search status - cbcsdk_mock.mock_request("GET", ("/api/investigate/v1/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920"), + cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" - "2c292717-80ed-4f0d-845f-779e09470920/results"), + "2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=500"), GET_PROCESS_SEARCH_JOB_RESULTS_RESP_1) # mock the Tree search cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/summary_jobs", POST_TREE_SEARCH_JOB_RESP) # mock the GET to check search status cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/summary_jobs" - "/ee158f11-4dfb-4ae2-8f1a-7707b712226d"), - GET_TREE_SEARCH_JOB_RESP) + "/ee158f11-4dfb-4ae2-8f1a-7707b712226d/results?format=summary"), + GET_PROCESS_SUMMARY_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/summary_jobs/" - "ee158f11-4dfb-4ae2-8f1a-7707b712226d/results"), + "ee158f11-4dfb-4ae2-8f1a-7707b712226d/results?format=tree"), GET_PROCESS_TREE_STR) api = cbcsdk_mock.api diff --git a/src/tests/unit/platform/test_platform_query.py b/src/tests/unit/platform/test_platform_query.py index 7a4d0d0cf..82660aa49 100644 --- a/src/tests/unit/platform/test_platform_query.py +++ b/src/tests/unit/platform/test_platform_query.py @@ -48,14 +48,16 @@ def test_query_count(cbcsdk_mock, get_summary_response, get_process_search_respo """Testing Process.process_pids property.""" api = cbcsdk_mock.api # mock the GET of query parameter validation - cbcsdk_mock.mock_request("GET", "/api/investigate/v1/orgs/test/processes/search_validation", + query = f"process_guid={guid}&q=process_guid%3A{guid}&query=process_guid%3A{guid}" + cbcsdk_mock.mock_request("GET", + f"/api/investigate/v1/orgs/test/processes/search_validation?{query}", GET_PROCESS_VALIDATION_RESP) # mock the POST of a search - cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_job", + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_jobs", POST_PROCESS_SEARCH_JOB_RESP) # mock the GET to check search status - cbcsdk_mock.mock_request("GET", ("/api/investigate/v1/orgs/test/processes/search_jobs/" - "2c292717-80ed-4f0d-845f-779e09470920"), + cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" + "2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" @@ -74,16 +76,24 @@ def test_query_get_query_parameters(cbcsdk_mock, get_process_search_response, gu """Testing Query._get_query_parameters().""" api = cbcsdk_mock.api # mock the GET of query parameter validation - cbcsdk_mock.mock_request("GET", "/api/investigate/v1/orgs/test/processes/search_validation", + query = f"process_guid={guid}&q=process_guid%3A{guid}&query=process_guid%3A{guid}" + cbcsdk_mock.mock_request("GET", + f"/api/investigate/v1/orgs/test/processes/search_validation?{query}", GET_PROCESS_VALIDATION_RESP) # mock the POST of a search - cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_job", POST_PROCESS_SEARCH_JOB_RESP) + cbcsdk_mock.mock_request("POST", + "/api/investigate/v2/orgs/test/processes/search_jobs", + POST_PROCESS_SEARCH_JOB_RESP) # mock the GET to check search status - cbcsdk_mock.mock_request("GET", ("/api/investigate/v1/orgs/test/processes/search_jobs/" - "2c292717-80ed-4f0d-845f-779e09470920"), GET_PROCESS_SEARCH_JOB_RESP) + cbcsdk_mock.mock_request("GET", + "/api/investigate/v2/orgs/test/processes/search_jobs/" + "2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1", + GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results - cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" - "2c292717-80ed-4f0d-845f-779e09470920/results"), get_process_search_response) + cbcsdk_mock.mock_request("GET", + "/api/investigate/v2/orgs/test/processes/search_jobs/" + "2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=500", + get_process_search_response) process_query = api.select(Process).where(f"process_guid:{guid}") assert process_query._get_query_parameters() == {"process_guid": guid, "query": f'process_guid:{guid}'} @@ -95,16 +105,11 @@ def test_query_validate_not_valid(cbcsdk_mock, get_process_search_response, guid """Testing Query._validate().""" api = cbcsdk_mock.api # mock the GET of query parameter validation - cbcsdk_mock.mock_request("GET", "/api/investigate/v1/orgs/test/processes/search_validation", + query = f"process_guid={guid}&q=process_guid%3A{guid}&query=process_guid%3A{guid}" + cbcsdk_mock.mock_request("GET", + f"/api/investigate/v1/orgs/test/processes/search_validation?{query}", GET_PROCESS_VALIDATION_RESP_INVALID) - # mock the POST of a search - cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_job", POST_PROCESS_SEARCH_JOB_RESP) - # mock the GET to check search status - cbcsdk_mock.mock_request("GET", ("/api/investigate/v1/orgs/test/processes/search_jobs/" - "2c292717-80ed-4f0d-845f-779e09470920"), GET_PROCESS_SEARCH_JOB_RESP) - # mock the GET to get search results - cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" - "2c292717-80ed-4f0d-845f-779e09470920/results"), get_process_search_response) + process_query = api.select(Process).where(f"process_guid:{guid}") with pytest.raises(ApiError): params = process_query._get_query_parameters() @@ -218,18 +223,22 @@ def test_query_execute_async(cbcsdk_mock, get_summary_response, get_process_sear """Testing Process.process_pids property.""" api = cbcsdk_mock.api # mock the GET of query parameter validation - cbcsdk_mock.mock_request("GET", "/api/investigate/v1/orgs/test/processes/search_validation", + query = f"process_guid={guid}&q=process_guid%3A{guid}&query=process_guid%3A{guid}" + cbcsdk_mock.mock_request("GET", + f"/api/investigate/v1/orgs/test/processes/search_validation?{query}", GET_PROCESS_VALIDATION_RESP) # mock the POST of a search - cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_job", + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_jobs", POST_PROCESS_SEARCH_JOB_RESP) # mock the GET to check search status - cbcsdk_mock.mock_request("GET", ("/api/investigate/v1/orgs/test/processes/search_jobs/" - "2c292717-80ed-4f0d-845f-779e09470920"), + cbcsdk_mock.mock_request("GET", + "/api/investigate/v2/orgs/test/processes/search_jobs/" + "2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1", GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results - cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" - "2c292717-80ed-4f0d-845f-779e09470920/results"), + cbcsdk_mock.mock_request("GET", + "/api/investigate/v2/orgs/test/processes/search_jobs/" + "2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=500", get_process_search_response) process_query = api.select(Process).where(f"process_guid:{guid}") future = process_query.execute_async() diff --git a/src/tests/unit/platform/test_policies.py b/src/tests/unit/platform/test_policies.py index 1dd1d4809..ef3565b95 100644 --- a/src/tests/unit/platform/test_policies.py +++ b/src/tests/unit/platform/test_policies.py @@ -106,7 +106,7 @@ def test_policy_lookup_by_id(cbcsdk_mock): def test_policy_get_summaries(cbcsdk_mock): """Tests getting the list of policy summaries.""" - cbcsdk_mock.mock_request('GET', '/policyservice/v1/orgs/test/policies', + cbcsdk_mock.mock_request('GET', '/policyservice/v1/orgs/test/policies/summary', {"policies": [SUMMARY_POLICY_1, SUMMARY_POLICY_2, SUMMARY_POLICY_3]}) api = cbcsdk_mock.api my_list = list(api.select(Policy)) @@ -137,7 +137,7 @@ def test_policy_get_summaries_async(cbcsdk_mock): def test_policy_filter_by_id(cbcsdk_mock): """Tests filtering the policy summaries by ID.""" - cbcsdk_mock.mock_request('GET', '/policyservice/v1/orgs/test/policies', + cbcsdk_mock.mock_request('GET', '/policyservice/v1/orgs/test/policies/summary', {"policies": [SUMMARY_POLICY_1, SUMMARY_POLICY_2, SUMMARY_POLICY_3]}) api = cbcsdk_mock.api query = api.select(Policy).add_policy_ids([10191, 74656]) diff --git a/src/tests/unit/platform/test_reputation_overrides.py b/src/tests/unit/platform/test_reputation_overrides.py index 33d1925f1..556c0499c 100644 --- a/src/tests/unit/platform/test_reputation_overrides.py +++ b/src/tests/unit/platform/test_reputation_overrides.py @@ -27,8 +27,7 @@ GET_PROCESS_SEARCH_JOB_RESULTS_RESP) from tests.unit.fixtures.endpoint_standard.mock_enriched_events import (POST_ENRICHED_EVENTS_SEARCH_JOB_RESP, - GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP, - GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP_1) + GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP) log = logging.basicConfig(format='%(asctime)s %(levelname)s:%(message)s', level=logging.DEBUG, filename='log.txt') @@ -181,18 +180,22 @@ def _test_request(url, body, **kwargs): def test_reputation_override_process_ban_process_sha256(cbcsdk_mock): """Testing Reputation Override creation from process""" # mock the search validation - cbcsdk_mock.mock_request("GET", "/api/investigate/v1/orgs/test/processes/search_validation", + cbcsdk_mock.mock_request("GET", + "/api/investigate/v1/orgs/test/processes/search_validation?" + "process_guid=WNEXFKQ7%5C-0002b226%5C-000015bd%5C-00000000%5C-1d6225bbba74c00" + "&q=process_guid%3AWNEXFKQ7%5C-0002b226%5C-000015bd%5C-00000000%5C-1d6225bbba74c00" + "&query=process_guid%3AWNEXFKQ7%5C-0002b226%5C-000015bd%5C-00000000%5C-1d6225bbba74c00", GET_PROCESS_VALIDATION_RESP) # mock the POST of a search - cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_job", + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_jobs", POST_PROCESS_SEARCH_JOB_RESP) # mock the GET to check search status - cbcsdk_mock.mock_request("GET", ("/api/investigate/v1/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920"), + cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" - "2c292717-80ed-4f0d-845f-779e09470920/results"), + "2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=500"), GET_PROCESS_SEARCH_JOB_RESULTS_RESP) api = cbcsdk_mock.api @@ -222,18 +225,22 @@ def _test_request(url, body, **kwargs): def test_reputation_override_process_approve_process_sha256(cbcsdk_mock): """Testing Reputation Override creation from process""" # mock the search validation - cbcsdk_mock.mock_request("GET", "/api/investigate/v1/orgs/test/processes/search_validation", + cbcsdk_mock.mock_request("GET", + "/api/investigate/v1/orgs/test/processes/search_validation?" + "process_guid=WNEXFKQ7%5C-0002b226%5C-000015bd%5C-00000000%5C-1d6225bbba74c00" + "&q=process_guid%3AWNEXFKQ7%5C-0002b226%5C-000015bd%5C-00000000%5C-1d6225bbba74c00" + "&query=process_guid%3AWNEXFKQ7%5C-0002b226%5C-000015bd%5C-00000000%5C-1d6225bbba74c00", GET_PROCESS_VALIDATION_RESP) # mock the POST of a search - cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_job", + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_jobs", POST_PROCESS_SEARCH_JOB_RESP) # mock the GET to check search status - cbcsdk_mock.mock_request("GET", ("/api/investigate/v1/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920"), + cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" - "2c292717-80ed-4f0d-845f-779e09470920/results"), + "2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=500"), GET_PROCESS_SEARCH_JOB_RESULTS_RESP) api = cbcsdk_mock.api @@ -262,14 +269,14 @@ def _test_request(url, body, **kwargs): def test_reputation_override_enriched_event_ban_process_sha256(cbcsdk_mock): """Testing Reputation Override creation from enriched event""" - cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/enriched_events/search_job", + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/enriched_events/search_jobs", POST_ENRICHED_EVENTS_SEARCH_JOB_RESP) cbcsdk_mock.mock_request("GET", - "/api/investigate/v1/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b", # noqa: E501 + "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=1", # noqa: E501 GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP) cbcsdk_mock.mock_request("GET", - "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results", # noqa: E501 - GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP_1) + "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=500", # noqa: E501 + GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP) api = cbcsdk_mock.api @@ -297,14 +304,14 @@ def _test_request(url, body, **kwargs): def test_reputation_override_enriched_event_approve_process_sha256(cbcsdk_mock): """Testing Reputation Override creation from enriched event""" - cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/enriched_events/search_job", + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/enriched_events/search_jobs", POST_ENRICHED_EVENTS_SEARCH_JOB_RESP) cbcsdk_mock.mock_request("GET", - "/api/investigate/v1/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b", # noqa: E501 + "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=1", # noqa: E501 GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP) cbcsdk_mock.mock_request("GET", - "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results", # noqa: E501 - GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP_1) + "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=500", # noqa: E501 + GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP) api = cbcsdk_mock.api event = api.select(EnrichedEvent, "27a278d5150911eb86f1011a55e73b72") diff --git a/src/tests/unit/platform/test_vulnerability_assessment.py b/src/tests/unit/platform/test_vulnerability_assessment.py index 9d3c7465d..1151b733e 100644 --- a/src/tests/unit/platform/test_vulnerability_assessment.py +++ b/src/tests/unit/platform/test_vulnerability_assessment.py @@ -69,7 +69,8 @@ def test_get_vulnerability_summary(cbcsdk_mock): def test_get_vulnerability_summary_per_severity(cbcsdk_mock): """Tests get organizational level vulnerability summary per severity""" - cbcsdk_mock.mock_request("GET", "/vulnerability/assessment/api/v1/orgs/test/vulnerabilities/summary", + cbcsdk_mock.mock_request("GET", + "/vulnerability/assessment/api/v1/orgs/test/vulnerabilities/summary?severity=CRITICAL", GET_VULNERABILITY_SUMMARY_ORG_LEVEL_PER_SEVERITY) api = cbcsdk_mock.api vsummary = api.select(Vulnerability.OrgSummary).set_severity('CRITICAL').submit() @@ -94,7 +95,8 @@ def test_get_vulnerability_summary_per_severity_fail(cbcsdk_mock): def test_get_vulnerability_summary_per_severity_per_vcenter(cbcsdk_mock): """Tests get organizational level vulnerability summary per severity""" cbcsdk_mock.mock_request("GET", - "/vulnerability/assessment/api/v1/orgs/test/vcenters/someid/vulnerabilities/summary", + "/vulnerability/assessment/api/v1/orgs/test/vcenters/someid/vulnerabilities/summary" + "?severity=CRITICAL", GET_VULNERABILITY_SUMMARY_ORG_LEVEL_PER_SEVERITY) api = cbcsdk_mock.api vsummary = api.select(Vulnerability.OrgSummary).set_severity('CRITICAL').set_vcenter('someid').submit() @@ -114,6 +116,10 @@ def test_get_asset_view_with_vulnerability_summary(cbcsdk_mock): cbcsdk_mock.mock_request("POST", "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/summary/_search", GET_ASSET_VIEW_VUL_RESP) + cbcsdk_mock.mock_request("POST", + "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/summary/_search" + "?dataForExport=true", + GET_ASSET_VIEW_VUL_RESP) api = cbcsdk_mock.api query = api.select(Vulnerability.AssetView) @@ -125,7 +131,7 @@ def test_get_asset_view_with_vulnerability_summary(cbcsdk_mock): def test_get_asset_view_with_vulnerability_summary_and_vcenter_async(cbcsdk_mock): """Test Get Asset View with Vulnerability Summary""" cbcsdk_mock.mock_request("POST", - "/vulnerability/assessment/api/v1/orgs/test/vcenters/testvcenter/devices/vulnerabilities/summary/_search", # noqa: E501 + "/vulnerability/assessment/api/v1/orgs/test/vcenters/testvcenter/devices/vulnerabilities/summary/_search?dataForExport=true", # noqa: E501 GET_ASSET_VIEW_VUL_RESP) api = cbcsdk_mock.api query_future = api.select(Vulnerability.AssetView).set_vcenter("testvcenter").execute_async() @@ -138,7 +144,9 @@ def test_get_asset_view_with_vulnerability_summary_and_vcenter_async(cbcsdk_mock def test_get_all_vulnerabilities(cbcsdk_mock): """Test Get Asset View with Vulnerability Summary""" - cbcsdk_mock.mock_request("POST", "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search", + cbcsdk_mock.mock_request("POST", + "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search" + "?dataForExport=true", GET_VULNERABILITY_RESP_MULTIPLE) api = cbcsdk_mock.api query = api.select(Vulnerability) @@ -149,7 +157,12 @@ def test_get_all_vulnerabilities(cbcsdk_mock): def test_get_vulnerability_by_id(cbcsdk_mock): """Tests a get vulnerabilty by cve_id.""" - cbcsdk_mock.mock_request("POST", "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search", + cbcsdk_mock.mock_request("POST", + "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search", + GET_VULNERABILITY_RESP) + cbcsdk_mock.mock_request("POST", + "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search" + "?dataForExport=true", GET_VULNERABILITY_RESP) api = cbcsdk_mock.api vulnerability = Vulnerability(api, "CVE-2014-4650") @@ -161,7 +174,12 @@ def test_get_vulnerability_by_id(cbcsdk_mock): def test_get_vulnerability_by_id_multiple(cbcsdk_mock): """Tests a get vulnerabilty by cve_id where cve affects multiple OS/Products""" - cbcsdk_mock.mock_request("POST", "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search", + cbcsdk_mock.mock_request("POST", + "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search", + GET_VULNERABILITY_RESP_MULTIPLE_SAME_CVE) + cbcsdk_mock.mock_request("POST", + "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search" + "?dataForExport=true", GET_VULNERABILITY_RESP_MULTIPLE_SAME_CVE) api = cbcsdk_mock.api vulnerability = Vulnerability(api, "CVE-2014-4650", os_product_id="89_1234") @@ -175,7 +193,12 @@ def test_get_vulnerability_by_id_multiple(cbcsdk_mock): def test_get_vulnerability_not_found(cbcsdk_mock): """Test Get Asset View with Vulnerability Summary""" - cbcsdk_mock.mock_request("POST", "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search", + cbcsdk_mock.mock_request("POST", + "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search", + {"num_found": 0, "results": []}) + cbcsdk_mock.mock_request("POST", + "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search" + "?dataForExport=true", {"num_found": 0, "results": []}) api = cbcsdk_mock.api with pytest.raises(ObjectNotFoundError) as ex: @@ -185,7 +208,18 @@ def test_get_vulnerability_not_found(cbcsdk_mock): def test_get_vulnerability_more_than_one(cbcsdk_mock): """Test Get Asset View with Vulnerability Summary""" - cbcsdk_mock.mock_request("POST", "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search", + cbcsdk_mock.mock_request("POST", + "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search", + { + "num_found": 2, + "results": [ + {"id": 1, "os_product_id": "a_1"}, + {"id": 2, "os_product_id": "a_2"} + ] + }) + cbcsdk_mock.mock_request("POST", + "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search" + "?dataForExport=true", { "num_found": 2, "results": [ @@ -205,7 +239,7 @@ def test_get_vulnerability_more_than_one(cbcsdk_mock): def test_get_vulnerability_per_vcenter(cbcsdk_mock): """Test Get Asset View with Vulnerability Summary""" cbcsdk_mock.mock_request("POST", "/vulnerability/assessment/api/v1/orgs/test/vcenters/testvcenter/devices/" - "vulnerabilities/_search", GET_VULNERABILITY_RESP_MULTIPLE) + "vulnerabilities/_search?dataForExport=true", GET_VULNERABILITY_RESP_MULTIPLE) api = cbcsdk_mock.api query = api.select(Vulnerability).set_vcenter('testvcenter') results = [result for result in query._perform_query()] @@ -235,7 +269,9 @@ def post_validate(url, body, **kwargs): assert crits['vuln_count'] == {"value": 30, "operator": "EQUALS"} return GET_VULNERABILITY_RESP_MULTIPLE - cbcsdk_mock.mock_request("POST", "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search", + cbcsdk_mock.mock_request("POST", + "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search" + "?dataForExport=true", post_validate) api = cbcsdk_mock.api query = api.select(Vulnerability).set_device_type('WORKLOAD', 'EQUALS') \ @@ -266,7 +302,9 @@ def test_vuln_query_with_all_bells_and_whistles_failures(cbcsdk_mock): def post_validate(url, body, **kwargs): crits = body['criteria'] assert crits is None - cbcsdk_mock.mock_request("POST", "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search", + cbcsdk_mock.mock_request("POST", + "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search" + "?dataForExport=true", post_validate) api = cbcsdk_mock.api @@ -335,6 +373,10 @@ def post_validate(url, body, **kwargs): cbcsdk_mock.mock_request("POST", "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search", GET_VULNERABILITY_RESP) + cbcsdk_mock.mock_request("POST", + "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search" + "?dataForExport=true", + GET_VULNERABILITY_RESP) cbcsdk_mock.mock_request("POST", "/vulnerability/assessment/api/v1/orgs/test/vulnerabilities/CVE-2014-4650/devices", post_validate) @@ -357,6 +399,10 @@ def post_validate(url, body, **kwargs): cbcsdk_mock.mock_request("POST", "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search", GET_VULNERABILITY_RESP) + cbcsdk_mock.mock_request("POST", + "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search" + "?dataForExport=true", + GET_VULNERABILITY_RESP) cbcsdk_mock.mock_request("POST", "/vulnerability/assessment/api/v1/orgs/test/vcenters/testvcenter/vulnerabilities/CVE-2014-4650/devices", # noqa: E501 post_validate) @@ -386,7 +432,9 @@ def test_device_vulnerability_summary_get(cbcsdk_mock): def test_device_vulnerability_summary_get_category(cbcsdk_mock): """Test Get an Operating System or Application Vulnerability Summary for a specific device""" - cbcsdk_mock.mock_request("GET", "/vulnerability/assessment/api/v1/orgs/test/devices/98765/vulnerabilities/summary", + cbcsdk_mock.mock_request("GET", + "/vulnerability/assessment/api/v1/orgs/test/devices/98765/vulnerabilities/summary" + "?category=OS", GET_DEVICE_VULNERABILITY_SUMMARY_RESP) cbcsdk_mock.mock_request("GET", "/appservices/v6/orgs/test/devices/98765", GET_DEVICE_RESP) api = cbcsdk_mock.api @@ -413,6 +461,10 @@ def test_device_vulnerability_search(cbcsdk_mock): cbcsdk_mock.mock_request("POST", "/vulnerability/assessment/api/v1/orgs/test/devices/98765/vulnerabilities/_search", GET_VULNERABILITY_RESP_MULTIPLE) + cbcsdk_mock.mock_request("POST", + "/vulnerability/assessment/api/v1/orgs/test/devices/98765/vulnerabilities/_search" + "?dataForExport=true", + GET_VULNERABILITY_RESP_MULTIPLE) api = cbcsdk_mock.api device = api.select(Device, 98765) query = device.get_vulnerabilties() diff --git a/src/tests/unit/test_helpers.py b/src/tests/unit/test_helpers.py index 143a11a4d..ad2b4c004 100755 --- a/src/tests/unit/test_helpers.py +++ b/src/tests/unit/test_helpers.py @@ -152,7 +152,13 @@ def post_validate(*args): return {"valid": True} return {"valid": False} - cbcsdk_mock.mock_request("GET", "/api/investigate/v1/orgs/test/processes/search_validation", post_validate) + cbcsdk_mock.mock_request("GET", + "/api/investigate/v1/orgs/test/processes/search_validation?q=process_name%3Achrome.exe", + post_validate) + cbcsdk_mock.mock_request("GET", + "/api/investigate/v1/orgs/test/processes/search_validation?q=invalid", + post_validate) + api = cbcsdk_mock.api sha256 = '8005557c1614c1e2c89f7db3702199de2b1e4605718fa32ff6ffdb2b41ed3759' md5 = 'f586835082f632dc8d9404d83bc16316' diff --git a/src/tests/unit/test_live_response_api.py b/src/tests/unit/test_live_response_api.py index b96afe09d..487af4f03 100755 --- a/src/tests/unit/test_live_response_api.py +++ b/src/tests/unit/test_live_response_api.py @@ -1237,7 +1237,7 @@ def test_registry_unsupported_command(cbcsdk_mock): cbcsdk_mock.mock_request('POST', '/appservices/v6/orgs/test/liveresponse/sessions', USESSION_INIT_RESP) cbcsdk_mock.mock_request('GET', '/appservices/v6/orgs/test/liveresponse/sessions/1:7777', USESSION_POLL_RESP) cbcsdk_mock.mock_request('GET', '/appservices/v6/orgs/test/devices/7777', UDEVICE_RESPONSE) - cbcsdk_mock.mock_request('DELETE', '/appservices/v6/orgs/test/liveresponse/sessions', None) + cbcsdk_mock.mock_request('DELETE', '/appservices/v6/orgs/test/liveresponse/sessions/1:7777', None) manager = LiveResponseSessionManager(cbcsdk_mock.api) with manager.request_session(7777) as session: with pytest.raises(ApiError) as excinfo: diff --git a/src/tests/unit/test_rest_api.py b/src/tests/unit/test_rest_api.py index 2a0e70251..69378fc5e 100644 --- a/src/tests/unit/test_rest_api.py +++ b/src/tests/unit/test_rest_api.py @@ -61,7 +61,7 @@ def test_get_auditlogs(cbcsdk_mock): def test_alert_search_suggestions(cbcsdk_mock): """Tests getting alert search suggestions""" api = cbcsdk_mock.api - cbcsdk_mock.mock_request("GET", "/appservices/v6/orgs/test/alerts/search_suggestions", + cbcsdk_mock.mock_request("GET", "/appservices/v6/orgs/test/alerts/search_suggestions?suggest.q=", ALERT_SEARCH_SUGGESTIONS_RESP) result = api.alert_search_suggestions('') assert len(result) == 20 @@ -70,7 +70,7 @@ def test_alert_search_suggestions(cbcsdk_mock): def test_process_search_validations(cbcsdk_mock): """Tests getting process search validations""" api = cbcsdk_mock.api - cbcsdk_mock.mock_request("GET", "/api/investigate/v1/orgs/test/processes/search_validation", + cbcsdk_mock.mock_request("GET", "/api/investigate/v1/orgs/test/processes/search_validation?q=process", PROCESS_SEARCH_VALIDATIONS_RESP) result = api.validate_process_query('process') assert result From 7a888a3967d74f6c54bccfeb603a37fc56dfcfa9 Mon Sep 17 00:00:00 2001 From: Alex Van Brunt Date: Fri, 13 Jan 2023 15:25:12 -0600 Subject: [PATCH 012/143] Change to rows 0 --- src/cbc_sdk/endpoint_standard/base.py | 2 +- src/cbc_sdk/platform/processes.py | 2 +- src/tests/unit/base/test_base_models.py | 2 +- .../test_endpoint_standard_enriched_events.py | 24 ++++++------ src/tests/unit/platform/test_alertsv6_api.py | 8 ++-- .../test_platform_dynamic_reference.py | 4 +- .../unit/platform/test_platform_events.py | 2 +- .../unit/platform/test_platform_process.py | 38 +++++++++---------- .../unit/platform/test_platform_query.py | 6 +-- .../platform/test_reputation_overrides.py | 8 ++-- 10 files changed, 48 insertions(+), 48 deletions(-) diff --git a/src/cbc_sdk/endpoint_standard/base.py b/src/cbc_sdk/endpoint_standard/base.py index 097a2e853..33b28457a 100644 --- a/src/cbc_sdk/endpoint_standard/base.py +++ b/src/cbc_sdk/endpoint_standard/base.py @@ -418,7 +418,7 @@ def _still_querying(self): if self._aggregation: return False - status_url = "/api/investigate/v2/orgs/{}/enriched_events/search_jobs/{}/results?start=0&rows=1".format( + status_url = "/api/investigate/v2/orgs/{}/enriched_events/search_jobs/{}/results?start=0&rows=0".format( self._cb.credentials.org_key, self._query_token, ) diff --git a/src/cbc_sdk/platform/processes.py b/src/cbc_sdk/platform/processes.py index a6f544b98..bb02583e6 100644 --- a/src/cbc_sdk/platform/processes.py +++ b/src/cbc_sdk/platform/processes.py @@ -658,7 +658,7 @@ def _still_querying(self): if not self._query_token: self._submit() - status_url = "/api/investigate/v2/orgs/{}/processes/search_jobs/{}/results?start=0&rows=1".format( + status_url = "/api/investigate/v2/orgs/{}/processes/search_jobs/{}/results?start=0&rows=0".format( self._cb.credentials.org_key, self._query_token, ) diff --git a/src/tests/unit/base/test_base_models.py b/src/tests/unit/base/test_base_models.py index 9e9291a05..872b4f27d 100644 --- a/src/tests/unit/base/test_base_models.py +++ b/src/tests/unit/base/test_base_models.py @@ -396,7 +396,7 @@ def test_print_unrefreshablemodel(cbcsdk_mock): POST_PROCESS_SEARCH_JOB_RESP) # mock the GET to check search status cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1"), + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=0"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" diff --git a/src/tests/unit/endpoint_standard/test_endpoint_standard_enriched_events.py b/src/tests/unit/endpoint_standard/test_endpoint_standard_enriched_events.py index 18a4df645..e1634aaf8 100644 --- a/src/tests/unit/endpoint_standard/test_endpoint_standard_enriched_events.py +++ b/src/tests/unit/endpoint_standard/test_endpoint_standard_enriched_events.py @@ -44,7 +44,7 @@ def test_enriched_event_select_where(cbcsdk_mock): cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/enriched_events/search_jobs", POST_ENRICHED_EVENTS_SEARCH_JOB_RESP) cbcsdk_mock.mock_request("GET", - "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=1", # noqa: E501 + "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=0", # noqa: E501 GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP) cbcsdk_mock.mock_request("GET", "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=500", # noqa: E501 @@ -62,7 +62,7 @@ def test_enriched_event_select_async(cbcsdk_mock): cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/enriched_events/search_jobs", POST_ENRICHED_EVENTS_SEARCH_JOB_RESP) cbcsdk_mock.mock_request("GET", - "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=1", # noqa: E501 + "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=0", # noqa: E501 GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP) cbcsdk_mock.mock_request("GET", "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=500", # noqa: E501 @@ -80,7 +80,7 @@ def test_enriched_event_select_details_async(cbcsdk_mock): cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/enriched_events/search_jobs", POST_ENRICHED_EVENTS_SEARCH_JOB_RESP) cbcsdk_mock.mock_request("GET", - "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=1", # noqa: E501 + "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=0", # noqa: E501 GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP) cbcsdk_mock.mock_request("GET", "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=500", # noqa: E501 @@ -141,7 +141,7 @@ def test_enriched_event_select_details_sync(cbcsdk_mock): cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/enriched_events/search_jobs", POST_ENRICHED_EVENTS_SEARCH_JOB_RESP) cbcsdk_mock.mock_request("GET", - "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=1", # noqa: E501 + "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=0", # noqa: E501 GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP) cbcsdk_mock.mock_request("GET", "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=500", # noqa: E501 @@ -167,7 +167,7 @@ def test_enriched_event_select_details_sync_zero(cbcsdk_mock): cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/enriched_events/search_jobs", POST_ENRICHED_EVENTS_SEARCH_JOB_RESP) cbcsdk_mock.mock_request("GET", - "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=1", # noqa: E501 + "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=0", # noqa: E501 GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP) cbcsdk_mock.mock_request("GET", "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=500", # noqa: E501 @@ -191,7 +191,7 @@ def test_enriched_event_select_compound(cbcsdk_mock): cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/enriched_events/search_jobs", POST_ENRICHED_EVENTS_SEARCH_JOB_RESP) cbcsdk_mock.mock_request("GET", - "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=1", # noqa: E501 + "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=0", # noqa: E501 GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP) cbcsdk_mock.mock_request("GET", "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=500", # noqa: E501 @@ -258,7 +258,7 @@ def test_enriched_event_query_implementation(cbcsdk_mock): cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/enriched_events/search_jobs", POST_ENRICHED_EVENTS_SEARCH_JOB_RESP) cbcsdk_mock.mock_request("GET", - "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=1", # noqa: E501 + "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=0", # noqa: E501 GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP) cbcsdk_mock.mock_request("GET", "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=500", # noqa: E501 @@ -284,7 +284,7 @@ def test_enriched_event_timeout_error(cbcsdk_mock): cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/enriched_events/search_jobs", POST_ENRICHED_EVENTS_SEARCH_JOB_RESP) cbcsdk_mock.mock_request("GET", - "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=1", # noqa: E501 + "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=0", # noqa: E501 GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING) api = cbcsdk_mock.api @@ -346,7 +346,7 @@ def test_enriched_events_count(cbcsdk_mock): cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/enriched_events/search_jobs", POST_ENRICHED_EVENTS_SEARCH_JOB_RESP) cbcsdk_mock.mock_request("GET", - "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=1", # noqa: E501 + "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=0", # noqa: E501 GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP_2) cbcsdk_mock.mock_request("GET", "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results", # noqa: E501 @@ -363,7 +363,7 @@ def test_enriched_events_search(cbcsdk_mock): cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/enriched_events/search_jobs", POST_ENRICHED_EVENTS_SEARCH_JOB_RESP) cbcsdk_mock.mock_request("GET", - "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=1", # noqa: E501 + "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=0", # noqa: E501 GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP_2) cbcsdk_mock.mock_request("GET", "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=500", # noqa: E501 @@ -382,7 +382,7 @@ def test_enriched_events_still_querying(cbcsdk_mock): cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/enriched_events/search_jobs", POST_ENRICHED_EVENTS_SEARCH_JOB_RESP) cbcsdk_mock.mock_request("GET", - "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=1", # noqa: E501 + "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=0", # noqa: E501 GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP_0) cbcsdk_mock.mock_request("GET", "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=500", # noqa: E501 @@ -398,7 +398,7 @@ def test_enriched_events_still_querying2(cbcsdk_mock): cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/enriched_events/search_jobs", POST_ENRICHED_EVENTS_SEARCH_JOB_RESP) cbcsdk_mock.mock_request("GET", - "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=1", # noqa: E501 + "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=0", # noqa: E501 GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP_ZERO_COMP) cbcsdk_mock.mock_request("GET", "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=500", # noqa: E501 diff --git a/src/tests/unit/platform/test_alertsv6_api.py b/src/tests/unit/platform/test_alertsv6_api.py index 83acc81f3..0b362858c 100755 --- a/src/tests/unit/platform/test_alertsv6_api.py +++ b/src/tests/unit/platform/test_alertsv6_api.py @@ -690,7 +690,7 @@ def test_get_process(cbcsdk_mock): POST_PROCESS_SEARCH_JOB_RESP) # mock the GET to check search status cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1"), + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=0"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" @@ -727,7 +727,7 @@ def test_get_process_zero_found(cbcsdk_mock): POST_PROCESS_SEARCH_JOB_RESP) # mock the GET to get process search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1"), + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=0"), GET_PROCESS_NOT_FOUND) # mock the GET to get process search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" @@ -756,7 +756,7 @@ def test_get_process_raises_api_error(cbcsdk_mock): POST_PROCESS_SEARCH_JOB_RESP) # mock the GET to check search status cbcsdk_mock.mock_request("GET", ("/api/investigate/v1/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1"), + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=0"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" @@ -785,7 +785,7 @@ def test_get_process_async(cbcsdk_mock): POST_PROCESS_SEARCH_JOB_RESP) # mock the GET to check search status cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1"), + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=0"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" diff --git a/src/tests/unit/platform/test_platform_dynamic_reference.py b/src/tests/unit/platform/test_platform_dynamic_reference.py index e8cf69822..d4a2d796d 100644 --- a/src/tests/unit/platform/test_platform_dynamic_reference.py +++ b/src/tests/unit/platform/test_platform_dynamic_reference.py @@ -153,7 +153,7 @@ def test_Process_select(self, cbcsdk_mock): "GET", ( "/api/investigate/v2/orgs/Z100/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1" + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=0" ), GET_PROCESS_SEARCH_JOB_RESP, ) @@ -226,7 +226,7 @@ def test_Process_Tree_select(self, cbcsdk_mock): "GET", ( "/api/investigate/v1/orgs/Z100/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1" + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=0" ), GET_PROCESS_SEARCH_JOB_RESP, ) diff --git a/src/tests/unit/platform/test_platform_events.py b/src/tests/unit/platform/test_platform_events.py index 11be01aee..6901aa9e9 100644 --- a/src/tests/unit/platform/test_platform_events.py +++ b/src/tests/unit/platform/test_platform_events.py @@ -53,7 +53,7 @@ def test_event_query_process_select_with_guid(cbcsdk_mock): POST_PROCESS_SEARCH_JOB_RESP) # mock the GET to check search status cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1"), + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=0"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" diff --git a/src/tests/unit/platform/test_platform_process.py b/src/tests/unit/platform/test_platform_process.py index 25d502e4f..4f4cde0ed 100644 --- a/src/tests/unit/platform/test_platform_process.py +++ b/src/tests/unit/platform/test_platform_process.py @@ -74,7 +74,7 @@ def test_process_select(cbcsdk_mock): POST_PROCESS_SEARCH_JOB_RESP) # mock the GET to check search status cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1"), + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=0"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" @@ -282,7 +282,7 @@ def test_process_events(cbcsdk_mock): POST_PROCESS_SEARCH_JOB_RESP) # mock the GET to check search status cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1"), + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=0"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" @@ -320,7 +320,7 @@ def test_process_events_with_criteria_exclusions(cbcsdk_mock): POST_PROCESS_SEARCH_JOB_RESP) # mock the GET to check search status cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1"), + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=0"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" @@ -371,7 +371,7 @@ def test_process_events_exceptions(cbcsdk_mock): POST_PROCESS_SEARCH_JOB_RESP) # mock the GET to check search status cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1"), + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=0"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" @@ -408,7 +408,7 @@ def test_process_with_criteria_exclusions(cbcsdk_mock): POST_PROCESS_SEARCH_JOB_RESP) # mock the GET to check search status cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1"), + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=0"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" @@ -554,7 +554,7 @@ def test_process_events_query_with_criteria_exclusions(cbcsdk_mock): POST_PROCESS_SEARCH_JOB_RESP) # mock the GET to check search status cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1"), + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=0"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" @@ -608,7 +608,7 @@ def test_process_events_raise_exceptions(cbcsdk_mock): POST_PROCESS_SEARCH_JOB_RESP) # mock the GET to check search status cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1"), + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=0"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" @@ -644,7 +644,7 @@ def test_process_query_with_criteria_exclusions(cbcsdk_mock): POST_PROCESS_SEARCH_JOB_RESP) # mock the GET to check search status cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1"), + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=0"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" @@ -783,7 +783,7 @@ def test_process_parents(cbcsdk_mock, get_summary_response, guid, process_search POST_PROCESS_SEARCH_JOB_RESP) # mock the GET to check search status cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1"), + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=0"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" @@ -846,7 +846,7 @@ def test_process_children(cbcsdk_mock, get_summary_response, guid, expected_num_ POST_PROCESS_SEARCH_JOB_RESP) # mock the GET to check process search status cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1"), + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=0"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get process search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" @@ -892,7 +892,7 @@ def test_process_md5(cbcsdk_mock, get_process_search_response, get_summary_respo POST_PROCESS_SEARCH_JOB_RESP) # mock the GET to check process search status cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1"), + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=0"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get process search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" @@ -933,7 +933,7 @@ def test_process_md5_not_found(cbcsdk_mock): POST_PROCESS_SEARCH_JOB_RESP) # mock the GET to check process search status cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1"), + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=0"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get process search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" @@ -980,7 +980,7 @@ def test_process_sha256(cbcsdk_mock, get_process_response, get_summary_response, POST_PROCESS_SEARCH_JOB_RESP) # mock the GET to check process search status cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1"), + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=0"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get process search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" @@ -1028,7 +1028,7 @@ def test_process_pids(cbcsdk_mock, get_process_response, get_summary_response, g POST_PROCESS_SEARCH_JOB_RESP) # mock the GET to check process search status cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1"), + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=0"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get process search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" @@ -1062,7 +1062,7 @@ def test_process_select_where(cbcsdk_mock): POST_PROCESS_SEARCH_JOB_RESP) # mock the GET to check search status cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1"), + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=0"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" @@ -1090,7 +1090,7 @@ def test_process_still_querying(cbcsdk_mock): GET_PROCESS_VALIDATION_RESP) # mock the GET to check search status cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1"), + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=0"), GET_PROCESS_SEARCH_JOB_RESULTS_RESP_ZERO) api = cbcsdk_mock.api guid = 'WNEXFKQ7-0002b226-000015bd-00000000-1d6225bbba74c00' @@ -1113,7 +1113,7 @@ def test_process_still_querying_zero(cbcsdk_mock): GET_PROCESS_VALIDATION_RESP) # mock the GET to check search status cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1"), + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=0"), GET_PROCESS_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING) api = cbcsdk_mock.api guid = 'WNEXFKQ7-0002b226-000015bd-00000000-1d6225bbba74c00' @@ -1230,7 +1230,7 @@ def test_process_facets(cbcsdk_mock): POST_PROCESS_SEARCH_JOB_RESP) # mock the GET to check search status cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1"), + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=0"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" @@ -1296,7 +1296,7 @@ def test_tree_select(cbcsdk_mock): POST_PROCESS_SEARCH_JOB_RESP) # mock the GET to check search status cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1"), + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=0"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" diff --git a/src/tests/unit/platform/test_platform_query.py b/src/tests/unit/platform/test_platform_query.py index 82660aa49..93a5d0cf6 100644 --- a/src/tests/unit/platform/test_platform_query.py +++ b/src/tests/unit/platform/test_platform_query.py @@ -57,7 +57,7 @@ def test_query_count(cbcsdk_mock, get_summary_response, get_process_search_respo POST_PROCESS_SEARCH_JOB_RESP) # mock the GET to check search status cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" - "2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1"), + "2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=0"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" @@ -87,7 +87,7 @@ def test_query_get_query_parameters(cbcsdk_mock, get_process_search_response, gu # mock the GET to check search status cbcsdk_mock.mock_request("GET", "/api/investigate/v2/orgs/test/processes/search_jobs/" - "2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1", + "2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=0", GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", @@ -233,7 +233,7 @@ def test_query_execute_async(cbcsdk_mock, get_summary_response, get_process_sear # mock the GET to check search status cbcsdk_mock.mock_request("GET", "/api/investigate/v2/orgs/test/processes/search_jobs/" - "2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1", + "2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=0", GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", diff --git a/src/tests/unit/platform/test_reputation_overrides.py b/src/tests/unit/platform/test_reputation_overrides.py index 556c0499c..a6359fa2b 100644 --- a/src/tests/unit/platform/test_reputation_overrides.py +++ b/src/tests/unit/platform/test_reputation_overrides.py @@ -191,7 +191,7 @@ def test_reputation_override_process_ban_process_sha256(cbcsdk_mock): POST_PROCESS_SEARCH_JOB_RESP) # mock the GET to check search status cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1"), + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=0"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" @@ -236,7 +236,7 @@ def test_reputation_override_process_approve_process_sha256(cbcsdk_mock): POST_PROCESS_SEARCH_JOB_RESP) # mock the GET to check search status cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" - "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=1"), + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=0"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" @@ -272,7 +272,7 @@ def test_reputation_override_enriched_event_ban_process_sha256(cbcsdk_mock): cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/enriched_events/search_jobs", POST_ENRICHED_EVENTS_SEARCH_JOB_RESP) cbcsdk_mock.mock_request("GET", - "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=1", # noqa: E501 + "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=0", # noqa: E501 GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP) cbcsdk_mock.mock_request("GET", "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=500", # noqa: E501 @@ -307,7 +307,7 @@ def test_reputation_override_enriched_event_approve_process_sha256(cbcsdk_mock): cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/enriched_events/search_jobs", POST_ENRICHED_EVENTS_SEARCH_JOB_RESP) cbcsdk_mock.mock_request("GET", - "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=1", # noqa: E501 + "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=0", # noqa: E501 GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP) cbcsdk_mock.mock_request("GET", "/api/investigate/v2/orgs/test/enriched_events/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=500", # noqa: E501 From e0881d8e155326fb08f0119b212be149ccc0a6fb Mon Sep 17 00:00:00 2001 From: Kylie Ebringer <55465092+kebringer-cb@users.noreply.github.com> Date: Mon, 30 Jan 2023 21:02:49 -0700 Subject: [PATCH 013/143] RTD fixes Jan 2023 (#326) * Fixing formatting --- docs/changelog.rst | 2 +- docs/developing-credential-providers.rst | 4 +- docs/differential-analysis.rst | 2 + src/cbc_sdk/audit_remediation/base.py | 71 +++++++++++-------- src/cbc_sdk/audit_remediation/differential.py | 4 +- .../endpoint_standard/usb_device_control.py | 1 + src/cbc_sdk/live_response_api.py | 2 + src/cbc_sdk/platform/processes.py | 11 ++- 8 files changed, 61 insertions(+), 36 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 72bd7ceff..62841736b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -77,7 +77,7 @@ New Features: Updates: -* Endpoint Standard specific ``Event``s have been decommissioned and removed. +* Endpoint Standard specific ``Event`` s have been decommissioned and removed. * SDK now uses Watchlist Manager apis ``v3`` instead of ``v2``. ``v2`` APIs are being decommissioned. Documentation: diff --git a/docs/developing-credential-providers.rst b/docs/developing-credential-providers.rst index 42d17af7d..45a9b9c63 100644 --- a/docs/developing-credential-providers.rst +++ b/docs/developing-credential-providers.rst @@ -17,7 +17,9 @@ to initialize your credential provider in any desired fashion. Using the Credential Provider ----------------------------- Create an instance of your credential provider object and pass it as the keyword parameter -``credential_provider`` when creating your ``CBCloudAPI`` object. Example: +``credential_provider`` when creating your ``CBCloudAPI`` object. + +Example: >>> provider = MyCredentialProvider() >>> cbc_api = CBCloudAPI(credential_provider=provider, profile='default') diff --git a/docs/differential-analysis.rst b/docs/differential-analysis.rst index 5c71bad62..744103aaf 100644 --- a/docs/differential-analysis.rst +++ b/docs/differential-analysis.rst @@ -34,6 +34,8 @@ This example shows the basic result of the ``Differential`` object. The ``.newer run id that you want to mark as the starting point-in-time snapshot. By default, only the number of changes between the two runs are returned. To receive the actual differential data, use the ``.count_only()`` method, as featured in the Actual Changes example. +.. code-block:: python + >>> from cbc_sdk import CBCloudAPI >>> from cbc_sdk.audit_remediation import Differential >>> diff --git a/src/cbc_sdk/audit_remediation/base.py b/src/cbc_sdk/audit_remediation/base.py index 2a0308c22..882c7656f 100644 --- a/src/cbc_sdk/audit_remediation/base.py +++ b/src/cbc_sdk/audit_remediation/base.py @@ -42,6 +42,7 @@ class Run(NewBaseModel): >>> print(run.status, run.match_count) >>> run.refresh() """ + primary_key = "id" swagger_meta_file = "audit_remediation/models/run.yaml" urlobject = "/livequery/v1/orgs/{}/runs" @@ -675,49 +676,57 @@ def schedule(self, rrule, timezone): is created with a schedule then the Run will contain a template_id to the corresponding template and a new Run will be created each time the schedule is met. - Example RRule: + Example RRule, Daily + + .. csv-table:: + :header: "Field", "Values" + :widths: 20, 20 + + "BYSECOND","0" + "BYMINUTE", "0 or 30" + "BYHOUR", "0 to 23" + + Daily at 1:30PM - DAILY + `RRULE:FREQ=DAILY;BYHOUR=13;BYMINUTE=30;BYSECOND=0` - | Field | Values | - | -------- | ------- | - | BYSECOND | 0 | - | BYMINUTE | 0 or 30 | - | BYHOUR | 0 to 23 | + Example RRule, Weekly - # Daily at 1:30PM - RRULE:FREQ=DAILY;BYHOUR=13;BYMINUTE=30;BYSECOND=0 + .. csv-table:: + :header: "Field", "Values" + :widths: 20, 20 - WEEKLY + "BYSECOND", "0" + "BYMINUTE", "0" + "BYHOUR", "0 to 23" + "BYDAY", "One or more: SU, MO, TU, WE, TH, FR, SA" - | Field | Values | - | -------- | --------------------------------------- | - | BYSECOND | 0 | - | BYMINUTE | 0 or 30 | - | BYHOUR | 0 to 23 | - | BYDAY | One or more: SU, MO, TU, WE, TH, FR, SA | + Monday and Friday of the week at 2:30 AM - # Monday and Friday of the week at 2:30 AM - RRULE:FREQ=WEEKLY;BYDAY=MO,FR;BYHOUR=13;BYMINUTE=30;BYSECOND=0 + `RRULE:FREQ=WEEKLY;BYDAY=MO,FR;BYHOUR=13;BYMINUTE=30;BYSECOND=0` - MONTHLY + Example RRule, Monthly Note: Either (BYDAY and BYSETPOS) or BYMONTHDAY is required. - | Field | Values | - | ---------- | --------------------------------------- | - | BYSECOND | 0 | - | BYMINUTE | 0 or 30 | - | BYHOUR | 0 to 23 | - | BYDAY | One or more: SU, MO, TU, WE, TH, FR, SA | - | BYSETPOS | -1, 1, 2, 3, 4 | - | BYMONTHDAY | One or more: 1 to 28 | + .. csv-table:: + :header: "Field", "Values" + :widths: 20, 20 + + "BYSECOND", "0" + "BYMINUTE", "0 or 30" + "BYHOUR", "0 to 23" + "BYDAY", "One or more: SU, MO, TU, WE, TH, FR, SA" + "BYSETPOS", "-1, 1, 2, 3, 4" + "BYMONTHDAY", "One or more: 1 to 28" + + Last Monday of the Month at 2:30 AM + + `RRULE:FREQ=MONTHLY;BYDAY=MO;BYSETPOS=-1;BYHOUR=2;BYMINUTE=30;BYSECOND=0` - # Last Monday of the Month at 2:30 AM - RRULE:FREQ=MONTHLY;BYDAY=MO;BYSETPOS=-1;BYHOUR=2;BYMINUTE=30;BYSECOND=0 + 1st and 15th of the Month at 2:30 AM - # 1st and 15th of the Month at 2:30 AM - RRULE:FREQ=DAILY;BYMONTHDAY=1,15;BYHOUR=2;BYMINUTE=30;BYSECOND=0 + `RRULE:FREQ=DAILY;BYMONTHDAY=1,15;BYHOUR=2;BYMINUTE=30;BYSECOND=0` Arguments: rrule (string): A recurrence rule (RFC 2445) specifying the frequency and time at which the query will recur diff --git a/src/cbc_sdk/audit_remediation/differential.py b/src/cbc_sdk/audit_remediation/differential.py index fc5e3d4f1..785b76d8e 100644 --- a/src/cbc_sdk/audit_remediation/differential.py +++ b/src/cbc_sdk/audit_remediation/differential.py @@ -27,8 +27,7 @@ class Differential(NewBaseModel): - """ - Represents a Differential Analysis run. + """Represents a Differential Analysis run. Example: >>> query = cb.select(Differential).newer_run_id(newer_run_id) @@ -36,6 +35,7 @@ class Differential(NewBaseModel): >>> print(run) >>> print(run.diff_results) """ + swagger_meta_file = "audit_remediation/models/differential.yaml" urlobject = "/livequery/v1/orgs/{}/differential/runs/_search" diff --git a/src/cbc_sdk/endpoint_standard/usb_device_control.py b/src/cbc_sdk/endpoint_standard/usb_device_control.py index e74e3c00d..b053ec088 100755 --- a/src/cbc_sdk/endpoint_standard/usb_device_control.py +++ b/src/cbc_sdk/endpoint_standard/usb_device_control.py @@ -161,6 +161,7 @@ def bulk_create_csv(cls, cb, approval_data): Example: vendor_id,product_id,serial_number,approval_name,notes + string,string,string,string,string Returns: diff --git a/src/cbc_sdk/live_response_api.py b/src/cbc_sdk/live_response_api.py index fcd3f4f54..fa06b87e1 100644 --- a/src/cbc_sdk/live_response_api.py +++ b/src/cbc_sdk/live_response_api.py @@ -411,7 +411,9 @@ def walk(self, top, topdown=True, onerror=None, followlinks=False): Perform a full directory walk with recursion into subdirectories on the remote machine. Note: walk does not support async_mode due to its behaviour, it can only be invoked synchronously + Example: + >>> with c.select(Device, 1).lr_session() as lr_session: ... for entry in lr_session.walk(directory_name): ... print(entry) diff --git a/src/cbc_sdk/platform/processes.py b/src/cbc_sdk/platform/processes.py index bb02583e6..b8796a975 100644 --- a/src/cbc_sdk/platform/processes.py +++ b/src/cbc_sdk/platform/processes.py @@ -33,8 +33,11 @@ class Process(UnrefreshableModel): Examples: # use the Process GUID directly + >>> process = api.select(Process, "WNEXFKQ7-00050603-0000066c-00000000-1d6c9acb43e29bb") + # use the Process GUID in a where() clause + >>> process_query = (api.select(Process).where(process_guid= "WNEXFKQ7-00050603-0000066c-00000000-1d6c9acb43e29bb")) >>> process_query_results = [proc for proc in process_query] @@ -485,17 +488,23 @@ class ProcessFacet(UnrefreshableModel): If you want full control over the query string specify Process Guid in the query string `.where("process_guid: example_guid OR parent_effective_reputation: KNOWN_MALWARE")` - Examples: + >>> process_facet_query = (api.select(ProcessFacet).where(process_guid= "WNEXFKQ7-00050603-0000066c-00000000-1d6c9acb43e29bb")) >>> process_facet_query.add_facet_field("device_name") + # retrieve results synchronously + >>> facet = process_facet_query.results + # retrieve results asynchronously + >>> future = process_facet_query.execute_async() >>> result = future.result() + # result is a list with one item, so access the first item + >>> facet = result[0] """ primary_key = "job_id" From 08473ec1cf322937984b17e65b2154babe44e03b Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Mon, 9 Jan 2023 17:01:57 -0700 Subject: [PATCH 014/143] partial implementation of PolicyRuleConfig --- .../platform/models/policy_ruleconfig.yaml | 20 ++ src/cbc_sdk/platform/policies.py | 246 ++++++++++++++++++ 2 files changed, 266 insertions(+) create mode 100644 src/cbc_sdk/platform/models/policy_ruleconfig.yaml diff --git a/src/cbc_sdk/platform/models/policy_ruleconfig.yaml b/src/cbc_sdk/platform/models/policy_ruleconfig.yaml new file mode 100644 index 000000000..50b72db6a --- /dev/null +++ b/src/cbc_sdk/platform/models/policy_ruleconfig.yaml @@ -0,0 +1,20 @@ +type: object +properties: + id: + type: string + description: The ID of this rule config + name: + type: string + description: The name of this rule config + description: + type: string + description: The description of this rule config + inherited_from: + type: string + description: Indicates where the rule config was inherited from + category: + type: string + description: The category for this rule config + parameters: + type: object + description: The parameters associated with this rule config diff --git a/src/cbc_sdk/platform/policies.py b/src/cbc_sdk/platform/policies.py index 6ceb2dcef..aa1b2d9ae 100644 --- a/src/cbc_sdk/platform/policies.py +++ b/src/cbc_sdk/platform/policies.py @@ -14,6 +14,7 @@ """Policy implementation as part of Platform API""" import copy import json +import weakref from cbc_sdk.base import MutableBaseModel, BaseQuery, IterableQueryMixin, AsyncQueryMixin from cbc_sdk.errors import ApiError, ServerError, InvalidObjectError @@ -72,6 +73,9 @@ def __init__(self, cb, model_unique_id=None, initial_data=None, force_init=False force_init=force_init if initial_data else True, full_doc=full_doc) self._object_rules = None self._object_rules_need_load = True + self._object_rule_configs = None + self._object_rule_configs_need_load = True + self._ruleconfig_presentation = None if "version" not in self._info: self._info["version"] = 2 if model_unique_id is None: @@ -512,6 +516,8 @@ def _subobject(self, name): """ if name == 'rules': return list(self.object_rules.values()) + if name == 'rule_configs' or name == 'rapid_configs': + return list(self.object_rule_configs.values()) return super(Policy, self)._subobject(name) @classmethod @@ -557,6 +563,22 @@ def _refresh(self): self._object_rules_need_load = True return rc + def _get_ruleconfig_presentation(self): + """ + Returns information about the rule config presentation for this policy. + + Returns: + dict: A mapping of key values (UUIDs) to rule config information (dicts). + """ + rc = self._ruleconfig_presentation() if self._ruleconfig_presentation else None + if rc is None and self._model_unique_id is not None: + uri = Policy.urlobject.format(self._cb.credentials.org_key) + \ + f"/{self._model_unique_id}/configs/presentation" + result = self._cb.get_object(uri) + rc = {cfg['id']: cfg for cfg in result.get('configs', [])} + self._ruleconfig_presentation = weakref.ref(rc) + return rc + @property def rules(self): """ @@ -582,6 +604,62 @@ def object_rules(self): self._object_rules_need_load = False return self._object_rules + @property + def object_rule_configs(self): + if self._object_rule_configs_need_load: + cfgs = self._info.get("rule_configs", self._info.get("rapid_configs", [])) + ruleconfigobjects = [PolicyRuleConfig._create_rule_config(self._cb, self, cfg) for cfg in cfgs] + self._object_rule_configs = dict([(rconf.id, rconf) for rconf in ruleconfigobjects]) + self._object_rule_configs_need_load = False + return self._object_rule_configs + + def valid_rule_configs(self): + """ + Returns a dictionary identifying all valid rule configurations for this policy. + + Returns: + dict: A dictionary mapping string ID values (UUIDs) to dicts containing entries for name, description, + and category. + """ + presentation = self._get_ruleconfig_presentation() + return {k: {'name': v['name'], 'description': v['description'], 'category': v['presentation']['category']} + for k, v in presentation.items()} + + @classmethod + def get_ruleconfig_parameter_schema_directly(cls, cb, ruleconfig_id): + url = f"/policyservice/v1/orgs/{cb.credentials.org_key}/rule_configs/{ruleconfig_id}/parameters/schema" + try: + result = cb.get_object(url) + return result.get('properties', {}) + except ServerError: + raise InvalidObjectError(f"invalid rule config ID {ruleconfig_id}") + + def get_ruleconfig_parameter_schema(self, ruleconfig_id): + presentation = self._ruleconfig_presentation() if self._ruleconfig_presentation else None + if presentation is not None: + block = presentation.get(ruleconfig_id, None) + if block is None: + raise InvalidObjectError(f"invalid rule config ID {ruleconfig_id}") + param_items = block.get("parameters", []) + # morph the parameter presentation into a parameter schema + schema = {} + for item in param_items: + schema_item = {'default': item['default'], 'description': item['description']} + for validation in item.get('validations', []): + val_type = validation.get('type', None) + if val_type == 'enum': + schema_item['type'] = 'string' + schema_item['enum'] = validation.get('values', []) + else: + # other item types may be defined later + schema_item['type'] = val_type + if 'type' not in schema_item: # fallback to string if nothing specified + schema_item['type'] = 'string' + schema[item['name']] = schema_item + return schema + else: + return self.get_ruleconfig_parameter_schema_directly(self._cb, ruleconfig_id) + def _on_updated_rule(self, rule): """ Called when a rule object is added or updated. @@ -619,6 +697,56 @@ def _on_deleted_rule(self, rule): new_raw_rules = [raw_rule for raw_rule in self._info.get("rules", []) if raw_rule['id'] != rule.id] self._info["rules"] = new_raw_rules + def _on_updated_rule_config(self, rule_config): + if rule_config._parent is not self: + raise ApiError("internal error: updated rule configuration does not belong to this policy") + existed = rule_config.id in self.object_rule_configs + old_rule_configs = dict(self.object_rule_configs) + self._object_rule_configs[rule_config.id] = rule_config + raw_rule_configs = self._info.get("rule_configs", self._info.get("rapid_configs", [])) + old_raw_rules = copy.deepcopy(raw_rule_configs) + if existed: + for index, raw_rule_config in enumerate(raw_rule_configs): + if raw_rule_config['id'] == rule_config.id: + raw_rule_configs[index] = copy.deepcopy(rule_config._info) + break + else: + raw_rule_configs.append(copy.deepcopy(rule_config._info)) + self._info['rule_configs'] = raw_rule_configs + if 'rapid_configs' in self._info: + del self._info['rapid_configs'] + rollback = True + try: + self.save() + rollback = False + finally: + if rollback: + self._object_rule_configs = old_rule_configs + self._info['rule_configs'] = old_raw_rules + + def _on_deleted_rule_config(self, rule_config): + if rule_config._parent is not self: + raise ApiError("internal error: updated rule configuration does not belong to this policy") + old_rule_configs = None + if rule_config.id in self.object_rule_configs: + old_rule_configs = dict(self.object_rule_configs) + del self._object_rule_configs[rule_config.id] + else: + raise ApiError("internal error: updated rule configuration does not belong to this policy") + old_raw_rule_configs = self._info.get("rule_configs", self._info.get("rapid_configs", [])) + new_raw_rule_configs = [raw for raw in old_raw_rule_configs if raw['id'] != rule_config.id] + self._info['rule_configs'] = new_raw_rule_configs + if 'rapid_configs' in self._info: + del self._info['rapid_configs'] + rollback = True + try: + self.save() + rollback = False + finally: + if rollback: + self._object_rule_configs = old_rule_configs + self._info['rule_configs'] = old_raw_rule_configs + def add_rule(self, new_rule): """Adds a rule to this Policy. @@ -697,6 +825,8 @@ def replace_rule(self, rule_id, new_rule): else: raise ApiError(f"rule #{rule_id} not found in policy") + # --- BEGIN policy v1 compatibility methods --- + @property def priorityLevel(self): """Returns the priority level of this policy (compatibility method).""" @@ -842,6 +972,8 @@ def policy(self, oldpolicy): newpolicy["rules"] = copy.deepcopy(oldpolicy["rules"]) self._info = newpolicy + # --- END policy v1 compatibility methods --- + @classmethod def create(cls, cb): """ @@ -986,6 +1118,120 @@ def validate(self): return True +# YOUAREHERE + + +class PolicyRuleConfig(MutableBaseModel): + primary_key = "id" + swagger_meta_file = "platform/models/policy_rule.yaml" + + def __init__(self, cb, parent, model_unique_id=None, initial_data=None, force_init=False, full_doc=False): + """ + Initialize the PolicyRuleConfig 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 (int): 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(PolicyRule, self).__init__(cb, model_unique_id=model_unique_id, initial_data=initial_data, + force_init=force_init, full_doc=full_doc) + self._parent = parent + if model_unique_id is None: + self.touch(True) + + @classmethod + def _create_rule_config(cls, cb, parent, data): + """ + Creates a PolicyRuleConfig object, or specialized subclass thereof, from a block of data in the policy. + + Args: + cb (BaseAPI): Reference to API object used to communicate with the server. + parent (Policy): The "parent" policy of this rule configuration. + data (dict): Initial data used to populate the rule configuration. + + Returns: + PolicyRuleConfig: The new object. + """ + return PolicyRuleConfig(cb, parent, data.get("id", None), data, False, True) + + def _refresh(self): + """ + Refreshes the rule configuration object from the server. + + Required Permissions: + org.policies (READ) + + Returns: + bool: True if the refresh was successful. + """ + 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 + + def _update_object(self): + self._parent._on_updated_rule_config(self) + + def _delete_object(self): + was_deleted = False + try: + self._parent._on_deleted_rule_config(self) + was_deleted = True + finally: + if was_deleted: + self._parent = None + + def validate(self): + """ + Validates this rule configuration against its constraints. + + Raises: + InvalidObjectError: If the rule object is not valid. + """ + super(PolicyRuleConfig, self).validate() + if self._parent is not None: + # validate configuration ID and set high-level fields + valid_configs = self._parent.valid_rule_configs() + data = valid_configs.get(self._model_unique_id, None) + if not data: + raise InvalidObjectError(f"rule configuration ID {self._model_unique_id} is not valid") + self._info.update(data) + if 'inherited_from' not in self._info: + self._info['inherited_from'] = 'psc:region' + + # validate parameters + if self._parent is None: + parameter_validations = Policy.get_ruleconfig_parameter_schema_directly(self._cb, self._model_unique_id) + else: + parameter_validations = self._parent.get_ruleconfig_parameter_schema(self._model_unique_id) + my_parameters = self._info.get('parameters', {}) + for k, v in parameter_validations.items(): + if k in my_parameters: + if v['type'] == 'string': + if not isinstance(my_parameters[k], str): + raise InvalidObjectError(f"rule configuration parameter '{k}' is not a string") + if ('enum' in v) and (my_parameters[k] not in v['enum']): + raise InvalidObjectError(f"invalid value '{my_parameters[k]}' " + f"for rule configuration parameter '{k}'") + else: + raise ApiError(f"internal error: unknown parameter type {v['type']}") + else: + my_parameters[k] = v['default'] + self._info['parameters'] = my_parameters + + @property + def is_deleted(self): + """Returns True if this rule configuration object has been deleted.""" + return self._parent is None + + """Query Class""" From 035e5a5b9aa662596a853c7841a20ad91b4f954c Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Wed, 11 Jan 2023 16:34:21 -0700 Subject: [PATCH 015/143] added additional implementation details and some missing docstrings --- src/cbc_sdk/platform/policies.py | 87 ++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/src/cbc_sdk/platform/policies.py b/src/cbc_sdk/platform/policies.py index aa1b2d9ae..51beeb0d0 100644 --- a/src/cbc_sdk/platform/policies.py +++ b/src/cbc_sdk/platform/policies.py @@ -606,6 +606,12 @@ def object_rules(self): @property def object_rule_configs(self): + """ + Returns a dictionary of rule configuration IDs and objects for this Policy. + + Returns: + dict: A dictionary with rule configuration IDs as keys and PolicyRuleConfig objects as values. + """ if self._object_rule_configs_need_load: cfgs = self._info.get("rule_configs", self._info.get("rapid_configs", [])) ruleconfigobjects = [PolicyRuleConfig._create_rule_config(self._cb, self, cfg) for cfg in cfgs] @@ -627,6 +633,19 @@ def valid_rule_configs(self): @classmethod def get_ruleconfig_parameter_schema_directly(cls, cb, ruleconfig_id): + """ + Returns the parameter schema for a specified rule configuration. + + Args: + cb (BaseAPI): Reference to API object used to communicate with the server. + ruleconfig_id (str): The rule configuration ID (UUID). + + Returns: + dict: The parameter schema for this particular rule configuration. + + Raises: + InvalidObjectError: If the rule configuration ID is not valid. + """ url = f"/policyservice/v1/orgs/{cb.credentials.org_key}/rule_configs/{ruleconfig_id}/parameters/schema" try: result = cb.get_object(url) @@ -635,6 +654,20 @@ def get_ruleconfig_parameter_schema_directly(cls, cb, ruleconfig_id): raise InvalidObjectError(f"invalid rule config ID {ruleconfig_id}") def get_ruleconfig_parameter_schema(self, ruleconfig_id): + """ + Returns the parameter schema for a specified rule configuration. + + Uses cached rule configuration presentation data if present. + + Args: + ruleconfig_id (str): The rule configuration ID (UUID). + + Returns: + dict: The parameter schema for this particular rule configuration. + + Raises: + InvalidObjectError: If the rule configuration ID is not valid. + """ presentation = self._ruleconfig_presentation() if self._ruleconfig_presentation else None if presentation is not None: block = presentation.get(ruleconfig_id, None) @@ -698,6 +731,12 @@ def _on_deleted_rule(self, rule): self._info["rules"] = new_raw_rules def _on_updated_rule_config(self, rule_config): + """ + Called when a rule configuration object is added or updated. + + Args: + rule_config (PolicyRuleConfig): The rule configuration being added or updated. + """ if rule_config._parent is not self: raise ApiError("internal error: updated rule configuration does not belong to this policy") existed = rule_config.id in self.object_rule_configs @@ -725,6 +764,12 @@ def _on_updated_rule_config(self, rule_config): self._info['rule_configs'] = old_raw_rules def _on_deleted_rule_config(self, rule_config): + """ + Called when a rule configuration object is deleted. + + Args: + rule_config (PolicyRuleConfig): The rule configuration being deleted. + """ if rule_config._parent is not self: raise ApiError("internal error: updated rule configuration does not belong to this policy") old_rule_configs = None @@ -825,6 +870,36 @@ def replace_rule(self, rule_id, new_rule): else: raise ApiError(f"rule #{rule_id} not found in policy") + def add_rule_config(self, new_rule_config): + new_obj = PolicyRuleConfig._create_rule_config(self._cb, self, new_rule_config) + new_obj.save() + + def delete_rule_config(self, rule_config_id): + old_rule_config = self.object_rule_configs.get(rule_config_id, None) + if old_rule_config: + old_rule_config.delete() + else: + raise ApiError(f"rule configuration '{rule_config_id}' not found in policy") + + def replace_rule_config(self, rule_config_id, new_rule_config): + old_rule_config = self.object_rule_configs.get(rule_config_id, None) + if old_rule_config: + new_rule_config_info = copy.deepcopy(new_rule_config) + new_rule_config_info['id'] = rule_config_id + saved_rule_config_info = old_rule_config._info + old_rule_config._info = new_rule_config_info + old_rule_config.touch() + restore_rule_config = True + try: + old_rule_config.save() + restore_rule_config = False + finally: + if restore_rule_config: + old_rule_config._info = saved_rule_config_info + old_rule_config._dirty_attributes = {} + else: + raise ApiError(f"rule configuration '{rule_config_id}' not found in policy") + # --- BEGIN policy v1 compatibility methods --- @property @@ -1177,9 +1252,21 @@ def _refresh(self): return rc def _update_object(self): + """ + Updates the rule configuration object on the policy on the server. + + Required Permissions: + org.policies(UPDATE) + """ self._parent._on_updated_rule_config(self) def _delete_object(self): + """ + Deletes this rule configuration object from the policy on the server. + + Required Permissions: + org.policies(UPDATE) + """ was_deleted = False try: self._parent._on_deleted_rule_config(self) From 65f98d2b33bbf9ce6c3db11b8d448614dd84709d Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Wed, 18 Jan 2023 14:28:23 -0700 Subject: [PATCH 016/143] finished basic implementation for rule config --- src/cbc_sdk/platform/policies.py | 82 +++++++++++++++++++++++++++++++- 1 file changed, 81 insertions(+), 1 deletion(-) diff --git a/src/cbc_sdk/platform/policies.py b/src/cbc_sdk/platform/policies.py index 51beeb0d0..da6968fa2 100644 --- a/src/cbc_sdk/platform/policies.py +++ b/src/cbc_sdk/platform/policies.py @@ -108,6 +108,7 @@ def __init__(self, cb): "is_system": False, "rapid_configs": []} self._sensor_settings = {} self._new_rules = [] + self._new_rule_configs = [] def set_name(self, name): """ @@ -412,6 +413,20 @@ def _add_rule(self, rule_data): new_rule.validate() self._new_rules.append(new_rule) + def _add_rule_config(self, rule_config_data): + """ + Add rule configuration data to the new policy. + + Args: + rule_config_data (dict): Rule configuration data specified as a dictionary. + + Raises: + InvalidObjectError: If the rule configuration data passed in is not valid. + """ + new_rule_config = PolicyRuleConfig._create_rule_config(self._cb, None, rule_config_data) + new_rule_config.validate() + self._new_rule_configs.append(new_rule_config) + def add_rule_copy(self, rule): """ Adds a copy of an existing rule to this new policy. @@ -453,6 +468,45 @@ def add_rule(self, app_type, app_value, operation, action, required=True): self._add_rule(ruledata) return self + def add_rule_config_copy(self, rule_config): + """ + Adds a copy of an existing rule configuration to this new policy. + + Args: + rule_config (PolicyRuleConfig): The rule configuration to copy and add to this object. + + Returns: + PolicyBuilder: This object. + + Raises: + InvalidObjectError: If the rule configuration data passed in is not valid. + """ + ruleconfigdata = copy.deepcopy(rule_config._info) + if "id" in ruleconfigdata: + del ruleconfigdata["id"] + self._add_rule_config(ruleconfigdata) + return self + + def add_rule_config(self, name, category, **kwargs): + """ + Add a new rule configuration as discrete data elements to the new policy. + + Args: + name (str): Name of the rule configuration object. + category (str): Category of the rule configuration object. + **kwargs (dict): Parameter values for the rule configuration object. + + Returns: + PolicyBuilder: This object. + + Raises: + InvalidObjectError: If the rule configuration data passed in is not valid. + """ + ruleconfigdata = {"name": name, "category": category, "inherited_from": "", + "parameters": copy.deepcopy(**kwargs)} + self._add_rule_config(ruleconfigdata) + return self + def add_sensor_setting(self, name, value): """ Add a sensor setting to the policy. @@ -502,6 +556,7 @@ def build(self): settings.sort(key=lambda item: item["name"]) new_policy["sensor_settings"] = settings new_policy["rules"] = [copy.deepcopy(r._info) for r in self._new_rules] + new_policy["rule_configs"] = [copy.deepcopy(rcfg._info) for rcfg in self._new_rule_configs] return Policy(self._cb, None, new_policy, False, True) def _subobject(self, name): @@ -871,10 +926,25 @@ def replace_rule(self, rule_id, new_rule): raise ApiError(f"rule #{rule_id} not found in policy") def add_rule_config(self, new_rule_config): + """ + Adds a rule configuration to this policy. + + Args: + new_rule_config (dict): The new rule configuration to add to this Policy. + """ new_obj = PolicyRuleConfig._create_rule_config(self._cb, self, new_rule_config) new_obj.save() def delete_rule_config(self, rule_config_id): + """ + Deletes a rule configuration from this Policy. + + Args: + rule_config_id (str): The ID of the rule configuration to be deleted. + + Raises: + ApiError: If the rule configuration ID does not exist in this policy. + """ old_rule_config = self.object_rule_configs.get(rule_config_id, None) if old_rule_config: old_rule_config.delete() @@ -882,6 +952,16 @@ def delete_rule_config(self, rule_config_id): raise ApiError(f"rule configuration '{rule_config_id}' not found in policy") def replace_rule_config(self, rule_config_id, new_rule_config): + """ + Replaces a rule configuration in this policy. + + Args: + rule_config_id (str): The ID of the rule configuration to be replaced. + new_rule_config (dict): The data for the new rule configuration. + + Raises: + ApiError: If the rule configuration ID does not exist in this policy. + """ old_rule_config = self.object_rule_configs.get(rule_config_id, None) if old_rule_config: new_rule_config_info = copy.deepcopy(new_rule_config) @@ -1231,7 +1311,7 @@ def _create_rule_config(cls, cb, parent, data): Returns: PolicyRuleConfig: The new object. """ - return PolicyRuleConfig(cb, parent, data.get("id", None), data, False, True) + return PolicyRuleConfig(cb, parent, data.get("id", None) if parent else None, data, False, True) def _refresh(self): """ From 29607bb7252c27e011e29cc68d7923f6fdaa0aed Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Wed, 18 Jan 2023 15:44:16 -0700 Subject: [PATCH 017/143] made the existing tests pass --- src/cbc_sdk/platform/policies.py | 3 +-- src/tests/unit/fixtures/platform/mock_policies.py | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/cbc_sdk/platform/policies.py b/src/cbc_sdk/platform/policies.py index da6968fa2..56e248ab6 100644 --- a/src/cbc_sdk/platform/policies.py +++ b/src/cbc_sdk/platform/policies.py @@ -104,8 +104,7 @@ def __init__(self, cb): cb (BaseAPI): Reference to API object used to communicate with the server. """ self._cb = cb - self._new_policy_data = {"org_key": cb.credentials.org_key, "priority_level": "MEDIUM", - "is_system": False, "rapid_configs": []} + self._new_policy_data = {"org_key": cb.credentials.org_key, "priority_level": "MEDIUM", "is_system": False} self._sensor_settings = {} self._new_rules = [] self._new_rule_configs = [] diff --git a/src/tests/unit/fixtures/platform/mock_policies.py b/src/tests/unit/fixtures/platform/mock_policies.py index e728f6203..54ba5a219 100644 --- a/src/tests/unit/fixtures/platform/mock_policies.py +++ b/src/tests/unit/fixtures/platform/mock_policies.py @@ -190,7 +190,7 @@ "value": "true" } ], - "rapid_configs": [] + "rule_configs": [] } SUMMARY_POLICY_1 = { @@ -1396,7 +1396,7 @@ "policy_modification": False, "quarantine": True }, - "rapid_configs": [] + "rule_configs": [] } NEW_POLICY_RETURN_1 = { @@ -1509,5 +1509,5 @@ "policy_modification": False, "quarantine": True }, - "rapid_configs": [] + "rule_configs": [] } From b9fcd1559cc4693125d8a2e7c9a126f9b748fbfa Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Fri, 20 Jan 2023 16:49:38 -0700 Subject: [PATCH 018/143] drove test coverage from 75% to 80% --- src/cbc_sdk/platform/__init__.py | 2 +- src/cbc_sdk/platform/policies.py | 6 +- .../unit/fixtures/platform/mock_policies.py | 112 +++++++++++++++++- src/tests/unit/platform/test_policies.py | 54 ++++++++- 4 files changed, 166 insertions(+), 8 deletions(-) diff --git a/src/cbc_sdk/platform/__init__.py b/src/cbc_sdk/platform/__init__.py index af3224ab1..6ecbcb2a3 100644 --- a/src/cbc_sdk/platform/__init__.py +++ b/src/cbc_sdk/platform/__init__.py @@ -9,7 +9,7 @@ from cbc_sdk.platform.events import Event, EventFacet -from cbc_sdk.platform.policies import Policy, PolicyRule +from cbc_sdk.platform.policies import Policy, PolicyRule, PolicyRuleConfig from cbc_sdk.platform.processes import (Process, ProcessFacet, AsyncProcessQuery, SummaryQuery) diff --git a/src/cbc_sdk/platform/policies.py b/src/cbc_sdk/platform/policies.py index 56e248ab6..78b2ade66 100644 --- a/src/cbc_sdk/platform/policies.py +++ b/src/cbc_sdk/platform/policies.py @@ -1277,7 +1277,7 @@ def validate(self): class PolicyRuleConfig(MutableBaseModel): primary_key = "id" - swagger_meta_file = "platform/models/policy_rule.yaml" + 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): """ @@ -1291,8 +1291,8 @@ def __init__(self, cb, parent, model_unique_id=None, initial_data=None, force_in 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(PolicyRule, self).__init__(cb, model_unique_id=model_unique_id, initial_data=initial_data, - force_init=force_init, full_doc=full_doc) + 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 if model_unique_id is None: self.touch(True) diff --git a/src/tests/unit/fixtures/platform/mock_policies.py b/src/tests/unit/fixtures/platform/mock_policies.py index 54ba5a219..dc3c68471 100644 --- a/src/tests/unit/fixtures/platform/mock_policies.py +++ b/src/tests/unit/fixtures/platform/mock_policies.py @@ -190,7 +190,48 @@ "value": "true" } ], - "rule_configs": [] + "rule_configs": [ + { + "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" + } + } + ] } SUMMARY_POLICY_1 = { @@ -1509,5 +1550,72 @@ "policy_modification": False, "quarantine": True }, - "rule_configs": [] + "rule_configs": [ + { + "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" + } + } + ] +} + +BASIC_CONFIG_TEMPLATE_RETURN = { + "type": "object", + "properties": { + "WindowsAssignmentMode": { + "default": "BLOCK", + "description": "Used to change assignment mode to PREVENT or BLOCK", + "type": "string", + "enum": [ + "REPORT", + "BLOCK" + ] + } + } +} + +TEMPLATE_RETURN_BOGUS_TYPE = { + "type": "object", + "properties": { + "WindowsAssignmentMode": { + "default": "BLOCK", + "description": "Used to change assignment mode to PREVENT or BLOCK", + "type": "bogus" + } + } } diff --git a/src/tests/unit/platform/test_policies.py b/src/tests/unit/platform/test_policies.py index ef3565b95..7ad21cfe4 100644 --- a/src/tests/unit/platform/test_policies.py +++ b/src/tests/unit/platform/test_policies.py @@ -17,13 +17,14 @@ import random from contextlib import ExitStack as does_not_raise from cbc_sdk.rest_api import CBCloudAPI -from cbc_sdk.platform import Policy, PolicyRule +from cbc_sdk.platform import Policy, PolicyRule, PolicyRuleConfig 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, SUMMARY_POLICY_1, SUMMARY_POLICY_2, SUMMARY_POLICY_3, OLD_POLICY_1, FULL_POLICY_2, OLD_POLICY_2, RULE_ADD_1, RULE_ADD_2, RULE_MODIFY_1, NEW_POLICY_CONSTRUCT_1, - NEW_POLICY_RETURN_1) + NEW_POLICY_RETURN_1, BASIC_CONFIG_TEMPLATE_RETURN, + TEMPLATE_RETURN_BOGUS_TYPE) logging.basicConfig(format='%(asctime)s %(levelname)s:%(message)s', level=logging.DEBUG, filename='log.txt') @@ -58,6 +59,11 @@ def test_policy_compatibility_aliases_read(cb): objs = policy.object_rules for raw_rule in FULL_POLICY_1["rules"]: assert objs[raw_rule["id"]]._info == raw_rule + rule_configs = policy.object_rule_configs + assert rule_configs["1f8a5e4b-34f2-4d31-9f8f-87c56facaec8"].name == "Advanced Scripting Prevention" + assert rule_configs["ac67fa14-f6be-4df9-93f2-6de0dbd96061"].name == "Credential Theft" + assert rule_configs["c4ed61b3-d5aa-41a9-814f-0f277451532b"].name == "Carbon Black Threat Intel" + assert rule_configs["88b19232-7ebb-48ef-a198-2a75a282de5d"].name == "Privilege Escalation" def test_policy_compatibility_aliases_write(cb): @@ -91,6 +97,8 @@ def on_get(uri, query_params, default): assert policy.auto_delete_known_bad_hashes_delay == 86400000 assert called_full_get is True assert policy.rules == FULL_POLICY_1["rules"] + rule_configs = policy.object_rule_configs + assert rule_configs["1f8a5e4b-34f2-4d31-9f8f-87c56facaec8"].name == "Advanced Scripting Prevention" def test_policy_lookup_by_id(cbcsdk_mock): @@ -102,6 +110,8 @@ def test_policy_lookup_by_id(cbcsdk_mock): assert policy.priority_level == "HIGH" assert policy.auto_delete_known_bad_hashes_delay == 86400000 assert policy.rules == FULL_POLICY_1["rules"] + rule_configs = policy.object_rule_configs + assert rule_configs["1f8a5e4b-34f2-4d31-9f8f-87c56facaec8"].name == "Advanced Scripting Prevention" def test_policy_get_summaries(cbcsdk_mock): @@ -473,6 +483,46 @@ def test_rule_delete_is_new(cb): new_rule.delete() +@pytest.mark.parametrize("initial_data, param_schema_return, handler, message", [ + ({"id": "88b19232-7ebb-48ef-a198-2a75a282de5d", "name": "Privilege Escalation", "inherited_from": "", + "category": "core_prevention", "parameters": {"WindowsAssignmentMode": "BLOCK"}}, + BASIC_CONFIG_TEMPLATE_RETURN, does_not_raise(), None), + ({"id": "88b19232-7ebb-48ef-a198-2a75a282de5d", "name": "Privilege Escalation", "inherited_from": "", + "category": "core_prevention", "parameters": {"WindowsAssignmentMode": "BLOCK"}}, + ServerError(error_code=400, message="blah"), pytest.raises(InvalidObjectError), + "invalid rule config ID 88b19232-7ebb-48ef-a198-2a75a282de5d"), + ({"id": "88b19232-7ebb-48ef-a198-2a75a282de5d", "name": "Privilege Escalation", "inherited_from": "", + "category": "core_prevention", "parameters": {}}, + BASIC_CONFIG_TEMPLATE_RETURN, does_not_raise(), None), + ({"id": "88b19232-7ebb-48ef-a198-2a75a282de5d", "name": "Privilege Escalation", "inherited_from": "", + "category": "core_prevention", "parameters": {"WindowsAssignmentMode": "BLOCK"}}, + TEMPLATE_RETURN_BOGUS_TYPE, pytest.raises(ApiError), "internal error: unknown parameter type bogus"), + ({"id": "88b19232-7ebb-48ef-a198-2a75a282de5d", "name": "Privilege Escalation", "inherited_from": "", + "category": "core_prevention", "parameters": {"WindowsAssignmentMode": 666}}, + BASIC_CONFIG_TEMPLATE_RETURN, pytest.raises(InvalidObjectError), + "rule configuration parameter 'WindowsAssignmentMode' is not a string"), + ({"id": "88b19232-7ebb-48ef-a198-2a75a282de5d", "name": "Privilege Escalation", "inherited_from": "", + "category": "core_prevention", "parameters": {"WindowsAssignmentMode": "BOGUSVALUE"}}, + BASIC_CONFIG_TEMPLATE_RETURN, pytest.raises(InvalidObjectError), + "invalid value 'BOGUSVALUE' for rule configuration parameter 'WindowsAssignmentMode'"), +]) +def test_rule_config_validate(cbcsdk_mock, initial_data, param_schema_return, handler, message): + """Tests rule configuration validation.""" + def param_schema(uri, query_params, default): + if isinstance(param_schema_return, Exception): + raise param_schema_return + return param_schema_return + + cbcsdk_mock.mock_request('GET', f"/policyservice/v1/orgs/test/rule_configs/{initial_data['id']}/parameters/schema", + param_schema) + api = cbcsdk_mock.api + rule_config = PolicyRuleConfig._create_rule_config(api, None, initial_data) + with handler as h: + rule_config.validate() + if message is not None: + assert h.value.args[0] == message + + def test_policy_builder_make_policy(cbcsdk_mock): """Tests using a policy builder to create a new policy.""" def on_post(uri, body, **kwargs): From 991033f0aa2d76adc8145d6e56215b58f6932420 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Mon, 23 Jan 2023 16:18:29 -0700 Subject: [PATCH 019/143] test coverage now 85% --- src/cbc_sdk/platform/policies.py | 22 +-- .../unit/fixtures/platform/mock_policies.py | 181 ++++++++++++++++++ src/tests/unit/platform/test_policies.py | 36 +++- 3 files changed, 225 insertions(+), 14 deletions(-) diff --git a/src/cbc_sdk/platform/policies.py b/src/cbc_sdk/platform/policies.py index 78b2ade66..03e9aa88f 100644 --- a/src/cbc_sdk/platform/policies.py +++ b/src/cbc_sdk/platform/policies.py @@ -14,7 +14,6 @@ """Policy implementation as part of Platform API""" import copy import json -import weakref from cbc_sdk.base import MutableBaseModel, BaseQuery, IterableQueryMixin, AsyncQueryMixin from cbc_sdk.errors import ApiError, ServerError, InvalidObjectError @@ -615,6 +614,7 @@ def _refresh(self): """ rc = super(Policy, self)._refresh() self._object_rules_need_load = True + self._object_rule_configs_need_load = True return rc def _get_ruleconfig_presentation(self): @@ -624,14 +624,12 @@ def _get_ruleconfig_presentation(self): Returns: dict: A mapping of key values (UUIDs) to rule config information (dicts). """ - rc = self._ruleconfig_presentation() if self._ruleconfig_presentation else None - if rc is None and self._model_unique_id is not None: + if self._ruleconfig_presentation is None and self._model_unique_id is not None: uri = Policy.urlobject.format(self._cb.credentials.org_key) + \ f"/{self._model_unique_id}/configs/presentation" result = self._cb.get_object(uri) - rc = {cfg['id']: cfg for cfg in result.get('configs', [])} - self._ruleconfig_presentation = weakref.ref(rc) - return rc + self._ruleconfig_presentation = {cfg['id']: cfg for cfg in result.get('configs', [])} + return self._ruleconfig_presentation @property def rules(self): @@ -722,9 +720,8 @@ def get_ruleconfig_parameter_schema(self, ruleconfig_id): Raises: InvalidObjectError: If the rule configuration ID is not valid. """ - presentation = self._ruleconfig_presentation() if self._ruleconfig_presentation else None - if presentation is not None: - block = presentation.get(ruleconfig_id, None) + if self._ruleconfig_presentation is not None: + block = self._ruleconfig_presentation.get(ruleconfig_id, None) if block is None: raise InvalidObjectError(f"invalid rule config ID {ruleconfig_id}") param_items = block.get("parameters", []) @@ -1362,12 +1359,11 @@ def validate(self): InvalidObjectError: If the rule object is not valid. """ super(PolicyRuleConfig, self).validate() + if self._parent is not None: - # validate configuration ID and set high-level fields + # set high-level fields valid_configs = self._parent.valid_rule_configs() - data = valid_configs.get(self._model_unique_id, None) - if not data: - raise InvalidObjectError(f"rule configuration ID {self._model_unique_id} is not valid") + data = valid_configs.get(self._model_unique_id, {}) self._info.update(data) if 'inherited_from' not in self._info: self._info['inherited_from'] = 'psc:region' diff --git a/src/tests/unit/fixtures/platform/mock_policies.py b/src/tests/unit/fixtures/platform/mock_policies.py index dc3c68471..267585c68 100644 --- a/src/tests/unit/fixtures/platform/mock_policies.py +++ b/src/tests/unit/fixtures/platform/mock_policies.py @@ -1619,3 +1619,184 @@ } } } + +POLICY_CONFIG_PRESENTATION = { + "configs": [ + { + "id": "1f8a5e4b-34f2-4d31-9f8f-87c56facaec8", + "name": "Advanced Scripting Prevention", + "description": "Addresses malicious fileless and file-backed scripts that leverage native programs and common scripting languages.", + "presentation": { + "name": "amsi.name", + "category": "core-prevention", + "description": [ + "amsi.description" + ], + "platforms": [ + { + "platform": "WINDOWS", + "header": "amsi.windows.heading", + "subHeader": [ + "amsi.windows.sub_heading" + ], + "actions": [ + { + "component": "assignment-mode-selector", + "parameter": "WindowsAssignmentMode" + } + ] + } + ] + }, + "parameters": [ + { + "default": "BLOCK", + "name": "WindowsAssignmentMode", + "description": "Used to change assignment mode to PREVENT or BLOCK", + "recommended": "BLOCK", + "validations": [ + { + "type": "enum", + "values": [ + "REPORT", + "BLOCK" + ] + } + ] + } + ] + }, + { + "id": "ac67fa14-f6be-4df9-93f2-6de0dbd96061", + "name": "Credential Theft", + "description": "Addresses threat actors obtaining credentials and relies on detecting the malicious use of TTPs/behaviors that indicate such activity.", + "presentation": { + "name": "cred_theft.name", + "category": "core-prevention", + "description": [ + "cred_theft.description" + ], + "platforms": [ + { + "platform": "WINDOWS", + "header": "cred_theft.windows.heading", + "subHeader": [ + "cred_theft.windows.sub_heading" + ], + "actions": [ + { + "component": "assignment-mode-selector", + "parameter": "WindowsAssignmentMode" + } + ] + } + ] + }, + "parameters": [ + { + "default": "BLOCK", + "name": "WindowsAssignmentMode", + "description": "Used to change assignment mode to PREVENT or BLOCK", + "recommended": "BLOCK", + "validations": [ + { + "type": "enum", + "values": [ + "REPORT", + "BLOCK" + ] + } + ] + } + ] + }, + { + "id": "c4ed61b3-d5aa-41a9-814f-0f277451532b", + "name": "Carbon Black Threat Intel", + "description": "Addresses common and pervasive TTPs used for malicious activity as well as living off the land TTPs/behaviors detected by Carbon Black’s Threat Analysis Unit.", + "presentation": { + "name": "cbti.name", + "category": "core-prevention", + "description": [ + "cbti.description" + ], + "platforms": [ + { + "platform": "WINDOWS", + "header": "cbti.windows.heading", + "subHeader": [ + "cbti.windows.sub_heading" + ], + "actions": [ + { + "component": "assignment-mode-selector", + "parameter": "WindowsAssignmentMode" + } + ] + } + ] + }, + "parameters": [ + { + "default": "BLOCK", + "name": "WindowsAssignmentMode", + "description": "Used to change assignment mode to PREVENT or BLOCK", + "recommended": "BLOCK", + "validations": [ + { + "type": "enum", + "values": [ + "REPORT", + "BLOCK" + ] + } + ] + } + ] + }, + { + "id": "88b19232-7ebb-48ef-a198-2a75a282de5d", + "name": "Privilege Escalation", + "description": "Addresses behaviors that indicate a threat actor has gained elevated access via a bug or misconfiguration within an operating system, and leverages the detection of TTPs/behaviors to prevent such activity.", + "presentation": { + "name": "privesc.name", + "category": "core-prevention", + "description": [ + "privesc.description" + ], + "platforms": [ + { + "platform": "WINDOWS", + "header": "privesc.windows.heading", + "subHeader": [ + "privesc.windows.sub_heading" + ], + "actions": [ + { + "component": "assignment-mode-selector", + "parameter": "WindowsAssignmentMode" + } + ] + } + ] + }, + "parameters": [ + { + "default": "BLOCK", + "name": "WindowsAssignmentMode", + "description": "Used to change assignment mode to PREVENT or BLOCK", + "recommended": "BLOCK", + "validations": [ + { + "type": "enum", + "values": [ + "REPORT", + "BLOCK" + ] + } + ] + } + ] + } + ] +} diff --git a/src/tests/unit/platform/test_policies.py b/src/tests/unit/platform/test_policies.py index 7ad21cfe4..520f685a4 100644 --- a/src/tests/unit/platform/test_policies.py +++ b/src/tests/unit/platform/test_policies.py @@ -24,7 +24,7 @@ SUMMARY_POLICY_3, OLD_POLICY_1, FULL_POLICY_2, OLD_POLICY_2, RULE_ADD_1, RULE_ADD_2, RULE_MODIFY_1, NEW_POLICY_CONSTRUCT_1, NEW_POLICY_RETURN_1, BASIC_CONFIG_TEMPLATE_RETURN, - TEMPLATE_RETURN_BOGUS_TYPE) + TEMPLATE_RETURN_BOGUS_TYPE, POLICY_CONFIG_PRESENTATION) logging.basicConfig(format='%(asctime)s %(levelname)s:%(message)s', level=logging.DEBUG, filename='log.txt') @@ -523,6 +523,40 @@ def param_schema(uri, query_params, default): assert h.value.args[0] == message +@pytest.mark.parametrize("new_data, get_id, handler, message", [ + ({"id": "88b19232-7ebb-48ef-a198-2a75a282de5d", "name": "Privilege Escalation", "inherited_from": "", + "category": "core_prevention", "parameters": {"WindowsAssignmentMode": "BLOCK"}}, + None, does_not_raise(), None), + ({"id": "88b19236-7ebb-48ef-a198-2a75a282de5d", "name": "Privilege Escalation", "inherited_from": "", + "category": "core_prevention", "parameters": {"WindowsAssignmentMode": "BLOCK"}}, + "88b19232-7ebb-48ef-a198-2a75a282de5d", pytest.raises(InvalidObjectError), + "invalid rule config ID 88b19236-7ebb-48ef-a198-2a75a282de5d"), + ({"id": "88b19232-7ebb-48ef-a198-2a75a282de5d", "name": "Privilege Escalation", "inherited_from": "", + "category": "core_prevention", "parameters": {}}, + None, does_not_raise(), None), + ({"id": "88b19232-7ebb-48ef-a198-2a75a282de5d", "name": "Privilege Escalation", "inherited_from": "", + "category": "core_prevention", "parameters": {"WindowsAssignmentMode": 666}}, + None, pytest.raises(InvalidObjectError), + "rule configuration parameter 'WindowsAssignmentMode' is not a string"), + ({"id": "88b19232-7ebb-48ef-a198-2a75a282de5d", "name": "Privilege Escalation", "inherited_from": "", + "category": "core_prevention", "parameters": {"WindowsAssignmentMode": "BOGUSVALUE"}}, + None, pytest.raises(InvalidObjectError), + "invalid value 'BOGUSVALUE' for rule configuration parameter 'WindowsAssignmentMode'"), +]) +def test_rule_config_validate_inside_policy(cbcsdk_mock, new_data, get_id, handler, message): + cbcsdk_mock.mock_request('GET', '/policyservice/v1/orgs/test/policies/65535/configs/presentation', + POLICY_CONFIG_PRESENTATION) + api = cbcsdk_mock.api + policy = Policy(api, 65535, copy.deepcopy(FULL_POLICY_1), False, True) + rule_config_id = get_id if get_id is not None else new_data['id'] + rule_config = policy.object_rule_configs[rule_config_id] + rule_config._info = copy.deepcopy(new_data) + with handler as h: + rule_config.validate() + if message is not None: + assert h.value.args[0] == message + + def test_policy_builder_make_policy(cbcsdk_mock): """Tests using a policy builder to create a new policy.""" def on_post(uri, body, **kwargs): From a960e6c9d4353caec2274f2ab442c91b158d06a3 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Wed, 25 Jan 2023 13:36:16 -0700 Subject: [PATCH 020/143] coverage now at 95% --- src/cbc_sdk/platform/policies.py | 48 ++++- .../unit/fixtures/platform/mock_policies.py | 35 ++-- src/tests/unit/platform/test_policies.py | 187 +++++++++++++++++- 3 files changed, 252 insertions(+), 18 deletions(-) diff --git a/src/cbc_sdk/platform/policies.py b/src/cbc_sdk/platform/policies.py index 03e9aa88f..0022605d0 100644 --- a/src/cbc_sdk/platform/policies.py +++ b/src/cbc_sdk/platform/policies.py @@ -805,6 +805,7 @@ def _on_updated_rule_config(self, rule_config): self._info['rule_configs'] = raw_rule_configs if 'rapid_configs' in self._info: del self._info['rapid_configs'] + self.touch() rollback = True try: self.save() @@ -813,6 +814,9 @@ def _on_updated_rule_config(self, rule_config): if rollback: self._object_rule_configs = old_rule_configs self._info['rule_configs'] = old_raw_rules + else: + self._object_rules_need_load = True + self._object_rule_configs_need_load = True def _on_deleted_rule_config(self, rule_config): """ @@ -823,7 +827,6 @@ def _on_deleted_rule_config(self, rule_config): """ if rule_config._parent is not self: raise ApiError("internal error: updated rule configuration does not belong to this policy") - old_rule_configs = None if rule_config.id in self.object_rule_configs: old_rule_configs = dict(self.object_rule_configs) del self._object_rule_configs[rule_config.id] @@ -834,6 +837,7 @@ def _on_deleted_rule_config(self, rule_config): self._info['rule_configs'] = new_raw_rule_configs if 'rapid_configs' in self._info: del self._info['rapid_configs'] + self.touch() rollback = True try: self.save() @@ -842,6 +846,9 @@ def _on_deleted_rule_config(self, rule_config): if rollback: self._object_rule_configs = old_rule_configs self._info['rule_configs'] = old_raw_rule_configs + else: + self._object_rules_need_load = True + self._object_rule_configs_need_load = True def add_rule(self, new_rule): """Adds a rule to this Policy. @@ -874,6 +881,7 @@ def add_rule(self, new_rule): "required": [True, False] """ new_obj = PolicyRule(self._cb, self, None, new_rule, False, True) + new_obj.touch() new_obj.save() def delete_rule(self, rule_id): @@ -929,6 +937,7 @@ def add_rule_config(self, new_rule_config): new_rule_config (dict): The new rule configuration to add to this Policy. """ new_obj = PolicyRuleConfig._create_rule_config(self._cb, self, new_rule_config) + new_obj.touch() new_obj.save() def delete_rule_config(self, rule_config_id): @@ -1269,9 +1278,6 @@ def validate(self): return True -# YOUAREHERE - - class PolicyRuleConfig(MutableBaseModel): primary_key = "id" swagger_meta_file = "platform/models/policy_ruleconfig.yaml" @@ -1283,7 +1289,7 @@ def __init__(self, cb, parent, model_unique_id=None, initial_data=None, force_in 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 (int): ID of the 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. @@ -1351,6 +1357,38 @@ def _delete_object(self): if was_deleted: self._parent = None + def get_parameter(self, name): + """ + Returns a parameter value from the rule configuration. + + Args: + name (str): The parameter name. + + Returns: + Any: The parameter value, or None if there is no value. + """ + params = self._info['parameters'] + return params.get(name, None) + + def set_parameter(self, name, value): + """ + Sets a parameter value into the rule configuration. + + Args: + name (str): The parameter name. + value (Any): The new value to be set. + """ + params = self._info['parameters'] + old_value = params.get(name, None) + if old_value != value: + if 'parameters' not in self._dirty_attributes: + self._dirty_attributes['parameters'] = params + new_params = copy.deepcopy(params) + else: + new_params = params + new_params[name] = value + self._info['parameters'] = new_params + def validate(self): """ Validates this rule configuration against its constraints. diff --git a/src/tests/unit/fixtures/platform/mock_policies.py b/src/tests/unit/fixtures/platform/mock_policies.py index 267585c68..152c1bfef 100644 --- a/src/tests/unit/fixtures/platform/mock_policies.py +++ b/src/tests/unit/fixtures/platform/mock_policies.py @@ -196,7 +196,7 @@ "name": "Advanced Scripting Prevention", "description": "Addresses malicious fileless and file-backed scripts that leverage native programs [...]", "inherited_from": "psc:region", - "category": "core_prevention", + "category": "core-prevention", "parameters": { "WindowsAssignmentMode": "BLOCK" } @@ -206,7 +206,7 @@ "name": "Credential Theft", "description": "Addresses threat actors obtaining credentials and relies on detecting the malicious [...]", "inherited_from": "psc:region", - "category": "core_prevention", + "category": "core-prevention", "parameters": { "WindowsAssignmentMode": "REPORT" } @@ -216,7 +216,7 @@ "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", + "category": "core-prevention", "parameters": { "WindowsAssignmentMode": "REPORT" } @@ -226,7 +226,7 @@ "name": "Privilege Escalation", "description": "Addresses behaviors that indicate a threat actor has gained elevated access via [...]", "inherited_from": "psc:region", - "category": "core_prevention", + "category": "core-prevention", "parameters": { "WindowsAssignmentMode": "BLOCK" } @@ -1556,7 +1556,7 @@ "name": "Advanced Scripting Prevention", "description": "Addresses malicious fileless and file-backed scripts that leverage native programs [...]", "inherited_from": "psc:region", - "category": "core_prevention", + "category": "core-prevention", "parameters": { "WindowsAssignmentMode": "BLOCK" } @@ -1566,7 +1566,7 @@ "name": "Credential Theft", "description": "Addresses threat actors obtaining credentials and relies on detecting the malicious [...]", "inherited_from": "psc:region", - "category": "core_prevention", + "category": "core-prevention", "parameters": { "WindowsAssignmentMode": "REPORT" } @@ -1576,7 +1576,7 @@ "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", + "category": "core-prevention", "parameters": { "WindowsAssignmentMode": "REPORT" } @@ -1586,7 +1586,7 @@ "name": "Privilege Escalation", "description": "Addresses behaviors that indicate a threat actor has gained elevated access via [...]", "inherited_from": "psc:region", - "category": "core_prevention", + "category": "core-prevention", "parameters": { "WindowsAssignmentMode": "BLOCK" } @@ -1625,7 +1625,7 @@ { "id": "1f8a5e4b-34f2-4d31-9f8f-87c56facaec8", "name": "Advanced Scripting Prevention", - "description": "Addresses malicious fileless and file-backed scripts that leverage native programs and common scripting languages.", + "description": "Addresses malicious fileless and file-backed scripts that leverage native programs [...]", "presentation": { "name": "amsi.name", "category": "core-prevention", @@ -1669,7 +1669,7 @@ { "id": "ac67fa14-f6be-4df9-93f2-6de0dbd96061", "name": "Credential Theft", - "description": "Addresses threat actors obtaining credentials and relies on detecting the malicious use of TTPs/behaviors that indicate such activity.", + "description": "Addresses threat actors obtaining credentials and relies on detecting the malicious [...]", "presentation": { "name": "cred_theft.name", "category": "core-prevention", @@ -1713,7 +1713,7 @@ { "id": "c4ed61b3-d5aa-41a9-814f-0f277451532b", "name": "Carbon Black Threat Intel", - "description": "Addresses common and pervasive TTPs used for malicious activity as well as living off the land TTPs/behaviors detected by Carbon Black’s Threat Analysis Unit.", + "description": "Addresses common and pervasive TTPs used for malicious activity as well as [...]", "presentation": { "name": "cbti.name", "category": "core-prevention", @@ -1757,7 +1757,7 @@ { "id": "88b19232-7ebb-48ef-a198-2a75a282de5d", "name": "Privilege Escalation", - "description": "Addresses behaviors that indicate a threat actor has gained elevated access via a bug or misconfiguration within an operating system, and leverages the detection of TTPs/behaviors to prevent such activity.", + "description": "Addresses behaviors that indicate a threat actor has gained elevated access via [...]", "presentation": { "name": "privesc.name", "category": "core-prevention", @@ -1800,3 +1800,14 @@ } ] } + +REPLACE_RULECONFIG = { + "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": "REPORT" + } +} diff --git a/src/tests/unit/platform/test_policies.py b/src/tests/unit/platform/test_policies.py index 520f685a4..dbdfdc348 100644 --- a/src/tests/unit/platform/test_policies.py +++ b/src/tests/unit/platform/test_policies.py @@ -15,6 +15,7 @@ import pytest import logging import random +import json from contextlib import ExitStack as does_not_raise from cbc_sdk.rest_api import CBCloudAPI from cbc_sdk.platform import Policy, PolicyRule, PolicyRuleConfig @@ -24,7 +25,8 @@ SUMMARY_POLICY_3, OLD_POLICY_1, FULL_POLICY_2, OLD_POLICY_2, RULE_ADD_1, RULE_ADD_2, RULE_MODIFY_1, NEW_POLICY_CONSTRUCT_1, NEW_POLICY_RETURN_1, BASIC_CONFIG_TEMPLATE_RETURN, - TEMPLATE_RETURN_BOGUS_TYPE, POLICY_CONFIG_PRESENTATION) + TEMPLATE_RETURN_BOGUS_TYPE, POLICY_CONFIG_PRESENTATION, + REPLACE_RULECONFIG) logging.basicConfig(format='%(asctime)s %(levelname)s:%(message)s', level=logging.DEBUG, filename='log.txt') @@ -544,6 +546,7 @@ def param_schema(uri, query_params, default): "invalid value 'BOGUSVALUE' for rule configuration parameter 'WindowsAssignmentMode'"), ]) def test_rule_config_validate_inside_policy(cbcsdk_mock, new_data, get_id, handler, message): + """Tests rule configuration validation when it's part of a policy.""" cbcsdk_mock.mock_request('GET', '/policyservice/v1/orgs/test/policies/65535/configs/presentation', POLICY_CONFIG_PRESENTATION) api = cbcsdk_mock.api @@ -557,6 +560,188 @@ def test_rule_config_validate_inside_policy(cbcsdk_mock, new_data, get_id, handl assert h.value.args[0] == message +def test_rule_config_refresh(cbcsdk_mock): + """Tests the rule config refresh() operation.""" + cbcsdk_mock.mock_request('GET', '/policyservice/v1/orgs/test/policies/65536', FULL_POLICY_1) + api = cbcsdk_mock.api + policy = Policy(api, 65536, FULL_POLICY_1, False, True) + rule_config = random.choice(list(policy.object_rule_configs.values())) + old_name = rule_config.name + old_category = rule_config.category + old_parameters = rule_config.parameters + rule_config.refresh() + assert rule_config.name == old_name + assert rule_config.category == old_category + assert rule_config.parameters == old_parameters + + +@pytest.mark.parametrize("give_error, handler", [ + (False, does_not_raise()), + (True, pytest.raises(ServerError)) +]) +def test_rule_config_add_by_object(cbcsdk_mock, give_error, handler): + """Tests using a PolicyRuleConfig object to add a rule configuration.""" + def on_put(url, body, **kwargs): + assert body == FULL_POLICY_1 + if give_error: + return CBCSDKMock.StubResponse("Failure", scode=404) + rc = copy.deepcopy(body) + return rc + + 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', on_put) + api = cbcsdk_mock.api + policy_data = copy.deepcopy(FULL_POLICY_1) + rule_config_data1 = [p for p in enumerate(policy_data['rule_configs']) + if p[1]['id'] == '88b19232-7ebb-48ef-a198-2a75a282de5d'] + rule_config_data = rule_config_data1[0][1] + del policy_data['rule_configs'][rule_config_data1[0][0]] + policy = Policy(api, 65536, policy_data, False, True) + rule_config_count = len(policy.object_rule_configs) + new_rule_config = PolicyRuleConfig._create_rule_config(api, policy, rule_config_data) + new_rule_config.touch() + with handler: + new_rule_config.save() + assert len(policy.object_rule_configs) == rule_config_count + 1 + assert '88b19232-7ebb-48ef-a198-2a75a282de5d' in policy.object_rule_configs + + +def test_rule_config_add_by_base_method(cbcsdk_mock): + """Tests using the base method on Policy to add a rule.""" + def on_put(url, body, **kwargs): + assert body == FULL_POLICY_1 + return copy.deepcopy(body) + + 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', on_put) + api = cbcsdk_mock.api + policy_data = copy.deepcopy(FULL_POLICY_1) + rule_config_data1 = [p for p in enumerate(policy_data['rule_configs']) + if p[1]['id'] == '88b19232-7ebb-48ef-a198-2a75a282de5d'] + rule_config_data = rule_config_data1[0][1] + del policy_data['rule_configs'][rule_config_data1[0][0]] + policy = Policy(api, 65536, policy_data, False, True) + rule_config_count = len(policy.object_rule_configs) + policy.add_rule_config(rule_config_data) + assert len(policy.object_rule_configs) == rule_config_count + 1 + assert '88b19232-7ebb-48ef-a198-2a75a282de5d' in policy.object_rule_configs + + +def test_rule_config_modify_by_object(cbcsdk_mock): + """Tests modifying a PolicyRuleConfig object within the policy.""" + def on_put(url, body, **kwargs): + nonlocal new_data + assert body == new_data + return copy.deepcopy(body) + + 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', on_put) + api = cbcsdk_mock.api + new_data = copy.deepcopy(FULL_POLICY_1) + new_data['rule_configs'][3]['parameters']['WindowsAssignmentMode'] = 'REPORT' + policy = Policy(api, 65536, copy.deepcopy(FULL_POLICY_1), False, True) + rule_config_count = len(policy.object_rule_configs) + rule_config = policy.object_rule_configs['88b19232-7ebb-48ef-a198-2a75a282de5d'] + rule_config.set_parameter('WindowsAssignmentMode', 'REPORT') + rule_config.save() + assert len(policy.object_rule_configs) == rule_config_count + rule_config = policy.object_rule_configs['88b19232-7ebb-48ef-a198-2a75a282de5d'] + assert rule_config.get_parameter('WindowsAssignmentMode') == 'REPORT' + + +def test_rule_config_modify_by_base_method(cbcsdk_mock): + """Tests modifying a PolicyRuleConfig object using the replace_rule_config method.""" + def on_put(url, body, **kwargs): + nonlocal new_data + assert body == new_data + return copy.deepcopy(body) + + 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', on_put) + api = cbcsdk_mock.api + new_data = copy.deepcopy(FULL_POLICY_1) + new_data['rule_configs'][3]['parameters']['WindowsAssignmentMode'] = 'REPORT' + policy = Policy(api, 65536, copy.deepcopy(FULL_POLICY_1), False, True) + rule_config_count = len(policy.object_rule_configs) + policy.replace_rule_config('88b19232-7ebb-48ef-a198-2a75a282de5d', REPLACE_RULECONFIG) + assert len(policy.object_rule_configs) == rule_config_count + rule_config = policy.object_rule_configs['88b19232-7ebb-48ef-a198-2a75a282de5d'] + assert rule_config.get_parameter('WindowsAssignmentMode') == 'REPORT' + + +def test_rule_config_modify_by_base_method_invalid_id(cb): + """Tests modifying a PolicyRuleConfig object using replace_rule_config, but with an invalid ID.""" + policy = Policy(cb, 65536, copy.deepcopy(FULL_POLICY_1), False, True) + with pytest.raises(ApiError): + policy.replace_rule_config('88b19266-7ebb-48ef-a198-2a75a282de5d', REPLACE_RULECONFIG) + + +def test_rule_config_delete_by_object(cbcsdk_mock): + """Tests deleting a PolicyRuleConfig object from a policy.""" + def on_put(url, body, **kwargs): + nonlocal new_data + assert body == new_data + return copy.deepcopy(body) + + 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', on_put) + api = cbcsdk_mock.api + new_data = copy.deepcopy(FULL_POLICY_1) + rule_config_data1 = [p for p in enumerate(new_data['rule_configs']) + if p[1]['id'] == '88b19232-7ebb-48ef-a198-2a75a282de5d'] + del new_data['rule_configs'][rule_config_data1[0][0]] + policy = Policy(api, 65536, copy.deepcopy(FULL_POLICY_1), False, True) + rule_config_count = len(policy.object_rule_configs) + rule_config = policy.object_rule_configs['88b19232-7ebb-48ef-a198-2a75a282de5d'] + assert not rule_config.is_deleted + rule_config.delete() + assert len(policy.object_rule_configs) == rule_config_count - 1 + assert '88b19232-7ebb-48ef-a198-2a75a282de5d' not in policy.object_rule_configs + assert rule_config.is_deleted + + +def test_rule_config_delete_by_base_method(cbcsdk_mock): + """Tests deleting a rule configuration from a policy via the delete_rule_config method.""" + def on_put(url, body, **kwargs): + nonlocal new_data + assert body == new_data + return copy.deepcopy(body) + + 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', on_put) + api = cbcsdk_mock.api + new_data = copy.deepcopy(FULL_POLICY_1) + rule_config_data1 = [p for p in enumerate(new_data['rule_configs']) + if p[1]['id'] == '88b19232-7ebb-48ef-a198-2a75a282de5d'] + del new_data['rule_configs'][rule_config_data1[0][0]] + policy = Policy(api, 65536, copy.deepcopy(FULL_POLICY_1), False, True) + rule_config_count = len(policy.object_rule_configs) + policy.delete_rule_config('88b19232-7ebb-48ef-a198-2a75a282de5d') + assert len(policy.object_rule_configs) == rule_config_count - 1 + assert '88b19232-7ebb-48ef-a198-2a75a282de5d' not in policy.object_rule_configs + + +def test_rule_config_delete_by_base_method_nonexistent(cb): + """Tests what happens when you try to delete a nonexistent rule configuration via delete_rule_config.""" + policy = Policy(cb, 65536, copy.deepcopy(FULL_POLICY_1), False, True) + with pytest.raises(ApiError): + policy.delete_rule_config('88b19266-7ebb-48ef-a198-2a75a282de5d') + + +def test_rule_config_delete_is_new(cb): + """Tests that deleting a new PolicyRuleConfig raises an error.""" + policy = Policy(cb, 65536, copy.deepcopy(FULL_POLICY_1), False, True) + new_rule_config = PolicyRuleConfig(cb, policy, None, REPLACE_RULECONFIG, False, True) + with pytest.raises(ApiError): + new_rule_config.delete() + + def test_policy_builder_make_policy(cbcsdk_mock): """Tests using a policy builder to create a new policy.""" def on_post(uri, body, **kwargs): From 0ddd2ff065e9e8ad80819f97458bfeac91318afa Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Wed, 25 Jan 2023 14:30:13 -0700 Subject: [PATCH 021/143] test coverage at 97% (maximum possible); code deflake8'd --- src/cbc_sdk/platform/policies.py | 25 ++++++++++----- .../unit/fixtures/platform/mock_policies.py | 31 ++++++++++++++++++- src/tests/unit/platform/test_policies.py | 13 ++++++-- 3 files changed, 59 insertions(+), 10 deletions(-) diff --git a/src/cbc_sdk/platform/policies.py b/src/cbc_sdk/platform/policies.py index 0022605d0..072d020b3 100644 --- a/src/cbc_sdk/platform/policies.py +++ b/src/cbc_sdk/platform/policies.py @@ -480,16 +480,15 @@ def add_rule_config_copy(self, rule_config): InvalidObjectError: If the rule configuration data passed in is not valid. """ ruleconfigdata = copy.deepcopy(rule_config._info) - if "id" in ruleconfigdata: - del ruleconfigdata["id"] self._add_rule_config(ruleconfigdata) return self - def add_rule_config(self, name, category, **kwargs): + def add_rule_config(self, config_id, name, category, **kwargs): """ Add a new rule configuration as discrete data elements to the new policy. Args: + config_id (str): ID of the rule configuration object (a GUID). name (str): Name of the rule configuration object. category (str): Category of the rule configuration object. **kwargs (dict): Parameter values for the rule configuration object. @@ -500,8 +499,8 @@ def add_rule_config(self, name, category, **kwargs): Raises: InvalidObjectError: If the rule configuration data passed in is not valid. """ - ruleconfigdata = {"name": name, "category": category, "inherited_from": "", - "parameters": copy.deepcopy(**kwargs)} + ruleconfigdata = {"id": config_id, "name": name, "category": category, "inherited_from": "", + "parameters": copy.deepcopy(kwargs)} self._add_rule_config(ruleconfigdata) return self @@ -626,7 +625,7 @@ def _get_ruleconfig_presentation(self): """ if self._ruleconfig_presentation is None and self._model_unique_id is not None: uri = Policy.urlobject.format(self._cb.credentials.org_key) + \ - f"/{self._model_unique_id}/configs/presentation" + f"/{self._model_unique_id}/configs/presentation" result = self._cb.get_object(uri) self._ruleconfig_presentation = {cfg['id']: cfg for cfg in result.get('configs', [])} return self._ruleconfig_presentation @@ -1279,6 +1278,18 @@ def validate(self): class PolicyRuleConfig(MutableBaseModel): + """ + Represents a 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 PolicyRuleConfig, change the values of its property fields, then call its save() method. This + requires the org.policies(UPDATE) permission. + + To delete an existing PolicyRuleConfig, call its delete() method. This requires the org.policies(UPDATE) permission. + + """ primary_key = "id" swagger_meta_file = "platform/models/policy_ruleconfig.yaml" @@ -1313,7 +1324,7 @@ def _create_rule_config(cls, cb, parent, data): Returns: PolicyRuleConfig: The new object. """ - return PolicyRuleConfig(cb, parent, data.get("id", None) if parent else None, data, False, True) + return PolicyRuleConfig(cb, parent, data.get("id", None), data, False, True) def _refresh(self): """ diff --git a/src/tests/unit/fixtures/platform/mock_policies.py b/src/tests/unit/fixtures/platform/mock_policies.py index 152c1bfef..6bc74295d 100644 --- a/src/tests/unit/fixtures/platform/mock_policies.py +++ b/src/tests/unit/fixtures/platform/mock_policies.py @@ -1437,7 +1437,26 @@ "policy_modification": False, "quarantine": True }, - "rule_configs": [] + "rule_configs": [ + { + "id": "88b19232-7ebb-48ef-a198-2a75a282de5d", + "name": "Privilege Escalation", + "inherited_from": "", + "category": "core-prevention", + "parameters": { + "WindowsAssignmentMode": "BLOCK" + } + }, + { + "id": "ac67fa14-f6be-4df9-93f2-6de0dbd96061", + "name": "Credential Theft", + "inherited_from": "", + "category": "core-prevention", + "parameters": { + "WindowsAssignmentMode": "REPORT" + } + } + ] } NEW_POLICY_RETURN_1 = { @@ -1811,3 +1830,13 @@ "WindowsAssignmentMode": "REPORT" } } + +BUILD_RULECONFIG_1 = { + "id": "88b19232-7ebb-48ef-a198-2a75a282de5d", + "name": "Privilege Escalation", + "inherited_from": "", + "category": "core-prevention", + "parameters": { + "WindowsAssignmentMode": "BLOCK" + } +} diff --git a/src/tests/unit/platform/test_policies.py b/src/tests/unit/platform/test_policies.py index dbdfdc348..0125a3331 100644 --- a/src/tests/unit/platform/test_policies.py +++ b/src/tests/unit/platform/test_policies.py @@ -15,7 +15,6 @@ import pytest import logging import random -import json from contextlib import ExitStack as does_not_raise from cbc_sdk.rest_api import CBCloudAPI from cbc_sdk.platform import Policy, PolicyRule, PolicyRuleConfig @@ -26,7 +25,7 @@ RULE_ADD_1, RULE_ADD_2, RULE_MODIFY_1, NEW_POLICY_CONSTRUCT_1, NEW_POLICY_RETURN_1, BASIC_CONFIG_TEMPLATE_RETURN, TEMPLATE_RETURN_BOGUS_TYPE, POLICY_CONFIG_PRESENTATION, - REPLACE_RULECONFIG) + REPLACE_RULECONFIG, BUILD_RULECONFIG_1) logging.basicConfig(format='%(asctime)s %(levelname)s:%(message)s', level=logging.DEBUG, filename='log.txt') @@ -748,6 +747,12 @@ def on_post(uri, body, **kwargs): assert body == NEW_POLICY_CONSTRUCT_1 return NEW_POLICY_RETURN_1 + cbcsdk_mock.mock_request('GET', "/policyservice/v1/orgs/test/rule_configs/" + "88b19232-7ebb-48ef-a198-2a75a282de5d/parameters/schema", + BASIC_CONFIG_TEMPLATE_RETURN) + cbcsdk_mock.mock_request('GET', "/policyservice/v1/orgs/test/rule_configs/" + "ac67fa14-f6be-4df9-93f2-6de0dbd96061/parameters/schema", + BASIC_CONFIG_TEMPLATE_RETURN) cbcsdk_mock.mock_request('POST', '/policyservice/v1/orgs/test/policies', on_post) api = cbcsdk_mock.api builder = Policy.create(api) @@ -766,6 +771,10 @@ def on_post(uri, body, **kwargs): builder.add_sensor_setting("SCAN_EXECUTE_ON_NETWORK_DRIVE", "false").add_sensor_setting("UBS_OPT_IN", "true") builder.add_sensor_setting("SCAN_EXECUTE_ON_NETWORK_DRIVE", "true").add_sensor_setting("ALLOW_UNINSTALL", "true") builder.set_managed_detection_response_permissions(False, True) + rule_config = PolicyRuleConfig(api, None, BUILD_RULECONFIG_1['id'], BUILD_RULECONFIG_1, False, True) + builder.add_rule_config_copy(rule_config) + builder.add_rule_config("ac67fa14-f6be-4df9-93f2-6de0dbd96061", "Credential Theft", "core-prevention", + WindowsAssignmentMode='REPORT') policy = builder.build() assert policy._info == NEW_POLICY_CONSTRUCT_1 policy.save() From 38143fe8d795869460e9222f6bf91be12cdbdaca Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Mon, 30 Jan 2023 16:56:13 -0700 Subject: [PATCH 022/143] [WIP] fixed some issues brought up in code review --- src/cbc_sdk/platform/policies.py | 34 ++++++++++---------------------- 1 file changed, 10 insertions(+), 24 deletions(-) diff --git a/src/cbc_sdk/platform/policies.py b/src/cbc_sdk/platform/policies.py index 072d020b3..acfd41aed 100644 --- a/src/cbc_sdk/platform/policies.py +++ b/src/cbc_sdk/platform/policies.py @@ -568,7 +568,7 @@ def _subobject(self, name): """ if name == 'rules': return list(self.object_rules.values()) - if name == 'rule_configs' or name == 'rapid_configs': + if name == 'rule_configs': return list(self.object_rule_configs.values()) return super(Policy, self)._subobject(name) @@ -616,20 +616,6 @@ def _refresh(self): self._object_rule_configs_need_load = True return rc - def _get_ruleconfig_presentation(self): - """ - Returns information about the rule config presentation for this policy. - - Returns: - dict: A mapping of key values (UUIDs) to rule config information (dicts). - """ - if self._ruleconfig_presentation is None and self._model_unique_id is not None: - uri = Policy.urlobject.format(self._cb.credentials.org_key) + \ - f"/{self._model_unique_id}/configs/presentation" - result = self._cb.get_object(uri) - self._ruleconfig_presentation = {cfg['id']: cfg for cfg in result.get('configs', [])} - return self._ruleconfig_presentation - @property def rules(self): """ @@ -664,7 +650,7 @@ def object_rule_configs(self): dict: A dictionary with rule configuration IDs as keys and PolicyRuleConfig objects as values. """ if self._object_rule_configs_need_load: - cfgs = self._info.get("rule_configs", self._info.get("rapid_configs", [])) + cfgs = self._info.get("rule_configs", []) ruleconfigobjects = [PolicyRuleConfig._create_rule_config(self._cb, self, cfg) for cfg in cfgs] self._object_rule_configs = dict([(rconf.id, rconf) for rconf in ruleconfigobjects]) self._object_rule_configs_need_load = False @@ -678,9 +664,13 @@ def valid_rule_configs(self): dict: A dictionary mapping string ID values (UUIDs) to dicts containing entries for name, description, and category. """ - presentation = self._get_ruleconfig_presentation() + if self._ruleconfig_presentation is None and self._model_unique_id is not None: + uri = Policy.urlobject.format(self._cb.credentials.org_key) + \ + f"/{self._model_unique_id}/configs/presentation" + result = self._cb.get_object(uri) + self._ruleconfig_presentation = {cfg['id']: cfg for cfg in result.get('configs', [])} return {k: {'name': v['name'], 'description': v['description'], 'category': v['presentation']['category']} - for k, v in presentation.items()} + for k, v in self._ruleconfig_presentation.items()} @classmethod def get_ruleconfig_parameter_schema_directly(cls, cb, ruleconfig_id): @@ -792,7 +782,7 @@ def _on_updated_rule_config(self, rule_config): existed = rule_config.id in self.object_rule_configs old_rule_configs = dict(self.object_rule_configs) self._object_rule_configs[rule_config.id] = rule_config - raw_rule_configs = self._info.get("rule_configs", self._info.get("rapid_configs", [])) + raw_rule_configs = self._info.get("rule_configs", []) old_raw_rules = copy.deepcopy(raw_rule_configs) if existed: for index, raw_rule_config in enumerate(raw_rule_configs): @@ -802,8 +792,6 @@ def _on_updated_rule_config(self, rule_config): else: raw_rule_configs.append(copy.deepcopy(rule_config._info)) self._info['rule_configs'] = raw_rule_configs - if 'rapid_configs' in self._info: - del self._info['rapid_configs'] self.touch() rollback = True try: @@ -831,11 +819,9 @@ def _on_deleted_rule_config(self, rule_config): del self._object_rule_configs[rule_config.id] else: raise ApiError("internal error: updated rule configuration does not belong to this policy") - old_raw_rule_configs = self._info.get("rule_configs", self._info.get("rapid_configs", [])) + old_raw_rule_configs = self._info.get("rule_configs", []) new_raw_rule_configs = [raw for raw in old_raw_rule_configs if raw['id'] != rule_config.id] self._info['rule_configs'] = new_raw_rule_configs - if 'rapid_configs' in self._info: - del self._info['rapid_configs'] self.touch() rollback = True try: From 52600984e4877b45bbc32117cc7784a84b6727b2 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Tue, 31 Jan 2023 12:38:21 -0700 Subject: [PATCH 023/143] moved a couple of functions that were in the wrong place --- src/cbc_sdk/platform/policies.py | 44 ++++++------------------ src/cbc_sdk/rest_api.py | 25 +++++++++++++- src/tests/unit/platform/test_policies.py | 4 +-- 3 files changed, 37 insertions(+), 36 deletions(-) diff --git a/src/cbc_sdk/platform/policies.py b/src/cbc_sdk/platform/policies.py index acfd41aed..b8ed7c185 100644 --- a/src/cbc_sdk/platform/policies.py +++ b/src/cbc_sdk/platform/policies.py @@ -421,7 +421,7 @@ def _add_rule_config(self, rule_config_data): Raises: InvalidObjectError: If the rule configuration data passed in is not valid. """ - new_rule_config = PolicyRuleConfig._create_rule_config(self._cb, None, rule_config_data) + new_rule_config = Policy._create_rule_config(self._cb, None, rule_config_data) new_rule_config.validate() self._new_rule_configs.append(new_rule_config) @@ -651,7 +651,7 @@ def object_rule_configs(self): """ if self._object_rule_configs_need_load: cfgs = self._info.get("rule_configs", []) - ruleconfigobjects = [PolicyRuleConfig._create_rule_config(self._cb, self, cfg) for cfg in cfgs] + ruleconfigobjects = [self._create_rule_config(self._cb, self, cfg) for cfg in cfgs] self._object_rule_configs = dict([(rconf.id, rconf) for rconf in ruleconfigobjects]) self._object_rule_configs_need_load = False return self._object_rule_configs @@ -673,26 +673,19 @@ def valid_rule_configs(self): for k, v in self._ruleconfig_presentation.items()} @classmethod - def get_ruleconfig_parameter_schema_directly(cls, cb, ruleconfig_id): + def _create_rule_config(cls, cb, parent, data): """ - Returns the parameter schema for a specified rule configuration. + Creates a PolicyRuleConfig object, or specialized subclass thereof, from a block of data in the policy. Args: cb (BaseAPI): Reference to API object used to communicate with the server. - ruleconfig_id (str): The rule configuration ID (UUID). + parent (Policy): The "parent" policy of this rule configuration. + data (dict): Initial data used to populate the rule configuration. Returns: - dict: The parameter schema for this particular rule configuration. - - Raises: - InvalidObjectError: If the rule configuration ID is not valid. + PolicyRuleConfig: The new object. """ - url = f"/policyservice/v1/orgs/{cb.credentials.org_key}/rule_configs/{ruleconfig_id}/parameters/schema" - try: - result = cb.get_object(url) - return result.get('properties', {}) - except ServerError: - raise InvalidObjectError(f"invalid rule config ID {ruleconfig_id}") + return PolicyRuleConfig(cb, parent, data.get("id", None), data, False, True) def get_ruleconfig_parameter_schema(self, ruleconfig_id): """ @@ -731,7 +724,7 @@ def get_ruleconfig_parameter_schema(self, ruleconfig_id): schema[item['name']] = schema_item return schema else: - return self.get_ruleconfig_parameter_schema_directly(self._cb, ruleconfig_id) + return self._cb.get_policy_ruleconfig_parameter_schema(ruleconfig_id) def _on_updated_rule(self, rule): """ @@ -921,7 +914,7 @@ def add_rule_config(self, new_rule_config): Args: new_rule_config (dict): The new rule configuration to add to this Policy. """ - new_obj = PolicyRuleConfig._create_rule_config(self._cb, self, new_rule_config) + new_obj = self._create_rule_config(self._cb, self, new_rule_config) new_obj.touch() new_obj.save() @@ -1297,21 +1290,6 @@ def __init__(self, cb, parent, model_unique_id=None, initial_data=None, force_in if model_unique_id is None: self.touch(True) - @classmethod - def _create_rule_config(cls, cb, parent, data): - """ - Creates a PolicyRuleConfig object, or specialized subclass thereof, from a block of data in the policy. - - Args: - cb (BaseAPI): Reference to API object used to communicate with the server. - parent (Policy): The "parent" policy of this rule configuration. - data (dict): Initial data used to populate the rule configuration. - - Returns: - PolicyRuleConfig: The new object. - """ - return PolicyRuleConfig(cb, parent, data.get("id", None), data, False, True) - def _refresh(self): """ Refreshes the rule configuration object from the server. @@ -1405,7 +1383,7 @@ def validate(self): # validate parameters if self._parent is None: - parameter_validations = Policy.get_ruleconfig_parameter_schema_directly(self._cb, self._model_unique_id) + parameter_validations = self._cb.get_policy_ruleconfig_parameter_schema(self._model_unique_id) else: parameter_validations = self._parent.get_ruleconfig_parameter_schema(self._model_unique_id) my_parameters = self._info.get('parameters', {}) diff --git a/src/cbc_sdk/rest_api.py b/src/cbc_sdk/rest_api.py index 82784dea6..6e12c96fb 100644 --- a/src/cbc_sdk/rest_api.py +++ b/src/cbc_sdk/rest_api.py @@ -14,7 +14,7 @@ """Definition of the CBCloudAPI object, the core object for interacting with the Carbon Black Cloud SDK.""" from cbc_sdk.connection import BaseAPI -from cbc_sdk.errors import ApiError, CredentialError, ServerError +from cbc_sdk.errors import ApiError, CredentialError, ServerError, InvalidObjectError from cbc_sdk.live_response_api import LiveResponseSessionManager from cbc_sdk.audit_remediation import Run, RunHistory from cbc_sdk.enterprise_edr.threat_intelligence import ReportSeverity @@ -489,3 +489,26 @@ def process_limits(self): self.credentials.org_key ) return self.get_object(url) + + # --- Policies + + def get_policy_ruleconfig_parameter_schema(self, ruleconfig_id): + """ + Returns the parameter schema for a specified rule configuration. + + Args: + cb (BaseAPI): Reference to API object used to communicate with the server. + ruleconfig_id (str): The rule configuration ID (UUID). + + Returns: + dict: The parameter schema for this particular rule configuration. + + Raises: + InvalidObjectError: If the rule configuration ID is not valid. + """ + url = f"/policyservice/v1/orgs/{self.credentials.org_key}/rule_configs/{ruleconfig_id}/parameters/schema" + try: + result = self.get_object(url) + return result.get('properties', {}) + except ServerError: + raise InvalidObjectError(f"invalid rule config ID {ruleconfig_id}") diff --git a/src/tests/unit/platform/test_policies.py b/src/tests/unit/platform/test_policies.py index 0125a3331..2e6e77e81 100644 --- a/src/tests/unit/platform/test_policies.py +++ b/src/tests/unit/platform/test_policies.py @@ -517,7 +517,7 @@ def param_schema(uri, query_params, default): cbcsdk_mock.mock_request('GET', f"/policyservice/v1/orgs/test/rule_configs/{initial_data['id']}/parameters/schema", param_schema) api = cbcsdk_mock.api - rule_config = PolicyRuleConfig._create_rule_config(api, None, initial_data) + rule_config = Policy._create_rule_config(api, None, initial_data) with handler as h: rule_config.validate() if message is not None: @@ -598,7 +598,7 @@ def on_put(url, body, **kwargs): del policy_data['rule_configs'][rule_config_data1[0][0]] policy = Policy(api, 65536, policy_data, False, True) rule_config_count = len(policy.object_rule_configs) - new_rule_config = PolicyRuleConfig._create_rule_config(api, policy, rule_config_data) + new_rule_config = Policy._create_rule_config(api, policy, rule_config_data) new_rule_config.touch() with handler: new_rule_config.save() From b74dd5b352de1fb0dc604ff5b9b7f73b6106b960 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Tue, 31 Jan 2023 13:55:01 -0700 Subject: [PATCH 024/143] rule parameter validation now uses jsonschema --- requirements.txt | 1 + src/cbc_sdk/platform/policies.py | 23 +++++++++-------------- src/cbc_sdk/rest_api.py | 5 ++--- src/tests/unit/platform/test_policies.py | 13 ++++++------- 4 files changed, 18 insertions(+), 24 deletions(-) diff --git a/requirements.txt b/requirements.txt index e913ae859..1138b55d4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,7 @@ python-dateutil schema solrq validators +jsonschema keyring;platform_system=='Darwin' boto3 diff --git a/src/cbc_sdk/platform/policies.py b/src/cbc_sdk/platform/policies.py index b8ed7c185..0784b1da2 100644 --- a/src/cbc_sdk/platform/policies.py +++ b/src/cbc_sdk/platform/policies.py @@ -14,6 +14,7 @@ """Policy implementation as part of Platform API""" import copy import json +import jsonschema from cbc_sdk.base import MutableBaseModel, BaseQuery, IterableQueryMixin, AsyncQueryMixin from cbc_sdk.errors import ApiError, ServerError, InvalidObjectError @@ -697,7 +698,7 @@ def get_ruleconfig_parameter_schema(self, ruleconfig_id): ruleconfig_id (str): The rule configuration ID (UUID). Returns: - dict: The parameter schema for this particular rule configuration. + dict: The parameter schema for this particular rule configuration (a JSON schema). Raises: InvalidObjectError: If the rule configuration ID is not valid. @@ -722,7 +723,7 @@ def get_ruleconfig_parameter_schema(self, ruleconfig_id): if 'type' not in schema_item: # fallback to string if nothing specified schema_item['type'] = 'string' schema[item['name']] = schema_item - return schema + return {'type': 'object', 'properties': schema} else: return self._cb.get_policy_ruleconfig_parameter_schema(ruleconfig_id) @@ -1387,18 +1388,12 @@ def validate(self): else: parameter_validations = self._parent.get_ruleconfig_parameter_schema(self._model_unique_id) my_parameters = self._info.get('parameters', {}) - for k, v in parameter_validations.items(): - if k in my_parameters: - if v['type'] == 'string': - if not isinstance(my_parameters[k], str): - raise InvalidObjectError(f"rule configuration parameter '{k}' is not a string") - if ('enum' in v) and (my_parameters[k] not in v['enum']): - raise InvalidObjectError(f"invalid value '{my_parameters[k]}' " - f"for rule configuration parameter '{k}'") - else: - raise ApiError(f"internal error: unknown parameter type {v['type']}") - else: - my_parameters[k] = v['default'] + try: + jsonschema.validate(instance=my_parameters, schema=parameter_validations) + except jsonschema.ValidationError as e: + raise InvalidObjectError(f"parameter error: {e.message}", e) + except jsonschema.exceptions.SchemaError as e: + raise ApiError(f"internal error: {e.message}", e) self._info['parameters'] = my_parameters @property diff --git a/src/cbc_sdk/rest_api.py b/src/cbc_sdk/rest_api.py index 6e12c96fb..12d346128 100644 --- a/src/cbc_sdk/rest_api.py +++ b/src/cbc_sdk/rest_api.py @@ -501,14 +501,13 @@ def get_policy_ruleconfig_parameter_schema(self, ruleconfig_id): ruleconfig_id (str): The rule configuration ID (UUID). Returns: - dict: The parameter schema for this particular rule configuration. + dict: The parameter schema for this particular rule configuration (as a JSON schema). Raises: InvalidObjectError: If the rule configuration ID is not valid. """ url = f"/policyservice/v1/orgs/{self.credentials.org_key}/rule_configs/{ruleconfig_id}/parameters/schema" try: - result = self.get_object(url) - return result.get('properties', {}) + return self.get_object(url) except ServerError: raise InvalidObjectError(f"invalid rule config ID {ruleconfig_id}") diff --git a/src/tests/unit/platform/test_policies.py b/src/tests/unit/platform/test_policies.py index 2e6e77e81..27c704b54 100644 --- a/src/tests/unit/platform/test_policies.py +++ b/src/tests/unit/platform/test_policies.py @@ -497,15 +497,16 @@ def test_rule_delete_is_new(cb): BASIC_CONFIG_TEMPLATE_RETURN, does_not_raise(), None), ({"id": "88b19232-7ebb-48ef-a198-2a75a282de5d", "name": "Privilege Escalation", "inherited_from": "", "category": "core_prevention", "parameters": {"WindowsAssignmentMode": "BLOCK"}}, - TEMPLATE_RETURN_BOGUS_TYPE, pytest.raises(ApiError), "internal error: unknown parameter type bogus"), + TEMPLATE_RETURN_BOGUS_TYPE, pytest.raises(ApiError), + "internal error: 'bogus' is not valid under any of the given schemas"), ({"id": "88b19232-7ebb-48ef-a198-2a75a282de5d", "name": "Privilege Escalation", "inherited_from": "", "category": "core_prevention", "parameters": {"WindowsAssignmentMode": 666}}, BASIC_CONFIG_TEMPLATE_RETURN, pytest.raises(InvalidObjectError), - "rule configuration parameter 'WindowsAssignmentMode' is not a string"), + "parameter error: 666 is not of type 'string'"), ({"id": "88b19232-7ebb-48ef-a198-2a75a282de5d", "name": "Privilege Escalation", "inherited_from": "", "category": "core_prevention", "parameters": {"WindowsAssignmentMode": "BOGUSVALUE"}}, BASIC_CONFIG_TEMPLATE_RETURN, pytest.raises(InvalidObjectError), - "invalid value 'BOGUSVALUE' for rule configuration parameter 'WindowsAssignmentMode'"), + "parameter error: 'BOGUSVALUE' is not one of ['REPORT', 'BLOCK']"), ]) def test_rule_config_validate(cbcsdk_mock, initial_data, param_schema_return, handler, message): """Tests rule configuration validation.""" @@ -537,12 +538,10 @@ def param_schema(uri, query_params, default): None, does_not_raise(), None), ({"id": "88b19232-7ebb-48ef-a198-2a75a282de5d", "name": "Privilege Escalation", "inherited_from": "", "category": "core_prevention", "parameters": {"WindowsAssignmentMode": 666}}, - None, pytest.raises(InvalidObjectError), - "rule configuration parameter 'WindowsAssignmentMode' is not a string"), + None, pytest.raises(InvalidObjectError), "parameter error: 666 is not of type 'string'"), ({"id": "88b19232-7ebb-48ef-a198-2a75a282de5d", "name": "Privilege Escalation", "inherited_from": "", "category": "core_prevention", "parameters": {"WindowsAssignmentMode": "BOGUSVALUE"}}, - None, pytest.raises(InvalidObjectError), - "invalid value 'BOGUSVALUE' for rule configuration parameter 'WindowsAssignmentMode'"), + None, pytest.raises(InvalidObjectError), "parameter error: 'BOGUSVALUE' is not one of ['REPORT', 'BLOCK']"), ]) def test_rule_config_validate_inside_policy(cbcsdk_mock, new_data, get_id, handler, message): """Tests rule configuration validation when it's part of a policy.""" From 3975c0ee738b48e861591b78561c03081f1e6e78 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Fri, 3 Feb 2023 14:27:14 -0700 Subject: [PATCH 025/143] changed permissions entries to DELETE as requested --- src/cbc_sdk/platform/policies.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cbc_sdk/platform/policies.py b/src/cbc_sdk/platform/policies.py index 0784b1da2..a751620b6 100644 --- a/src/cbc_sdk/platform/policies.py +++ b/src/cbc_sdk/platform/policies.py @@ -1267,7 +1267,7 @@ class PolicyRuleConfig(MutableBaseModel): To update a PolicyRuleConfig, change the values of its property fields, then call its save() method. This requires the org.policies(UPDATE) permission. - To delete an existing PolicyRuleConfig, call its delete() method. This requires the org.policies(UPDATE) permission. + To delete an existing PolicyRuleConfig, call its delete() method. This requires the org.policies(DELETE) permission. """ primary_key = "id" @@ -1323,7 +1323,7 @@ def _delete_object(self): Deletes this rule configuration object from the policy on the server. Required Permissions: - org.policies(UPDATE) + org.policies(DELETE) """ was_deleted = False try: From 0ff89ca2034334755ffabcfdf773388f46eddadc Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Fri, 3 Feb 2023 14:24:29 -0700 Subject: [PATCH 026/143] partial script implementation --- examples/platform/export_users_grants.py | 45 ++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 examples/platform/export_users_grants.py diff --git a/examples/platform/export_users_grants.py b/examples/platform/export_users_grants.py new file mode 100644 index 000000000..3775b6f53 --- /dev/null +++ b/examples/platform/export_users_grants.py @@ -0,0 +1,45 @@ +#!/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. + +"""Export users with their grant information""" + +import sys +from cbc_sdk.helpers import build_cli_parser, get_cb_cloud_object +from cbc_sdk.platform import User, Grant + + +def main(): + parser = build_cli_parser('Export User and Grant Information') + + args = parser.parse_args() + cb = get_cb_cloud_object(args) + + # Obtain a list of users paired with their grants. + user_query = cb.select(User); + all_users = {user.urn: user for user in user_query} + grant_query = cb.select(Grant) + for user in all_users.values(): + grant_query.add_principal(user.urn, user.org_urn) + paired_user_grants = [(all_users[g.principal], g) for g in grant_query if g.principal in all_users] + + # TODO: filter by role + + # TODO: sort? + + # TODO: extract data and give result + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) + From 132d945381d7c8a06dffd08c5c6b60a0ce3b5606 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Fri, 3 Feb 2023 15:51:54 -0700 Subject: [PATCH 027/143] remaining implementation of script, not tested yet --- examples/platform/export_users_grants.py | 121 ++++++++++++++++++++++- 1 file changed, 118 insertions(+), 3 deletions(-) diff --git a/examples/platform/export_users_grants.py b/examples/platform/export_users_grants.py index 3775b6f53..9886aef50 100644 --- a/examples/platform/export_users_grants.py +++ b/examples/platform/export_users_grants.py @@ -13,16 +13,105 @@ """Export users with their grant information""" import sys +import copy +import json +import csv +from io import StringIO from cbc_sdk.helpers import build_cli_parser, get_cb_cloud_object from cbc_sdk.platform import User, Grant +CSV_FIELDNAMES = ["login_id", "login_name", "email", "phone", "first_name", "last_name", "urn", + "grant_created", "grant_updated", "grant_created_by", "grant_updated_by", "roles", "profiles"] + + +def matches_roles(grant, roles): + """ + Determines if the given grant matches any of the specified roles. + + Args: + grant (Grant): A grant to be tested. + roles (set[str]): A set of roles to test against. + + Returns: + bool: True if the grant has any of the specified roles, False if not. + """ + if grant.roles: + if not set(grant.roles).isdisjoint(roles): + return True + if grant.profiles: + for profile in grant.profiles: + if profile.roles: + if not set(profile.roles).isdisjoint(roles): + return True + return False + + +def extract_row(user, grant): + """ + Folds data from a User and a Grant into a single dict full of information. + + Args: + user (User): The user to extract data from. + grant (Grant): The grant to extract data from. + + Returns: + dict: The dictionary containing data extracted from the User and the Grant. + """ + rc = {"login_id": user.login_id, "login_name": user.login_name, "email": user.email, "phone": user.phone, + "first_name": user.first_name, "last_name": user.last_name, "urn": user.urn, + "grant_created": grant.create_time, "grant_updated": grant.update_time, "grant_created_by": grant.created_by, + "grant_updated_by": grant.updated_by} + roles = [] + profiles = [] + if grant.roles: + roles.extend(grant.roles) + if grant.profiles: + for profile in grant.profiles: + profiles.append(profile.profile_uuid) + if profile.roles: + roles.extend(profile.roles) + rc["roles"] = roles + rc["profiles"] = profiles + return rc + + +def flatten_row(row): + """ + "Flattens" the given row by turning its "roles" and "profiles" arrays into pipe-delimited text strings. + + Args: + row (dict): The row to be flattened. + + Returns: + dict: The flattened row. + """ + rc = copy.deepcopy(row) + rc["roles"] = "|".join(row["roles"]) + rc["profiles"] = "|".join(row["profiles"]) + return rc + + def main(): parser = build_cli_parser('Export User and Grant Information') + parser.add_argument('-r', '--role', action='append', nargs='+', + help="If specified, users returned will match at least one of these roles.") + parser.add_argument('-o', '--output', + help="File to output the exported data to; if not specified, standard output is used.") + group = parser.add_mutually_exclusive_group() + group.add_argument('-J', '--json', action='store_true', help="Specifies output in JSON format (default).") + group.add_argument('-C', '--csv', action='store_true', help="Specifies output in CSV format.") args = parser.parse_args() cb = get_cb_cloud_object(args) + if args.json: + output_type = 'JSON' + elif args.csv: + output_type = 'CSV' + else: + output_type = 'JSON' + # Obtain a list of users paired with their grants. user_query = cb.select(User); all_users = {user.urn: user for user in user_query} @@ -31,11 +120,37 @@ def main(): grant_query.add_principal(user.urn, user.org_urn) paired_user_grants = [(all_users[g.principal], g) for g in grant_query if g.principal in all_users] - # TODO: filter by role + # If specified, filter the list by roles. + if args.roles: + roleset = set(args.roles) + output_list = list(filter(lambda p: matches_roles(p[1], roleset), paired_user_grants)) + else: + output_list = paired_user_grants + + # extract data to JSON format + data_list = list(filter(lambda p: extract_row(p[0], p[1]), output_list)) - # TODO: sort? + if output_type == 'JSON': + if args.output: + with open(args.output, "w") as f: + f.write(json.dumps(data_list, indent=4)) + else: + print(json.dumps(data_list, indent=4)) + return 0 - # TODO: extract data and give result + # handle CSV output from here + rows_list = list(filter(lambda r: flatten_row(r), data_list)) + if args.output: + with open(args.output, "w", newline='') as stream: + writer = csv.DictWriter(stream, fieldnames=CSV_FIELDNAMES, extrasaction='ignore') + writer.writeheader() + writer.writerows(rows_list) + else: + with StringIO('', newline='') as stream: + writer = csv.DictWriter(stream, fieldnames=CSV_FIELDNAMES, extrasaction='ignore') + writer.writeheader() + writer.writerows(rows_list) + print(stream.getvalue()) return 0 From 4a7e91e8d78ebbdf59e9c32a456f2ceefcccda71 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Fri, 3 Feb 2023 15:56:22 -0700 Subject: [PATCH 028/143] a couple of map() operations were incorrectly specified as filter() --- examples/platform/export_users_grants.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/platform/export_users_grants.py b/examples/platform/export_users_grants.py index 9886aef50..408cd5cdb 100644 --- a/examples/platform/export_users_grants.py +++ b/examples/platform/export_users_grants.py @@ -128,7 +128,7 @@ def main(): output_list = paired_user_grants # extract data to JSON format - data_list = list(filter(lambda p: extract_row(p[0], p[1]), output_list)) + data_list = list(map(lambda p: extract_row(p[0], p[1]), output_list)) if output_type == 'JSON': if args.output: @@ -139,7 +139,7 @@ def main(): return 0 # handle CSV output from here - rows_list = list(filter(lambda r: flatten_row(r), data_list)) + rows_list = list(map(lambda r: flatten_row(r), data_list)) if args.output: with open(args.output, "w", newline='') as stream: writer = csv.DictWriter(stream, fieldnames=CSV_FIELDNAMES, extrasaction='ignore') From eb5d007b8a06c2bcdb0f6995bb24d597dab7f910 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Fri, 3 Feb 2023 15:57:31 -0700 Subject: [PATCH 029/143] didn't need a lambda with flatten_row --- examples/platform/export_users_grants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/platform/export_users_grants.py b/examples/platform/export_users_grants.py index 408cd5cdb..9c380e16a 100644 --- a/examples/platform/export_users_grants.py +++ b/examples/platform/export_users_grants.py @@ -139,7 +139,7 @@ def main(): return 0 # handle CSV output from here - rows_list = list(map(lambda r: flatten_row(r), data_list)) + rows_list = list(map(flatten_row, data_list)) if args.output: with open(args.output, "w", newline='') as stream: writer = csv.DictWriter(stream, fieldnames=CSV_FIELDNAMES, extrasaction='ignore') From 9723c775246f0928b29b310247f4505f4521510f Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Fri, 3 Feb 2023 16:27:02 -0700 Subject: [PATCH 030/143] a couple of places, we don't need to fully realize lists --- examples/platform/export_users_grants.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/platform/export_users_grants.py b/examples/platform/export_users_grants.py index 9c380e16a..39f09110d 100644 --- a/examples/platform/export_users_grants.py +++ b/examples/platform/export_users_grants.py @@ -123,7 +123,7 @@ def main(): # If specified, filter the list by roles. if args.roles: roleset = set(args.roles) - output_list = list(filter(lambda p: matches_roles(p[1], roleset), paired_user_grants)) + output_list = filter(lambda p: matches_roles(p[1], roleset), paired_user_grants) else: output_list = paired_user_grants @@ -139,7 +139,7 @@ def main(): return 0 # handle CSV output from here - rows_list = list(map(flatten_row, data_list)) + rows_list = map(flatten_row, data_list) if args.output: with open(args.output, "w", newline='') as stream: writer = csv.DictWriter(stream, fieldnames=CSV_FIELDNAMES, extrasaction='ignore') From 5440bdd6fdbd5bc5cd9581cea2e391dd9f3eea16 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Fri, 3 Feb 2023 16:28:56 -0700 Subject: [PATCH 031/143] simplified realizations even further --- examples/platform/export_users_grants.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/platform/export_users_grants.py b/examples/platform/export_users_grants.py index 39f09110d..099ed04b8 100644 --- a/examples/platform/export_users_grants.py +++ b/examples/platform/export_users_grants.py @@ -128,14 +128,14 @@ def main(): output_list = paired_user_grants # extract data to JSON format - data_list = list(map(lambda p: extract_row(p[0], p[1]), output_list)) + data_list = map(lambda p: extract_row(p[0], p[1]), output_list) if output_type == 'JSON': if args.output: with open(args.output, "w") as f: - f.write(json.dumps(data_list, indent=4)) + f.write(json.dumps(list(data_list), indent=4)) else: - print(json.dumps(data_list, indent=4)) + print(json.dumps(list(data_list), indent=4)) return 0 # handle CSV output from here From cc2314ee4722d43bcc28e689d9b4b37d5025e6e6 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Mon, 6 Feb 2023 17:03:25 -0700 Subject: [PATCH 032/143] worked the kinks out of the script --- examples/platform/export_users_grants.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/examples/platform/export_users_grants.py b/examples/platform/export_users_grants.py index 099ed04b8..5506f3b47 100644 --- a/examples/platform/export_users_grants.py +++ b/examples/platform/export_users_grants.py @@ -41,8 +41,9 @@ def matches_roles(grant, roles): return True if grant.profiles: for profile in grant.profiles: - if profile.roles: - if not set(profile.roles).isdisjoint(roles): + profile_roles = profile.get("roles", []) + if profile_roles: + if not set(profile_roles).isdisjoint(roles): return True return False @@ -68,9 +69,10 @@ def extract_row(user, grant): roles.extend(grant.roles) if grant.profiles: for profile in grant.profiles: - profiles.append(profile.profile_uuid) - if profile.roles: - roles.extend(profile.roles) + profiles.append(profile["profile_uuid"]) + profile_roles = profile.get("roles", []) + if profile_roles: + roles.extend(profile_roles) rc["roles"] = roles rc["profiles"] = profiles return rc @@ -120,9 +122,9 @@ def main(): grant_query.add_principal(user.urn, user.org_urn) paired_user_grants = [(all_users[g.principal], g) for g in grant_query if g.principal in all_users] - # If specified, filter the list by roles. - if args.roles: - roleset = set(args.roles) + # If specified, filter the list by roles. Note that "args.roles" is actually a list of lists. + if args.role: + roleset = set([item for role_list in args.role for item in role_list]) output_list = filter(lambda p: matches_roles(p[1], roleset), paired_user_grants) else: output_list = paired_user_grants From 6c2f143a0a459759b8d6947f202f8b9627930d4d Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Tue, 7 Feb 2023 15:19:18 -0700 Subject: [PATCH 033/143] reformatted the JSON output so it shows all the profile data; CSV still smashes it --- examples/platform/export_users_grants.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/examples/platform/export_users_grants.py b/examples/platform/export_users_grants.py index 5506f3b47..6ed82a197 100644 --- a/examples/platform/export_users_grants.py +++ b/examples/platform/export_users_grants.py @@ -68,11 +68,7 @@ def extract_row(user, grant): if grant.roles: roles.extend(grant.roles) if grant.profiles: - for profile in grant.profiles: - profiles.append(profile["profile_uuid"]) - profile_roles = profile.get("roles", []) - if profile_roles: - roles.extend(profile_roles) + profiles = [copy.deepcopy(profile) for profile in grant.profiles] rc["roles"] = roles rc["profiles"] = profiles return rc @@ -89,8 +85,15 @@ def flatten_row(row): dict: The flattened row. """ rc = copy.deepcopy(row) - rc["roles"] = "|".join(row["roles"]) - rc["profiles"] = "|".join(row["profiles"]) + flat_roles = rc["roles"] + flat_profiles = [] + for profile in rc["profiles"]: + flat_profiles.append(profile["profile_uuid"]) + profile_roles = profile.get("roles", []) + if profile_roles: + flat_roles.extend(profile_roles) + rc["roles"] = "|".join(flat_roles) + rc["profiles"] = "|".join(flat_profiles) return rc From 86cf9882954829a020b2f948f2a9f0b1f88635d0 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Wed, 8 Feb 2023 08:19:29 -0700 Subject: [PATCH 034/143] deflake8'd --- examples/platform/export_users_grants.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/platform/export_users_grants.py b/examples/platform/export_users_grants.py index 6ed82a197..6bf4f747f 100644 --- a/examples/platform/export_users_grants.py +++ b/examples/platform/export_users_grants.py @@ -98,6 +98,7 @@ def flatten_row(row): def main(): + """The main function of the script; executes the search, filters and presents the results.""" parser = build_cli_parser('Export User and Grant Information') parser.add_argument('-r', '--role', action='append', nargs='+', help="If specified, users returned will match at least one of these roles.") @@ -118,7 +119,7 @@ def main(): output_type = 'JSON' # Obtain a list of users paired with their grants. - user_query = cb.select(User); + user_query = cb.select(User) all_users = {user.urn: user for user in user_query} grant_query = cb.select(Grant) for user in all_users.values(): @@ -162,4 +163,3 @@ def main(): if __name__ == "__main__": sys.exit(main()) - From a6ab6d2d5ad3d38b531b62283854ef016d6a22c6 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Mon, 25 Jul 2022 16:20:22 -0600 Subject: [PATCH 035/143] fixed unit test that was broken when I fixed the UAT --- src/tests/unit/enterprise_edr/test_enterprise_edr_threatintel.py | 1 + 1 file changed, 1 insertion(+) 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..dbef2c545 100644 --- a/src/tests/unit/enterprise_edr/test_enterprise_edr_threatintel.py +++ b/src/tests/unit/enterprise_edr/test_enterprise_edr_threatintel.py @@ -86,6 +86,7 @@ def test_watchlist_save(cbcsdk_mock): cbcsdk_mock.mock_request("POST", "/threathunter/watchlistmgr/v3/orgs/test/watchlists", WATCHLIST_GET_SPECIFIC_RESP) watchlist = Watchlist(api, model_unique_id=None, initial_data=CREATE_WATCHLIST_DATA) watchlist.validate() + print(f"{watchlist._model_unique_id =}") watchlist.save() # if Watchlist response is missing a required field per enterprise_edr.models.Watchlist, raise InvalidObjectError From 73faa1c9fe9fadf9d89285372d8af75298baddc8 Mon Sep 17 00:00:00 2001 From: Jasmine Clark <89797061+jclark-vmware@users.noreply.github.com> Date: Mon, 9 Jan 2023 13:37:16 -0500 Subject: [PATCH 036/143] dismiss vuln --- .../platform/vulnerability_assessment.py | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/src/cbc_sdk/platform/vulnerability_assessment.py b/src/cbc_sdk/platform/vulnerability_assessment.py index 19ad2a506..ecfe48e6b 100644 --- a/src/cbc_sdk/platform/vulnerability_assessment.py +++ b/src/cbc_sdk/platform/vulnerability_assessment.py @@ -268,6 +268,8 @@ class VulnerabilityQuery(BaseQuery, QueryBuilderSupportMixin, "NOT_SUPPORTED", "CANCELLED", "IN_PROGRESS", "ACTIVE", "COMPLETED"] VALID_DIRECTIONS = ["ASC", "DESC"] + VALID_DISMISS_REASON = ["FALSE_POSITIVE", "RESOLUTION_DEFERRED", "NON_ISSUE", "NON_CRITICAL_ASSET", + "UNDER_RESOLUTION", "OTHER"] def __init__(self, doc_class, cb, device=None): """ @@ -320,6 +322,22 @@ def add_criteria(self, key, value, operator='EQUALS'): self._update_criteria(key, value, operator) return self + def set_deployment_type(self, deployment_type, operator): + """ + Restricts the vulnerabilities that this query is performed on to the specified deployment type. + + Args: + deployment_type (str): deployment type ("ENDPOINT", "AWS") + operator (str): logic operator to apply to property value. + + Returns: + VulnerabilityQuery: This instance. + """ + if not deployment_type: + raise ApiError("Invalid deployment type") + self._update_criteria("deployment_type", deployment_type, operator) + return self + def set_device_type(self, device_type, operator): """ Restricts the vulnerabilities that this query is performed on to the specified device type. @@ -400,6 +418,22 @@ def set_os_name(self, os_name, operator): self._update_criteria("os_name", os_name, operator) return self + def set_os_product_id(self, os_product_id, operator): + """ + Restricts the vulnerabilities that this query is performed on to the specified os_product_id. + + Args: + os_product_id (str): os_product_id. + operator (str): logic operator to apply to property value. + + Returns: + VulnerabilityQuery: This instance. + """ + if not os_product_id: + raise ApiError("Invalid os product id") + self._update_criteria("os_product_id", os_product_id, operator) + return self + def set_os_type(self, os_type, operator): """ Restricts the vulnerabilities that this query is performed on to the specified os type. @@ -538,6 +572,7 @@ def set_last_sync_ts(self, last_sync_ts, operator): } } """ + def _update_criteria(self, key, value, operator, overwrite=False): """ Updates a list of criteria being collected for a query, by setting or appending items. @@ -694,6 +729,71 @@ def sort_by(self, key, direction="ASC"): self._sortcriteria = {"field": key, "order": direction} return self + def perform_action(self, cve, action_type, ): + additional = "/vulnerabilities/{}/actions".format(cve) + url = self._doc_class.urlobject.format(self._cb.credentials.org_key) + additional + + request = {"action_type": action_type, "criteria": self._criteria, "query": self._query_builder._collapse(), + "rows": 100} + + resp = self._cb.post_object(url, body=request) + result = resp.json() + self._total_results = result["num_found"] + self._count_valid = True + results = result.get("results", []) + return [self._doc_class(self._cb, item.get('vuln_info', {}).get('cve_id', None), initial_data=item) + for item in results] + return self + + def dismiss(self, cve, dismiss_reason, dismiss_until=None, notes=None, rule_ids={}): + if dismiss_reason not in VulnerabilityQuery.VALID_DISMISS_REASON: + raise ApiError("Invalid dismiss_reason") + if dismiss_reason == "OTHER" and len(notes) == 0: + raise ApiError("Notes are required when dismiss_reason is OTHER") + if self._criteria.get("os_product_id", None) is None: + raise ApiError("os_product_id is required") + + additional = "/vulnerabilities/{}/actions".format(cve) + url = self._doc_class.urlobject.format(self._cb.credentials.org_key) + additional + + request = {"action_type": "DISMISS", "dismiss_reason": dismiss_reason, "dismiss_until": dismiss_until, + "notes": notes, "rule_ids": rule_ids, + "criteria": self._criteria} + + resp = self._cb.post_object(url, body=request) + result = resp.json() + results = result.get("results", []) + return results + + def undismiss(self, cve, rule_ids={}): + additional = "/vulnerabilities/{}/actions".format(cve) + url = self._doc_class.urlobject.format(self._cb.credentials.org_key) + additional + + request = {"action_type": "UNDISMISS", "rule_ids": rule_ids} + + resp = self._cb.post_object(url, body=request) + result = resp.json() + results = result.get("results", []) + return results + + def dismiss_edit(self, cve, dismiss_reason, dismiss_until=None, notes=None, rule_ids={}): + if dismiss_reason not in VulnerabilityQuery.VALID_DISMISS_REASON: + raise ApiError("Invalid dismiss_reason") + if dismiss_reason == "OTHER" and len(notes) == 0: + raise ApiError("Notes are required when dismiss_reason is OTHER") + + additional = "/vulnerabilities/{}/actions".format(cve) + url = self._doc_class.urlobject.format(self._cb.credentials.org_key) + additional + + request = {"action_type": "DISMISS_EDIT", "dismiss_reason": dismiss_reason, "dismiss_until": dismiss_until, + "notes": notes, "rule_ids": rule_ids, + "criteria": self._criteria} + + resp = self._cb.post_object(url, body=request) + result = resp.json() + results = result.get("results", []) + return results + class VulnerabilityAssetViewQuery(VulnerabilityQuery): """Represents a query that is used fetch the Vulnerability Asset View""" From d82bd2b48e8f2bcb15a32ab49694f5e3be58e4f2 Mon Sep 17 00:00:00 2001 From: Alex Van Brunt Date: Thu, 2 Feb 2023 15:47:47 -0700 Subject: [PATCH 037/143] Add vulnerability perform_action and set_visibility --- .../platform/vulnerability_assessment.py | 185 ++++++++++-------- .../fixtures/platform/mock_vulnerabilities.py | 72 ++++++- .../platform/test_vulnerability_assessment.py | 159 ++++++++++++++- 3 files changed, 329 insertions(+), 87 deletions(-) diff --git a/src/cbc_sdk/platform/vulnerability_assessment.py b/src/cbc_sdk/platform/vulnerability_assessment.py index ecfe48e6b..39e128056 100644 --- a/src/cbc_sdk/platform/vulnerability_assessment.py +++ b/src/cbc_sdk/platform/vulnerability_assessment.py @@ -104,6 +104,50 @@ def _query_implementation(cls, cb, **kwargs): """ return VulnerabilityQuery(cls, cb) + def perform_action(self, type, reason=None, notes=None): + """ + Take an action on to manage the Vulnerability. + + Args: + type (str): The type of action. (supports DISMISS, DISMISS_EDIT, or UNDISMISS) + reason (str): The reason the vulnerabilty is dismissed. Required when type is DISMISS or DISMISS_EDIT. + (supports FALSE_POSITIVE, RESOLUTION_DEFERRED, NON_ISSUE, NON_CRITICAL_ASSET, UNDER_RESOLUTION, OTHER) + notes (str): Notes to be associated with the dismissal. Required when reason is OTHER. + + Returns: + obj: The action response + + Raises: + ApiError: If the request is invalid or missing required properties + """ + url = self.urlobject.format(self._cb.credentials.org_key) + \ + "/vulnerabilities/{}/actions".format(self.vuln_info["cve_id"]) + + request = { + "action_type": type, + "dismiss_reason": reason, + "notes": notes + } + + if reason == "OTHER" and not isinstance(notes, str): + raise ApiError("Notes is required when reason is OTHER") + + if type == "UNDISMISS" or type == "DISMISS_EDIT": + if self.rule_id is None: + raise ApiError("Vulnerability is not dismissed") + request["rule_ids"] = [self.rule_id] + elif type == "DISMISS": + request["criteria"] = { + "os_product_id": { + "operator": "EQUALS", + "value": self.os_product_id + } + } + else: + raise ApiError(f"Vulnerability action: {type} not supported") + + return self._cb.post_object(url, body=request).json() + class AssetView(list): """Represents a list of Vulnerability for an organization.""" urlobject = "/vulnerability/assessment/api/v1/orgs/{}" @@ -132,7 +176,7 @@ def _query_implementation(cls, cb, **kwargs): **kwargs (dict): Not used, retained for compatibility. Returns: - VulnerabilityOrgSummaryQuery: The query object + VulnerabilityAssetViewQuery: The query object """ return VulnerabilityAssetViewQuery(cls, cb) @@ -180,6 +224,7 @@ class VulnerabilityOrgSummaryQuery(BaseQuery): """Represents a query that is used fetch the VulnerabiltitySummary""" VALID_SEVERITY = ["CRITICAL", "IMPORTANT", "MODERATE", "LOW"] + VALID_VISIBILITY = ["DISMISSED", "ACTIVE", "ALL"] def __init__(self, doc_class, cb, device=None): """ @@ -196,6 +241,7 @@ def __init__(self, doc_class, cb, device=None): self._vcenter_uuid = None self._severity = None + self._visibility = None def set_vcenter(self, vcenter_uuid): """ @@ -211,6 +257,21 @@ def set_vcenter(self, vcenter_uuid): self._vcenter_uuid = vcenter_uuid return self + def set_visibility(self, visibility): + """ + Restricts the vulnerabilities that this query is performed on to the specified visibility + + Args: + visibility (str): The visibility state of the vulnerabilty. (supports ACTIVE, ALL, DISMISSED) + + Returns: + VulnerabilityQuery: This instance. + """ + if visibility not in self.VALID_VISIBILITY: + raise ApiError(f"{visibility} is not a valid visibility. Supported visibilities {self.VALID_VISIBILITY}") + self._visibility = visibility + return self + def set_severity(self, severity): """ Restricts the vulnerability summary to a severity level @@ -245,6 +306,9 @@ def _perform_query(self): req_url = Vulnerability.OrgSummary.urlobject.format(self._cb.credentials.org_key) + url + if self._visibility: + req_url += f"?vulnerabilityVisibility={self._visibility}" + return self._doc_class(self._cb, initial_data=self._cb.get_object(req_url, query_params)) def submit(self): @@ -268,8 +332,7 @@ class VulnerabilityQuery(BaseQuery, QueryBuilderSupportMixin, "NOT_SUPPORTED", "CANCELLED", "IN_PROGRESS", "ACTIVE", "COMPLETED"] VALID_DIRECTIONS = ["ASC", "DESC"] - VALID_DISMISS_REASON = ["FALSE_POSITIVE", "RESOLUTION_DEFERRED", "NON_ISSUE", "NON_CRITICAL_ASSET", - "UNDER_RESOLUTION", "OTHER"] + VALID_VISIBILITY = ["DISMISSED", "ACTIVE", "ALL"] def __init__(self, doc_class, cb, device=None): """ @@ -292,6 +355,7 @@ def __init__(self, doc_class, cb, device=None): self.device = device self._vcenter_uuid = None + self._visibility = None def set_vcenter(self, vcenter_uuid): """ @@ -307,6 +371,21 @@ def set_vcenter(self, vcenter_uuid): self._vcenter_uuid = vcenter_uuid return self + def set_visibility(self, visibility): + """ + Restricts the vulnerabilities that this query is performed on to the specified visibility + + Args: + visibility (str): The visibility state of the vulnerabilty. (supports ACTIVE, ALL, DISMISSED) + + Returns: + VulnerabilityQuery: This instance. + """ + if visibility not in self.VALID_VISIBILITY: + raise ApiError(f"{visibility} is not a valid visibility. Supported visibilities {self.VALID_VISIBILITY}") + self._visibility = visibility + return self + def add_criteria(self, key, value, operator='EQUALS'): """ Restricts the vulnerabilities that this query is performed on to the specified key value pair. @@ -418,22 +497,6 @@ def set_os_name(self, os_name, operator): self._update_criteria("os_name", os_name, operator) return self - def set_os_product_id(self, os_product_id, operator): - """ - Restricts the vulnerabilities that this query is performed on to the specified os_product_id. - - Args: - os_product_id (str): os_product_id. - operator (str): logic operator to apply to property value. - - Returns: - VulnerabilityQuery: This instance. - """ - if not os_product_id: - raise ApiError("Invalid os product id") - self._update_criteria("os_product_id", os_product_id, operator) - return self - def set_os_type(self, os_type, operator): """ Restricts the vulnerabilities that this query is performed on to the specified os type. @@ -630,6 +693,9 @@ def _build_url(self, tail_end): additional = f"/vcenters/{self._vcenter_uuid}" + additional url = self._doc_class.urlobject.format(self._cb.credentials.org_key) + additional + tail_end + if self._visibility: + url += f"&vulnerabilityVisibility={self._visibility}" + return url def _count(self): @@ -729,71 +795,6 @@ def sort_by(self, key, direction="ASC"): self._sortcriteria = {"field": key, "order": direction} return self - def perform_action(self, cve, action_type, ): - additional = "/vulnerabilities/{}/actions".format(cve) - url = self._doc_class.urlobject.format(self._cb.credentials.org_key) + additional - - request = {"action_type": action_type, "criteria": self._criteria, "query": self._query_builder._collapse(), - "rows": 100} - - resp = self._cb.post_object(url, body=request) - result = resp.json() - self._total_results = result["num_found"] - self._count_valid = True - results = result.get("results", []) - return [self._doc_class(self._cb, item.get('vuln_info', {}).get('cve_id', None), initial_data=item) - for item in results] - return self - - def dismiss(self, cve, dismiss_reason, dismiss_until=None, notes=None, rule_ids={}): - if dismiss_reason not in VulnerabilityQuery.VALID_DISMISS_REASON: - raise ApiError("Invalid dismiss_reason") - if dismiss_reason == "OTHER" and len(notes) == 0: - raise ApiError("Notes are required when dismiss_reason is OTHER") - if self._criteria.get("os_product_id", None) is None: - raise ApiError("os_product_id is required") - - additional = "/vulnerabilities/{}/actions".format(cve) - url = self._doc_class.urlobject.format(self._cb.credentials.org_key) + additional - - request = {"action_type": "DISMISS", "dismiss_reason": dismiss_reason, "dismiss_until": dismiss_until, - "notes": notes, "rule_ids": rule_ids, - "criteria": self._criteria} - - resp = self._cb.post_object(url, body=request) - result = resp.json() - results = result.get("results", []) - return results - - def undismiss(self, cve, rule_ids={}): - additional = "/vulnerabilities/{}/actions".format(cve) - url = self._doc_class.urlobject.format(self._cb.credentials.org_key) + additional - - request = {"action_type": "UNDISMISS", "rule_ids": rule_ids} - - resp = self._cb.post_object(url, body=request) - result = resp.json() - results = result.get("results", []) - return results - - def dismiss_edit(self, cve, dismiss_reason, dismiss_until=None, notes=None, rule_ids={}): - if dismiss_reason not in VulnerabilityQuery.VALID_DISMISS_REASON: - raise ApiError("Invalid dismiss_reason") - if dismiss_reason == "OTHER" and len(notes) == 0: - raise ApiError("Notes are required when dismiss_reason is OTHER") - - additional = "/vulnerabilities/{}/actions".format(cve) - url = self._doc_class.urlobject.format(self._cb.credentials.org_key) + additional - - request = {"action_type": "DISMISS_EDIT", "dismiss_reason": dismiss_reason, "dismiss_until": dismiss_until, - "notes": notes, "rule_ids": rule_ids, - "criteria": self._criteria} - - resp = self._cb.post_object(url, body=request) - result = resp.json() - results = result.get("results", []) - return results - class VulnerabilityAssetViewQuery(VulnerabilityQuery): """Represents a query that is used fetch the Vulnerability Asset View""" @@ -932,6 +933,22 @@ def __init__(self, vulnerability, cb): self.vulnerability = vulnerability super().__init__(Device, cb) + def set_os_product_id(self, os_product_id, operator): + """ + Restricts the vulnerabilities that this query is performed on to the specified os_product_id. + + Args: + os_product_id (str): os_product_id. + operator (str): logic operator to apply to property value. + + Returns: + VulnerabilityQuery: This instance. + """ + if not os_product_id: + raise ApiError("Invalid os product id") + self._update_criteria("os_product_id", os_product_id, operator) + return self + def _build_url(self): """ Creates the URL to be used for an API call. diff --git a/src/tests/unit/fixtures/platform/mock_vulnerabilities.py b/src/tests/unit/fixtures/platform/mock_vulnerabilities.py index 1d0101f11..c02a689af 100644 --- a/src/tests/unit/fixtures/platform/mock_vulnerabilities.py +++ b/src/tests/unit/fixtures/platform/mock_vulnerabilities.py @@ -240,7 +240,77 @@ "device_count": 1, "affected_assets": [ "jdoe-windows_2012" - ] + ], + "rule_id": None, + "dismissed": False, + "dismiss_until": None, + "dismiss_reason": None, + "notes": None, + "dismissed_on": None, + "dismissed_by": None + } + ] +} + +GET_DISMISSED_VULNERABILITY_RESP = { + "num_found": 1, + "results": [ + { + "os_product_id": "90_5372", + "category": "APP", + "os_info": { + "os_type": "CENTOS", + "os_name": "CentOS Linux", + "os_version": "7.1.1503", + "os_arch": "x86_64" + }, + "product_info": { + "vendor": "CentOS", + "product": "python-libs", + "version": "2.7.5", + "release": "16.el7", + "arch": "x86_64" + }, + "vuln_info": { + "cve_id": "CVE-2014-4650", + "cve_description": "The CGIHTTPServer module in Python 2.7.5 and 3.3.4 does not properly handle...", + "risk_meter_score": 4.9, + "severity": "MODERATE", + "fixed_by": "0:2.7.5-34.el7", + "solution": None, + "created_at": "2020-02-20T17:15:00Z", + "nvd_link": "https://nvd.nist.gov/vuln/detail/CVE-2014-4650", + "cvss_access_complexity": "Low", + "cvss_access_vector": "Local access", + "cvss_authentication": "None required", + "cvss_availability_impact": "Partial", + "cvss_confidentiality_impact": "None", + "cvss_integrity_impact": "None", + "easily_exploitable": False, + "malware_exploitable": False, + "active_internet_breach": False, + "cvss_exploit_subscore": 3.9, + "cvss_impact_subscore": 2.9, + "cvss_vector": "AV:L/AC:L/Au:N/C:N/I:N/A:P/E:U/RL:OF/RC:C", + "cvss_v3_exploit_subscore": 3.9, + "cvss_v3_impact_subscore": 2.9, + "cvss_v3_vector": "CVSS:3.0/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H/E:U/RL:O/RC:C", + "cvss_score": 3.9, + "cvss_v3_score": 3.9 + }, + "device_count": 1, + "affected_assets": [ + "jdoe-windows_2012" + ], + "rule_id": 9061, + "dismissed": True, + "dismiss_until": None, + "dismiss_reason": "FALSE_POSITIVE", + "notes": None, + "created_by": "anonymous", + "updated_by": "anonymous", + "created_at": "2023-02-02T22:05:04.430281Z", + "updated_at": "2023-02-02T22:05:04.430281Z" } ] } diff --git a/src/tests/unit/platform/test_vulnerability_assessment.py b/src/tests/unit/platform/test_vulnerability_assessment.py index 1151b733e..855cbecb4 100644 --- a/src/tests/unit/platform/test_vulnerability_assessment.py +++ b/src/tests/unit/platform/test_vulnerability_assessment.py @@ -23,6 +23,7 @@ GET_VULNERABILITY_SUMMARY_ORG_LEVEL_PER_SEVERITY, GET_ASSET_VIEW_VUL_RESP, GET_VULNERABILITY_RESP, + GET_DISMISSED_VULNERABILITY_RESP, GET_AFFECTED_ASSETS_SPECIFIC_VULNERABILITY, GET_VULNERABILITY_RESP_MULTIPLE, GET_VULNERABILITY_RESP_MULTIPLE_SAME_CVE, @@ -52,10 +53,12 @@ def cbcsdk_mock(monkeypatch, cb): # ==================================== UNIT TESTS BELOW ==================================== def test_get_vulnerability_summary(cbcsdk_mock): """Tests get organizational level vulnerability summary""" - cbcsdk_mock.mock_request("GET", "/vulnerability/assessment/api/v1/orgs/test/vulnerabilities/summary", + cbcsdk_mock.mock_request("GET", + "/vulnerability/assessment/api/v1/orgs/test/vulnerabilities/summary" + "?vulnerabilityVisibility=ACTIVE", GET_VULNERABILITY_SUMMARY_ORG_LEVEL) api = cbcsdk_mock.api - vsummary = api.select(Vulnerability.OrgSummary).submit() + vsummary = api.select(Vulnerability.OrgSummary).set_visibility("ACTIVE").submit() assert vsummary.monitored_assets == 13 assert vsummary.severity_summary.get('ALL', None) @@ -494,3 +497,155 @@ def test_device_vulnerability_refresh(cbcsdk_mock): device = api.select(Device, 98765) result = device.vulnerability_refresh() assert result['device_id'] == 98765 + + +def test_dismiss_vulnerability(cbcsdk_mock): + """Test dismiss vulnerability""" + cbcsdk_mock.mock_request("POST", "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search", + GET_VULNERABILITY_RESP) + + def post_action(url, body, **kwargs): + assert body["criteria"]["os_product_id"]["value"] == GET_VULNERABILITY_RESP["results"][0]["os_product_id"] + return { + "results": [ + { + "rule_id": 9061, + "dismiss_until": None, + "dismiss_reason": "FALSE_POSITIVE", + "notes": None, + "created_by": "anonymous", + "updated_by": "anonymous", + "created_at": "2023-02-02T22:05:04.430281Z", + "updated_at": "2023-02-02T22:05:04.430281Z" + } + ] + } + + cbcsdk_mock.mock_request("POST", + "/vulnerability/assessment/api/v1/orgs/test/vulnerabilities/CVE-2014-4650/actions", + post_action) + + api = cbcsdk_mock.api + + vulnerability = api.select(Vulnerability, "CVE-2014-4650") + response = vulnerability.perform_action("DISMISS", "FALSE_POSITIVE") + assert response["results"][0]["rule_id"] == 9061 + + +def test_dismiss_edit_vulnerability(cbcsdk_mock): + """Test editting an already dismissed vulnerabilty""" + cbcsdk_mock.mock_request("POST", "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search", + GET_DISMISSED_VULNERABILITY_RESP) + + def post_action(url, body, **kwargs): + assert body.get("criteria", None) is None + assert isinstance(body["notes"], str) + assert body["rule_ids"][0] == 9061 + return { + "results": [ + { + "rule_id": 9061, + "dismiss_until": None, + "dismiss_reason": "OTHER", + "notes": "Needs more investigation", + "created_by": "anonymous", + "updated_by": "anonymous", + "created_at": "2023-02-02T22:05:04.430281Z", + "updated_at": "2023-02-02T22:05:04.430281Z" + } + ] + } + + cbcsdk_mock.mock_request("POST", + "/vulnerability/assessment/api/v1/orgs/test/vulnerabilities/CVE-2014-4650/actions", + post_action) + + api = cbcsdk_mock.api + + vulnerability = api.select(Vulnerability, "CVE-2014-4650") + response = vulnerability.perform_action("DISMISS_EDIT", "OTHER", "Needs more investigation") + assert response["results"][0]["dismiss_reason"] == "OTHER" + + +def test_undismiss_vulnerability(cbcsdk_mock): + """Test undismissing a dismissed vulnerability""" + cbcsdk_mock.mock_request("POST", "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search", + GET_DISMISSED_VULNERABILITY_RESP) + + def post_action(url, body, **kwargs): + assert body.get("criteria", None) is None + assert body["rule_ids"][0] == 9061 + return { + "results": [ + { + "rule_id": 9061, + "dismiss_until": None, + "dismiss_reason": None, + "notes": None, + "created_by": None, + "updated_by": None, + "created_at": None, + "updated_at": None + } + ] + } + + cbcsdk_mock.mock_request("POST", + "/vulnerability/assessment/api/v1/orgs/test/vulnerabilities/CVE-2014-4650/actions", + post_action) + + api = cbcsdk_mock.api + + vulnerability = api.select(Vulnerability, "CVE-2014-4650") + response = vulnerability.perform_action("UNDISMISS") + assert response["results"][0]["created_at"] is None + assert response["results"][0]["updated_at"] is None + + +def test_undismiss_vulnerability_not_dismissed(cbcsdk_mock): + """Test undismiss a vulnerability which has not be dismissed""" + cbcsdk_mock.mock_request("POST", "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search", + GET_VULNERABILITY_RESP) + + api = cbcsdk_mock.api + + vulnerability = api.select(Vulnerability, "CVE-2014-4650") + with pytest.raises(ApiError): + vulnerability.perform_action("UNDISMISS") + + +def test_dismiss_other_vulnerability_no_notes(cbcsdk_mock): + """Test dismiss vulnerability with reason OTHER and no notes""" + cbcsdk_mock.mock_request("POST", "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search", + GET_VULNERABILITY_RESP) + + api = cbcsdk_mock.api + + vulnerability = api.select(Vulnerability, "CVE-2014-4650") + with pytest.raises(ApiError): + vulnerability.perform_action("DISMISS", "OTHER") + + +def test_invalid_action_vulnerability(cbcsdk_mock): + """Test performing an invalid action on a vulnerability""" + cbcsdk_mock.mock_request("POST", "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search", + GET_VULNERABILITY_RESP) + + api = cbcsdk_mock.api + + vulnerability = api.select(Vulnerability, "CVE-2014-4650") + with pytest.raises(ApiError): + vulnerability.perform_action("INVALID") + + +def test_vulernability_visibility(cbcsdk_mock): + """Test vulnerability visibility query""" + cbcsdk_mock.mock_request("POST", + "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search" + "?dataForExport=true&vulnerabilityVisibility=ACTIVE", + GET_VULNERABILITY_RESP) + + api = cbcsdk_mock.api + + vulnerability = api.select(Vulnerability).add_criteria("cve_id", "CVE-2014-4650").set_visibility("ACTIVE") + vulnerability[0].os_product_id == GET_VULNERABILITY_RESP["results"][0]["os_product_id"] From e6faacc1f755c6c0357c7114cac9e4fed0eed856 Mon Sep 17 00:00:00 2001 From: Alex Van Brunt Date: Thu, 2 Feb 2023 17:05:46 -0700 Subject: [PATCH 038/143] Add vulnerability export --- .../platform/vulnerability_assessment.py | 44 +++++++++++++++++++ .../fixtures/platform/mock_vulnerabilities.py | 12 +++++ .../platform/test_vulnerability_assessment.py | 37 +++++++++++++++- 3 files changed, 91 insertions(+), 2 deletions(-) diff --git a/src/cbc_sdk/platform/vulnerability_assessment.py b/src/cbc_sdk/platform/vulnerability_assessment.py index 39e128056..0f72128df 100644 --- a/src/cbc_sdk/platform/vulnerability_assessment.py +++ b/src/cbc_sdk/platform/vulnerability_assessment.py @@ -773,6 +773,28 @@ def _run_async_query(self, context): return [self._doc_class(self._cb, item.get('vuln_info', {}).get('cve_id', None), initial_data=item) for item in results] + def export(self): + """ + Performs the query and export the results in the form of a Job. + + Returns: + Job: The export job. + """ + from cbc_sdk.platform import Job + url = self._build_url("/export?async=true") + + request = { + "criteria": self._criteria, + "query": self._query_builder._collapse() + } + + # Sort not supported for export + # if self._sortcriteria != {}: + # request["sort"] = [self._sortcriteria] + + resp = self._cb.post_object(url, body=request).json() + return Job(self._cb, resp["jobId"]) + def sort_by(self, key, direction="ASC"): """ Sets the sorting behavior on a query's results. @@ -916,6 +938,28 @@ def _run_async_query(self, context): if current >= self._total_results: return self._doc_class(self._cb, initial_data=results) + def export(self): + """ + Performs the query and export the results in the form of a Job. + + Returns: + Job: The export job. + """ + from cbc_sdk.platform import Job + url = self._build_url("/export?async=true") + + request = { + "criteria": self._criteria, + "query": self._query_builder._collapse() + } + + # Sort not supported for export + # if self._sortcriteria != {}: + # request["sort"] = [self._sortcriteria] + + resp = self._cb.post_object(url, body=request).json() + return Job(self._cb, resp["jobId"]) + class AffectedAssetQuery(VulnerabilityQuery): """Query Class for the Vulnerability""" diff --git a/src/tests/unit/fixtures/platform/mock_vulnerabilities.py b/src/tests/unit/fixtures/platform/mock_vulnerabilities.py index c02a689af..2739f3cc6 100644 --- a/src/tests/unit/fixtures/platform/mock_vulnerabilities.py +++ b/src/tests/unit/fixtures/platform/mock_vulnerabilities.py @@ -638,3 +638,15 @@ "results": [MOCK_WORKLOAD], "num_found": 1 } + +MOCK_VULNERABILITY_EXPORT_JOB = { + "id": 4677844, + "type": "EXTERNAL", + "job_parameters": { + "job_parameters": None + }, + "org_key": "7DESJ9GN", + "status": "COMPLETED", + "create_time": "2023-02-02T23:16:25.625583Z", + "last_update_time": "2023-02-02T23:16:29.079184Z" +} diff --git a/src/tests/unit/platform/test_vulnerability_assessment.py b/src/tests/unit/platform/test_vulnerability_assessment.py index 855cbecb4..a52d88af2 100644 --- a/src/tests/unit/platform/test_vulnerability_assessment.py +++ b/src/tests/unit/platform/test_vulnerability_assessment.py @@ -18,7 +18,7 @@ from cbc_sdk.rest_api import CBCloudAPI from cbc_sdk.errors import ApiError, ObjectNotFoundError, MoreThanOneResultError from tests.unit.fixtures.CBCSDKMock import CBCSDKMock -from cbc_sdk.platform import Device, Vulnerability +from cbc_sdk.platform import Device, Vulnerability, Job from tests.unit.fixtures.platform.mock_vulnerabilities import (GET_VULNERABILITY_SUMMARY_ORG_LEVEL, GET_VULNERABILITY_SUMMARY_ORG_LEVEL_PER_SEVERITY, GET_ASSET_VIEW_VUL_RESP, @@ -29,7 +29,8 @@ GET_VULNERABILITY_RESP_MULTIPLE_SAME_CVE, GET_DEVICE_VULNERABILITY_SUMMARY_RESP, REFRESH_DEVICE_RESP, - MOCK_WORKLOAD_RESP) + MOCK_WORKLOAD_RESP, + MOCK_VULNERABILITY_EXPORT_JOB) from tests.unit.fixtures.platform.mock_devices import (GET_DEVICE_RESP, GET_DEVICE_RESP_NO_VCENTER) logging.basicConfig(format='%(asctime)s %(levelname)s:%(message)s', level=logging.DEBUG, filename='log.txt') @@ -131,6 +132,22 @@ def test_get_asset_view_with_vulnerability_summary(cbcsdk_mock): assert asset["name"] == "jdoe-windows_2012" or asset["name"] == "cwp-windows_2012_r2" +def test_export_vulnerability_summary(cbcsdk_mock): + """Test Export Vulnerability Summary""" + cbcsdk_mock.mock_request("POST", + "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/summary/export" + "?async=true", + {"jobId": 4677844}) + + cbcsdk_mock.mock_request("GET", + "/jobs/v1/orgs/test/jobs/4677844", + MOCK_VULNERABILITY_EXPORT_JOB) + + api = cbcsdk_mock.api + job = api.select(Vulnerability.AssetView).export() + isinstance(job, Job) + + def test_get_asset_view_with_vulnerability_summary_and_vcenter_async(cbcsdk_mock): """Test Get Asset View with Vulnerability Summary""" cbcsdk_mock.mock_request("POST", @@ -158,6 +175,22 @@ def test_get_all_vulnerabilities(cbcsdk_mock): assert query._count() == len(results) +def test_export_vulnerabilities(cbcsdk_mock): + """Test Export Vulnerabilities""" + cbcsdk_mock.mock_request("POST", + "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/export" + "?async=true", + {"jobId": 4677844}) + + cbcsdk_mock.mock_request("GET", + "/jobs/v1/orgs/test/jobs/4677844", + MOCK_VULNERABILITY_EXPORT_JOB) + + api = cbcsdk_mock.api + job = api.select(Vulnerability).export() + isinstance(job, Job) + + def test_get_vulnerability_by_id(cbcsdk_mock): """Tests a get vulnerabilty by cve_id.""" cbcsdk_mock.mock_request("POST", From c443ba6b1f4ffa2bf926cbbebc9149efce0675c2 Mon Sep 17 00:00:00 2001 From: Alex Van Brunt Date: Mon, 6 Feb 2023 17:27:02 -0700 Subject: [PATCH 039/143] Fix broken tests --- .../platform/test_vulnerability_assessment.py | 42 ++++++++++++++++--- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/src/tests/unit/platform/test_vulnerability_assessment.py b/src/tests/unit/platform/test_vulnerability_assessment.py index a52d88af2..2c2bcbf36 100644 --- a/src/tests/unit/platform/test_vulnerability_assessment.py +++ b/src/tests/unit/platform/test_vulnerability_assessment.py @@ -534,7 +534,12 @@ def test_device_vulnerability_refresh(cbcsdk_mock): def test_dismiss_vulnerability(cbcsdk_mock): """Test dismiss vulnerability""" - cbcsdk_mock.mock_request("POST", "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search", + cbcsdk_mock.mock_request("POST", + "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search", + GET_VULNERABILITY_RESP) + cbcsdk_mock.mock_request("POST", + "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search" + "?dataForExport=true", GET_VULNERABILITY_RESP) def post_action(url, body, **kwargs): @@ -567,7 +572,12 @@ def post_action(url, body, **kwargs): def test_dismiss_edit_vulnerability(cbcsdk_mock): """Test editting an already dismissed vulnerabilty""" - cbcsdk_mock.mock_request("POST", "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search", + cbcsdk_mock.mock_request("POST", + "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search", + GET_VULNERABILITY_RESP) + cbcsdk_mock.mock_request("POST", + "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search" + "?dataForExport=true", GET_DISMISSED_VULNERABILITY_RESP) def post_action(url, body, **kwargs): @@ -602,7 +612,12 @@ def post_action(url, body, **kwargs): def test_undismiss_vulnerability(cbcsdk_mock): """Test undismissing a dismissed vulnerability""" - cbcsdk_mock.mock_request("POST", "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search", + cbcsdk_mock.mock_request("POST", + "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search", + GET_VULNERABILITY_RESP) + cbcsdk_mock.mock_request("POST", + "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search" + "?dataForExport=true", GET_DISMISSED_VULNERABILITY_RESP) def post_action(url, body, **kwargs): @@ -637,7 +652,12 @@ def post_action(url, body, **kwargs): def test_undismiss_vulnerability_not_dismissed(cbcsdk_mock): """Test undismiss a vulnerability which has not be dismissed""" - cbcsdk_mock.mock_request("POST", "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search", + cbcsdk_mock.mock_request("POST", + "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search", + GET_VULNERABILITY_RESP) + cbcsdk_mock.mock_request("POST", + "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search" + "?dataForExport=true", GET_VULNERABILITY_RESP) api = cbcsdk_mock.api @@ -649,7 +669,12 @@ def test_undismiss_vulnerability_not_dismissed(cbcsdk_mock): def test_dismiss_other_vulnerability_no_notes(cbcsdk_mock): """Test dismiss vulnerability with reason OTHER and no notes""" - cbcsdk_mock.mock_request("POST", "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search", + cbcsdk_mock.mock_request("POST", + "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search", + GET_VULNERABILITY_RESP) + cbcsdk_mock.mock_request("POST", + "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search" + "?dataForExport=true", GET_VULNERABILITY_RESP) api = cbcsdk_mock.api @@ -661,7 +686,12 @@ def test_dismiss_other_vulnerability_no_notes(cbcsdk_mock): def test_invalid_action_vulnerability(cbcsdk_mock): """Test performing an invalid action on a vulnerability""" - cbcsdk_mock.mock_request("POST", "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search", + cbcsdk_mock.mock_request("POST", + "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search", + GET_VULNERABILITY_RESP) + cbcsdk_mock.mock_request("POST", + "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search" + "?dataForExport=true", GET_VULNERABILITY_RESP) api = cbcsdk_mock.api From ea4bf84060d2454b45a10dd000f2f6e32805bfdb Mon Sep 17 00:00:00 2001 From: Alex Van Brunt Date: Mon, 6 Feb 2023 17:35:19 -0700 Subject: [PATCH 040/143] Remove unintended print statement in test file --- src/tests/unit/enterprise_edr/test_enterprise_edr_threatintel.py | 1 - 1 file changed, 1 deletion(-) 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 dbef2c545..1a91574d3 100644 --- a/src/tests/unit/enterprise_edr/test_enterprise_edr_threatintel.py +++ b/src/tests/unit/enterprise_edr/test_enterprise_edr_threatintel.py @@ -86,7 +86,6 @@ def test_watchlist_save(cbcsdk_mock): cbcsdk_mock.mock_request("POST", "/threathunter/watchlistmgr/v3/orgs/test/watchlists", WATCHLIST_GET_SPECIFIC_RESP) watchlist = Watchlist(api, model_unique_id=None, initial_data=CREATE_WATCHLIST_DATA) watchlist.validate() - print(f"{watchlist._model_unique_id =}") watchlist.save() # if Watchlist response is missing a required field per enterprise_edr.models.Watchlist, raise InvalidObjectError From fe754926c078b400b7c6eab7d655cec85d1e568e Mon Sep 17 00:00:00 2001 From: Alex Van Brunt Date: Tue, 7 Feb 2023 13:49:04 -0700 Subject: [PATCH 041/143] Add example to docstring and fix return values in docstrings --- src/cbc_sdk/platform/vulnerability_assessment.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/cbc_sdk/platform/vulnerability_assessment.py b/src/cbc_sdk/platform/vulnerability_assessment.py index 0f72128df..26c1e7c33 100644 --- a/src/cbc_sdk/platform/vulnerability_assessment.py +++ b/src/cbc_sdk/platform/vulnerability_assessment.py @@ -106,7 +106,7 @@ def _query_implementation(cls, cb, **kwargs): def perform_action(self, type, reason=None, notes=None): """ - Take an action on to manage the Vulnerability. + Take an action to manage the Vulnerability. Args: type (str): The type of action. (supports DISMISS, DISMISS_EDIT, or UNDISMISS) @@ -265,7 +265,7 @@ def set_visibility(self, visibility): visibility (str): The visibility state of the vulnerabilty. (supports ACTIVE, ALL, DISMISSED) Returns: - VulnerabilityQuery: This instance. + VulnerabilityOrgSummaryQuery: This instance. """ if visibility not in self.VALID_VISIBILITY: raise ApiError(f"{visibility} is not a valid visibility. Supported visibilities {self.VALID_VISIBILITY}") @@ -777,6 +777,16 @@ def export(self): """ Performs the query and export the results in the form of a Job. + Example: + >>> # Create the Vulnerability query + >>> query = cb.select(Vulnerability).set_severity('CRITICAL') + >>> # Export the results + >>> job = query.export() + >>> # wait for the export to finish + >>> job.await_completion() + >>> # write the results to a file + >>> job.get_output_as_file("vulnerabilities.csv") + Returns: Job: The export job. """ @@ -986,7 +996,7 @@ def set_os_product_id(self, os_product_id, operator): operator (str): logic operator to apply to property value. Returns: - VulnerabilityQuery: This instance. + AffectedAssetQuery: This instance. """ if not os_product_id: raise ApiError("Invalid os product id") From 9421e8a30b9dd740deb1e3e82a1ac59d6d75216c Mon Sep 17 00:00:00 2001 From: Davide Date: Tue, 14 Feb 2023 12:44:39 +0100 Subject: [PATCH 042/143] Cleanup code to use requests to parse query params --- src/cbc_sdk/connection.py | 17 ++--------------- src/cbc_sdk/utils.py | 21 --------------------- src/tests/unit/test_base_api.py | 16 ++++++++-------- src/tests/unit/test_utils.py | 17 +---------------- 4 files changed, 11 insertions(+), 60 deletions(-) diff --git a/src/cbc_sdk/connection.py b/src/cbc_sdk/connection.py index 73ac9f414..1f6d9a18f 100644 --- a/src/cbc_sdk/connection.py +++ b/src/cbc_sdk/connection.py @@ -42,8 +42,6 @@ import logging import json -import urllib - from .credentials import Credentials from .credential_providers.default import default_credential_provider from .errors import ClientError, QuerySyntaxError, ServerError, TimeoutError, ApiError, ObjectNotFoundError, \ @@ -52,7 +50,6 @@ from .cache.lru import lru_cache_function from .base import CreatableModelMixin, NewBaseModel -from .utils import convert_query_params log = logging.getLogger(__name__) DEFAULT_STREAM_BUFFER_SIZE = 1024 @@ -470,12 +467,7 @@ def get_object(self, uri, query_parameters=None, default=None): Returns: object: Result of the GET request. """ - if query_parameters: - if isinstance(query_parameters, dict): - query_parameters = convert_query_params(query_parameters) - uri += '?%s' % (urllib.parse.urlencode(sorted(query_parameters))) - - result = self.api_json_request("GET", uri) + result = self.api_json_request("GET", uri, params=query_parameters) if result.status_code == 200: try: return result.json() @@ -500,13 +492,8 @@ def get_raw_data(self, uri, query_parameters=None, default=None, **kwargs): Returns: object: Result of the GET request. """ - if query_parameters: - if isinstance(query_parameters, dict): - query_parameters = convert_query_params(query_parameters) - uri += '?%s' % (urllib.parse.urlencode(sorted(query_parameters))) - hdrs = kwargs.pop("headers", {}) - result = self.api_json_request("GET", uri, headers=hdrs) + result = self.api_json_request("GET", uri, headers=hdrs, params=query_parameters) if result.status_code == 200: return result.text elif result.status_code == 204: diff --git a/src/cbc_sdk/utils.py b/src/cbc_sdk/utils.py index 9763d59d3..9347ce3e5 100755 --- a/src/cbc_sdk/utils.py +++ b/src/cbc_sdk/utils.py @@ -20,27 +20,6 @@ cb_datetime_format = "%Y-%m-%d %H:%M:%S.%f" -def convert_query_params(qd): - """ - Expand a dictionary of query parameters by turning "list" values into multiple pairings of key with value. - - Args: - qd (dict): A mapping of parameter names to values. - - Returns: - list: A list of query parameters, each one a tuple containing name and value, after the expansion is applied. - """ - o = [] - for k, v in iter(qd.items()): - if type(v) == list: - for item in v: - o.append((k, item)) - else: - o.append((k, v)) - - return o - - def convert_from_cb(s): """ Parse a date and time value into a datetime object. diff --git a/src/tests/unit/test_base_api.py b/src/tests/unit/test_base_api.py index b7a0b37c2..525bb1baa 100755 --- a/src/tests/unit/test_base_api.py +++ b/src/tests/unit/test_base_api.py @@ -185,15 +185,15 @@ def test_BaseAPI_raise_unless_json_raises(response, expected, 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?x=1&y=2', StubResponse({'a': 1, 'b': 2}), [('x', 1), ('y', 2)], {'a': 8, 'b': 9}, {'a': 1, 'b': 2}), - ('/path?x=1&y=2', StubResponse({'a': 1, 'b': 2}), {'x': 1, 'y': 2}, {'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}), + ('/path', StubResponse({'a': 1, 'b': 2}), {'x': 1, 'y': 2}, {'a': 8, 'b': 9}, {'a': 1, 'b': 2}), ('/path', StubResponse({'a': 1, 'b': 2}, 204), None, {'a': 8, 'b': 9}, {'a': 8, 'b': 9}) ]) def test_BaseAPI_get_object_returns(mox, expath, response, params, default, expected): """Test the cases where get_object returns a value.""" sut = BaseAPI(url='https://example.com', token='ABCDEFGH', org_key='A1B2C3D4') mox.StubOutWithMock(sut.session, 'http_request') - sut.session.http_request('GET', expath, headers={}, data=None).AndReturn(response) + sut.session.http_request('GET', expath, headers={}, data=None, params=params).AndReturn(response) mox.ReplayAll() rc = sut.get_object('/path', params, default) assert rc == expected @@ -209,7 +209,7 @@ def test_BaseAPI_get_object_raises_from_returns(mox, response, errcode, prefix): """Test the cases where get_object raises an exception based on what it receives.""" sut = BaseAPI(url='https://example.com', token='ABCDEFGH', org_key='A1B2C3D4') mox.StubOutWithMock(sut.session, 'http_request') - sut.session.http_request('GET', '/path', headers={}, data=None).AndReturn(response) + sut.session.http_request('GET', '/path', headers={}, data=None, params=None).AndReturn(response) mox.ReplayAll() with pytest.raises(ServerError) as excinfo: sut.get_object('/path') @@ -220,15 +220,15 @@ def test_BaseAPI_get_object_raises_from_returns(mox, response, errcode, prefix): @pytest.mark.parametrize("expath, code, response, params, default, expected", [ ('/path', 200, 'Boston1', None, 'Denver0', 'Boston1'), - ('/path?x=1&y=2', 200, 'Boston1', [('x', 1), ('y', 2)], 'Denver0', 'Boston1'), - ('/path?x=1&y=2', 200, 'Boston1', {'x': 1, 'y': 2}, 'Denver0', 'Boston1'), + ('/path', 200, 'Boston1', [('x', 1), ('y', 2)], 'Denver0', 'Boston1'), + ('/path', 200, 'Boston1', {'x': 1, 'y': 2}, 'Denver0', 'Boston1'), ('/path', 204, 'Boston1', None, 'Denver0', 'Denver0') ]) def test_BaseAPI_get_raw_data_returns(mox, expath, code, response, params, default, expected): """Test the cases where get_raw_data returns a value.""" sut = BaseAPI(url='https://example.com', token='ABCDEFGH', org_key='A1B2C3D4') mox.StubOutWithMock(sut.session, 'http_request') - sut.session.http_request('GET', expath, headers={}, data=None).AndReturn(StubResponse(None, code, response)) + sut.session.http_request('GET', expath, headers={}, data=None, params=params).AndReturn(StubResponse(None, code, response)) mox.ReplayAll() rc = sut.get_raw_data('/path', params, default) assert rc == expected @@ -243,7 +243,7 @@ def test_BaseAPI_get_raw_data_raises_from_returns(mox, response, errcode, prefix """Test the cases where get_raw_data raises an exception based on what it receives.""" sut = BaseAPI(url='https://example.com', token='ABCDEFGH', org_key='A1B2C3D4') mox.StubOutWithMock(sut.session, 'http_request') - sut.session.http_request('GET', '/path', headers={}, data=None).AndReturn(response) + sut.session.http_request('GET', '/path', headers={}, data=None, params=None).AndReturn(response) mox.ReplayAll() with pytest.raises(ServerError) as excinfo: sut.get_raw_data('/path') diff --git a/src/tests/unit/test_utils.py b/src/tests/unit/test_utils.py index 6e92f6d7f..a0af54356 100755 --- a/src/tests/unit/test_utils.py +++ b/src/tests/unit/test_utils.py @@ -13,27 +13,12 @@ # import pytest from datetime import datetime -from cbc_sdk.utils import convert_query_params, convert_from_cb, convert_to_cb +from cbc_sdk.utils import convert_from_cb, convert_to_cb # ==================================== Unit TESTS BELOW ==================================== -def test_convert_query_params(): - """Test that query parameter dicts are properly converted.""" - lv = convert_query_params({'answer': 42, 'hup': [2, 3, 4], 'goody': 'twoshoes'}) - assert isinstance(lv, list) - assert len(lv) == 5 - assert ('answer', 42) in lv - assert ('hup', 2) in lv - assert ('hup', 3) in lv - assert ('hup', 4) in lv - assert ('goody', 'twoshoes') in lv - lv = convert_query_params({}) - assert isinstance(lv, list) - assert len(lv) == 0 - - def test_convert_from_cb(): """Test the conversion of dates from CB format strings.""" t = convert_from_cb("2020-03-11T18:34:11") From 00b219658360468afdf3271579e6c67c80978c46 Mon Sep 17 00:00:00 2001 From: Emanuela Mitreva Date: Thu, 16 Feb 2023 12:24:33 +0200 Subject: [PATCH 043/143] Initial --- src/cbc_sdk/platform/__init__.py | 2 + src/cbc_sdk/platform/observations.py | 296 +++++++++++++++++++++++++++ 2 files changed, 298 insertions(+) create mode 100644 src/cbc_sdk/platform/observations.py diff --git a/src/cbc_sdk/platform/__init__.py b/src/cbc_sdk/platform/__init__.py index 6ecbcb2a3..5e82b8d43 100644 --- a/src/cbc_sdk/platform/__init__.py +++ b/src/cbc_sdk/platform/__init__.py @@ -24,3 +24,5 @@ from cbc_sdk.platform.vulnerability_assessment import Vulnerability from cbc_sdk.platform.jobs import Job + +from cbc_sdk.platform.observations import Observation diff --git a/src/cbc_sdk/platform/observations.py b/src/cbc_sdk/platform/observations.py new file mode 100644 index 000000000..4c65a3e3f --- /dev/null +++ b/src/cbc_sdk/platform/observations.py @@ -0,0 +1,296 @@ +# ******************************************************* +# 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. + +"""Model and Query Classes for Observations""" + +from cbc_sdk.base import UnrefreshableModel +from cbc_sdk.base import Query as BaseEventQuery +from cbc_sdk.errors import ApiError, TimeoutError +from cbc_sdk.platform.reputation import ReputationOverride +from pathlib import Path + +import logging +import time +import os + +log = logging.getLogger(__name__) + + +class Observation(UnrefreshableModel): + """Represents an observations""" + default_sort = "device_timestamp" + primary_key = "observation_id" + + @classmethod + def _query_implementation(self, cb, **kwargs): + """ + Returns the appropriate query object for this object type. + + Args: + cb (BaseAPI): Reference to API object used to communicate with the server. + **kwargs (dict): Not used, retained for compatibility. + + Returns: + Query: The query object for this alert type. + """ + # This will emulate a synchronous observation query, for now. + return ObservationQuery(self, cb) + + def __init__(self, cb, model_unique_id=None, initial_data=None, force_init=False, full_doc=True): + """ + Initialize the Observation object. + + Args: + cb (CBCloudAPI): A reference to the CBCloudAPI object. + model_unique_id (Any): The unique ID for this particular instance of the model object. + initial_data (dict): The data to use when initializing the model object. + force_init (bool): True to force object initialization. + full_doc (bool): True to mark the object as fully initialized. + """ + self._details_timeout = 0 + self._info = None + if model_unique_id is not None and initial_data is None: + observations_future = cb.select(Observation).where(observation_id=model_unique_id).execute_async() + result = observations_future.result() + if len(result) == 1: + initial_data = result[0] + super(Observation, self).__init__( + cb, + model_unique_id=model_unique_id, + initial_data=initial_data, + force_init=force_init, + full_doc=full_doc + ) + + def get_details(self, timeout=0, async_mode=False): + """Requests detailed results. + + Args: + timeout (int): Observations details request timeout in milliseconds. + async_mode (bool): True to request details in an asynchronous manner. + + Note: + - When using asynchronous mode, this method returns a python future. + You can call result() on the future object to wait for completion and get the results. + """ + self._details_timeout = timeout + if not self.observation_id: + raise ApiError("Trying to get event details on an invalid observation_id") + if async_mode: + return self._cb._async_submit(lambda arg, kwarg: self._get_detailed_results()) + else: + return self._get_detailed_results() + + def _get_detailed_results(self): + """Actual search details implementation""" + args = {"observation_ids": [self.observation_id]} + url = "/api/investigate/v2/orgs/{}/observations/detail_jobs".format(self._cb.credentials.org_key) + query_start = self._cb.post_object(url, body=args) + job_id = query_start.json().get("job_id") + timed_out = False + submit_time = time.time() * 1000 + + while True: + status_url = "/api/investigate/v2/orgs/{}/observations/detail_jobs/{}/results".format( + self._cb.credentials.org_key, + job_id, + ) + result = self._cb.get_object(status_url) + searchers_contacted = result.get("contacted", 0) + searchers_completed = result.get("completed", 0) + log.debug("contacted = {}, completed = {}".format(searchers_contacted, searchers_completed)) + if searchers_contacted == 0: + time.sleep(.5) + continue + if searchers_completed < searchers_contacted: + if self._details_timeout != 0 and (time.time() * 1000) - submit_time > self._details_timeout: + timed_out = True + break + else: + break + + time.sleep(.5) + + if timed_out: + raise TimeoutError(message="user-specified timeout exceeded while waiting for results") + + log.debug("Pulling detailed results, timed_out={}".format(timed_out)) + + still_fetching = True + result_url = "/api/investigate/v2/orgs/{}/observations/detail_jobs/{}/results".format( + self._cb.credentials.org_key, + job_id + ) + query_parameters = {} + while still_fetching: + result = self._cb.get_object(result_url, query_parameters=query_parameters) + total_results = result.get('num_available', 0) + found_results = result.get('num_found', 0) + # if found is 0, then no observations were found + if found_results == 0: + return self + if total_results != 0: + results = result.get('results', []) + self._info = results[0] + return self + + +class ObservationQuery(BaseEventQuery): + """Represents the query logic for an Observation query. + + This class specializes `Query` to handle the particulars of observations querying. + """ + + def __init__(self, doc_class, cb): + """ + Initialize the ObservationQuery object. + + Args: + doc_class (class): The class of the model this query returns. + cb (CBCloudAPI): A reference to the CBCloudAPI object. + """ + super(ObservationQuery, self).__init__(doc_class, cb) + self._default_args["rows"] = self._batch_size + self._query_token = None + self._timeout = 0 + self._timed_out = False + + def set_rows(self, rows): + """ + Sets the 'rows' query body parameter to the 'start search' API call, determining how many rows to request. + + Args: + rows (int): How many rows to request. + """ + if not isinstance(rows, int): + raise ApiError(f"Rows must be an integer. {rows} is a {type(rows)}.") + if rows > 10000: + raise ApiError("Maximum allowed value for rows is 10000") + super(ObservationQuery, self).set_rows(rows) + return self + + def timeout(self, msecs): + """Sets the timeout on a event query. + + Arguments: + msecs (int): Timeout duration, in milliseconds. + + Returns: + Query (ObservationQuery): The Query object with new milliseconds + parameter. + + Example: + >>> cb.select(Observation).where(process_name="foo.exe").timeout(5000) + """ + self._timeout = msecs + return self + + def _submit(self): + if self._query_token: + raise ApiError("Query already submitted: token {0}".format(self._query_token)) + + args = self._get_query_parameters() + url = "/api/investigate/v2/orgs/{}/observations/search_jobs".format(self._cb.credentials.org_key) + query_start = self._cb.post_object(url, body=args) + self._query_token = query_start.json().get("job_id") + self._timed_out = False + self._submit_time = time.time() * 1000 + + def _still_querying(self): + if not self._query_token: + self._submit() + + status_url = "/api/investigate/v2/orgs/{}/observations/search_jobs/{}/results".format( + self._cb.credentials.org_key, + self._query_token, + ) + result = self._cb.get_object(status_url) + searchers_contacted = result.get("contacted", 0) + searchers_completed = result.get("completed", 0) + log.debug("contacted = {}, completed = {}".format(searchers_contacted, searchers_completed)) + if searchers_contacted == 0: + return True + if searchers_completed < searchers_contacted: + if self._timeout != 0 and (time.time() * 1000) - self._submit_time > self._timeout: + self._timed_out = True + return False + return True + + return False + + def _count(self): + if self._count_valid: + return self._total_results + + while self._still_querying(): + time.sleep(.5) + + if self._timed_out: + raise TimeoutError(message="user-specified timeout exceeded while waiting for results") + + result_url = "/api/investigate/v2/orgs/{}/observations/search_jobs/{}/results".format( + self._cb.credentials.org_key, + self._query_token, + ) + result = self._cb.get_object(result_url) + + self._total_results = result.get('num_available', 0) + self._count_valid = True + + return self._total_results + + def _search(self, start=0, rows=0): + if not self._query_token: + self._submit() + + while self._still_querying(): + time.sleep(.5) + + if self._timed_out: + raise TimeoutError(message="user-specified timeout exceeded while waiting for results") + + log.debug("Pulling results, timed_out={}".format(self._timed_out)) + + current = start + rows_fetched = 0 + still_fetching = True + result_url_template = "/api/investigate/v2/orgs/{}/observations/search_jobs/{}/results".format( + self._cb.credentials.org_key, + self._query_token + ) + query_parameters = {} + while still_fetching: + result_url = '{}?start={}&rows={}'.format( + result_url_template, + current, + self._batch_size + ) + + result = self._cb.get_object(result_url, query_parameters=query_parameters) + + self._total_results = result.get('num_available', 0) + self._count_valid = True + + results = result.get('results', []) + + for item in results: + yield item + current += 1 + rows_fetched += 1 + + if rows and rows_fetched >= rows: + still_fetching = False + break + + if current >= self._total_results: + still_fetching = False + + log.debug("current: {}, total_results: {}".format(current, self._total_results)) From 6d169865729d6389f70587265c449bebef00ec8c Mon Sep 17 00:00:00 2001 From: Emanuela Mitreva Date: Thu, 16 Feb 2023 20:21:41 +0200 Subject: [PATCH 044/143] Unit tests and models --- src/cbc_sdk/platform/models/observation.yaml | 80 +++ src/cbc_sdk/platform/observations.py | 14 +- .../fixtures/platform/mock_observations.py | 340 +++++++++++ src/tests/unit/platform/test_observations.py | 530 ++++++++++++++++++ 4 files changed, 962 insertions(+), 2 deletions(-) create mode 100644 src/cbc_sdk/platform/models/observation.yaml create mode 100644 src/tests/unit/fixtures/platform/mock_observations.py create mode 100644 src/tests/unit/platform/test_observations.py diff --git a/src/cbc_sdk/platform/models/observation.yaml b/src/cbc_sdk/platform/models/observation.yaml new file mode 100644 index 000000000..ef8548ac7 --- /dev/null +++ b/src/cbc_sdk/platform/models/observation.yaml @@ -0,0 +1,80 @@ +type: object +properties: + alert_category: + type: array + items: + type: string + alert_id: + type: array + items: + type: string + backend_timestamp: + type: string + device_group_id: + type: integer + device_id: + type: integer + device_name: + type: string + device_policy: + type: string + device_policy_id: + type: integer + device_timestamp: + type: string + enriched: + type: boolean + enriched_event_type: + type: string + event_description: + type: string + event_id: + type: string + event_network_inbound: + type: boolean + event_network_local_ipv4: + type: string + event_network_location: + type: string + event_network_protocol: + type: string + event_network_remote_ipv4: + type: string + event_network_remote_port: + type: integer + event_type: + type: array + items: + type: string + ingress_time: + type: integer + legacy: + type: boolean + observation_description: + type: string + observation_id: + type: string + observation_type: + type: string + org_id: + type: string + parent_guid: + type: string + parent_pid: + type: integer + process_guid: + type: string + process_hash: + type: array + items: + type: string + process_name: + type: string + process_pid: + type: array + items: + type: integer + process_username: + type: array + items: + type: string \ No newline at end of file diff --git a/src/cbc_sdk/platform/observations.py b/src/cbc_sdk/platform/observations.py index 4c65a3e3f..ec29309ee 100644 --- a/src/cbc_sdk/platform/observations.py +++ b/src/cbc_sdk/platform/observations.py @@ -26,8 +26,8 @@ class Observation(UnrefreshableModel): """Represents an observations""" - default_sort = "device_timestamp" primary_key = "observation_id" + swagger_meta_file = "platform/models/observation.yaml" @classmethod def _query_implementation(self, cb, **kwargs): @@ -83,7 +83,7 @@ def get_details(self, timeout=0, async_mode=False): """ self._details_timeout = timeout if not self.observation_id: - raise ApiError("Trying to get event details on an invalid observation_id") + raise ApiError("Trying to get observation details on an invalid observation_id") if async_mode: return self._cb._async_submit(lambda arg, kwarg: self._get_detailed_results()) else: @@ -163,6 +163,15 @@ def __init__(self, doc_class, cb): self._timeout = 0 self._timed_out = False + def or_(self, **kwargs): + """ + :meth:`or_` criteria are explicitly provided to Observation queries. + + This method overrides the base class in order to provide or_() functionality rather than raising an exception. + """ + self._query_builder.or_(None, **kwargs) + return self + def set_rows(self, rows): """ Sets the 'rows' query body parameter to the 'start search' API call, determining how many rows to request. @@ -294,3 +303,4 @@ def _search(self, start=0, rows=0): still_fetching = False log.debug("current: {}, total_results: {}".format(current, self._total_results)) + diff --git a/src/tests/unit/fixtures/platform/mock_observations.py b/src/tests/unit/fixtures/platform/mock_observations.py new file mode 100644 index 000000000..7026df054 --- /dev/null +++ b/src/tests/unit/fixtures/platform/mock_observations.py @@ -0,0 +1,340 @@ +"""Mock responses for observations queries.""" + +POST_OBSERVATIONS_SEARCH_JOB_RESP = {"job_id": "08ffa932-b633-4107-ba56-8741e929e48b"} + + +GET_OBSERVATIONS_SEARCH_JOB_RESULTS_RESP = { + "approximate_unaggregated": 1, + "completed": 4, + "contacted": 4, + "num_aggregated": 1, + "num_available": 1, + "num_found": 1, + "results": [ + { + "alert_category": ["OBSERVED"], + "alert_id": None, + "backend_timestamp": "2023-02-08T03:22:59.196Z", + "device_group_id": 0, + "device_id": 17482451, + "device_name": "dev01-39x-1", + "device_policy_id": 20792247, + "device_timestamp": "2023-02-08T03:20:33.751Z", + "enriched": True, + "enriched_event_type": ["NETWORK"], + "event_description": "The script", + "event_id": "8fbccc2da75f11ed937ae3cb089984c6", + "event_network_inbound": False, + "event_network_local_ipv4": "10.203.105.21", + "event_network_location": "Santa Clara,CA,United States", + "event_network_protocol": "TCP", + "event_network_remote_ipv4": "23.44.229.234", + "event_network_remote_port": 80, + "event_type": ["netconn"], + "ingress_time": 1675826462036, + "legacy": True, + "observation_description": "The application firefox.exe invoked ", + "observation_id": "8fbccc2da75f11ed937ae3cb089984c6:be6ff259-88e3-6286-789f-74defa192d2e", + "observation_type": "CB_ANALYTICS", + "org_id": "ABCD123456", + "parent_guid": "ABCD123456-010ac2d3-00001c68-00000000-1d93b6c4d1f20ad", + "parent_pid": 7272, + "process_guid": "ABCD123456-010ac2d3-00001cf8-00000000-1d93b6c4d2b16a4", + "process_hash": [ + "9df1ec5e25919660a1b0b85d3965d55797b9aac81e028008428106c4dcda7b29" + ], + "process_name": "c:\\programdata\\mozilla-1de4eec8-1241-4177-a864-e594e8d1fb38\\updates\\308046b0af4a39cb\\backgroundupdate.moz_log", + "process_pid": [2000], + "process_username": ["DEV01-39X-1\\bit9qa"], + } + ], +} + + +GET_OBSERVATIONS_SEARCH_JOB_RESULTS_RESP_0 = { + "contacted": 0, + "completed": 0, + "results": [], +} + + +GET_OBSERVATIONS_SEARCH_JOB_RESULTS_RESP_ZERO_COMP = { + "contacted": 10, + "completed": 0, + "results": [], +} + + +GET_OBSERVATIONS_SEARCH_JOB_RESULTS_ZERO = { + "num_found": 0, + "num_available": 0, + "contacted": 10, + "completed": 10, + "results": [], +} + + +GET_OBSERVATIONS_SEARCH_JOB_RESULTS_RESP_2 = { + "num_found": 808, + "num_available": 52, + "contacted": 6, + "completed": 6, + "results": [ + { + "alert_category": ["OBSERVED"], + "alert_id": None, + "backend_timestamp": "2023-02-08T03:22:59.196Z", + "device_group_id": 0, + "device_id": 17482451, + "device_name": "dev01-39x-1", + "device_policy_id": 20792247, + "device_timestamp": "2023-02-08T03:20:33.751Z", + "enriched": True, + "enriched_event_type": ["NETWORK"], + "event_description": "The script", + "event_id": "8fbccc2da75f11ed937ae3cb089984c6", + "event_network_inbound": False, + "event_network_local_ipv4": "10.203.105.21", + "event_network_location": "Santa Clara,CA,United States", + "event_network_protocol": "TCP", + "event_network_remote_ipv4": "23.44.229.234", + "event_network_remote_port": 80, + "event_type": ["netconn"], + "ingress_time": 1675826462036, + "legacy": True, + "observation_description": "The application firefox.exe invoked ", + "observation_id": "8fbccc2da75f11ed937ae3cb089984c6:be6ff259-88e3-6286-789f-74defa192d2e", + "observation_type": "CB_ANALYTICS", + "org_id": "ABCD123456", + "parent_guid": "ABCD123456-010ac2d3-00001c68-00000000-1d93b6c4d1f20ad", + "parent_pid": 7272, + "process_guid": "ABCD123456-010ac2d3-00001cf8-00000000-1d93b6c4d2b16a4", + "process_hash": [ + "9df1ec5e25919660a1b0b85d3965d55797b9aac81e028008428106c4dcda7b29" + ], + "process_name": "c:\\programdata\\mozilla-1de4eec8-1241-4177-a864-e594e8d1fb38\\updates\\308046b0af4a39cb\\backgroundupdate.moz_log", + "process_pid": [2000], + "process_username": ["DEV01-39X-1\\bit9qa"], + }, + { + "alert_category": ["OBSERVED"], + "alert_id": None, + "backend_timestamp": "2023-02-08T03:22:59.196Z", + "device_group_id": 0, + "device_id": 17482451, + "device_name": "dev01-39x-1", + "device_policy_id": 20792247, + "device_timestamp": "2023-02-08T03:20:33.751Z", + "enriched": True, + "enriched_event_type": ["NETWORK"], + "event_description": "The script", + "event_id": "8fbccc2da75f11ed937ae3cb089984c6", + "event_network_inbound": False, + "event_network_local_ipv4": "10.203.105.21", + "event_network_location": "Santa Clara,CA,United States", + "event_network_protocol": "TCP", + "event_network_remote_ipv4": "23.44.229.234", + "event_network_remote_port": 80, + "event_type": ["netconn"], + "ingress_time": 1675826462036, + "legacy": True, + "observation_description": "The application firefox.exe invoked ", + "observation_id": "8fbccc2da75f11ed937ae3cb089984c6:be6ff259-88e3-6286-789f-74defa192d2e", + "observation_type": "CB_ANALYTICS", + "org_id": "ABCD123456", + "parent_guid": "ABCD123456-010ac2d3-00001c68-00000000-1d93b6c4d1f20ad", + "parent_pid": 7272, + "process_guid": "ABCD123456-010ac2d3-00001cf8-00000000-1d93b6c4d2b16a4", + "process_hash": [ + "9df1ec5e25919660a1b0b85d3965d55797b9aac81e028008428106c4dcda7b29" + ], + "process_name": "c:\\programdata\\mozilla-1de4eec8-1241-4177-a864-e594e8d1fb38\\updates\\308046b0af4a39cb\\backgroundupdate.moz_log", + "process_pid": [2000], + "process_username": ["DEV01-39X-1\\bit9qa"], + }, + ], +} + + +GET_OBSERVATIONS_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING = { + "num_found": 808, + "num_available": 1, + "contacted": 6, + "completed": 0, + "results": [], +} + + +GET_OBSERVATIONS_DETAIL_JOB_RESULTS_RESP = { + "approximate_unaggregated": 2, + "completed": 4, + "contacted": 4, + "num_aggregated": 1, + "num_available": 1, + "num_found": 1, + "results": [ + { + "alert_category": ["OBSERVED"], + "alert_id": None, + "backend_timestamp": "2023-02-08T03:22:21.570Z", + "device_external_ip": "127.0.0.1", + "device_group_id": 0, + "device_id": 17482451, + "device_installed_by": "bit9qa", + "device_internal_ip": "127.0.0.1", + "device_location": "ONSITE", + "device_name": "dev01-39x-1", + "device_os": "WINDOWS", + "device_os_version": "Windows 10 x64", + "device_policy": "lonergan policy", + "device_policy_id": 12345, + "device_target_priority": "MEDIUM", + "device_timestamp": "2023-02-08T03:20:33.751Z", + "document_guid": "KBrOYUNlTYe116ADgNvGw", + "enriched": True, + "enriched_event_type": "NETWORK", + "event_description": "The script...", + "event_id": "8fbccc2da75f11ed937ae3cb089984c6", + "event_network_inbound": False, + "event_network_local_ipv4": "127.0.0.1", + "event_network_location": "Santa Clara,CA,United States", + "event_network_protocol": "TCP", + "event_network_remote_ipv4": "127.0.0.1", + "event_network_remote_port": 80, + "event_report_code": "SUB_RPT_NONE", + "event_threat_score": [3], + "event_type": "netconn", + "ingress_time": 1675826462036, + "legacy": True, + "netconn_actions": ["ACTION_CONNECTION_ESTABLISHED"], + "netconn_domain": "a1887..dscq..akamai..net", + "netconn_inbound": False, + "netconn_ipv4": 388818410, + "netconn_local_ipv4": 11111, + "netconn_local_port": 11, + "netconn_location": "Santa Clara,CA,United States", + "netconn_port": 80, + "netconn_protocol": "PROTO_TCP", + "observation_description": "The application firefox.exe invoked ", + "observation_id": "8fbccc2da75f11ed937ae3cb089984c6:be6ff259-88e3-6286-789f-74defa192d2e", + "observation_type": "CB_ANALYTICS", + "org_id": "ABCD123456", + "parent_effective_reputation": "ADAPTIVE_WHITE_LIST", + "parent_effective_reputation_source": "CLOUD", + "parent_guid": "TEST-010ac2d3-00001c68-00000000-1d93b6c4d1f20ad", + "parent_hash": [ + "69c8bd1c1dc6103df6bfa9882b5717c0dc4acb8c0c85d8f5c9900db860b6c29b" + ], + "parent_name": "c:\\program files\\mozilla firefox\\firefox.exe", + "parent_pid": 7272, + "parent_reputation": "NOT_LISTED", + "process_cmdline": ["C:\\Program Files\\Mozilla "], + "process_cmdline_length": [268], + "process_effective_reputation": "NOT_LISTED", + "process_effective_reputation_source": "AV", + "process_guid": "ABCD123456-010ac2d3-00001cf8-00000000-1d93b6c4d2b16a4", + "process_hash": [ + "9df1ec5e25919660a1b0b85d3965d55797b9aac81e028008428106c4dc" + ], + "process_name": "c:\\programdata\\mozilla-1de4eec8-1241-4177-a864-e594e8d1fb38\\updates\\308046b0af4a39cb\\backgroundupdate.moz_log", + "process_pid": [2000], + "process_reputation": "NOT_LISTED", + "process_sha256": "9df1ec5e25919660a1b0b85d3965d55797b9aac81e028008428106c4dc", + "process_start_time": "2023-02-08T03:20:32.131Z", + "process_username": ["DEV01-39X-1\\bit9qa"], + "ttp": [ + "INTERNATIONAL_SITE", + "ACTIVE_CLIENT", + "NETWORK_ACCESS", + "UNKNOWN_APP", + ], + } + ], +} + +GET_OBSERVATIONS_SEARCH_JOB_RESULTS_RESP_ALERTS = { + "approximate_unaggregated": 2, + "completed": 4, + "contacted": 4, + "num_aggregated": 1, + "num_available": 1, + "num_found": 1, + "results": [ + { + "alert_category": ["OBSERVED"], + "alert_id": ["be6ff259-88e3-6286-789f-74defa192fff"], + "backend_timestamp": "2023-02-08T03:22:21.570Z", + "device_external_ip": "127.0.0.1", + "device_group_id": 0, + "device_id": 17482451, + "device_installed_by": "bit9qa", + "device_internal_ip": "127.0.0.1", + "device_location": "ONSITE", + "device_name": "dev01-39x-1", + "device_os": "WINDOWS", + "device_os_version": "Windows 10 x64", + "device_policy": "lonergan policy", + "device_policy_id": 12345, + "device_target_priority": "MEDIUM", + "device_timestamp": "2023-02-08T03:20:33.751Z", + "document_guid": "KBrOYUNlTYe116ADgNvGw", + "enriched": True, + "enriched_event_type": "NETWORK", + "event_description": "The script...", + "event_id": "8fbccc2da75f11ed937ae3cb089984c6", + "event_network_inbound": False, + "event_network_local_ipv4": "127.0.0.1", + "event_network_location": "Santa Clara,CA,United States", + "event_network_protocol": "TCP", + "event_network_remote_ipv4": "127.0.0.1", + "event_network_remote_port": 80, + "event_report_code": "SUB_RPT_NONE", + "event_threat_score": [3], + "event_type": "netconn", + "ingress_time": 1675826462036, + "legacy": True, + "netconn_actions": ["ACTION_CONNECTION_ESTABLISHED"], + "netconn_domain": "a1887..dscq..akamai..net", + "netconn_inbound": False, + "netconn_ipv4": 388818410, + "netconn_local_ipv4": 11111, + "netconn_local_port": 11, + "netconn_location": "Santa Clara,CA,United States", + "netconn_port": 80, + "netconn_protocol": "PROTO_TCP", + "observation_description": "The application firefox.exe invoked ", + "observation_id": "8fbccc2da75f11ed937ae3cb089984c6:be6ff259-88e3-6286-789f-74defa192d2e", + "observation_type": "CB_ANALYTICS", + "org_id": "ABCD123456", + "parent_effective_reputation": "ADAPTIVE_WHITE_LIST", + "parent_effective_reputation_source": "CLOUD", + "parent_guid": "TEST-010ac2d3-00001c68-00000000-1d93b6c4d1f20ad", + "parent_hash": [ + "69c8bd1c1dc6103df6bfa9882b5717c0dc4acb8c0c85d8f5c9900db860b6c29b" + ], + "parent_name": "c:\\program files\\mozilla firefox\\firefox.exe", + "parent_pid": 7272, + "parent_reputation": "NOT_LISTED", + "process_cmdline": ["C:\\Program Files\\Mozilla "], + "process_cmdline_length": [268], + "process_effective_reputation": "NOT_LISTED", + "process_effective_reputation_source": "AV", + "process_guid": "ABCD123456-010ac2d3-00001cf8-00000000-1d93b6c4d2b16a4", + "process_hash": [ + "9df1ec5e25919660a1b0b85d3965d55797b9aac81e028008428106c4dc" + ], + "process_name": "c:\\programdata\\mozilla-1de4eec8-1241-4177-a864-e594e8d1fb38\\updates\\308046b0af4a39cb\\backgroundupdate.moz_log", + "process_pid": [2000], + "process_reputation": "NOT_LISTED", + "process_sha256": "9df1ec5e25919660a1b0b85d3965d55797b9aac81e028008428106c4dc", + "process_start_time": "2023-02-08T03:20:32.131Z", + "process_username": ["DEV01-39X-1\\bit9qa"], + "ttp": [ + "INTERNATIONAL_SITE", + "ACTIVE_CLIENT", + "NETWORK_ACCESS", + "UNKNOWN_APP", + ], + } + ], +} diff --git a/src/tests/unit/platform/test_observations.py b/src/tests/unit/platform/test_observations.py new file mode 100644 index 000000000..759397fd5 --- /dev/null +++ b/src/tests/unit/platform/test_observations.py @@ -0,0 +1,530 @@ +"""Testing Observation objects of cbc_sdk.endpoint_standard""" + +import pytest +import logging +from cbc_sdk.platform import Observation +from cbc_sdk.platform.observations import ObservationQuery +from cbc_sdk.rest_api import CBCloudAPI +from cbc_sdk.errors import ApiError, TimeoutError +from tests.unit.fixtures.CBCSDKMock import CBCSDKMock +from tests.unit.fixtures.platform.mock_observations import ( + GET_OBSERVATIONS_SEARCH_JOB_RESULTS_ZERO, + POST_OBSERVATIONS_SEARCH_JOB_RESP, + GET_OBSERVATIONS_SEARCH_JOB_RESULTS_RESP_2, + GET_OBSERVATIONS_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING, + GET_OBSERVATIONS_SEARCH_JOB_RESULTS_RESP_0, + GET_OBSERVATIONS_SEARCH_JOB_RESULTS_RESP_ZERO_COMP, + GET_OBSERVATIONS_DETAIL_JOB_RESULTS_RESP, + GET_OBSERVATIONS_SEARCH_JOB_RESULTS_RESP, +) + +log = logging.basicConfig( + format="%(asctime)s %(levelname)s:%(message)s", + level=logging.DEBUG, + filename="log.txt", +) + + +@pytest.fixture(scope="function") +def cb(): + """Create CBCloudAPI singleton""" + return CBCloudAPI( + url="https://example.com", org_key="test", token="abcd/1234", ssl_verify=False + ) + + +@pytest.fixture(scope="function") +def cbcsdk_mock(monkeypatch, cb): + """Mocks CBC SDK for unit tests""" + return CBCSDKMock(monkeypatch, cb) + + +# ==================================== UNIT TESTS BELOW ==================================== + + +def test_observation_select_where(cbcsdk_mock): + """Testing Observation Querying with select()""" + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/observations/search_jobs", + POST_OBSERVATIONS_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results", # noqa: E501 + GET_OBSERVATIONS_SEARCH_JOB_RESULTS_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=500", # noqa: E501 + GET_OBSERVATIONS_SEARCH_JOB_RESULTS_RESP, + ) + + api = cbcsdk_mock.api + obs_list = api.select(Observation).where( + observation_id="8fbccc2da75f11ed937ae3cb089984c6:be6ff259-88e3-6286-789f-74defa192d2e" + ) + for obs in obs_list: + assert obs.device_name is not None + assert obs.enriched is not None + + +def test_observation_select_async(cbcsdk_mock): + """Testing Observation Querying with select() - asynchronous way""" + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/observations/search_jobs", + POST_OBSERVATIONS_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results", # noqa: E501 + GET_OBSERVATIONS_SEARCH_JOB_RESULTS_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=500", # noqa: E501 + GET_OBSERVATIONS_SEARCH_JOB_RESULTS_RESP, + ) + + api = cbcsdk_mock.api + obs_list = ( + api.select(Observation) + .where( + observation_id="8fbccc2da75f11ed937ae3cb089984c6:be6ff259-88e3-6286-789f-74defa192d2e" + ) + .execute_async() + ) + for obs in obs_list.result(): + assert obs["device_name"] is not None + assert obs["enriched"] is not None + + +def test_observation_select_by_id(cbcsdk_mock): + """Testing Observation Querying with select() - asynchronous way""" + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/observations/search_jobs", + POST_OBSERVATIONS_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results", # noqa: E501 + GET_OBSERVATIONS_SEARCH_JOB_RESULTS_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=500", # noqa: E501 + GET_OBSERVATIONS_SEARCH_JOB_RESULTS_RESP, + ) + + api = cbcsdk_mock.api + obs = api.select(Observation, "8fbccc2da75f11ed937ae3cb089984c6:be6ff259-88e3-6286-789f-74defa192d2e") + assert obs["device_name"] is not None + assert obs["enriched"] is not None + + +def test_observation_select_details_async(cbcsdk_mock): + """Testing Observation Querying with get_details - asynchronous mode""" + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/observations/search_jobs", + POST_OBSERVATIONS_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results", # noqa: E501 + GET_OBSERVATIONS_SEARCH_JOB_RESULTS_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=500", # noqa: E501 + GET_OBSERVATIONS_SEARCH_JOB_RESULTS_RESP, + ) + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/observations/detail_jobs", + POST_OBSERVATIONS_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/detail_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results", # noqa: E501 + GET_OBSERVATIONS_DETAIL_JOB_RESULTS_RESP, + ) + + api = cbcsdk_mock.api + obs_list = api.select(Observation).where(process_pid=2000) + obs = obs_list[0] + details = obs.get_details(async_mode=True, timeout=500) + results = details.result() + assert results.device_name is not None + assert results.enriched is not None + assert obs._details_timeout == 500 + assert results.process_pid[0] == 2000 + assert results["device_name"] is not None + assert results["enriched"] is not None + assert results["process_pid"][0] == 2000 + + +def test_observations_details_only(cbcsdk_mock): + """Testing Observation with get_details - just the get_details REST API calls""" + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/observations/detail_jobs", + POST_OBSERVATIONS_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/detail_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results", # noqa: E501 + GET_OBSERVATIONS_DETAIL_JOB_RESULTS_RESP, + ) + + api = cbcsdk_mock.api + obs = Observation(api, initial_data={"observation_id": "test"}) + results = obs._get_detailed_results() + assert results._info["device_name"] is not None + assert results._info["enriched"] is not None + assert results._info["process_pid"][0] == 2000 + + +def test_observations_details_timeout(cbcsdk_mock): + """Testing Observation get_details() timeout handling""" + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/observations/detail_jobs", + POST_OBSERVATIONS_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/detail_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results", # noqa: E501 + GET_OBSERVATIONS_SEARCH_JOB_RESULTS_RESP_ZERO_COMP, + ) + + api = cbcsdk_mock.api + obs = Observation(api, initial_data={"observation_id": "test"}) + obs._details_timeout = 1 + with pytest.raises(TimeoutError): + obs._get_detailed_results() + + +def test_observations_select_details_sync(cbcsdk_mock): + """Testing Observation Querying with get_details""" + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/observations/search_jobs", + POST_OBSERVATIONS_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results", # noqa: E501 + GET_OBSERVATIONS_SEARCH_JOB_RESULTS_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=500", # noqa: E501 + GET_OBSERVATIONS_SEARCH_JOB_RESULTS_RESP, + ) + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/observations/detail_jobs", + POST_OBSERVATIONS_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/detail_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results", # noqa: E501 + GET_OBSERVATIONS_DETAIL_JOB_RESULTS_RESP, + ) + + s_api = cbcsdk_mock.api + obs_list = s_api.select(Observation).where(process_pid=2000) + obs = obs_list[0] + results = obs.get_details() + assert results["device_name"] is not None + assert results.device_name is not None + assert results.enriched is True + assert results.process_pid[0] == 2000 + + +def test_observations_select_details_sync_zero(cbcsdk_mock): + """Testing Observation Querying with get_details""" + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/observations/search_jobs", + POST_OBSERVATIONS_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results", # noqa: E501 + GET_OBSERVATIONS_SEARCH_JOB_RESULTS_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=500", # noqa: E501 + GET_OBSERVATIONS_SEARCH_JOB_RESULTS_RESP, + ) + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/observations/detail_jobs", + POST_OBSERVATIONS_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/detail_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results", # noqa: E501 + GET_OBSERVATIONS_SEARCH_JOB_RESULTS_ZERO, + ) + + s_api = cbcsdk_mock.api + obs_list = s_api.select(Observation).where(process_pid=2000) + obs = obs_list[0] + results = obs.get_details() + assert results["device_name"] is not None + assert results.get("alert_id") == [] + + +def test_observations_select_compound(cbcsdk_mock): + """Testing Observation Querying with select() and more complex criteria""" + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/observations/search_jobs", + POST_OBSERVATIONS_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results", # noqa: E501 + GET_OBSERVATIONS_SEARCH_JOB_RESULTS_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=500", # noqa: E501 + GET_OBSERVATIONS_SEARCH_JOB_RESULTS_RESP, + ) + + api = cbcsdk_mock.api + obs_list = api.select(Observation).where(process_pid=1000).or_(process_pid=1000) + for obs in obs_list: + assert obs.device_name is not None + assert obs.enriched is not None + + +def test_observations_query_implementation(cbcsdk_mock): + """Testing Observation querying with where().""" + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/observations/search_jobs", + POST_OBSERVATIONS_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results", # noqa: E501 + GET_OBSERVATIONS_SEARCH_JOB_RESULTS_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=500", # noqa: E501 + GET_OBSERVATIONS_SEARCH_JOB_RESULTS_RESP, + ) + api = cbcsdk_mock.api + observation_id = ( + "8fbccc2da75f11ed937ae3cb089984c6:be6ff259-88e3-6286-789f-74defa192d2e" + ) + obs_list = api.select(Observation).where(f"observation_id:{observation_id}") + assert isinstance(obs_list, ObservationQuery) + assert ( + obs_list[0].observation_id + == "8fbccc2da75f11ed937ae3cb089984c6:be6ff259-88e3-6286-789f-74defa192d2e" + ) + + +def test_observations_timeout(cbcsdk_mock): + """Testing ObservationQuery.timeout().""" + api = cbcsdk_mock.api + query = api.select(Observation).where("observation_id:some_id") + assert query._timeout == 0 + query.timeout(msecs=500) + assert query._timeout == 500 + + +def test_observations_timeout_error(cbcsdk_mock): + """Testing that a timeout in Observation querying throws a TimeoutError correctly""" + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/observations/search_jobs", + POST_OBSERVATIONS_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results", # noqa: E501 + GET_OBSERVATIONS_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING, + ) + + api = cbcsdk_mock.api + obs_list = ( + api.select(Observation) + .where( + "observation_id:8fbccc2da75f11ed937ae3cb089984c6:be6ff259-88e3-6286-789f-74defa192d2e" + ) + .timeout(1) + ) + with pytest.raises(TimeoutError): + list(obs_list) + obs_list = ( + api.select(Observation) + .where( + "observation_id:8fbccc2da75f11ed937ae3cb089984c6:be6ff259-88e3-6286-789f-74defa192d2e" + ) + .timeout(1) + ) + with pytest.raises(TimeoutError): + obs_list._count() + + +def test_observations_query_sort(cbcsdk_mock): + """Testing Observation results sort.""" + api = cbcsdk_mock.api + obs_list = ( + api.select(Observation) + .where(process_pid=1000) + .or_(process_pid=1000) + .sort_by("process_pid", direction="DESC") + ) + assert obs_list._sort_by == [{"field": "process_pid", "order": "DESC"}] + + +def test_observations_rows(cbcsdk_mock): + """Testing Observation results sort.""" + api = cbcsdk_mock.api + obs_list = api.select(Observation).where(process_pid=1000).set_rows(1500) + assert obs_list._batch_size == 1500 + with pytest.raises(ApiError) as ex: + api.select(Observation).where(process_pid=1000).set_rows("alabala") + assert "Rows must be an integer." in str(ex) + with pytest.raises(ApiError) as ex: + api.select(Observation).where(process_pid=1000).set_rows(10001) + assert "Maximum allowed value for rows is 10000" in str(ex) + + +def test_observations_time_range(cbcsdk_mock): + """Testing Observation results sort.""" + api = cbcsdk_mock.api + obs_list = ( + api.select(Observation) + .where(process_pid=1000) + .set_time_range( + start="2020-10-10T20:34:07Z", end="2020-10-20T20:34:07Z", window="-1d" + ) + ) + assert obs_list._time_range["start"] == "2020-10-10T20:34:07Z" + assert obs_list._time_range["end"] == "2020-10-20T20:34:07Z" + assert obs_list._time_range["window"] == "-1d" + + +def test_observations_submit(cbcsdk_mock): + """Test _submit method of ObservationQuery class""" + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/observations/search_jobs", + POST_OBSERVATIONS_SEARCH_JOB_RESP, + ) + api = cbcsdk_mock.api + obs_list = api.select(Observation).where(process_pid=1000) + obs_list._submit() + assert obs_list._query_token == "08ffa932-b633-4107-ba56-8741e929e48b" + with pytest.raises(ApiError) as ex: + obs_list._submit() + assert "Query already submitted: token" in str(ex) + + +def test_observations_count(cbcsdk_mock): + """Test _submit method of Observationquery class""" + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/observations/search_jobs", + POST_OBSERVATIONS_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results", # noqa: E501 + GET_OBSERVATIONS_SEARCH_JOB_RESULTS_RESP_2, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results", # noqa: E501 + GET_OBSERVATIONS_SEARCH_JOB_RESULTS_RESP_2, + ) + + api = cbcsdk_mock.api + obs_list = api.select(Observation).where(process_pid=1000) + obs_list._count() + assert obs_list._count() == 52 + + +def test_observations_search(cbcsdk_mock): + """Test _search method of Observationquery class""" + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/observations/search_jobs", + POST_OBSERVATIONS_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results", # noqa: E501 + GET_OBSERVATIONS_SEARCH_JOB_RESULTS_RESP_2, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=500", # noqa: E501 + GET_OBSERVATIONS_SEARCH_JOB_RESULTS_RESP_2, + ) + + api = cbcsdk_mock.api + obs_list = api.select(Observation).where(process_pid=2000) + obs_list._search() + assert obs_list[0].process_pid[0] == 2000 + obs_list._search(start=1) + assert obs_list[0].process_pid[0] == 2000 + + +def test_observations_still_querying(cbcsdk_mock): + """Test _search method of Observationquery class""" + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/observations/search_jobs", + POST_OBSERVATIONS_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results", # noqa: E501 + GET_OBSERVATIONS_SEARCH_JOB_RESULTS_RESP_0, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=500", # noqa: E501 + GET_OBSERVATIONS_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING, + ) + + api = cbcsdk_mock.api + obs_list = api.select(Observation).where(process_pid=1000) + assert obs_list._still_querying() is True + + +def test_observations_still_querying2(cbcsdk_mock): + """Test _search method of Observationquery class""" + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/observations/search_jobs", + POST_OBSERVATIONS_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results", # noqa: E501 + GET_OBSERVATIONS_SEARCH_JOB_RESULTS_RESP_ZERO_COMP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=500", # noqa: E501 + GET_OBSERVATIONS_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING, + ) + + api = cbcsdk_mock.api + obs_list = api.select(Observation).where(process_pid=1000) + assert obs_list._still_querying() is True From f74123941ab8b4336a8239622a38fa6503b9429c Mon Sep 17 00:00:00 2001 From: Emanuela Mitreva Date: Thu, 16 Feb 2023 20:28:35 +0200 Subject: [PATCH 045/143] Deflaking --- src/cbc_sdk/platform/observations.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/cbc_sdk/platform/observations.py b/src/cbc_sdk/platform/observations.py index ec29309ee..5b159c1fd 100644 --- a/src/cbc_sdk/platform/observations.py +++ b/src/cbc_sdk/platform/observations.py @@ -14,12 +14,9 @@ from cbc_sdk.base import UnrefreshableModel from cbc_sdk.base import Query as BaseEventQuery from cbc_sdk.errors import ApiError, TimeoutError -from cbc_sdk.platform.reputation import ReputationOverride -from pathlib import Path import logging import time -import os log = logging.getLogger(__name__) @@ -246,9 +243,9 @@ def _count(self): raise TimeoutError(message="user-specified timeout exceeded while waiting for results") result_url = "/api/investigate/v2/orgs/{}/observations/search_jobs/{}/results".format( - self._cb.credentials.org_key, - self._query_token, - ) + self._cb.credentials.org_key, + self._query_token, + ) result = self._cb.get_object(result_url) self._total_results = result.get('num_available', 0) @@ -284,7 +281,7 @@ def _search(self, start=0, rows=0): ) result = self._cb.get_object(result_url, query_parameters=query_parameters) - + self._total_results = result.get('num_available', 0) self._count_valid = True @@ -303,4 +300,3 @@ def _search(self, start=0, rows=0): still_fetching = False log.debug("current: {}, total_results: {}".format(current, self._total_results)) - From c468350e8ebf8f7f7d35d9725b59ea002b6494e8 Mon Sep 17 00:00:00 2001 From: Emanuela Mitreva Date: Thu, 16 Feb 2023 20:35:52 +0200 Subject: [PATCH 046/143] Deflake --- src/tests/unit/fixtures/platform/mock_observations.py | 10 +++++----- src/tests/unit/platform/test_observations.py | 9 ++------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/tests/unit/fixtures/platform/mock_observations.py b/src/tests/unit/fixtures/platform/mock_observations.py index 7026df054..e8c415535 100644 --- a/src/tests/unit/fixtures/platform/mock_observations.py +++ b/src/tests/unit/fixtures/platform/mock_observations.py @@ -43,7 +43,7 @@ "process_hash": [ "9df1ec5e25919660a1b0b85d3965d55797b9aac81e028008428106c4dcda7b29" ], - "process_name": "c:\\programdata\\mozilla-1de4eec8-1241-4177-a864-e594e8d1fb38\\updates\\308046b0af4a39cb\\backgroundupdate.moz_log", + "process_name": "c:\\programdata\\mozilla-1de4eec8-1241-4177-a864-e594e8d1fb38\\updates", "process_pid": [2000], "process_username": ["DEV01-39X-1\\bit9qa"], } @@ -112,7 +112,7 @@ "process_hash": [ "9df1ec5e25919660a1b0b85d3965d55797b9aac81e028008428106c4dcda7b29" ], - "process_name": "c:\\programdata\\mozilla-1de4eec8-1241-4177-a864-e594e8d1fb38\\updates\\308046b0af4a39cb\\backgroundupdate.moz_log", + "process_name": "c:\\programdata\\mozilla-1de4eec8-1241-4177-a864-e594e8d1fb38\\updates", "process_pid": [2000], "process_username": ["DEV01-39X-1\\bit9qa"], }, @@ -148,7 +148,7 @@ "process_hash": [ "9df1ec5e25919660a1b0b85d3965d55797b9aac81e028008428106c4dcda7b29" ], - "process_name": "c:\\programdata\\mozilla-1de4eec8-1241-4177-a864-e594e8d1fb38\\updates\\308046b0af4a39cb\\backgroundupdate.moz_log", + "process_name": "c:\\programdata\\mozilla-1de4eec8-1241-4177-a864-e594e8d1fb38\\updates", "process_pid": [2000], "process_username": ["DEV01-39X-1\\bit9qa"], }, @@ -236,7 +236,7 @@ "process_hash": [ "9df1ec5e25919660a1b0b85d3965d55797b9aac81e028008428106c4dc" ], - "process_name": "c:\\programdata\\mozilla-1de4eec8-1241-4177-a864-e594e8d1fb38\\updates\\308046b0af4a39cb\\backgroundupdate.moz_log", + "process_name": "c:\\programdata\\mozilla-1de4eec8-1241-4177-a864-e594e8d1fb38\\updates", "process_pid": [2000], "process_reputation": "NOT_LISTED", "process_sha256": "9df1ec5e25919660a1b0b85d3965d55797b9aac81e028008428106c4dc", @@ -323,7 +323,7 @@ "process_hash": [ "9df1ec5e25919660a1b0b85d3965d55797b9aac81e028008428106c4dc" ], - "process_name": "c:\\programdata\\mozilla-1de4eec8-1241-4177-a864-e594e8d1fb38\\updates\\308046b0af4a39cb\\backgroundupdate.moz_log", + "process_name": "c:\\programdata\\mozilla-1de4eec8-1241-4177-a864-e594e8d1fb38\\updates", "process_pid": [2000], "process_reputation": "NOT_LISTED", "process_sha256": "9df1ec5e25919660a1b0b85d3965d55797b9aac81e028008428106c4dc", diff --git a/src/tests/unit/platform/test_observations.py b/src/tests/unit/platform/test_observations.py index 759397fd5..386f3108d 100644 --- a/src/tests/unit/platform/test_observations.py +++ b/src/tests/unit/platform/test_observations.py @@ -324,15 +324,10 @@ def test_observations_query_implementation(cbcsdk_mock): GET_OBSERVATIONS_SEARCH_JOB_RESULTS_RESP, ) api = cbcsdk_mock.api - observation_id = ( - "8fbccc2da75f11ed937ae3cb089984c6:be6ff259-88e3-6286-789f-74defa192d2e" - ) + observation_id = "8fbccc2da75f11ed937ae3cb089984c6:be6ff259-88e3-6286-789f-74defa192d2e" obs_list = api.select(Observation).where(f"observation_id:{observation_id}") assert isinstance(obs_list, ObservationQuery) - assert ( - obs_list[0].observation_id - == "8fbccc2da75f11ed937ae3cb089984c6:be6ff259-88e3-6286-789f-74defa192d2e" - ) + assert obs_list[0].observation_id == observation_id def test_observations_timeout(cbcsdk_mock): From c835430ebbcd3b2e52021b4bf7aaa063e1099d0f Mon Sep 17 00:00:00 2001 From: Emanuela Mitreva Date: Mon, 20 Feb 2023 11:14:10 +0200 Subject: [PATCH 047/143] Add grouped + others --- src/cbc_sdk/platform/observations.py | 245 +++++++++++++++++++++++++-- src/cbc_sdk/rest_api.py | 41 +++++ 2 files changed, 270 insertions(+), 16 deletions(-) diff --git a/src/cbc_sdk/platform/observations.py b/src/cbc_sdk/platform/observations.py index 5b159c1fd..16e655728 100644 --- a/src/cbc_sdk/platform/observations.py +++ b/src/cbc_sdk/platform/observations.py @@ -12,7 +12,7 @@ """Model and Query Classes for Observations""" from cbc_sdk.base import UnrefreshableModel -from cbc_sdk.base import Query as BaseEventQuery +from cbc_sdk.base import Query from cbc_sdk.errors import ApiError, TimeoutError import logging @@ -140,11 +140,101 @@ def _get_detailed_results(self): return self -class ObservationQuery(BaseEventQuery): +class ObservationFacet(UnrefreshableModel): + """Represents an observation retrieved.""" + primary_key = "job_id" + swagger_meta_file = "platform/models/observation_facet.yaml" + submit_url = "/api/investigate/v2/orgs/{}/observations/facet_jobs" + result_url = "/api/investigate/v2/orgs/{}/observations/facet_jobs/{}/results" + + class Terms(UnrefreshableModel): + """Represents the facet fields and values associated with an Observation Facet query.""" + def __init__(self, cb, initial_data): + """Initialize an ObservationFacet Terms object with initial_data.""" + super(ObservationFacet.Terms, self).__init__( + cb, + model_unique_id=None, + initial_data=initial_data, + force_init=False, + full_doc=True, + ) + self._facets = {} + for facet_term_data in initial_data: + field = facet_term_data["field"] + values = facet_term_data["values"] + self._facets[field] = values + + @property + def facets(self): + """Returns the terms' facets for this result.""" + return self._facets + + @property + def fields(self): + """Returns the terms facets' fields for this result.""" + return [field for field in self._facets] + + class Ranges(UnrefreshableModel): + """Represents the range (bucketed) facet fields and values associated with an Observation Facet query.""" + def __init__(self, cb, initial_data): + """Initialize an ObservationFacet Ranges object with initial_data.""" + super(ObservationFacet.Ranges, self).__init__( + cb, + model_unique_id=None, + initial_data=initial_data, + force_init=False, + full_doc=True, + ) + self._facets = {} + for facet_range_data in initial_data: + field = facet_range_data["field"] + values = facet_range_data["values"] + self._facets[field] = values + + @property + def facets(self): + """Returns the reified `ObservationFacet.Terms._facets` for this result.""" + return self._facets + + @property + def fields(self): + """Returns the ranges fields for this result.""" + return [field for field in self._facets] + + @classmethod + def _query_implementation(self, cb, **kwargs): + # This will emulate a synchronous observation facet query, for now. + return FacetQuery(self, cb) + + def __init__(self, cb, model_unique_id, initial_data): + """Initialize the Terms object with initial data.""" + super(ObservationFacet, self).__init__( + cb, + model_unique_id=model_unique_id, + initial_data=initial_data, + force_init=False, + full_doc=True + ) + self._terms = ObservationFacet.Terms(cb, initial_data=initial_data["terms"]) + self._ranges = ObservationFacet.Ranges(cb, initial_data=initial_data["ranges"]) + + @property + def terms_(self): + """Returns the reified `ObservationFacet.Terms` for this result.""" + return self._terms + + @property + def ranges_(self): + """Returns the reified `ObservationFacet.Ranges` for this result.""" + return self._ranges + + +class ObservationQuery(Query): """Represents the query logic for an Observation query. This class specializes `Query` to handle the particulars of observations querying. """ + VALID_GROUP_FIELDS = ["observation_type", "device_name", "process_username", "attack_tactic"] def __init__(self, doc_class, cb): """ @@ -160,6 +250,13 @@ def __init__(self, doc_class, cb): self._timeout = 0 self._timed_out = False + self._aggregate = False + self._aggregate_fields = None + self._max_events_per_group = None + self._range = None + self._rows = None + self._start = None + def or_(self, **kwargs): """ :meth:`or_` criteria are explicitly provided to Observation queries. @@ -184,7 +281,7 @@ def set_rows(self, rows): return self def timeout(self, msecs): - """Sets the timeout on a event query. + """Sets the timeout on a observation query. Arguments: msecs (int): Timeout duration, in milliseconds. @@ -214,6 +311,9 @@ def _still_querying(self): if not self._query_token: self._submit() + if self._aggregation: + return False + status_url = "/api/investigate/v2/orgs/{}/observations/search_jobs/{}/results".format( self._cb.credentials.org_key, self._query_token, @@ -242,17 +342,120 @@ def _count(self): if self._timed_out: raise TimeoutError(message="user-specified timeout exceeded while waiting for results") - result_url = "/api/investigate/v2/orgs/{}/observations/search_jobs/{}/results".format( - self._cb.credentials.org_key, - self._query_token, - ) - result = self._cb.get_object(result_url) + if self._aggregation: + result_url = "/api/investigate/v1/orgs/{}/observations/search_jobs/{}/group_results".format( + self._cb.credentials.org_key, + self._query_token, + ) + result = self._cb.post_object(result_url, self._build_aggregated_body()) + else: + result_url = "/api/investigate/v2/orgs/{}/observations/search_jobs/{}/results".format( + self._cb.credentials.org_key, + self._query_token, + ) + result = self._cb.get_object(result_url) self._total_results = result.get('num_available', 0) self._count_valid = True return self._total_results + def aggregate(self, fields): + """ + Performs an aggregation search where results are grouped by an aggregation field + + Args: + field (str): The aggregation field or fields, valid ones are: + observation_type, device_name, process_username, attack_tactic + """ + if not isinstance(fields, list): + raise ApiError("Fields should be list of values") + + if not all((gtype in ObservationQuery.VALID_GROUP_FIELDS) for gt in fields): + raise ApiError("One or more invalid aggregation fields") + + self._aggregate = True + self._aggregate_fields = [field] if isinstance(field, str) else fields + return self + + def max_events_per_group(self, max_events_per_group): + """ + Sets the max events per aggregate for aggregated results + + Args: + max_events_per_group (int): Max events per aggregate + """ + if not self._aggregate: + raise ApiError("You should first aggregate the records.") + self._max_events_per_group = max_events_per_group + return self + + def rows(self, rows): + """ + Sets the rows for aggregated results + + Args: + rows (int): Max events per group + """ + if not self._aggregate: + raise ApiError("You should first aggregate the records.") + self._rows = rows + return self + + def start(self, start): + """ + Sets the start for the aggregated results + + Args: + start (int): start index + """ + if not self._aggregate: + raise ApiError("You should first aggregate the records.") + self._start = start + return self + + def range(self, duration, field, method="interval"): + """ + Describes a time window to restrict the search. + + Args: + duration (str): duration for the range e.g. -2w + field (str): field + method (str): either bucket or inteval + """ + if not self._aggregate: + raise ApiError("You should first aggregate the records.") + self._range = dict(duration=duration,field=field, method=method) + return self + + def _build_aggregated_body(self): + """ + Helper to build the group results body: + + { + "fields": ["string"], + "max_events_per_group": integer, + "range": { + "duration": "string", + "field": "string", + "method": "string" + }, + "rows": integer, + "start": integer + } + """ + data = dict(fields=self._aggregate_fields) + if self._max_events_per_group: + data["max_events_per_group"] = self._max_events_per_group + if self._ranges: + data["ranges"] = self._ranges + if self._rows: + data["rows"] = self._rows + if self._start: + data["start"] = self._start + return data + + def _search(self, start=0, rows=0): if not self._query_token: self._submit() @@ -268,24 +471,33 @@ def _search(self, start=0, rows=0): current = start rows_fetched = 0 still_fetching = True + query_parameters = {} result_url_template = "/api/investigate/v2/orgs/{}/observations/search_jobs/{}/results".format( self._cb.credentials.org_key, self._query_token ) - query_parameters = {} while still_fetching: - result_url = '{}?start={}&rows={}'.format( - result_url_template, - current, - self._batch_size - ) + if self._aggregation: + result_url = "/api/investigate/v1/orgs/{}/observations/search_jobs/{}/group_results".format( + self._cb.credentials.org_key, + self._query_token, + ) + result = self._cb.post_object(result_url, self._build_aggregated_body()) + results = result.get("group_results", []) + else: + + result_url = '{}?start={}&rows={}'.format( + result_url_template, + current, + self._batch_size + ) + result = self._cb.get_object(result_url, query_parameters=query_parameters) + results = result.get('results', []) - result = self._cb.get_object(result_url, query_parameters=query_parameters) self._total_results = result.get('num_available', 0) self._count_valid = True - results = result.get('results', []) for item in results: yield item @@ -300,3 +512,4 @@ def _search(self, start=0, rows=0): still_fetching = False log.debug("current: {}, total_results: {}".format(current, self._total_results)) + diff --git a/src/cbc_sdk/rest_api.py b/src/cbc_sdk/rest_api.py index 12d346128..070bcb181 100644 --- a/src/cbc_sdk/rest_api.py +++ b/src/cbc_sdk/rest_api.py @@ -405,6 +405,47 @@ def bulk_threat_dismiss(self, threat_ids, remediation=None, comment=None): """ return self._bulk_threat_update_status(threat_ids, "DISMISSED", remediation, comment) + # ---- Observations + + def observations_search_suggestions(self, query, count=None): + """ + Returns suggestions for keys and field values that can be used in a search. + + Args: + query (str): A search query to use. + count (int): (optional) Number of suggestions to be returned + + Returns: + list: A list of search suggestions expressed as dict objects. + """ + query_params = {"suggest.q": query} + if count: + query_params["suggest.count"] = count + url = "/api/investigate/v2/orgs/{}/observations/search_suggestions".format(self.credentials.org_key) + output = self.get_object(url, query_params) + return output["suggestions"] + + def observations_search_validation(self, query, min_backend_timestamp=None, max_backend_timestamp=None): + """ + Returns suggestions for keys and field values that can be used in a search. + + Args: + query (str): A search query to be validated. + min_backend_timestamp (str): (optional) The start time for the query + max_backend_timestamp (str): (optional) The end time for the query + + Returns: + dict: A dict with status of the validation + """ + query_params = {"suggest.q": query} + if min_backend_timestamp: + query_params["cb.min_backend_timestamp"] = min_backend_timestamp + if max_backend_timestamp: + query_params["cb.max_backend_timestamp"] = max_backend_timestamp + url = "/api/investigate/v2/orgs/{}/observations/search_validation".format(self.credentials.org_key) + output = self.get_object(url, query_params) + return output + # ---- Enterprise EDR def create(self, cls, data=None): From 797d8c15f39143200e9e16bf528d9294c2525009 Mon Sep 17 00:00:00 2001 From: Emanuela Mitreva Date: Mon, 20 Feb 2023 11:23:22 +0200 Subject: [PATCH 048/143] Fixes --- src/cbc_sdk/platform/observations.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/cbc_sdk/platform/observations.py b/src/cbc_sdk/platform/observations.py index 16e655728..546b5478c 100644 --- a/src/cbc_sdk/platform/observations.py +++ b/src/cbc_sdk/platform/observations.py @@ -11,7 +11,7 @@ """Model and Query Classes for Observations""" -from cbc_sdk.base import UnrefreshableModel +from cbc_sdk.base import UnrefreshableModel, FacetQuery from cbc_sdk.base import Query from cbc_sdk.errors import ApiError, TimeoutError @@ -365,17 +365,17 @@ def aggregate(self, fields): Performs an aggregation search where results are grouped by an aggregation field Args: - field (str): The aggregation field or fields, valid ones are: + fields (str): The aggregation field or fields, valid ones are: observation_type, device_name, process_username, attack_tactic """ if not isinstance(fields, list): raise ApiError("Fields should be list of values") - if not all((gtype in ObservationQuery.VALID_GROUP_FIELDS) for gt in fields): + if not all((gt in ObservationQuery.VALID_GROUP_FIELDS) for gt in fields): raise ApiError("One or more invalid aggregation fields") self._aggregate = True - self._aggregate_fields = [field] if isinstance(field, str) else fields + self._aggregate_fields = fields return self def max_events_per_group(self, max_events_per_group): @@ -425,7 +425,7 @@ def range(self, duration, field, method="interval"): """ if not self._aggregate: raise ApiError("You should first aggregate the records.") - self._range = dict(duration=duration,field=field, method=method) + self._range = dict(duration=duration, field=field, method=method) return self def _build_aggregated_body(self): @@ -455,7 +455,6 @@ def _build_aggregated_body(self): data["start"] = self._start return data - def _search(self, start=0, rows=0): if not self._query_token: self._submit() @@ -485,7 +484,6 @@ def _search(self, start=0, rows=0): result = self._cb.post_object(result_url, self._build_aggregated_body()) results = result.get("group_results", []) else: - result_url = '{}?start={}&rows={}'.format( result_url_template, current, @@ -494,11 +492,9 @@ def _search(self, start=0, rows=0): result = self._cb.get_object(result_url, query_parameters=query_parameters) results = result.get('results', []) - self._total_results = result.get('num_available', 0) self._count_valid = True - for item in results: yield item current += 1 @@ -512,4 +508,3 @@ def _search(self, start=0, rows=0): still_fetching = False log.debug("current: {}, total_results: {}".format(current, self._total_results)) - From b4af66eec64af15205eb4cf5d3e2a3775c07feb3 Mon Sep 17 00:00:00 2001 From: Emanuela Mitreva Date: Mon, 20 Feb 2023 15:47:16 +0200 Subject: [PATCH 049/143] Add facet yaml --- .../platform/models/observation_facet.yaml | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 src/cbc_sdk/platform/models/observation_facet.yaml diff --git a/src/cbc_sdk/platform/models/observation_facet.yaml b/src/cbc_sdk/platform/models/observation_facet.yaml new file mode 100644 index 000000000..84bc9cb8d --- /dev/null +++ b/src/cbc_sdk/platform/models/observation_facet.yaml @@ -0,0 +1,61 @@ +type: object +properties: + terms: + type: array + description: Contains the Observations Facet search results + items: + field: + type: string + description: The name of the field being summarized + values: + type: array + items: + type: object + properties: + total: + type: integer + format: int32 + description: The total number of times this value appears in the query output + id: + type: string + description: The ID of the value being enumerated + name: + type: string + description: The name of the value being enumerated + ranges: + type: array + description: Groupings for search result properties that are ISO 8601 timestamps or numbers + items: + bucket_size: + type: string + description: How large of a bucket to group results in. If grouping an ISO 8601 property, use a string like '-3DAYS' + start: + oneOf: + - type: integer + - type: string + description: What value to begin grouping at + end: + type: string + description: What value to end grouping at + field: + type: string + description: The name of the field being grouped + values: + type: array + description: The result values of the field being grouped + items: + name: + type: string + description: The name of the value being enumerated + total: + type: integer + description: The total number of times this value appears in the query bucket output + num_found: + type: integer + descrption: The total number of results of the query + contacted: + type: integer + description: The number of searchers contacted for this query + completed: + type: integer + description: The number of searchers that have reported their results \ No newline at end of file From a72f2b922f73a322537030b2071a7f80c4716b26 Mon Sep 17 00:00:00 2001 From: Emanuela Mitreva Date: Mon, 20 Feb 2023 15:57:10 +0200 Subject: [PATCH 050/143] Fix data item name --- src/cbc_sdk/platform/observations.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/cbc_sdk/platform/observations.py b/src/cbc_sdk/platform/observations.py index 546b5478c..21a7b35a5 100644 --- a/src/cbc_sdk/platform/observations.py +++ b/src/cbc_sdk/platform/observations.py @@ -250,7 +250,7 @@ def __init__(self, doc_class, cb): self._timeout = 0 self._timed_out = False - self._aggregate = False + self._aggregation = False self._aggregate_fields = None self._max_events_per_group = None self._range = None @@ -374,7 +374,7 @@ def aggregate(self, fields): if not all((gt in ObservationQuery.VALID_GROUP_FIELDS) for gt in fields): raise ApiError("One or more invalid aggregation fields") - self._aggregate = True + self._aggregation = True self._aggregate_fields = fields return self @@ -385,7 +385,7 @@ def max_events_per_group(self, max_events_per_group): Args: max_events_per_group (int): Max events per aggregate """ - if not self._aggregate: + if not self._aggregation: raise ApiError("You should first aggregate the records.") self._max_events_per_group = max_events_per_group return self @@ -397,7 +397,7 @@ def rows(self, rows): Args: rows (int): Max events per group """ - if not self._aggregate: + if not self._aggregation: raise ApiError("You should first aggregate the records.") self._rows = rows return self @@ -409,7 +409,7 @@ def start(self, start): Args: start (int): start index """ - if not self._aggregate: + if not self._aggregation: raise ApiError("You should first aggregate the records.") self._start = start return self @@ -423,7 +423,7 @@ def range(self, duration, field, method="interval"): field (str): field method (str): either bucket or inteval """ - if not self._aggregate: + if not self._aggregation: raise ApiError("You should first aggregate the records.") self._range = dict(duration=duration, field=field, method=method) return self From 06523456241b0605a46a310f704daa2198aff4a2 Mon Sep 17 00:00:00 2001 From: Emanuela Mitreva Date: Mon, 20 Feb 2023 16:50:06 +0200 Subject: [PATCH 051/143] Add unit tests for facets + search validation / suggestions --- src/cbc_sdk/rest_api.py | 4 +- src/tests/unit/fixtures/mock_rest_api.py | 189 ++++----- .../fixtures/platform/mock_observations.py | 53 +++ src/tests/unit/platform/test_observations.py | 368 +++++++++++++++++- src/tests/unit/test_rest_api.py | 109 ++++-- 5 files changed, 575 insertions(+), 148 deletions(-) diff --git a/src/cbc_sdk/rest_api.py b/src/cbc_sdk/rest_api.py index 070bcb181..eddf06d01 100644 --- a/src/cbc_sdk/rest_api.py +++ b/src/cbc_sdk/rest_api.py @@ -405,7 +405,7 @@ def bulk_threat_dismiss(self, threat_ids, remediation=None, comment=None): """ return self._bulk_threat_update_status(threat_ids, "DISMISSED", remediation, comment) - # ---- Observations + # ---- Observations ----- def observations_search_suggestions(self, query, count=None): """ @@ -444,7 +444,7 @@ def observations_search_validation(self, query, min_backend_timestamp=None, max_ query_params["cb.max_backend_timestamp"] = max_backend_timestamp url = "/api/investigate/v2/orgs/{}/observations/search_validation".format(self.credentials.org_key) output = self.get_object(url, query_params) - return output + return output.get("valid", False) # ---- Enterprise EDR diff --git a/src/tests/unit/fixtures/mock_rest_api.py b/src/tests/unit/fixtures/mock_rest_api.py index 24340fafe..724a93942 100644 --- a/src/tests/unit/fixtures/mock_rest_api.py +++ b/src/tests/unit/fixtures/mock_rest_api.py @@ -18,13 +18,13 @@ "sha256Hash": "2552332222112552332222112552332222112552332222112552332222112552", "action": "TERMINATE", "reputation": "KNOWN_MALWARE", - "applicationName": "firefox.exe" + "applicationName": "firefox.exe", }, "type": "POLICY_ACTION", "eventTime": 1423163263482, "eventId": "EV1", "url": "http://carbonblack.com/ui#device/100/hash/25523322221125523322221125523322221125523" - "32222112552332222112552/app/firefox.exe/keyword/terminate policy action", + "32222112552332222112552/app/firefox.exe/keyword/terminate policy action", "deviceInfo": { "deviceType": "WINDOWS", "email": "tester@carbonblack.com", @@ -36,10 +36,10 @@ "targetPriorityCode": 0, "internalIpAddress": "55.33.22.11", "groupName": "Executives", - "externalIpAddress": "255.233.222.211" + "externalIpAddress": "255.233.222.211", }, "eventDescription": "Policy action 1", - "ruleName": "Alert Rule 1" + "ruleName": "Alert Rule 1", }, { "threatInfo": { @@ -48,17 +48,17 @@ { "sha256Hash": "aafafafafafafafafafafafafafafafafafafa7347878", "indicatorName": "BUFFER_OVERFLOW", - "applicationName": "chrome.exe" + "applicationName": "chrome.exe", }, { "sha256Hash": "ddfdhjhjdfjdfjdhjfdjfhjdfhjdhfjdhfjdhfjdh7347878", "indicatorName": "INJECT_CODE", - "applicationName": "firefox.exe" - } + "applicationName": "firefox.exe", + }, ], "summary": "Threat Summary 23", "score": 8, - "incidentId": "ABCDEF" + "incidentId": "ABCDEF", }, "type": "THREAT", "eventTime": 1423163263501, @@ -75,16 +75,17 @@ "targetPriorityCode": 0, "internalIpAddress": "55.33.22.11", "groupName": "Executives", - "externalIpAddress": "255.233.222.211" + "externalIpAddress": "255.233.222.211", }, "eventDescription": "time|Threat summary 23|score", - "ruleName": "Alert Rule 2" - } + "ruleName": "Alert Rule 2", + }, ], "message": "Success", - "success": True + "success": True, } + AUDITLOGS_RESP = { "notifications": [ { @@ -96,7 +97,7 @@ "flagged": False, "clientIp": "192.0.2.3", "verbose": False, - "description": "Logged in successfully" + "description": "Logged in successfully", }, { "requestUrl": None, @@ -107,7 +108,7 @@ "flagged": False, "clientIp": "192.0.2.3", "verbose": False, - "description": "Logged in successfully" + "description": "Logged in successfully", }, { "requestUrl": None, @@ -118,7 +119,7 @@ "flagged": False, "clientIp": "192.0.2.1", "verbose": False, - "description": "Updated connector jason-splunk-test with api key Y8JNJZFBDRUJ2ZSM" + "description": "Updated connector jason-splunk-test with api key Y8JNJZFBDRUJ2ZSM", }, { "requestUrl": None, @@ -129,7 +130,7 @@ "flagged": False, "clientIp": "192.0.2.2", "verbose": False, - "description": "Updated connector Training with api key GRJSDHRR8YVRML3Q" + "description": "Updated connector Training with api key GRJSDHRR8YVRML3Q", }, { "requestUrl": None, @@ -140,118 +141,76 @@ "flagged": False, "clientIp": "192.0.2.2", "verbose": False, - "description": "Logged in successfully" - } + "description": "Logged in successfully", + }, ], "success": True, - "message": "Success" + "message": "Success", } + ALERT_SEARCH_SUGGESTIONS_RESP = { "suggestions": [ - { - "term": "threat_category", - "weight": 525 - }, - { - "term": "watchlist_name", - "weight": 512 - }, - { - "term": "ttp", - "weight": 486 - }, - { - "term": "run_state", - "weight": 481 - }, - { - "term": "device_name", - "weight": 477 - }, - { - "term": "alert_id", - "weight": 472 - }, - { - "term": "event_id", - "weight": 472 - }, - { - "term": "threat_vector", - "weight": 468 - }, - { - "term": "device_username", - "weight": 461 - }, - { - "term": "report_id", - "weight": 458 - }, - { - "term": "process_guid", - "weight": 431 - }, - { - "term": "process_name", - "weight": 431 - }, - { - "term": "sensor_action", - "weight": 424 - }, - { - "term": "alert_severity", - "weight": 419 - }, - { - "term": "device_id", - "weight": 412 - }, - { - "term": "device_os", - "weight": 412 - }, - { - "term": "device_policy", - "weight": 401 - }, - { - "term": "process_pid", - "weight": 311 - }, - { - "term": "process_hash", - "weight": 306 - }, - { - "term": "process_reputation", - "weight": 287 - } + {"term": "threat_category", "weight": 525}, + {"term": "watchlist_name", "weight": 512}, + {"term": "ttp", "weight": 486}, + {"term": "run_state", "weight": 481}, + {"term": "device_name", "weight": 477}, + {"term": "alert_id", "weight": 472}, + {"term": "event_id", "weight": 472}, + {"term": "threat_vector", "weight": 468}, + {"term": "device_username", "weight": 461}, + {"term": "report_id", "weight": 458}, + {"term": "process_guid", "weight": 431}, + {"term": "process_name", "weight": 431}, + {"term": "sensor_action", "weight": 424}, + {"term": "alert_severity", "weight": 419}, + {"term": "device_id", "weight": 412}, + {"term": "device_os", "weight": 412}, + {"term": "device_policy", "weight": 401}, + {"term": "process_pid", "weight": 311}, + {"term": "process_hash", "weight": 306}, + {"term": "process_reputation", "weight": 287}, ] } -PROCESS_SEARCH_VALIDATIONS_RESP = { - "valid": True, - "value_search_query": True -} -CUSTOM_SEVERITY_RESP = { - "results": [{"report_id": "id", "severity": 10}] -} +PROCESS_SEARCH_VALIDATIONS_RESP = {"valid": True, "value_search_query": True} + + +CUSTOM_SEVERITY_RESP = {"results": [{"report_id": "id", "severity": 10}]} + + +PROCESS_LIMITS_RESP = {"time_bounds": {"lower": 1564686473166, "upper": 1579023412990}} -PROCESS_LIMITS_RESP = { - "time_bounds": { - "lower": 1564686473166, - "upper": 1579023412990 - } -} FETCH_PROCESS_QUERY_RESP = { - "query_ids": ['4JDT3MX9Q/3867b4e7-b329-4caa-8f80-76899b1360fa', '4JDT3MX9Q/3871eab1-bb9b-4cb7-9ac4-a840f4a84fab'] + "query_ids": [ + "4JDT3MX9Q/3867b4e7-b329-4caa-8f80-76899b1360fa", + "4JDT3MX9Q/3871eab1-bb9b-4cb7-9ac4-a840f4a84fab", + ] } -CONVERT_FEED_QUERY_RESP = { - "query": '(process_guid:123) -enriched:true' + +CONVERT_FEED_QUERY_RESP = {"query": "(process_guid:123) -enriched:true"} + + +OBSERVATIONS_SEARCH_VALIDATIONS_RESP = {"valid": True, "value_search_query": True} + + +OBSERVATIONS_SEARCH_SUGGESTIONS_RESP = { + "suggestions": [ + { + "required_skus_all": [], + "required_skus_some": ["threathunter", "defense"], + "term": "device_id", + "weight": 100, + }, + { + "required_skus_all": ["xdr"], + "required_skus_some": [], + "term": "netconn_remote_device_id", + "weight": 70, + }, + ] } diff --git a/src/tests/unit/fixtures/platform/mock_observations.py b/src/tests/unit/fixtures/platform/mock_observations.py index e8c415535..cea1e244f 100644 --- a/src/tests/unit/fixtures/platform/mock_observations.py +++ b/src/tests/unit/fixtures/platform/mock_observations.py @@ -338,3 +338,56 @@ } ], } + + +"""Mocks for observations facet query testing.""" + + +POST_OBSERVATIONS_FACET_SEARCH_JOB_RESP = { + "job_id": "08ffa932-b633-4107-ba56-8741e929e48b" +} + + +GET_OBSERVATIONS_FACET_SEARCH_JOB_RESULTS_RESP_1 = { + "ranges": [ + { + "start": "2020-08-04T08:01:32.077Z", + "end": "2020-08-05T08:01:32.077Z", + "bucket_size": "+1HOUR", + "field": "device_timestamp", + "values": [{"total": 456, "name": "2020-08-04T08:01:32.077Z"}], + } + ], + "terms": [ + { + "values": [{"total": 116, "id": "chrome.exe", "name": "chrome.exe"}], + "field": "process_name", + } + ], + "num_found": 116, + "contacted": 34, + "completed": 34, +} + + +GET_OBSERVATIONS_FACET_SEARCH_JOB_RESULTS_RESP_2 = { + "ranges": [], + "terms": [ + { + "values": [{"total": 116, "id": "chrome.exe", "name": "chrome.exe"}], + "field": "process_name", + } + ], + "num_found": 116, + "contacted": 34, + "completed": 34, +} + + +GET_OBSERVATIONS_FACET_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING = { + "ranges": [], + "terms": [], + "num_found": 0, + "contacted": 34, + "completed": 0, +} diff --git a/src/tests/unit/platform/test_observations.py b/src/tests/unit/platform/test_observations.py index 386f3108d..dad06b90b 100644 --- a/src/tests/unit/platform/test_observations.py +++ b/src/tests/unit/platform/test_observations.py @@ -2,11 +2,14 @@ import pytest import logging + +from cbc_sdk.base import FacetQuery from cbc_sdk.platform import Observation -from cbc_sdk.platform.observations import ObservationQuery +from cbc_sdk.platform.observations import ObservationQuery, ObservationFacet from cbc_sdk.rest_api import CBCloudAPI from cbc_sdk.errors import ApiError, TimeoutError from tests.unit.fixtures.CBCSDKMock import CBCSDKMock + from tests.unit.fixtures.platform.mock_observations import ( GET_OBSERVATIONS_SEARCH_JOB_RESULTS_ZERO, POST_OBSERVATIONS_SEARCH_JOB_RESP, @@ -16,6 +19,10 @@ GET_OBSERVATIONS_SEARCH_JOB_RESULTS_RESP_ZERO_COMP, GET_OBSERVATIONS_DETAIL_JOB_RESULTS_RESP, GET_OBSERVATIONS_SEARCH_JOB_RESULTS_RESP, + POST_OBSERVATIONS_FACET_SEARCH_JOB_RESP, + GET_OBSERVATIONS_FACET_SEARCH_JOB_RESULTS_RESP_1, + GET_OBSERVATIONS_FACET_SEARCH_JOB_RESULTS_RESP_2, + GET_OBSERVATIONS_FACET_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING, ) log = logging.basicConfig( @@ -119,7 +126,10 @@ def test_observation_select_by_id(cbcsdk_mock): ) api = cbcsdk_mock.api - obs = api.select(Observation, "8fbccc2da75f11ed937ae3cb089984c6:be6ff259-88e3-6286-789f-74defa192d2e") + obs = api.select( + Observation, + "8fbccc2da75f11ed937ae3cb089984c6:be6ff259-88e3-6286-789f-74defa192d2e", + ) assert obs["device_name"] is not None assert obs["enriched"] is not None @@ -324,7 +334,9 @@ def test_observations_query_implementation(cbcsdk_mock): GET_OBSERVATIONS_SEARCH_JOB_RESULTS_RESP, ) api = cbcsdk_mock.api - observation_id = "8fbccc2da75f11ed937ae3cb089984c6:be6ff259-88e3-6286-789f-74defa192d2e" + observation_id = ( + "8fbccc2da75f11ed937ae3cb089984c6:be6ff259-88e3-6286-789f-74defa192d2e" + ) obs_list = api.select(Observation).where(f"observation_id:{observation_id}") assert isinstance(obs_list, ObservationQuery) assert obs_list[0].observation_id == observation_id @@ -523,3 +535,353 @@ def test_observations_still_querying2(cbcsdk_mock): api = cbcsdk_mock.api obs_list = api.select(Observation).where(process_pid=1000) assert obs_list._still_querying() is True + + +# --------------------- ObservationFacet -------------------------------------- + + +def test_observation_facet_select_where(cbcsdk_mock): + """Testing Observation Querying with select()""" + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/observations/facet_jobs", + POST_OBSERVATIONS_FACET_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/facet_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results", # noqa: E501 + GET_OBSERVATIONS_FACET_SEARCH_JOB_RESULTS_RESP_2, + ) + + api = cbcsdk_mock.api + observations = ( + api.select(ObservationFacet) + .where(process_name="chrome.exe") + .add_facet_field("process_name") + ) + observation = observations.results + assert observation.terms is not None + assert observation.ranges is not None + assert observation.ranges == [] + assert observation.terms[0]["field"] == "process_name" + + +def test_observation_facet_select_async(cbcsdk_mock): + """Testing Observation Querying with select()""" + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/observations/facet_jobs", + POST_OBSERVATIONS_FACET_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/facet_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results", # noqa: E501 + GET_OBSERVATIONS_FACET_SEARCH_JOB_RESULTS_RESP_2, + ) + + api = cbcsdk_mock.api + future = ( + api.select(ObservationFacet) + .where(process_name="chrome.exe") + .add_facet_field("process_name") + .execute_async() + ) + observation = future.result() + assert observation.terms is not None + assert observation.ranges is not None + assert observation.ranges == [] + assert observation.terms[0]["field"] == "process_name" + + +def test_observation_facet_select_compound(cbcsdk_mock): + """Testing Observation Querying with select() and more complex criteria""" + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/observations/facet_jobs", + POST_OBSERVATIONS_FACET_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/facet_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results", # noqa: E501 + GET_OBSERVATIONS_FACET_SEARCH_JOB_RESULTS_RESP_2, + ) + + api = cbcsdk_mock.api + observations = ( + api.select(ObservationFacet) + .where(process_name="chrome.exe") + .or_(process_name="firefox.exe") + .add_facet_field("process_name") + ) + observation = observations.results + assert observation.terms_.fields == ["process_name"] + assert observation.ranges == [] + + +def test_observation_facet_query_implementation(cbcsdk_mock): + """Testing Observation querying with where().""" + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/observations/facet_jobs", + POST_OBSERVATIONS_FACET_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/facet_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results", # noqa: E501 + GET_OBSERVATIONS_FACET_SEARCH_JOB_RESULTS_RESP_1, + ) + + api = cbcsdk_mock.api + field = "process_name" + observations = ( + api.select(ObservationFacet) + .where(process_name="test") + .add_facet_field("process_name") + ) + assert isinstance(observations, FacetQuery) + observation = observations.results + assert observation.terms[0]["field"] == field + assert observation.terms_.facets["process_name"] is not None + assert observation.terms_.fields[0] == "process_name" + assert observation.ranges_.facets is not None + assert observation.ranges_.fields[0] == "device_timestamp" + assert isinstance(observation._query_implementation(api), FacetQuery) + + +def test_observation_facet_timeout(cbcsdk_mock): + """Testing ObservationQuery.timeout().""" + api = cbcsdk_mock.api + query = ( + api.select(ObservationFacet) + .where("process_name:some_name") + .add_facet_field("process_name") + ) + assert query._timeout == 0 + query.timeout(msecs=500) + assert query._timeout == 500 + + +def test_observation_facet_timeout_error(cbcsdk_mock): + """Testing that a timeout in ObservationQuery throws the right TimeoutError.""" + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/observations/facet_jobs", + POST_OBSERVATIONS_FACET_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/facet_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results", # noqa: E501 + GET_OBSERVATIONS_FACET_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING, + ) + + api = cbcsdk_mock.api + query = ( + api.select(ObservationFacet) + .where("process_name:some_name") + .add_facet_field("process_name") + .timeout(1) + ) + with pytest.raises(TimeoutError): + query.results() + query = ( + api.select(ObservationFacet) + .where("process_name:some_name") + .add_facet_field("process_name") + .timeout(1) + ) + with pytest.raises(TimeoutError): + query._count() + + +def test_observation_facet_query_add_range(cbcsdk_mock): + """Testing Observation results sort.""" + api = cbcsdk_mock.api + range = ({"bucket_size": 30, "start": "0D", "end": "20D", "field": "something"},) + observations = ( + api.select(ObservationFacet) + .where(process_pid=1000) + .add_range(range) + .add_facet_field("process_name") + ) + assert observations._ranges[0]["bucket_size"] == 30 + assert observations._ranges[0]["start"] == "0D" + assert observations._ranges[0]["end"] == "20D" + assert observations._ranges[0]["field"] == "something" + + +def test_observation_facet_query_check_range(cbcsdk_mock): + """Testing Observation results sort.""" + api = cbcsdk_mock.api + range = ({"bucket_size": [], "start": "0D", "end": "20D", "field": "something"},) + with pytest.raises(ApiError): + api.select(ObservationFacet).where(process_pid=1000).add_range( + range + ).add_facet_field("process_name") + + range = ({"bucket_size": 30, "start": [], "end": "20D", "field": "something"},) + with pytest.raises(ApiError): + api.select(ObservationFacet).where(process_pid=1000).add_range( + range + ).add_facet_field("process_name") + + range = ({"bucket_size": 30, "start": "0D", "end": [], "field": "something"},) + with pytest.raises(ApiError): + api.select(ObservationFacet).where(process_pid=1000).add_range( + range + ).add_facet_field("process_name") + + range = ({"bucket_size": 30, "start": "0D", "end": "20D", "field": []},) + with pytest.raises(ApiError): + api.select(ObservationFacet).where(process_pid=1000).add_range( + range + ).add_facet_field("process_name") + + +def test_observation_facet_query_add_facet_field(cbcsdk_mock): + """Testing Observation results sort.""" + api = cbcsdk_mock.api + observations = ( + api.select(ObservationFacet) + .where(process_pid=1000) + .add_facet_field("process_name") + ) + assert observations._facet_fields[0] == "process_name" + + +def test_observation_facet_query_add_facet_fields(cbcsdk_mock): + """Testing Observation results sort.""" + api = cbcsdk_mock.api + observations = ( + api.select(ObservationFacet) + .where(process_pid=1000) + .add_facet_field(["process_name", "process_pid"]) + ) + assert "process_pid" in observations._facet_fields + assert "process_name" in observations._facet_fields + + +def test_observation_facet_query_add_facet_invalid_fields(cbcsdk_mock): + """Testing Observation results sort.""" + api = cbcsdk_mock.api + with pytest.raises(TypeError): + api.select(ObservationFacet).where(process_pid=1000).add_facet_field(1337) + + +def test_observation_facet_limit(cbcsdk_mock): + """Testing Observation results sort.""" + api = cbcsdk_mock.api + observations = ( + api.select(ObservationFacet) + .where(process_pid=1000) + .limit(123) + .add_facet_field("process_name") + ) + assert observations._limit == 123 + + +def test_observation_facet_time_range(cbcsdk_mock): + """Testing Observation results sort.""" + api = cbcsdk_mock.api + observations = ( + api.select(ObservationFacet) + .where(process_pid=1000) + .set_time_range( + start="2020-10-10T20:34:07Z", end="2020-10-20T20:34:07Z", window="-1d" + ) + .add_facet_field("process_name") + ) + assert observations._time_range["start"] == "2020-10-10T20:34:07Z" + assert observations._time_range["end"] == "2020-10-20T20:34:07Z" + assert observations._time_range["window"] == "-1d" + + +def test_observation_facet_submit(cbcsdk_mock): + """Test _submit method of ObservationQuery class""" + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/observations/facet_jobs", + POST_OBSERVATIONS_FACET_SEARCH_JOB_RESP, + ) + api = cbcsdk_mock.api + observations = ( + api.select(ObservationFacet) + .where(process_pid=1000) + .add_facet_field("process_name") + ) + observations._submit() + assert observations._query_token == "08ffa932-b633-4107-ba56-8741e929e48b" + + +def test_observation_facet_count(cbcsdk_mock): + """Test _submit method of ObservationQuery class""" + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/observations/facet_jobs", + POST_OBSERVATIONS_FACET_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/facet_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results", # noqa: E501 + GET_OBSERVATIONS_FACET_SEARCH_JOB_RESULTS_RESP_1, + ) + + api = cbcsdk_mock.api + observations = ( + api.select(ObservationFacet) + .where(process_pid=1000) + .add_facet_field("process_name") + ) + observations._count() + assert observations._count() == 116 + + +def test_observation_search(cbcsdk_mock): + """Test _search method of ObservationQuery class""" + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/observations/facet_jobs", + POST_OBSERVATIONS_FACET_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/facet_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results", # noqa: E501 + GET_OBSERVATIONS_FACET_SEARCH_JOB_RESULTS_RESP_2, + ) + + api = cbcsdk_mock.api + observations = ( + api.select(ObservationFacet) + .where(process_pid=1000) + .add_facet_field("process_name") + ) + future = observations.execute_async() + result = future.result() + assert result.terms is not None + assert len(result.ranges) == 0 + assert result.terms[0]["field"] == "process_name" + + +def test_observation_search_async(cbcsdk_mock): + """Test _search method of ObservationQuery class""" + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/observations/facet_jobs", + POST_OBSERVATIONS_FACET_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/facet_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results", # noqa: E501 + GET_OBSERVATIONS_FACET_SEARCH_JOB_RESULTS_RESP_2, + ) + + api = cbcsdk_mock.api + observations = ( + api.select(ObservationFacet) + .where(process_pid=1000) + .add_facet_field("process_name") + ) + future = observations.execute_async() + result = future.result() + assert result.terms is not None + assert len(result.ranges) == 0 + assert result.terms[0]["field"] == "process_name" diff --git a/src/tests/unit/test_rest_api.py b/src/tests/unit/test_rest_api.py index 69378fc5e..eece650dc 100644 --- a/src/tests/unit/test_rest_api.py +++ b/src/tests/unit/test_rest_api.py @@ -14,18 +14,26 @@ import pytest from cbc_sdk.rest_api import CBCloudAPI from tests.unit.fixtures.CBCSDKMock import CBCSDKMock -from tests.unit.fixtures.mock_rest_api import (NOTIFICATIONS_RESP, AUDITLOGS_RESP, ALERT_SEARCH_SUGGESTIONS_RESP, - PROCESS_SEARCH_VALIDATIONS_RESP, CUSTOM_SEVERITY_RESP, - PROCESS_LIMITS_RESP, FETCH_PROCESS_QUERY_RESP, CONVERT_FEED_QUERY_RESP) +from tests.unit.fixtures.mock_rest_api import ( + NOTIFICATIONS_RESP, + AUDITLOGS_RESP, + ALERT_SEARCH_SUGGESTIONS_RESP, + PROCESS_SEARCH_VALIDATIONS_RESP, + CUSTOM_SEVERITY_RESP, + PROCESS_LIMITS_RESP, + FETCH_PROCESS_QUERY_RESP, + CONVERT_FEED_QUERY_RESP, + OBSERVATIONS_SEARCH_VALIDATIONS_RESP, + OBSERVATIONS_SEARCH_SUGGESTIONS_RESP, +) @pytest.fixture(scope="function") def cb(): """Create CBCloudAPI singleton""" - return CBCloudAPI(url="https://example.com", - org_key="test", - token="abcd/1234", - ssl_verify=False) + return CBCloudAPI( + url="https://example.com", org_key="test", token="abcd/1234", ssl_verify=False + ) @pytest.fixture(scope="function") @@ -36,6 +44,7 @@ def cbcsdk_mock(monkeypatch, cb): # ==================================== UNIT TESTS BELOW ==================================== + def test_org_urn(cbcsdk_mock): """Tests the org_urn property.""" api = cbcsdk_mock.api @@ -44,7 +53,9 @@ def test_org_urn(cbcsdk_mock): def test_get_notifications(cbcsdk_mock): """Tests getting notifications""" - cbcsdk_mock.mock_request("GET", "/integrationServices/v3/notification", NOTIFICATIONS_RESP) + cbcsdk_mock.mock_request( + "GET", "/integrationServices/v3/notification", NOTIFICATIONS_RESP + ) api = cbcsdk_mock.api result = api.get_notifications() assert len(result) == 2 @@ -61,56 +72,98 @@ def test_get_auditlogs(cbcsdk_mock): def test_alert_search_suggestions(cbcsdk_mock): """Tests getting alert search suggestions""" api = cbcsdk_mock.api - cbcsdk_mock.mock_request("GET", "/appservices/v6/orgs/test/alerts/search_suggestions?suggest.q=", - ALERT_SEARCH_SUGGESTIONS_RESP) - result = api.alert_search_suggestions('') + cbcsdk_mock.mock_request( + "GET", + "/appservices/v6/orgs/test/alerts/search_suggestions?suggest.q=", + ALERT_SEARCH_SUGGESTIONS_RESP, + ) + result = api.alert_search_suggestions("") assert len(result) == 20 def test_process_search_validations(cbcsdk_mock): """Tests getting process search validations""" api = cbcsdk_mock.api - cbcsdk_mock.mock_request("GET", "/api/investigate/v1/orgs/test/processes/search_validation?q=process", - PROCESS_SEARCH_VALIDATIONS_RESP) - result = api.validate_process_query('process') + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v1/orgs/test/processes/search_validation?q=process", + PROCESS_SEARCH_VALIDATIONS_RESP, + ) + result = api.validate_process_query("process") assert result def test_custom_severities(cbcsdk_mock): """Tests getting custom severities""" api = cbcsdk_mock.api - cbcsdk_mock.mock_request("GET", "/threathunter/watchlistmgr/v3/orgs/test/reports/severity", - CUSTOM_SEVERITY_RESP) + cbcsdk_mock.mock_request( + "GET", + "/threathunter/watchlistmgr/v3/orgs/test/reports/severity", + CUSTOM_SEVERITY_RESP, + ) result = api.custom_severities assert len(result) == 1 - assert result[0].report_id == 'id' + assert result[0].report_id == "id" assert result[0].severity == 10 def test_process_limits(cbcsdk_mock): """Tests getting process limits""" api = cbcsdk_mock.api - cbcsdk_mock.mock_request("GET", "/api/investigate/v1/orgs/test/processes/limits", - PROCESS_LIMITS_RESP) + cbcsdk_mock.mock_request( + "GET", "/api/investigate/v1/orgs/test/processes/limits", PROCESS_LIMITS_RESP + ) result = api.process_limits() - assert result['time_bounds'].get('upper') is not None - assert result['time_bounds'].get('lower') is not None + assert result["time_bounds"].get("upper") is not None + assert result["time_bounds"].get("lower") is not None def test_fetch_process_queries(cbcsdk_mock): """Tests getting process queries""" api = cbcsdk_mock.api - cbcsdk_mock.mock_request("GET", "/api/investigate/v1/orgs/test/processes/search_jobs", - FETCH_PROCESS_QUERY_RESP) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v1/orgs/test/processes/search_jobs", + FETCH_PROCESS_QUERY_RESP, + ) result = api.fetch_process_queries() assert len(result) == 2 - assert result[0] == '4JDT3MX9Q/3867b4e7-b329-4caa-8f80-76899b1360fa' + assert result[0] == "4JDT3MX9Q/3867b4e7-b329-4caa-8f80-76899b1360fa" def test_convert_feed_query(cbcsdk_mock): """Tests getting process queries""" api = cbcsdk_mock.api - cbcsdk_mock.mock_request("POST", "/threathunter/feedmgr/v2/query/translate", - CONVERT_FEED_QUERY_RESP) - result = api.convert_feed_query('id:123') - assert 'process_guid:123' in result + cbcsdk_mock.mock_request( + "POST", "/threathunter/feedmgr/v2/query/translate", CONVERT_FEED_QUERY_RESP + ) + result = api.convert_feed_query("id:123") + assert "process_guid:123" in result + + +def test_observations_search_validations(cbcsdk_mock): + """Tests getting observations search validations""" + api = cbcsdk_mock.api + q = "?cb.max_backend_timestamp=2020-08-05T08%3A01%3A32.077Z&cb.min_backend_timestamp=2020-08-04T08%3A01%3A32.077Z&suggest.q=device_id" + cbcsdk_mock.mock_request( + "GET", + f"/api/investigate/v2/orgs/test/observations/search_validation{q}", + OBSERVATIONS_SEARCH_VALIDATIONS_RESP, + ) + result = api.observations_search_validation( + "device_id", "2020-08-04T08:01:32.077Z", "2020-08-05T08:01:32.077Z" + ) + assert result + + +def test_observations_search_suggestions(cbcsdk_mock): + """Tests getting observations search suggestions""" + api = cbcsdk_mock.api + q = "suggest.count=10&suggest.q=device_id" + cbcsdk_mock.mock_request( + "GET", + f"/api/investigate/v2/orgs/test/observations/search_suggestions?{q}", + OBSERVATIONS_SEARCH_SUGGESTIONS_RESP, + ) + result = api.observations_search_suggestions("device_id", 10) + assert len(result) != 0 From f92e03c5bb9650720314e9e7c1978d96c82d9aef Mon Sep 17 00:00:00 2001 From: Emanuela Mitreva Date: Mon, 20 Feb 2023 16:52:40 +0200 Subject: [PATCH 052/143] fix --- src/tests/unit/test_rest_api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/tests/unit/test_rest_api.py b/src/tests/unit/test_rest_api.py index eece650dc..e6ea7c224 100644 --- a/src/tests/unit/test_rest_api.py +++ b/src/tests/unit/test_rest_api.py @@ -144,7 +144,8 @@ def test_convert_feed_query(cbcsdk_mock): def test_observations_search_validations(cbcsdk_mock): """Tests getting observations search validations""" api = cbcsdk_mock.api - q = "?cb.max_backend_timestamp=2020-08-05T08%3A01%3A32.077Z&cb.min_backend_timestamp=2020-08-04T08%3A01%3A32.077Z&suggest.q=device_id" + q = "?cb.max_backend_timestamp=2020-08-05T08%3A01%3A32.077Z&cb.min_backend_timestamp=" \ + "2020-08-04T08%3A01%3A32.077Z&suggest.q=device_id" cbcsdk_mock.mock_request( "GET", f"/api/investigate/v2/orgs/test/observations/search_validation{q}", From 8aa60e08c346c4418e657cee3ccca17642d4a908 Mon Sep 17 00:00:00 2001 From: Emanuela Mitreva Date: Mon, 20 Feb 2023 17:45:15 +0200 Subject: [PATCH 053/143] Fixes --- src/cbc_sdk/platform/observations.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/cbc_sdk/platform/observations.py b/src/cbc_sdk/platform/observations.py index 21a7b35a5..729227e82 100644 --- a/src/cbc_sdk/platform/observations.py +++ b/src/cbc_sdk/platform/observations.py @@ -347,7 +347,7 @@ def _count(self): self._cb.credentials.org_key, self._query_token, ) - result = self._cb.post_object(result_url, self._build_aggregated_body()) + result = self._cb.post_object(result_url, self._build_aggregated_body()).json() else: result_url = "/api/investigate/v2/orgs/{}/observations/search_jobs/{}/results".format( self._cb.credentials.org_key, @@ -360,16 +360,19 @@ def _count(self): return self._total_results - def aggregate(self, fields): + def aggregation(self, fields): """ Performs an aggregation search where results are grouped by an aggregation field Args: - fields (str): The aggregation field or fields, valid ones are: + fields (str or list): The aggregation field or fields, valid ones are: observation_type, device_name, process_username, attack_tactic """ - if not isinstance(fields, list): - raise ApiError("Fields should be list of values") + if not isinstance(fields, list) and not isinstance(fields, str): + raise ApiError("Fields should be either a single field or list of fields") + + if isinstance(fields, str): + fields = [fields] if not all((gt in ObservationQuery.VALID_GROUP_FIELDS) for gt in fields): raise ApiError("One or more invalid aggregation fields") @@ -447,8 +450,8 @@ def _build_aggregated_body(self): data = dict(fields=self._aggregate_fields) if self._max_events_per_group: data["max_events_per_group"] = self._max_events_per_group - if self._ranges: - data["ranges"] = self._ranges + if self._range: + data["ranges"] = self._range if self._rows: data["rows"] = self._rows if self._start: @@ -481,8 +484,10 @@ def _search(self, start=0, rows=0): self._cb.credentials.org_key, self._query_token, ) - result = self._cb.post_object(result_url, self._build_aggregated_body()) - results = result.get("group_results", []) + result = self._cb.post_object(result_url, self._build_aggregated_body()).json() + results = [] + for group in result["group_results"]: + results.extend(group["results"]) else: result_url = '{}?start={}&rows={}'.format( result_url_template, From b8a5f365d3b4ab84d2c6f72d30ab010a46d0ce78 Mon Sep 17 00:00:00 2001 From: Emanuela Mitreva Date: Mon, 20 Feb 2023 17:53:47 +0200 Subject: [PATCH 054/143] fixes --- src/cbc_sdk/platform/observations.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/cbc_sdk/platform/observations.py b/src/cbc_sdk/platform/observations.py index 729227e82..f92375479 100644 --- a/src/cbc_sdk/platform/observations.py +++ b/src/cbc_sdk/platform/observations.py @@ -448,13 +448,13 @@ def _build_aggregated_body(self): } """ data = dict(fields=self._aggregate_fields) - if self._max_events_per_group: + if self._max_events_per_group is not None: data["max_events_per_group"] = self._max_events_per_group - if self._range: + if self._range is not None: data["ranges"] = self._range - if self._rows: + if self._rows is not None: data["rows"] = self._rows - if self._start: + if self._start is not None: data["start"] = self._start return data From 7f6cb065541675904780ae57ffdbc7059c54eb37 Mon Sep 17 00:00:00 2001 From: Emanuela Mitreva Date: Mon, 20 Feb 2023 19:34:59 +0200 Subject: [PATCH 055/143] Handle group results differently --- src/cbc_sdk/platform/observations.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/cbc_sdk/platform/observations.py b/src/cbc_sdk/platform/observations.py index f92375479..ca058f610 100644 --- a/src/cbc_sdk/platform/observations.py +++ b/src/cbc_sdk/platform/observations.py @@ -17,6 +17,7 @@ import logging import time +from copy import deepcopy log = logging.getLogger(__name__) @@ -486,8 +487,15 @@ def _search(self, start=0, rows=0): ) result = self._cb.post_object(result_url, self._build_aggregated_body()).json() results = [] - for group in result["group_results"]: - results.extend(group["results"]) + # flatten the list of results and add the common fields of each group + # for each element of the group + for group in result.get("group_results", []): + common = deepcopy(group) + del common["results"] + for item in group["results"]: + ditem = deepcopy(item) + ditem.update(common) + results.append(ditem) else: result_url = '{}?start={}&rows={}'.format( result_url_template, From 1806ee97d3a22be3c887ef9d39f21d881b81b733 Mon Sep 17 00:00:00 2001 From: Emanuela Mitreva Date: Tue, 21 Feb 2023 07:35:30 +0200 Subject: [PATCH 056/143] unit tests for grouped results --- .../fixtures/platform/mock_observations.py | 70 +++++++++++++++++ src/tests/unit/platform/test_observations.py | 77 +++++++++++++++++++ 2 files changed, 147 insertions(+) diff --git a/src/tests/unit/fixtures/platform/mock_observations.py b/src/tests/unit/fixtures/platform/mock_observations.py index cea1e244f..4602b718d 100644 --- a/src/tests/unit/fixtures/platform/mock_observations.py +++ b/src/tests/unit/fixtures/platform/mock_observations.py @@ -391,3 +391,73 @@ "contacted": 34, "completed": 0, } + + +GET_OBSERVATIONS_GROUPED_RESULTS_RESP = { + "approximate_unaggregated": 442, + "completed": 7, + "contacted": 7, + "group_results": [ + { + "group_end_timestamp": "2023-02-14T06:20:35.696Z", + "group_key": "device_name", + "group_start_timestamp": "2023-02-05T09:34:57.499Z", + "group_value": "dev01-39x-1", + "results": [ + { + "alert_category": [ + "OBSERVED" + ], + "alert_id": [ + "fbb78467-f63c-ac52-622a-f41c6f07d815" + ], + "backend_timestamp": "2023-02-14T06:23:18.887Z", + "device_group_id": 0, + "device_id": 17482451, + "device_name": "dev01-39x-1", + "device_policy_id": 20792247, + "device_timestamp": "2023-02-14T06:20:35.696Z", + "enriched": True, + "enriched_event_type": [ + "NETWORK" + ], + "event_description": "The script ...", + "event_id": "c7bdd379ac2f11ed92c0b59a6de446c9", + "event_network_inbound": False, + "event_network_local_ipv4": "10.203.105.21", + "event_network_location": "Santa Clara,CA,United States", + "event_network_protocol": "TCP", + "event_network_remote_ipv4": "23.67.33.87", + "event_network_remote_port": 80, + "event_type": [ + "netconn" + ], + "ingress_time": 1676355686412, + "legacy": True, + "observation_description": "The application", + "observation_id": "c7bdd379ac2f11ed92c0b59a6de446c9:fbb78467-f63c-ac52-622a-f41c6f07d815", + "observation_type": "CB_ANALYTICS", + "org_id": "ABCD123456", + "parent_guid": "ABCD123456-010ac2d3-00001164-00000000-1d9403c70fffc03", + "parent_pid": 4452, + "process_guid": "ABCD123456-010ac2d3-0000066c-00000000-1d9403c710932fb", + "process_hash": [ + "dcb5ffb192d9bce84d21f274a87cb5f839ed92094121cc254b5f3bae2f266d62" + ], + "process_name": "c:\\programdata\\mozilla-1de4eec8-1241-4177-a864-e594e8d1fb38\\updates\\308046b0af4a39cb\\backgroundupdate.moz_log", + "process_pid": [ + 2000 + ], + "process_username": [ + "DEV01-39X-1\\bit9qa" + ] + } + ], + "total_events": 1 + } + ], + "groups_num_available": 0, + "num_aggregated": 0, + "num_available": 1, + "num_found": 1 +} diff --git a/src/tests/unit/platform/test_observations.py b/src/tests/unit/platform/test_observations.py index dad06b90b..eef478abd 100644 --- a/src/tests/unit/platform/test_observations.py +++ b/src/tests/unit/platform/test_observations.py @@ -23,6 +23,7 @@ GET_OBSERVATIONS_FACET_SEARCH_JOB_RESULTS_RESP_1, GET_OBSERVATIONS_FACET_SEARCH_JOB_RESULTS_RESP_2, GET_OBSERVATIONS_FACET_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING, + GET_OBSERVATIONS_GROUPED_RESULTS_RESP ) log = logging.basicConfig( @@ -885,3 +886,79 @@ def test_observation_search_async(cbcsdk_mock): assert result.terms is not None assert len(result.ranges) == 0 assert result.terms[0]["field"] == "process_name" + + +def test_observation_aggregation_setup_failures(cbcsdk_mock): + """Testing whether exceptions are raised, when incorrectly setting aggregation properties""" + api = cbcsdk_mock.api + with pytest.raises(ApiError): + observation = api.select(Observation).where(process_pid=2000).max_events_per_group(10) + with pytest.raises(ApiError): + observation = api.select(Observation).where(process_pid=2000).rows(5) + with pytest.raises(ApiError): + observation = api.select(Observation).where(process_pid=2000).start(0) + with pytest.raises(ApiError): + observation = api.select(Observation).where(process_pid=2000).range("-2y", "backend_timestamp") + with pytest.raises(ApiError): + observation = api.select(Observation).where(process_pid=2000).aggregation(123) + + +def test_observation_aggregation_wrong_field(cbcsdk_mock): + """Testing passing wrong aggregation_field""" + api = cbcsdk_mock.api + with pytest.raises(ApiError): + api.select(Observation).where(process_pid=2000).aggregation("wrong_field") + + +def test_observation_select_aggregation(cbcsdk_mock): + """Testing Observation Querying with select() and more complex criteria""" + cbcsdk_mock.mock_request("POST", + "/api/investigate/v2/orgs/test/observations/search_jobs", + POST_OBSERVATIONS_SEARCH_JOB_RESP) + cbcsdk_mock.mock_request("POST", + "/api/investigate/v1/orgs/test/observations/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/group_results", # noqa: E501 + GET_OBSERVATIONS_GROUPED_RESULTS_RESP) + + api = cbcsdk_mock.api + observations = api.select(Observation).where(process_pid=2000).aggregation("device_name").\ + max_events_per_group(10).rows(5).start(0).range("-2y", "backend_timestamp") + assert observations._count() == 1 + for observation in observations: + assert observation.device_name is not None + assert observation.enriched is not None + assert observation.process_pid[0] == 2000 + + +def test_observation_select_aggregation_async(cbcsdk_mock): + """Testing Observation Querying with select() and more complex criteria""" + cbcsdk_mock.mock_request("POST", + "/api/investigate/v2/orgs/test/observations/search_jobs", + POST_OBSERVATIONS_SEARCH_JOB_RESP) + cbcsdk_mock.mock_request("POST", + "/api/investigate/v1/orgs/test/observations/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/group_results", # noqa: E501 + GET_OBSERVATIONS_GROUPED_RESULTS_RESP) + + api = cbcsdk_mock.api + observations = api.select(Observation).where(process_pid=2000).aggregation("device_name").execute_async() + results = observations.result() + for observation in results: + assert observation["device_name"] is not None + assert observation["enriched"] is not None + assert observation["process_pid"][0] == 2000 + + +def test_observation_aggregation_setup(cbcsdk_mock): + """Testing whether aggregation-related properties are setup correctly""" + api = cbcsdk_mock.api + observation = api.select(Observation).where(process_pid=2000).aggregation("device_name").\ + max_events_per_group(10).rows(5).start(0).range("-2y", "backend_timestamp") + assert observation._aggregation is True + assert observation._aggregate_fields == ["device_name"] + assert observation._start == 0 + assert observation._rows == 5 + assert observation._max_events_per_group == 10 + assert observation._range == { + "method": "interval", + "field": "backend_timestamp", + "duration": "-2y" + } From 64487b584f196bb11e2f7fd008c4940c341223f9 Mon Sep 17 00:00:00 2001 From: Emanuela Mitreva Date: Tue, 21 Feb 2023 10:25:23 +0200 Subject: [PATCH 057/143] Deflake --- .../fixtures/platform/mock_observations.py | 116 ++++++++---------- src/tests/unit/platform/test_observations.py | 77 ++++++++---- 2 files changed, 105 insertions(+), 88 deletions(-) diff --git a/src/tests/unit/fixtures/platform/mock_observations.py b/src/tests/unit/fixtures/platform/mock_observations.py index 4602b718d..c95d5cfb6 100644 --- a/src/tests/unit/fixtures/platform/mock_observations.py +++ b/src/tests/unit/fixtures/platform/mock_observations.py @@ -394,70 +394,58 @@ GET_OBSERVATIONS_GROUPED_RESULTS_RESP = { - "approximate_unaggregated": 442, - "completed": 7, - "contacted": 7, - "group_results": [ - { - "group_end_timestamp": "2023-02-14T06:20:35.696Z", - "group_key": "device_name", - "group_start_timestamp": "2023-02-05T09:34:57.499Z", - "group_value": "dev01-39x-1", - "results": [ + "approximate_unaggregated": 442, + "completed": 7, + "contacted": 7, + "group_results": [ { - "alert_category": [ - "OBSERVED" - ], - "alert_id": [ - "fbb78467-f63c-ac52-622a-f41c6f07d815" - ], - "backend_timestamp": "2023-02-14T06:23:18.887Z", - "device_group_id": 0, - "device_id": 17482451, - "device_name": "dev01-39x-1", - "device_policy_id": 20792247, - "device_timestamp": "2023-02-14T06:20:35.696Z", - "enriched": True, - "enriched_event_type": [ - "NETWORK" - ], - "event_description": "The script ...", - "event_id": "c7bdd379ac2f11ed92c0b59a6de446c9", - "event_network_inbound": False, - "event_network_local_ipv4": "10.203.105.21", - "event_network_location": "Santa Clara,CA,United States", - "event_network_protocol": "TCP", - "event_network_remote_ipv4": "23.67.33.87", - "event_network_remote_port": 80, - "event_type": [ - "netconn" - ], - "ingress_time": 1676355686412, - "legacy": True, - "observation_description": "The application", - "observation_id": "c7bdd379ac2f11ed92c0b59a6de446c9:fbb78467-f63c-ac52-622a-f41c6f07d815", - "observation_type": "CB_ANALYTICS", - "org_id": "ABCD123456", - "parent_guid": "ABCD123456-010ac2d3-00001164-00000000-1d9403c70fffc03", - "parent_pid": 4452, - "process_guid": "ABCD123456-010ac2d3-0000066c-00000000-1d9403c710932fb", - "process_hash": [ - "dcb5ffb192d9bce84d21f274a87cb5f839ed92094121cc254b5f3bae2f266d62" - ], - "process_name": "c:\\programdata\\mozilla-1de4eec8-1241-4177-a864-e594e8d1fb38\\updates\\308046b0af4a39cb\\backgroundupdate.moz_log", - "process_pid": [ - 2000 - ], - "process_username": [ - "DEV01-39X-1\\bit9qa" - ] + "group_end_timestamp": "2023-02-14T06:20:35.696Z", + "group_key": "device_name", + "group_start_timestamp": "2023-02-05T09:34:57.499Z", + "group_value": "dev01-39x-1", + "results": [ + { + "alert_category": ["OBSERVED"], + "alert_id": ["fbb78467-f63c-ac52-622a-f41c6f07d815"], + "backend_timestamp": "2023-02-14T06:23:18.887Z", + "device_group_id": 0, + "device_id": 17482451, + "device_name": "dev01-39x-1", + "device_policy_id": 20792247, + "device_timestamp": "2023-02-14T06:20:35.696Z", + "enriched": True, + "enriched_event_type": ["NETWORK"], + "event_description": "The script ...", + "event_id": "c7bdd379ac2f11ed92c0b59a6de446c9", + "event_network_inbound": False, + "event_network_local_ipv4": "10.203.105.21", + "event_network_location": "Santa Clara,CA,United States", + "event_network_protocol": "TCP", + "event_network_remote_ipv4": "23.67.33.87", + "event_network_remote_port": 80, + "event_type": ["netconn"], + "ingress_time": 1676355686412, + "legacy": True, + "observation_description": "The application", + "observation_id": "c7bdd379ac2f11ed92c0b59a6de446c9:fbb78467-f63c-ac52-622a-f41c6f07d815", + "observation_type": "CB_ANALYTICS", + "org_id": "ABCD123456", + "parent_guid": "ABCD123456-010ac2d3-00001164-00000000-1d9403c70fffc03", + "parent_pid": 4452, + "process_guid": "ABCD123456-010ac2d3-0000066c-00000000-1d9403c710932fb", + "process_hash": [ + "dcb5ffb192d9bce84d21f274a87cb5f839ed92094121cc254b5f3bae2f266d62" + ], + "process_name": "c:\\programdata\\mozilla-1de4eec8-1241-4177-a864-e594e8d1fb38", + "process_pid": [2000], + "process_username": ["DEV01-39X-1\\bit9qa"], + } + ], + "total_events": 1, } - ], - "total_events": 1 - } - ], - "groups_num_available": 0, - "num_aggregated": 0, - "num_available": 1, - "num_found": 1 + ], + "groups_num_available": 0, + "num_aggregated": 0, + "num_available": 1, + "num_found": 1, } diff --git a/src/tests/unit/platform/test_observations.py b/src/tests/unit/platform/test_observations.py index eef478abd..db9eb7a98 100644 --- a/src/tests/unit/platform/test_observations.py +++ b/src/tests/unit/platform/test_observations.py @@ -23,7 +23,7 @@ GET_OBSERVATIONS_FACET_SEARCH_JOB_RESULTS_RESP_1, GET_OBSERVATIONS_FACET_SEARCH_JOB_RESULTS_RESP_2, GET_OBSERVATIONS_FACET_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING, - GET_OBSERVATIONS_GROUPED_RESULTS_RESP + GET_OBSERVATIONS_GROUPED_RESULTS_RESP, ) log = logging.basicConfig( @@ -892,15 +892,17 @@ def test_observation_aggregation_setup_failures(cbcsdk_mock): """Testing whether exceptions are raised, when incorrectly setting aggregation properties""" api = cbcsdk_mock.api with pytest.raises(ApiError): - observation = api.select(Observation).where(process_pid=2000).max_events_per_group(10) + api.select(Observation).where(process_pid=2000).max_events_per_group(10) with pytest.raises(ApiError): - observation = api.select(Observation).where(process_pid=2000).rows(5) + api.select(Observation).where(process_pid=2000).rows(5) with pytest.raises(ApiError): - observation = api.select(Observation).where(process_pid=2000).start(0) + api.select(Observation).where(process_pid=2000).start(0) with pytest.raises(ApiError): - observation = api.select(Observation).where(process_pid=2000).range("-2y", "backend_timestamp") + api.select(Observation).where(process_pid=2000).range( + "-2y", "backend_timestamp" + ) with pytest.raises(ApiError): - observation = api.select(Observation).where(process_pid=2000).aggregation(123) + api.select(Observation).where(process_pid=2000).aggregation(123) def test_observation_aggregation_wrong_field(cbcsdk_mock): @@ -912,16 +914,27 @@ def test_observation_aggregation_wrong_field(cbcsdk_mock): def test_observation_select_aggregation(cbcsdk_mock): """Testing Observation Querying with select() and more complex criteria""" - cbcsdk_mock.mock_request("POST", - "/api/investigate/v2/orgs/test/observations/search_jobs", - POST_OBSERVATIONS_SEARCH_JOB_RESP) - cbcsdk_mock.mock_request("POST", - "/api/investigate/v1/orgs/test/observations/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/group_results", # noqa: E501 - GET_OBSERVATIONS_GROUPED_RESULTS_RESP) + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/observations/search_jobs", + POST_OBSERVATIONS_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v1/orgs/test/observations/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/group_results", # noqa: E501 + GET_OBSERVATIONS_GROUPED_RESULTS_RESP, + ) api = cbcsdk_mock.api - observations = api.select(Observation).where(process_pid=2000).aggregation("device_name").\ - max_events_per_group(10).rows(5).start(0).range("-2y", "backend_timestamp") + observations = ( + api.select(Observation) + .where(process_pid=2000) + .aggregation("device_name") + .max_events_per_group(10) + .rows(5) + .start(0) + .range("-2y", "backend_timestamp") + ) assert observations._count() == 1 for observation in observations: assert observation.device_name is not None @@ -931,15 +944,24 @@ def test_observation_select_aggregation(cbcsdk_mock): def test_observation_select_aggregation_async(cbcsdk_mock): """Testing Observation Querying with select() and more complex criteria""" - cbcsdk_mock.mock_request("POST", - "/api/investigate/v2/orgs/test/observations/search_jobs", - POST_OBSERVATIONS_SEARCH_JOB_RESP) - cbcsdk_mock.mock_request("POST", - "/api/investigate/v1/orgs/test/observations/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/group_results", # noqa: E501 - GET_OBSERVATIONS_GROUPED_RESULTS_RESP) + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/observations/search_jobs", + POST_OBSERVATIONS_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v1/orgs/test/observations/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/group_results", # noqa: E501 + GET_OBSERVATIONS_GROUPED_RESULTS_RESP, + ) api = cbcsdk_mock.api - observations = api.select(Observation).where(process_pid=2000).aggregation("device_name").execute_async() + observations = ( + api.select(Observation) + .where(process_pid=2000) + .aggregation("device_name") + .execute_async() + ) results = observations.result() for observation in results: assert observation["device_name"] is not None @@ -950,8 +972,15 @@ def test_observation_select_aggregation_async(cbcsdk_mock): def test_observation_aggregation_setup(cbcsdk_mock): """Testing whether aggregation-related properties are setup correctly""" api = cbcsdk_mock.api - observation = api.select(Observation).where(process_pid=2000).aggregation("device_name").\ - max_events_per_group(10).rows(5).start(0).range("-2y", "backend_timestamp") + observation = ( + api.select(Observation) + .where(process_pid=2000) + .aggregation("device_name") + .max_events_per_group(10) + .rows(5) + .start(0) + .range("-2y", "backend_timestamp") + ) assert observation._aggregation is True assert observation._aggregate_fields == ["device_name"] assert observation._start == 0 @@ -960,5 +989,5 @@ def test_observation_aggregation_setup(cbcsdk_mock): assert observation._range == { "method": "interval", "field": "backend_timestamp", - "duration": "-2y" + "duration": "-2y", } From 6174efb82d1507851d708a17f22e643c7bb7fd0c Mon Sep 17 00:00:00 2001 From: Emanuela Mitreva Date: Wed, 22 Feb 2023 12:10:15 +0200 Subject: [PATCH 058/143] Improvements based on Amy's and Alex's feedback. --- src/cbc_sdk/platform/observations.py | 412 ++++++++++--------- src/cbc_sdk/rest_api.py | 2 +- src/tests/unit/platform/test_observations.py | 139 +++---- 3 files changed, 278 insertions(+), 275 deletions(-) diff --git a/src/cbc_sdk/platform/observations.py b/src/cbc_sdk/platform/observations.py index ca058f610..47b2ef1c8 100644 --- a/src/cbc_sdk/platform/observations.py +++ b/src/cbc_sdk/platform/observations.py @@ -17,46 +17,45 @@ import logging import time -from copy import deepcopy log = logging.getLogger(__name__) class Observation(UnrefreshableModel): - """Represents an observations""" + """Represents an Observation""" + primary_key = "observation_id" swagger_meta_file = "platform/models/observation.yaml" - @classmethod - def _query_implementation(self, cb, **kwargs): - """ - Returns the appropriate query object for this object type. - - Args: - cb (BaseAPI): Reference to API object used to communicate with the server. - **kwargs (dict): Not used, retained for compatibility. - - Returns: - Query: The query object for this alert type. - """ - # This will emulate a synchronous observation query, for now. - return ObservationQuery(self, cb) - - def __init__(self, cb, model_unique_id=None, initial_data=None, force_init=False, full_doc=True): + def __init__( + self, + cb, + model_unique_id=None, + initial_data=None, + force_init=False, + full_doc=False, + ): """ Initialize the Observation object. + Required Permissions: + org.search.events (READ) + Args: cb (CBCloudAPI): A reference to the CBCloudAPI object. model_unique_id (Any): The unique ID for this particular instance of the model object. initial_data (dict): The data to use when initializing the model object. force_init (bool): True to force object initialization. - full_doc (bool): True to mark the object as fully initialized. + full_doc (bool): False to mark the object as not fully initialized. """ self._details_timeout = 0 self._info = None if model_unique_id is not None and initial_data is None: - observations_future = cb.select(Observation).where(observation_id=model_unique_id).execute_async() + observations_future = ( + cb.select(Observation) + .where(observation_id=model_unique_id) + .execute_async() + ) result = observations_future.result() if len(result) == 1: initial_data = result[0] @@ -65,9 +64,36 @@ def __init__(self, cb, model_unique_id=None, initial_data=None, force_init=False model_unique_id=model_unique_id, initial_data=initial_data, force_init=force_init, - full_doc=full_doc + full_doc=full_doc, ) + def _refresh(self): + """ + Refreshes the observation object from the server. + + Required Permissions: + org.search.events (READ) + + Returns: + True if the refresh was successful. + """ + self._get_detailed_results() + return True + + @classmethod + def _query_implementation(self, cb, **kwargs): + """ + Returns the appropriate query object for this object type. + + Args: + cb (BaseAPI): Reference to API object used to communicate with the server. + **kwargs (dict): Not used, retained for compatibility. + + Returns: + Query: The query object for this observation. + """ + return ObservationQuery(self, cb) + def get_details(self, timeout=0, async_mode=False): """Requests detailed results. @@ -78,19 +104,32 @@ def get_details(self, timeout=0, async_mode=False): Note: - When using asynchronous mode, this method returns a python future. You can call result() on the future object to wait for completion and get the results. + + Examples: + >>> observation = api.select(Observation, observation_id) + >>> observation.get_details() + + >>> observations = api.select(Observation.where(process_pid=2000) + >>> observations[0].get_details() """ self._details_timeout = timeout if not self.observation_id: - raise ApiError("Trying to get observation details on an invalid observation_id") + raise ApiError( + "Trying to get observation details on an invalid observation_id" + ) if async_mode: - return self._cb._async_submit(lambda arg, kwarg: self._get_detailed_results()) + return self._cb._async_submit( + lambda arg, kwarg: self._get_detailed_results() + ) else: return self._get_detailed_results() def _get_detailed_results(self): - """Actual search details implementation""" + """Actual get details implementation""" args = {"observation_ids": [self.observation_id]} - url = "/api/investigate/v2/orgs/{}/observations/detail_jobs".format(self._cb.credentials.org_key) + url = "/api/investigate/v2/orgs/{}/observations/detail_jobs".format( + self._cb.credentials.org_key + ) query_start = self._cb.post_object(url, body=args) job_id = query_start.json().get("job_id") timed_out = False @@ -104,45 +143,56 @@ def _get_detailed_results(self): result = self._cb.get_object(status_url) searchers_contacted = result.get("contacted", 0) searchers_completed = result.get("completed", 0) - log.debug("contacted = {}, completed = {}".format(searchers_contacted, searchers_completed)) + log.debug( + "contacted = {}, completed = {}".format( + searchers_contacted, searchers_completed + ) + ) if searchers_contacted == 0: - time.sleep(.5) + time.sleep(0.5) continue if searchers_completed < searchers_contacted: - if self._details_timeout != 0 and (time.time() * 1000) - submit_time > self._details_timeout: + if ( + self._details_timeout != 0 + and (time.time() * 1000) - submit_time > self._details_timeout + ): timed_out = True break else: break - time.sleep(.5) + time.sleep(0.5) if timed_out: - raise TimeoutError(message="user-specified timeout exceeded while waiting for results") + raise TimeoutError( + message="user-specified timeout exceeded while waiting for results" + ) log.debug("Pulling detailed results, timed_out={}".format(timed_out)) still_fetching = True - result_url = "/api/investigate/v2/orgs/{}/observations/detail_jobs/{}/results".format( - self._cb.credentials.org_key, - job_id + result_url = ( + "/api/investigate/v2/orgs/{}/observations/detail_jobs/{}/results".format( + self._cb.credentials.org_key, job_id + ) ) query_parameters = {} while still_fetching: result = self._cb.get_object(result_url, query_parameters=query_parameters) - total_results = result.get('num_available', 0) - found_results = result.get('num_found', 0) + total_results = result.get("num_available", 0) + found_results = result.get("num_found", 0) # if found is 0, then no observations were found if found_results == 0: return self if total_results != 0: - results = result.get('results', []) + results = result.get("results", []) self._info = results[0] return self class ObservationFacet(UnrefreshableModel): """Represents an observation retrieved.""" + primary_key = "job_id" swagger_meta_file = "platform/models/observation_facet.yaml" submit_url = "/api/investigate/v2/orgs/{}/observations/facet_jobs" @@ -150,6 +200,7 @@ class ObservationFacet(UnrefreshableModel): class Terms(UnrefreshableModel): """Represents the facet fields and values associated with an Observation Facet query.""" + def __init__(self, cb, initial_data): """Initialize an ObservationFacet Terms object with initial_data.""" super(ObservationFacet.Terms, self).__init__( @@ -177,6 +228,7 @@ def fields(self): class Ranges(UnrefreshableModel): """Represents the range (bucketed) facet fields and values associated with an Observation Facet query.""" + def __init__(self, cb, initial_data): """Initialize an ObservationFacet Ranges object with initial_data.""" super(ObservationFacet.Ranges, self).__init__( @@ -214,7 +266,7 @@ def __init__(self, cb, model_unique_id, initial_data): model_unique_id=model_unique_id, initial_data=initial_data, force_init=False, - full_doc=True + full_doc=True, ) self._terms = ObservationFacet.Terms(cb, initial_data=initial_data["terms"]) self._ranges = ObservationFacet.Ranges(cb, initial_data=initial_data["ranges"]) @@ -235,7 +287,13 @@ class ObservationQuery(Query): This class specializes `Query` to handle the particulars of observations querying. """ - VALID_GROUP_FIELDS = ["observation_type", "device_name", "process_username", "attack_tactic"] + + VALID_GROUP_FIELDS = [ + "observation_type", + "device_name", + "process_username", + "attack_tactic", + ] def __init__(self, doc_class, cb): """ @@ -251,13 +309,6 @@ def __init__(self, doc_class, cb): self._timeout = 0 self._timed_out = False - self._aggregation = False - self._aggregate_fields = None - self._max_events_per_group = None - self._range = None - self._rows = None - self._start = None - def or_(self, **kwargs): """ :meth:`or_` criteria are explicitly provided to Observation queries. @@ -298,11 +349,16 @@ def timeout(self, msecs): return self def _submit(self): + """Submit the search job""" if self._query_token: - raise ApiError("Query already submitted: token {0}".format(self._query_token)) + raise ApiError( + "Query already submitted: token {0}".format(self._query_token) + ) args = self._get_query_parameters() - url = "/api/investigate/v2/orgs/{}/observations/search_jobs".format(self._cb.credentials.org_key) + url = "/api/investigate/v2/orgs/{}/observations/search_jobs".format( + self._cb.credentials.org_key + ) query_start = self._cb.post_object(url, body=args) self._query_token = query_start.json().get("job_id") self._timed_out = False @@ -312,21 +368,27 @@ def _still_querying(self): if not self._query_token: self._submit() - if self._aggregation: - return False - - status_url = "/api/investigate/v2/orgs/{}/observations/search_jobs/{}/results".format( - self._cb.credentials.org_key, - self._query_token, + status_url = ( + "/api/investigate/v2/orgs/{}/observations/search_jobs/{}/results".format( + self._cb.credentials.org_key, + self._query_token, + ) ) result = self._cb.get_object(status_url) searchers_contacted = result.get("contacted", 0) searchers_completed = result.get("completed", 0) - log.debug("contacted = {}, completed = {}".format(searchers_contacted, searchers_completed)) + log.debug( + "contacted = {}, completed = {}".format( + searchers_contacted, searchers_completed + ) + ) if searchers_contacted == 0: return True if searchers_completed < searchers_contacted: - if self._timeout != 0 and (time.time() * 1000) - self._submit_time > self._timeout: + if ( + self._timeout != 0 + and (time.time() * 1000) - self._submit_time > self._timeout + ): self._timed_out = True return False return True @@ -338,36 +400,91 @@ def _count(self): return self._total_results while self._still_querying(): - time.sleep(.5) + time.sleep(0.5) if self._timed_out: - raise TimeoutError(message="user-specified timeout exceeded while waiting for results") - - if self._aggregation: - result_url = "/api/investigate/v1/orgs/{}/observations/search_jobs/{}/group_results".format( - self._cb.credentials.org_key, - self._query_token, + raise TimeoutError( + message="user-specified timeout exceeded while waiting for results" ) - result = self._cb.post_object(result_url, self._build_aggregated_body()).json() - else: - result_url = "/api/investigate/v2/orgs/{}/observations/search_jobs/{}/results".format( + + result_url = ( + "/api/investigate/v2/orgs/{}/observations/search_jobs/{}/results".format( self._cb.credentials.org_key, self._query_token, ) - result = self._cb.get_object(result_url) + ) + result = self._cb.get_object(result_url) - self._total_results = result.get('num_available', 0) + self._total_results = result.get("num_available", 0) self._count_valid = True return self._total_results - def aggregation(self, fields): + def _search(self, start=0, rows=0): + if not self._query_token: + self._submit() + + while self._still_querying(): + time.sleep(0.5) + + if self._timed_out: + raise TimeoutError( + message="user-specified timeout exceeded while waiting for results" + ) + + log.debug("Pulling results, timed_out={}".format(self._timed_out)) + + current = start + rows_fetched = 0 + still_fetching = True + query_parameters = {} + result_url_template = ( + "/api/investigate/v2/orgs/{}/observations/search_jobs/{}/results".format( + self._cb.credentials.org_key, self._query_token + ) + ) + + while still_fetching: + result_url = "{}?start={}&rows={}".format( + result_url_template, current, self._batch_size + ) + result = self._cb.get_object(result_url, query_parameters=query_parameters) + results = result.get("results", []) + + self._total_results = result.get("num_available", 0) + self._count_valid = True + + for item in results: + yield item + current += 1 + rows_fetched += 1 + + if rows and rows_fetched >= rows: + still_fetching = False + break + + if current >= self._total_results: + still_fetching = False + + log.debug( + "current: {}, total_results: {}".format(current, self._total_results) + ) + + def get_group_results( + self, fields, max_events_per_group=None, rows=500, start=None, ranges=None + ): """ - Performs an aggregation search where results are grouped by an aggregation field + Get group results grouped by provided fields. Args: - fields (str or list): The aggregation field or fields, valid ones are: - observation_type, device_name, process_username, attack_tactic + fields (str / list): field or fields by which to perform the grouping + max_events_per_group (int):Maximum number of events in a group, if not provided, all events will be returned + rows (int): Number of rows to request, can be paginated + start (int): First row to use for pagination + ranges (dict): dict with information about duration, field, method + + Returns: + dict: grouped results """ if not isinstance(fields, list) and not isinstance(fields, str): raise ApiError("Fields should be either a single field or list of fields") @@ -378,61 +495,37 @@ def aggregation(self, fields): if not all((gt in ObservationQuery.VALID_GROUP_FIELDS) for gt in fields): raise ApiError("One or more invalid aggregation fields") - self._aggregation = True - self._aggregate_fields = fields - return self - - def max_events_per_group(self, max_events_per_group): - """ - Sets the max events per aggregate for aggregated results - - Args: - max_events_per_group (int): Max events per aggregate - """ - if not self._aggregation: - raise ApiError("You should first aggregate the records.") - self._max_events_per_group = max_events_per_group - return self - - def rows(self, rows): - """ - Sets the rows for aggregated results + if not self._query_token: + self._submit() - Args: - rows (int): Max events per group - """ - if not self._aggregation: - raise ApiError("You should first aggregate the records.") - self._rows = rows - return self + result_url = "/api/investigate/v2/orgs/{}/observations/search_jobs/{}/group_results".format( + self._cb.credentials.org_key, + self._query_token, + ) - def start(self, start): - """ - Sets the start for the aggregated results + data = self._build_aggregated_body( + fields, max_events_per_group, ranges, rows, start + ) - Args: - start (int): start index - """ - if not self._aggregation: - raise ApiError("You should first aggregate the records.") - self._start = start - return self + still_fetching = True - def range(self, duration, field, method="interval"): - """ - Describes a time window to restrict the search. + while still_fetching: + result = self._cb.post_object(result_url, data).json() + contacted = result.get("contacted", 0) + completed = result.get("completed", 0) + if contacted < completed: + time.sleep(0.5) + still_fetching = True + continue + else: + still_fetching = False - Args: - duration (str): duration for the range e.g. -2w - field (str): field - method (str): either bucket or inteval - """ - if not self._aggregation: - raise ApiError("You should first aggregate the records.") - self._range = dict(duration=duration, field=field, method=method) - return self + for group in result.get("group_results", []): + yield group - def _build_aggregated_body(self): + def _build_aggregated_body( + self, fields, max_events_per_group=None, rows=500, start=None, ranges=None + ): """ Helper to build the group results body: @@ -448,76 +541,11 @@ def _build_aggregated_body(self): "start": integer } """ - data = dict(fields=self._aggregate_fields) - if self._max_events_per_group is not None: - data["max_events_per_group"] = self._max_events_per_group - if self._range is not None: - data["ranges"] = self._range - if self._rows is not None: - data["rows"] = self._rows - if self._start is not None: - data["start"] = self._start + data = dict(fields=fields, rows=rows) + if max_events_per_group is not None: + data["max_events_per_group"] = max_events_per_group + if ranges is not None: + data["ranges"] = ranges + if start is not None: + data["start"] = start return data - - def _search(self, start=0, rows=0): - if not self._query_token: - self._submit() - - while self._still_querying(): - time.sleep(.5) - - if self._timed_out: - raise TimeoutError(message="user-specified timeout exceeded while waiting for results") - - log.debug("Pulling results, timed_out={}".format(self._timed_out)) - - current = start - rows_fetched = 0 - still_fetching = True - query_parameters = {} - result_url_template = "/api/investigate/v2/orgs/{}/observations/search_jobs/{}/results".format( - self._cb.credentials.org_key, - self._query_token - ) - while still_fetching: - if self._aggregation: - result_url = "/api/investigate/v1/orgs/{}/observations/search_jobs/{}/group_results".format( - self._cb.credentials.org_key, - self._query_token, - ) - result = self._cb.post_object(result_url, self._build_aggregated_body()).json() - results = [] - # flatten the list of results and add the common fields of each group - # for each element of the group - for group in result.get("group_results", []): - common = deepcopy(group) - del common["results"] - for item in group["results"]: - ditem = deepcopy(item) - ditem.update(common) - results.append(ditem) - else: - result_url = '{}?start={}&rows={}'.format( - result_url_template, - current, - self._batch_size - ) - result = self._cb.get_object(result_url, query_parameters=query_parameters) - results = result.get('results', []) - - self._total_results = result.get('num_available', 0) - self._count_valid = True - - for item in results: - yield item - current += 1 - rows_fetched += 1 - - if rows and rows_fetched >= rows: - still_fetching = False - break - - if current >= self._total_results: - still_fetching = False - - log.debug("current: {}, total_results: {}".format(current, self._total_results)) diff --git a/src/cbc_sdk/rest_api.py b/src/cbc_sdk/rest_api.py index eddf06d01..19e5db067 100644 --- a/src/cbc_sdk/rest_api.py +++ b/src/cbc_sdk/rest_api.py @@ -427,7 +427,7 @@ def observations_search_suggestions(self, query, count=None): def observations_search_validation(self, query, min_backend_timestamp=None, max_backend_timestamp=None): """ - Returns suggestions for keys and field values that can be used in a search. + Returns validation result of a query. Args: query (str): A search query to be validated. diff --git a/src/tests/unit/platform/test_observations.py b/src/tests/unit/platform/test_observations.py index db9eb7a98..2c6d78495 100644 --- a/src/tests/unit/platform/test_observations.py +++ b/src/tests/unit/platform/test_observations.py @@ -256,6 +256,44 @@ def test_observations_select_details_sync(cbcsdk_mock): assert results.process_pid[0] == 2000 +def test_observations_select_details_refresh(cbcsdk_mock): + """Testing Observation Querying with get_details""" + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/observations/search_jobs", + POST_OBSERVATIONS_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results", # noqa: E501 + GET_OBSERVATIONS_SEARCH_JOB_RESULTS_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=500", # noqa: E501 + GET_OBSERVATIONS_SEARCH_JOB_RESULTS_RESP, + ) + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/observations/detail_jobs", + POST_OBSERVATIONS_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/detail_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results", # noqa: E501 + GET_OBSERVATIONS_DETAIL_JOB_RESULTS_RESP, + ) + + s_api = cbcsdk_mock.api + obs_list = s_api.select(Observation).where(process_pid=2000) + obs = obs_list[0] + assert obs.device_name is not None + assert obs.enriched is True + assert obs.process_pid[0] == 2000 + # this one is present only in the details + assert len(obs.ttp) == 4 + + def test_observations_select_details_sync_zero(cbcsdk_mock): """Testing Observation Querying with get_details""" cbcsdk_mock.mock_request( @@ -888,61 +926,22 @@ def test_observation_search_async(cbcsdk_mock): assert result.terms[0]["field"] == "process_name" -def test_observation_aggregation_setup_failures(cbcsdk_mock): - """Testing whether exceptions are raised, when incorrectly setting aggregation properties""" - api = cbcsdk_mock.api - with pytest.raises(ApiError): - api.select(Observation).where(process_pid=2000).max_events_per_group(10) - with pytest.raises(ApiError): - api.select(Observation).where(process_pid=2000).rows(5) - with pytest.raises(ApiError): - api.select(Observation).where(process_pid=2000).start(0) - with pytest.raises(ApiError): - api.select(Observation).where(process_pid=2000).range( - "-2y", "backend_timestamp" - ) - with pytest.raises(ApiError): - api.select(Observation).where(process_pid=2000).aggregation(123) - - def test_observation_aggregation_wrong_field(cbcsdk_mock): """Testing passing wrong aggregation_field""" api = cbcsdk_mock.api with pytest.raises(ApiError): - api.select(Observation).where(process_pid=2000).aggregation("wrong_field") - - -def test_observation_select_aggregation(cbcsdk_mock): - """Testing Observation Querying with select() and more complex criteria""" - cbcsdk_mock.mock_request( - "POST", - "/api/investigate/v2/orgs/test/observations/search_jobs", - POST_OBSERVATIONS_SEARCH_JOB_RESP, - ) - cbcsdk_mock.mock_request( - "POST", - "/api/investigate/v1/orgs/test/observations/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/group_results", # noqa: E501 - GET_OBSERVATIONS_GROUPED_RESULTS_RESP, - ) - - api = cbcsdk_mock.api - observations = ( - api.select(Observation) - .where(process_pid=2000) - .aggregation("device_name") - .max_events_per_group(10) - .rows(5) - .start(0) - .range("-2y", "backend_timestamp") - ) - assert observations._count() == 1 - for observation in observations: - assert observation.device_name is not None - assert observation.enriched is not None - assert observation.process_pid[0] == 2000 + for i in ( + api.select(Observation) + .where(process_pid=2000) + .get_group_results("wrong_field") + ): + print(i) + with pytest.raises(ApiError): + for i in api.select(Observation).where(process_pid=2000).get_group_results(1): + print(i) -def test_observation_select_aggregation_async(cbcsdk_mock): +def test_observation_select_group_results(cbcsdk_mock): """Testing Observation Querying with select() and more complex criteria""" cbcsdk_mock.mock_request( "POST", @@ -951,43 +950,19 @@ def test_observation_select_aggregation_async(cbcsdk_mock): ) cbcsdk_mock.mock_request( "POST", - "/api/investigate/v1/orgs/test/observations/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/group_results", # noqa: E501 + "/api/investigate/v2/orgs/test/observations/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/group_results", # noqa: E501 GET_OBSERVATIONS_GROUPED_RESULTS_RESP, ) api = cbcsdk_mock.api - observations = ( + ranges = {"field": "backend_timestamp", "duration": "-2y"} + observation_groups = list( api.select(Observation) .where(process_pid=2000) - .aggregation("device_name") - .execute_async() + .get_group_results( + "device_name", max_events_per_group=10, rows=5, start=0, ranges=ranges + ) ) - results = observations.result() - for observation in results: - assert observation["device_name"] is not None - assert observation["enriched"] is not None - assert observation["process_pid"][0] == 2000 - - -def test_observation_aggregation_setup(cbcsdk_mock): - """Testing whether aggregation-related properties are setup correctly""" - api = cbcsdk_mock.api - observation = ( - api.select(Observation) - .where(process_pid=2000) - .aggregation("device_name") - .max_events_per_group(10) - .rows(5) - .start(0) - .range("-2y", "backend_timestamp") - ) - assert observation._aggregation is True - assert observation._aggregate_fields == ["device_name"] - assert observation._start == 0 - assert observation._rows == 5 - assert observation._max_events_per_group == 10 - assert observation._range == { - "method": "interval", - "field": "backend_timestamp", - "duration": "-2y", - } + assert observation_groups[0]["group_key"] is not None + assert observation_groups[0]["results"][0]["enriched"] is not None + assert observation_groups[0]["results"][0]["process_pid"][0] == 2000 From 6499c1cf19bc3b44a1b8d729e50b9ac9fc6eddb9 Mon Sep 17 00:00:00 2001 From: Emanuela Mitreva Date: Wed, 22 Feb 2023 12:17:32 +0200 Subject: [PATCH 059/143] Fix based on Kylie's feedback. --- docs/cbc_sdk.platform.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/cbc_sdk.platform.rst b/docs/cbc_sdk.platform.rst index 3ee9f3df9..61ed01708 100644 --- a/docs/cbc_sdk.platform.rst +++ b/docs/cbc_sdk.platform.rst @@ -52,6 +52,14 @@ cbc\_sdk.platform.jobs module :undoc-members: :show-inheritance: +cbc\_sdk.platform.observations module +---------------------------------- + +.. automodule:: cbc_sdk.platform.observations + :members: + :undoc-members: + :show-inheritance: + cbc\_sdk.platform.policies module ---------------------------------- From 8986ec4b1b88e05eb66492851db3cf1350699fc2 Mon Sep 17 00:00:00 2001 From: Emanuela Mitreva Date: Wed, 22 Feb 2023 13:22:17 +0200 Subject: [PATCH 060/143] Cleanup and improvements --- src/cbc_sdk/platform/observations.py | 131 +++++++++---------- src/tests/unit/platform/test_observations.py | 8 +- 2 files changed, 70 insertions(+), 69 deletions(-) diff --git a/src/cbc_sdk/platform/observations.py b/src/cbc_sdk/platform/observations.py index 47b2ef1c8..d5644f086 100644 --- a/src/cbc_sdk/platform/observations.py +++ b/src/cbc_sdk/platform/observations.py @@ -69,7 +69,7 @@ def __init__( def _refresh(self): """ - Refreshes the observation object from the server. + Refreshes the observation object from the server, by getting the details. Required Permissions: org.search.events (READ) @@ -136,30 +136,32 @@ def _get_detailed_results(self): submit_time = time.time() * 1000 while True: - status_url = "/api/investigate/v2/orgs/{}/observations/detail_jobs/{}/results".format( + result_url = "/api/investigate/v2/orgs/{}/observations/detail_jobs/{}/results".format( self._cb.credentials.org_key, job_id, ) - result = self._cb.get_object(status_url) - searchers_contacted = result.get("contacted", 0) - searchers_completed = result.get("completed", 0) - log.debug( - "contacted = {}, completed = {}".format( - searchers_contacted, searchers_completed - ) - ) - if searchers_contacted == 0: + result = self._cb.get_object(result_url) + contacted = result.get("contacted", 0) + completed = result.get("completed", 0) + log.debug("contacted = {}, completed = {}".format(contacted, completed)) + + if contacted == 0: time.sleep(0.5) continue - if searchers_completed < searchers_contacted: - if ( - self._details_timeout != 0 - and (time.time() * 1000) - submit_time > self._details_timeout - ): + if completed < contacted: + if self._details_timeout != 0 and (time.time() * 1000) - submit_time > self._details_timeout: timed_out = True break else: - break + total_results = result.get("num_available", 0) + found_results = result.get("num_found", 0) + # if found is 0, then no observations were found + if found_results == 0: + return self + if total_results != 0: + results = result.get("results", []) + self._info = results[0] + return self time.sleep(0.5) @@ -168,30 +170,9 @@ def _get_detailed_results(self): message="user-specified timeout exceeded while waiting for results" ) - log.debug("Pulling detailed results, timed_out={}".format(timed_out)) - - still_fetching = True - result_url = ( - "/api/investigate/v2/orgs/{}/observations/detail_jobs/{}/results".format( - self._cb.credentials.org_key, job_id - ) - ) - query_parameters = {} - while still_fetching: - result = self._cb.get_object(result_url, query_parameters=query_parameters) - total_results = result.get("num_available", 0) - found_results = result.get("num_found", 0) - # if found is 0, then no observations were found - if found_results == 0: - return self - if total_results != 0: - results = result.get("results", []) - self._info = results[0] - return self - class ObservationFacet(UnrefreshableModel): - """Represents an observation retrieved.""" + """Represents an observation facet retrieved.""" primary_key = "job_id" swagger_meta_file = "platform/models/observation_facet.yaml" @@ -368,27 +349,21 @@ def _still_querying(self): if not self._query_token: self._submit() - status_url = ( + results_url = ( "/api/investigate/v2/orgs/{}/observations/search_jobs/{}/results".format( self._cb.credentials.org_key, self._query_token, ) ) - result = self._cb.get_object(status_url) - searchers_contacted = result.get("contacted", 0) - searchers_completed = result.get("completed", 0) - log.debug( - "contacted = {}, completed = {}".format( - searchers_contacted, searchers_completed - ) - ) - if searchers_contacted == 0: + result = self._cb.get_object(results_url) + contacted = result.get("contacted", 0) + completed = result.get("completed", 0) + log.debug("contacted = {}, completed = {}".format(contacted, completed)) + + if contacted == 0: return True - if searchers_completed < searchers_contacted: - if ( - self._timeout != 0 - and (time.time() * 1000) - self._submit_time > self._timeout - ): + if completed < contacted: + if self._timeout != 0 and (time.time() * 1000) - self._submit_time > self._timeout: self._timed_out = True return False return True @@ -466,12 +441,17 @@ def _search(self, start=0, rows=0): if current >= self._total_results: still_fetching = False - log.debug( - "current: {}, total_results: {}".format(current, self._total_results) - ) + log.debug("current: {}, total_results: {}".format(current, self._total_results)) def get_group_results( - self, fields, max_events_per_group=None, rows=500, start=None, ranges=None + self, + fields, + max_events_per_group=None, + rows=500, + start=None, + range_duration=None, + range_field=None, + range_method=None ): """ Get group results grouped by provided fields. @@ -492,7 +472,7 @@ def get_group_results( if isinstance(fields, str): fields = [fields] - if not all((gt in ObservationQuery.VALID_GROUP_FIELDS) for gt in fields): + if not all((gf in ObservationQuery.VALID_GROUP_FIELDS) for gf in fields): raise ApiError("One or more invalid aggregation fields") if not self._query_token: @@ -503,19 +483,23 @@ def get_group_results( self._query_token, ) - data = self._build_aggregated_body( - fields, max_events_per_group, ranges, rows, start + data = self._build_grouped_body( + fields, + max_events_per_group, + rows, + start, + range_duration, + range_field, + range_method ) still_fetching = True - while still_fetching: result = self._cb.post_object(result_url, data).json() contacted = result.get("contacted", 0) completed = result.get("completed", 0) if contacted < completed: time.sleep(0.5) - still_fetching = True continue else: still_fetching = False @@ -523,8 +507,15 @@ def get_group_results( for group in result.get("group_results", []): yield group - def _build_aggregated_body( - self, fields, max_events_per_group=None, rows=500, start=None, ranges=None + def _build_grouped_body( + self, + fields, + max_events_per_group=None, + rows=500, + start=None, + range_duration=None, + range_field=None, + range_method=None ): """ Helper to build the group results body: @@ -544,8 +535,14 @@ def _build_aggregated_body( data = dict(fields=fields, rows=rows) if max_events_per_group is not None: data["max_events_per_group"] = max_events_per_group - if ranges is not None: - data["ranges"] = ranges + if range_duration or range_field or range_method: + data["range"] = {} + if range_method: + data["range"]["method"] = range_method + if range_duration: + data["range"]["duration"] = range_duration + if range_field: + data["range"]["field"] = range_field if start is not None: data["start"] = start return data diff --git a/src/tests/unit/platform/test_observations.py b/src/tests/unit/platform/test_observations.py index 2c6d78495..e4847f78a 100644 --- a/src/tests/unit/platform/test_observations.py +++ b/src/tests/unit/platform/test_observations.py @@ -955,12 +955,16 @@ def test_observation_select_group_results(cbcsdk_mock): ) api = cbcsdk_mock.api - ranges = {"field": "backend_timestamp", "duration": "-2y"} observation_groups = list( api.select(Observation) .where(process_pid=2000) .get_group_results( - "device_name", max_events_per_group=10, rows=5, start=0, ranges=ranges + "device_name", + max_events_per_group=10, + rows=5, + start=0, + range_field="backend_timestamp", + range_duration="-2y" ) ) assert observation_groups[0]["group_key"] is not None From 9dd8aee7580cf2783198758ff8f7d82d4e1091fc Mon Sep 17 00:00:00 2001 From: Emanuela Mitreva Date: Thu, 23 Feb 2023 15:58:55 +0200 Subject: [PATCH 061/143] Fixes based on Alex's feedback --- src/cbc_sdk/platform/observations.py | 81 +++++++++++----------------- 1 file changed, 31 insertions(+), 50 deletions(-) diff --git a/src/cbc_sdk/platform/observations.py b/src/cbc_sdk/platform/observations.py index d5644f086..4a39b61e5 100644 --- a/src/cbc_sdk/platform/observations.py +++ b/src/cbc_sdk/platform/observations.py @@ -69,7 +69,7 @@ def __init__( def _refresh(self): """ - Refreshes the observation object from the server, by getting the details. + Refreshes the observation object from the server by getting the details. Required Permissions: org.search.events (READ) @@ -101,6 +101,9 @@ def get_details(self, timeout=0, async_mode=False): timeout (int): Observations details request timeout in milliseconds. async_mode (bool): True to request details in an asynchronous manner. + Returns: + Observation: Observation object enriched with the details fields + Note: - When using asynchronous mode, this method returns a python future. You can call result() on the future object to wait for completion and get the results. @@ -305,6 +308,12 @@ def set_rows(self, rows): Args: rows (int): How many rows to request. + + Returns: + Query: ObservationQuery object + + Example: + >>> cb.select(Observation).where(process_name="foo.exe").set_rows(50) """ if not isinstance(rows, int): raise ApiError(f"Rows must be an integer. {rows} is a {type(rows)}.") @@ -346,6 +355,7 @@ def _submit(self): self._submit_time = time.time() * 1000 def _still_querying(self): + """Check whether there are still records to be collected.""" if not self._query_token: self._submit() @@ -371,6 +381,7 @@ def _still_querying(self): return False def _count(self): + """Returns the number of records.""" if self._count_valid: return self._total_results @@ -396,6 +407,7 @@ def _count(self): return self._total_results def _search(self, start=0, rows=0): + """Start a search job and get the results.""" if not self._query_token: self._submit() @@ -465,6 +477,10 @@ def get_group_results( Returns: dict: grouped results + + Examples: + >>> for group in api.select(Observation.where(process_pid=2000).get_group_results("device_name"): + >>> ... """ if not isinstance(fields, list) and not isinstance(fields, str): raise ApiError("Fields should be either a single field or list of fields") @@ -483,15 +499,20 @@ def get_group_results( self._query_token, ) - data = self._build_grouped_body( - fields, - max_events_per_group, - rows, - start, - range_duration, - range_field, - range_method - ) + # construct the group results body, required ones are fields and rows + data = dict(fields=fields, rows=rows) + if max_events_per_group is not None: + data["max_events_per_group"] = max_events_per_group + if range_duration or range_field or range_method: + data["range"] = {} + if range_method: + data["range"]["method"] = range_method + if range_duration: + data["range"]["duration"] = range_duration + if range_field: + data["range"]["field"] = range_field + if start is not None: + data["start"] = start still_fetching = True while still_fetching: @@ -506,43 +527,3 @@ def get_group_results( for group in result.get("group_results", []): yield group - - def _build_grouped_body( - self, - fields, - max_events_per_group=None, - rows=500, - start=None, - range_duration=None, - range_field=None, - range_method=None - ): - """ - Helper to build the group results body: - - { - "fields": ["string"], - "max_events_per_group": integer, - "range": { - "duration": "string", - "field": "string", - "method": "string" - }, - "rows": integer, - "start": integer - } - """ - data = dict(fields=fields, rows=rows) - if max_events_per_group is not None: - data["max_events_per_group"] = max_events_per_group - if range_duration or range_field or range_method: - data["range"] = {} - if range_method: - data["range"]["method"] = range_method - if range_duration: - data["range"]["duration"] = range_duration - if range_field: - data["range"]["field"] = range_field - if start is not None: - data["start"] = start - return data From 7b254120c4bd6d5c90a9e2f6f80b6e5a0e237e87 Mon Sep 17 00:00:00 2001 From: Emanuela Mitreva Date: Thu, 23 Feb 2023 19:29:03 +0200 Subject: [PATCH 062/143] Adding ObservationGroup class --- src/cbc_sdk/platform/observations.py | 67 +++++++++++++++++++- src/tests/unit/platform/test_observations.py | 18 +++++- 2 files changed, 82 insertions(+), 3 deletions(-) diff --git a/src/cbc_sdk/platform/observations.py b/src/cbc_sdk/platform/observations.py index 4a39b61e5..30973482f 100644 --- a/src/cbc_sdk/platform/observations.py +++ b/src/cbc_sdk/platform/observations.py @@ -526,4 +526,69 @@ def get_group_results( still_fetching = False for group in result.get("group_results", []): - yield group + yield ObservationGroup(self._cb, initial_data=group) + + +class ObservationGroup: + """Represents ObservationGroup""" + def __init__(self, cb, initial_data=None): + """ + Initialize ObservationGroup object + + Args: + cb (CBCloudAPI): A reference to the CBCloudAPI object. + initial_data (dict): The data to use when initializing the model object. + """ + self._info = initial_data + self._cb = cb + self.observations = [Observation(cb, initial_data=x) for x in initial_data.get("results", [])] + + def __getattr__(self, item): + """ + Return an attribute of this object. + + Args: + item (str): Name of the attribute to be returned. + + Returns: + Any: The returned attribute value. + + Raises: + AttributeError: If the object has no such attribute. + """ + try: + super(ObservationGroup, self).__getattribute__(item) + except AttributeError: + pass # fall through to the rest of the logic... + + # try looking up via self._info, if we already have it. + if item in self._info: + return self._info[item] + else: + raise AttributeError("'{0}' object has no attribute '{1}'".format(self.__class__.__name__, + item)) + + def __getitem__(self, item): + """ + Return an attribute of this object. + + Args: + item (str): Name of the attribute to be returned. + + Returns: + Any: The returned attribute value. + + Raises: + AttributeError: If the object has no such attribute. + """ + try: + super(ObservationGroup, self).__getattribute__(item) + except AttributeError: + pass # fall through to the rest of the logic... + + # try looking up via self._info, if we already have it. + if item in self._info: + return self._info[item] + else: + raise AttributeError("'{0}' object has no attribute '{1}'".format(self.__class__.__name__, + item)) diff --git a/src/tests/unit/platform/test_observations.py b/src/tests/unit/platform/test_observations.py index e4847f78a..39a05e49a 100644 --- a/src/tests/unit/platform/test_observations.py +++ b/src/tests/unit/platform/test_observations.py @@ -953,6 +953,16 @@ def test_observation_select_group_results(cbcsdk_mock): "/api/investigate/v2/orgs/test/observations/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/group_results", # noqa: E501 GET_OBSERVATIONS_GROUPED_RESULTS_RESP, ) + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/observations/detail_jobs", + POST_OBSERVATIONS_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/detail_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results", # noqa: E501 + GET_OBSERVATIONS_DETAIL_JOB_RESULTS_RESP, + ) api = cbcsdk_mock.api observation_groups = list( @@ -967,6 +977,10 @@ def test_observation_select_group_results(cbcsdk_mock): range_duration="-2y" ) ) + # invoke get_details() on the first Observation in the list + observation_groups[0].observations[0].get_details() + assert len(observation_groups[0].observations[0].ttp) == 4 + assert observation_groups[0].group_key is not None assert observation_groups[0]["group_key"] is not None - assert observation_groups[0]["results"][0]["enriched"] is not None - assert observation_groups[0]["results"][0]["process_pid"][0] == 2000 + assert observation_groups[0].observations[0]["enriched"] is not None + assert observation_groups[0].observations[0]["process_pid"][0] == 2000 From aca658f3937433f0885d02b09428235af09c938c Mon Sep 17 00:00:00 2001 From: Emanuela Mitreva Date: Thu, 23 Feb 2023 19:35:26 +0200 Subject: [PATCH 063/143] fix --- src/cbc_sdk/platform/observations.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/cbc_sdk/platform/observations.py b/src/cbc_sdk/platform/observations.py index 30973482f..7144b7b08 100644 --- a/src/cbc_sdk/platform/observations.py +++ b/src/cbc_sdk/platform/observations.py @@ -13,7 +13,7 @@ from cbc_sdk.base import UnrefreshableModel, FacetQuery from cbc_sdk.base import Query -from cbc_sdk.errors import ApiError, TimeoutError +from cbc_sdk.errors import ApiError, TimeoutError, InvalidObjectError import logging import time @@ -539,6 +539,8 @@ def __init__(self, cb, initial_data=None): cb (CBCloudAPI): A reference to the CBCloudAPI object. initial_data (dict): The data to use when initializing the model object. """ + if not initial_data: + raise InvalidObjectError("Cannot create object without initial data") self._info = initial_data self._cb = cb self.observations = [Observation(cb, initial_data=x) for x in initial_data.get("results", [])] From 8b8a515f4f8f3faf9550cf89b061d5098cea1660 Mon Sep 17 00:00:00 2001 From: Emanuela Mitreva Date: Thu, 23 Feb 2023 19:58:11 +0200 Subject: [PATCH 064/143] add docstring --- src/cbc_sdk/platform/observations.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/cbc_sdk/platform/observations.py b/src/cbc_sdk/platform/observations.py index 7144b7b08..cb41e4c6b 100644 --- a/src/cbc_sdk/platform/observations.py +++ b/src/cbc_sdk/platform/observations.py @@ -538,6 +538,13 @@ def __init__(self, cb, initial_data=None): Args: cb (CBCloudAPI): A reference to the CBCloudAPI object. initial_data (dict): The data to use when initializing the model object. + + Notes: + The constructed object will have the following data: + - group_start_timestamp + - group_end_timestamp + - group_key + - group_value """ if not initial_data: raise InvalidObjectError("Cannot create object without initial data") From 76800d762148729e9f8b953d811994ffb25b10d0 Mon Sep 17 00:00:00 2001 From: Emanuela Mitreva Date: Mon, 6 Mar 2023 14:33:44 +0200 Subject: [PATCH 065/143] CBAPI-4452: Create NetworkThreatMetadata --- .../models/network_threat_metadata.yaml | 17 +++++ src/cbc_sdk/platform/models/observation.yaml | 4 +- src/cbc_sdk/platform/observations.py | 55 +++++++++++++++- .../fixtures/platform/mock_observations.py | 10 +++ src/tests/unit/platform/test_observations.py | 64 ++++++++++++++++++- src/tests/unit/test_base_api.py | 3 +- 6 files changed, 148 insertions(+), 5 deletions(-) create mode 100644 src/cbc_sdk/platform/models/network_threat_metadata.yaml diff --git a/src/cbc_sdk/platform/models/network_threat_metadata.yaml b/src/cbc_sdk/platform/models/network_threat_metadata.yaml new file mode 100644 index 000000000..c85e3b768 --- /dev/null +++ b/src/cbc_sdk/platform/models/network_threat_metadata.yaml @@ -0,0 +1,17 @@ +type: object +properties: + detector_abstract: + type: string + description: Abstract or description of the detector + detector_goal: + type: string + description: Description of what the detector is achieving + false_negatives: + type: string + description: Highlights why detector could not have been triggered + false_positives: + type: string + description: Highlights why detector could have been triggered + threat_public_comment: + type: string + description: Public comment of the threat \ No newline at end of file diff --git a/src/cbc_sdk/platform/models/observation.yaml b/src/cbc_sdk/platform/models/observation.yaml index ef8548ac7..18c69cb42 100644 --- a/src/cbc_sdk/platform/models/observation.yaml +++ b/src/cbc_sdk/platform/models/observation.yaml @@ -77,4 +77,6 @@ properties: process_username: type: array items: - type: string \ No newline at end of file + type: string + tms_rule_id: + type: string \ No newline at end of file diff --git a/src/cbc_sdk/platform/observations.py b/src/cbc_sdk/platform/observations.py index cb41e4c6b..fa41dadfc 100644 --- a/src/cbc_sdk/platform/observations.py +++ b/src/cbc_sdk/platform/observations.py @@ -11,7 +11,7 @@ """Model and Query Classes for Observations""" -from cbc_sdk.base import UnrefreshableModel, FacetQuery +from cbc_sdk.base import UnrefreshableModel, NewBaseModel, FacetQuery from cbc_sdk.base import Query from cbc_sdk.errors import ApiError, TimeoutError, InvalidObjectError @@ -173,6 +173,14 @@ def _get_detailed_results(self): message="user-specified timeout exceeded while waiting for results" ) + def get_network_threat_metadata(self): + """Requests Network Threat Metadata. + + Returns: + NetworkThreatMetadata: Get the metadata for a given detector (rule). + """ + return NetworkThreatMetadata(self._cb, self.tms_rule_id) + class ObservationFacet(UnrefreshableModel): """Represents an observation facet retrieved.""" @@ -531,6 +539,7 @@ def get_group_results( class ObservationGroup: """Represents ObservationGroup""" + def __init__(self, cb, initial_data=None): """ Initialize ObservationGroup object @@ -601,3 +610,47 @@ def __getitem__(self, item): else: raise AttributeError("'{0}' object has no attribute '{1}'".format(self.__class__.__name__, item)) + + +class NetworkThreatMetadata(NewBaseModel): + """Represents an NetworkThreatMetadata""" + + primary_key = "tms_rule_id" + swagger_meta_file = "platform/models/network_threat_metadata.yaml" + urlobject = "/threatmetadata/v1/orgs/{0}/detectors" + + def __init__( + self, + cb, + model_unique_id=None, + initial_data=None, + force_init=True, + full_doc=True, + ): + """ + Initialize the NetworkThreatMetadata object. + + Required Permissions: + org.xdr.metadata (READ) + + Args: + cb (CBCloudAPI): A reference to the CBCloudAPI object. + model_unique_id (Any): The unique ID for this particular instance of the model object. + initial_data (dict): Not used, retained for compatibility. + force_init (bool): True to force object initialization. + full_doc (bool): True to mark the object as fully initialized. + Raises: + ApiError: if model_unique_id is not provided + """ + self._info = None + + if model_unique_id is None: + raise ApiError("model_unique_id is required.") + + super(NetworkThreatMetadata, self).__init__( + cb, + model_unique_id=model_unique_id, + initial_data=None, + force_init=force_init, + full_doc=full_doc, + ) diff --git a/src/tests/unit/fixtures/platform/mock_observations.py b/src/tests/unit/fixtures/platform/mock_observations.py index c95d5cfb6..f330e5f27 100644 --- a/src/tests/unit/fixtures/platform/mock_observations.py +++ b/src/tests/unit/fixtures/platform/mock_observations.py @@ -46,6 +46,7 @@ "process_name": "c:\\programdata\\mozilla-1de4eec8-1241-4177-a864-e594e8d1fb38\\updates", "process_pid": [2000], "process_username": ["DEV01-39X-1\\bit9qa"], + "tms_rule_id": "8a4b43c5-5e0a-4f7d-aa46-bd729f1989a7", } ], } @@ -449,3 +450,12 @@ "num_available": 1, "num_found": 1, } + + +GET_NETWORK_THREAT_METADATA_RESP = { + "detector_abstract": "QE Test signature", + "detector_goal": "QE Test signature", + "false_negatives": None, + "false_positives": None, + "threat_public_comment": "Threat class used for VMWARE NSX Testing", +} diff --git a/src/tests/unit/platform/test_observations.py b/src/tests/unit/platform/test_observations.py index 39a05e49a..5bc89889a 100644 --- a/src/tests/unit/platform/test_observations.py +++ b/src/tests/unit/platform/test_observations.py @@ -5,7 +5,11 @@ from cbc_sdk.base import FacetQuery from cbc_sdk.platform import Observation -from cbc_sdk.platform.observations import ObservationQuery, ObservationFacet +from cbc_sdk.platform.observations import ( + ObservationQuery, + ObservationFacet, + NetworkThreatMetadata, +) from cbc_sdk.rest_api import CBCloudAPI from cbc_sdk.errors import ApiError, TimeoutError from tests.unit.fixtures.CBCSDKMock import CBCSDKMock @@ -24,6 +28,7 @@ GET_OBSERVATIONS_FACET_SEARCH_JOB_RESULTS_RESP_2, GET_OBSERVATIONS_FACET_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING, GET_OBSERVATIONS_GROUPED_RESULTS_RESP, + GET_NETWORK_THREAT_METADATA_RESP, ) log = logging.basicConfig( @@ -974,7 +979,7 @@ def test_observation_select_group_results(cbcsdk_mock): rows=5, start=0, range_field="backend_timestamp", - range_duration="-2y" + range_duration="-2y", ) ) # invoke get_details() on the first Observation in the list @@ -984,3 +989,58 @@ def test_observation_select_group_results(cbcsdk_mock): assert observation_groups[0]["group_key"] is not None assert observation_groups[0].observations[0]["enriched"] is not None assert observation_groups[0].observations[0]["process_pid"][0] == 2000 + + +# ---------- Network Threat Metadata + + +def test_observation_get_threat_metadata(cbcsdk_mock): + """Testing get network threat metadata through observation""" + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/observations/search_jobs", + POST_OBSERVATIONS_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results", # noqa: E501 + GET_OBSERVATIONS_SEARCH_JOB_RESULTS_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=500", # noqa: E501 + GET_OBSERVATIONS_SEARCH_JOB_RESULTS_RESP, + ) + + cbcsdk_mock.mock_request( + "GET", + "/threatmetadata/v1/orgs/test/detectors/8a4b43c5-5e0a-4f7d-aa46-bd729f1989a7", + GET_NETWORK_THREAT_METADATA_RESP, + ) + + api = cbcsdk_mock.api + obs_list = api.select(Observation).where( + observation_id="8fbccc2da75f11ed937ae3cb089984c6:be6ff259-88e3-6286-789f-74defa192d2e" + ) + obs = obs_list[0] + threat_meta_data = obs.get_network_threat_metadata() + assert threat_meta_data["detector_abstract"] + assert threat_meta_data["detector_goal"] + assert threat_meta_data["threat_public_comment"] + + +def test_get_threat_metadata(cbcsdk_mock): + """Testing get network threat metadata""" + cbcsdk_mock.mock_request( + "GET", + "/threatmetadata/v1/orgs/test/detectors/8a4b43c5-5e0a-4f7d-aa46-bd729f1989a7", + GET_NETWORK_THREAT_METADATA_RESP, + ) + + api = cbcsdk_mock.api + threat_meta_data = cb.select( + NetworkThreatMetadata, "8a4b43c5-5e0a-4f7d-aa46-bd729f1989a7" + ) + assert threat_meta_data["detector_abstract"] + assert threat_meta_data["detector_goal"] + assert threat_meta_data["threat_public_comment"] diff --git a/src/tests/unit/test_base_api.py b/src/tests/unit/test_base_api.py index 525bb1baa..cc654a813 100755 --- a/src/tests/unit/test_base_api.py +++ b/src/tests/unit/test_base_api.py @@ -228,7 +228,8 @@ def test_BaseAPI_get_raw_data_returns(mox, expath, code, response, params, defau """Test the cases where get_raw_data returns a value.""" sut = BaseAPI(url='https://example.com', token='ABCDEFGH', org_key='A1B2C3D4') mox.StubOutWithMock(sut.session, 'http_request') - sut.session.http_request('GET', expath, headers={}, data=None, params=params).AndReturn(StubResponse(None, code, response)) + response = StubResponse(None, code, response) + sut.session.http_request('GET', expath, headers={}, data=None, params=params).AndReturn(response) mox.ReplayAll() rc = sut.get_raw_data('/path', params, default) assert rc == expected From f1c60aa79f1e055a799eacdf370384b52a84e7bd Mon Sep 17 00:00:00 2001 From: Emanuela Mitreva Date: Mon, 6 Mar 2023 15:03:06 +0200 Subject: [PATCH 066/143] Fixes --- src/cbc_sdk/platform/observations.py | 13 +++++++++---- src/tests/unit/platform/test_observations.py | 9 ++++++++- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/cbc_sdk/platform/observations.py b/src/cbc_sdk/platform/observations.py index fa41dadfc..e529ccb46 100644 --- a/src/cbc_sdk/platform/observations.py +++ b/src/cbc_sdk/platform/observations.py @@ -617,14 +617,14 @@ class NetworkThreatMetadata(NewBaseModel): primary_key = "tms_rule_id" swagger_meta_file = "platform/models/network_threat_metadata.yaml" - urlobject = "/threatmetadata/v1/orgs/{0}/detectors" + urlobject = "/threatmetadata/v1/orgs/{0}/detectors/{1}" def __init__( self, cb, model_unique_id=None, initial_data=None, - force_init=True, + force_init=False, full_doc=True, ): """ @@ -637,8 +637,9 @@ def __init__( cb (CBCloudAPI): A reference to the CBCloudAPI object. model_unique_id (Any): The unique ID for this particular instance of the model object. initial_data (dict): Not used, retained for compatibility. - force_init (bool): True to force object initialization. + force_init (bool): False to not force object initialization. full_doc (bool): True to mark the object as fully initialized. + Raises: ApiError: if model_unique_id is not provided """ @@ -647,10 +648,14 @@ def __init__( if model_unique_id is None: raise ApiError("model_unique_id is required.") + url = NetworkThreatMetadata.urlobject.format(cb.credentials.org_key, model_unique_id) + data = cb.get_object(url) + data[NetworkThreatMetadata.primary_key] = model_unique_id + super(NetworkThreatMetadata, self).__init__( cb, model_unique_id=model_unique_id, - initial_data=None, + initial_data=data, force_init=force_init, full_doc=full_doc, ) diff --git a/src/tests/unit/platform/test_observations.py b/src/tests/unit/platform/test_observations.py index 5bc89889a..ed956cbfd 100644 --- a/src/tests/unit/platform/test_observations.py +++ b/src/tests/unit/platform/test_observations.py @@ -1038,9 +1038,16 @@ def test_get_threat_metadata(cbcsdk_mock): ) api = cbcsdk_mock.api - threat_meta_data = cb.select( + threat_meta_data = api.select( NetworkThreatMetadata, "8a4b43c5-5e0a-4f7d-aa46-bd729f1989a7" ) assert threat_meta_data["detector_abstract"] assert threat_meta_data["detector_goal"] assert threat_meta_data["threat_public_comment"] + + +def test_get_threat_metadata_without_id(cbcsdk_mock): + """Testing get network threat metadata - exception""" + api = cbcsdk_mock.api + with pytest.raises(ApiError): + api.select(NetworkThreatMetadata) From 25dbafd1b167a6a7880b41f2cd6e90236ece8b67 Mon Sep 17 00:00:00 2001 From: Emanuela Mitreva Date: Mon, 6 Mar 2023 16:12:12 +0200 Subject: [PATCH 067/143] Fix bug caught by Hristo --- src/cbc_sdk/rest_api.py | 2 +- src/tests/unit/test_rest_api.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cbc_sdk/rest_api.py b/src/cbc_sdk/rest_api.py index 19e5db067..294d32ead 100644 --- a/src/cbc_sdk/rest_api.py +++ b/src/cbc_sdk/rest_api.py @@ -437,7 +437,7 @@ def observations_search_validation(self, query, min_backend_timestamp=None, max_ Returns: dict: A dict with status of the validation """ - query_params = {"suggest.q": query} + query_params = {"q": query} if min_backend_timestamp: query_params["cb.min_backend_timestamp"] = min_backend_timestamp if max_backend_timestamp: diff --git a/src/tests/unit/test_rest_api.py b/src/tests/unit/test_rest_api.py index e6ea7c224..b626e05bf 100644 --- a/src/tests/unit/test_rest_api.py +++ b/src/tests/unit/test_rest_api.py @@ -145,7 +145,7 @@ def test_observations_search_validations(cbcsdk_mock): """Tests getting observations search validations""" api = cbcsdk_mock.api q = "?cb.max_backend_timestamp=2020-08-05T08%3A01%3A32.077Z&cb.min_backend_timestamp=" \ - "2020-08-04T08%3A01%3A32.077Z&suggest.q=device_id" + "2020-08-04T08%3A01%3A32.077Z&q=device_id" cbcsdk_mock.mock_request( "GET", f"/api/investigate/v2/orgs/test/observations/search_validation{q}", From 047d6af102aef17b371de7572238a89f15896331 Mon Sep 17 00:00:00 2001 From: Emanuela Mitreva Date: Mon, 6 Mar 2023 20:39:36 +0200 Subject: [PATCH 068/143] fix --- src/cbc_sdk/platform/observations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cbc_sdk/platform/observations.py b/src/cbc_sdk/platform/observations.py index e529ccb46..9d016140b 100644 --- a/src/cbc_sdk/platform/observations.py +++ b/src/cbc_sdk/platform/observations.py @@ -21,7 +21,7 @@ log = logging.getLogger(__name__) -class Observation(UnrefreshableModel): +class Observation(NewBaseModel): """Represents an Observation""" primary_key = "observation_id" From 7b8e4007131d278019ecf6d880c85946d111c9de Mon Sep 17 00:00:00 2001 From: Emanuela Mitreva Date: Mon, 6 Mar 2023 22:06:46 +0200 Subject: [PATCH 069/143] Fix the name of the field --- src/cbc_sdk/platform/models/observation.yaml | 2 +- src/cbc_sdk/platform/observations.py | 4 ++-- src/tests/unit/fixtures/platform/mock_observations.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/cbc_sdk/platform/models/observation.yaml b/src/cbc_sdk/platform/models/observation.yaml index 18c69cb42..3a5a96abe 100644 --- a/src/cbc_sdk/platform/models/observation.yaml +++ b/src/cbc_sdk/platform/models/observation.yaml @@ -78,5 +78,5 @@ properties: type: array items: type: string - tms_rule_id: + rule_id: type: string \ No newline at end of file diff --git a/src/cbc_sdk/platform/observations.py b/src/cbc_sdk/platform/observations.py index 9d016140b..bab867587 100644 --- a/src/cbc_sdk/platform/observations.py +++ b/src/cbc_sdk/platform/observations.py @@ -179,7 +179,7 @@ def get_network_threat_metadata(self): Returns: NetworkThreatMetadata: Get the metadata for a given detector (rule). """ - return NetworkThreatMetadata(self._cb, self.tms_rule_id) + return NetworkThreatMetadata(self._cb, self.rule_id) class ObservationFacet(UnrefreshableModel): @@ -615,7 +615,7 @@ def __getitem__(self, item): class NetworkThreatMetadata(NewBaseModel): """Represents an NetworkThreatMetadata""" - primary_key = "tms_rule_id" + primary_key = "rule_id" swagger_meta_file = "platform/models/network_threat_metadata.yaml" urlobject = "/threatmetadata/v1/orgs/{0}/detectors/{1}" diff --git a/src/tests/unit/fixtures/platform/mock_observations.py b/src/tests/unit/fixtures/platform/mock_observations.py index f330e5f27..63ab328e1 100644 --- a/src/tests/unit/fixtures/platform/mock_observations.py +++ b/src/tests/unit/fixtures/platform/mock_observations.py @@ -46,7 +46,7 @@ "process_name": "c:\\programdata\\mozilla-1de4eec8-1241-4177-a864-e594e8d1fb38\\updates", "process_pid": [2000], "process_username": ["DEV01-39X-1\\bit9qa"], - "tms_rule_id": "8a4b43c5-5e0a-4f7d-aa46-bd729f1989a7", + "rule_id": "8a4b43c5-5e0a-4f7d-aa46-bd729f1989a7", } ], } From 2e6cd76c5e58a4e05d6348cf4e30890963502758 Mon Sep 17 00:00:00 2001 From: Emanuela Mitreva Date: Tue, 7 Mar 2023 06:53:41 +0200 Subject: [PATCH 070/143] fix typo --- src/cbc_sdk/platform/observations.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/cbc_sdk/platform/observations.py b/src/cbc_sdk/platform/observations.py index bab867587..caf84d92c 100644 --- a/src/cbc_sdk/platform/observations.py +++ b/src/cbc_sdk/platform/observations.py @@ -178,6 +178,10 @@ def get_network_threat_metadata(self): Returns: NetworkThreatMetadata: Get the metadata for a given detector (rule). + + Examples: + >>> observation = api.select(Observation, observation_id) + >>> threat_metadata = observation.get_network_threat_metadata() """ return NetworkThreatMetadata(self._cb, self.rule_id) @@ -613,7 +617,7 @@ def __getitem__(self, item): class NetworkThreatMetadata(NewBaseModel): - """Represents an NetworkThreatMetadata""" + """Represents a NetworkThreatMetadata""" primary_key = "rule_id" swagger_meta_file = "platform/models/network_threat_metadata.yaml" From c8750f228dc55c204fbf272058c82a6dbbcd159c Mon Sep 17 00:00:00 2001 From: Emanuela Mitreva Date: Tue, 7 Mar 2023 10:04:55 +0200 Subject: [PATCH 071/143] Fixed. --- docs/cbc_sdk.platform.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/cbc_sdk.platform.rst b/docs/cbc_sdk.platform.rst index 61ed01708..3361af62f 100644 --- a/docs/cbc_sdk.platform.rst +++ b/docs/cbc_sdk.platform.rst @@ -53,7 +53,7 @@ cbc\_sdk.platform.jobs module :show-inheritance: cbc\_sdk.platform.observations module ----------------------------------- +------------------------------------- .. automodule:: cbc_sdk.platform.observations :members: From 57fe9e2d9c515eff660e73b1eb45dd2e756629e6 Mon Sep 17 00:00:00 2001 From: Emanuela Mitreva Date: Tue, 7 Mar 2023 13:32:47 +0200 Subject: [PATCH 072/143] fix --- src/cbc_sdk/platform/observations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cbc_sdk/platform/observations.py b/src/cbc_sdk/platform/observations.py index caf84d92c..1852e3cd8 100644 --- a/src/cbc_sdk/platform/observations.py +++ b/src/cbc_sdk/platform/observations.py @@ -491,7 +491,7 @@ def get_group_results( dict: grouped results Examples: - >>> for group in api.select(Observation.where(process_pid=2000).get_group_results("device_name"): + >>> for group in api.select(Observation).where(process_pid=2000).get_group_results("device_name"): >>> ... """ if not isinstance(fields, list) and not isinstance(fields, str): From c74c8dad6bd8f271f80ef3376ae7c8d554a8050f Mon Sep 17 00:00:00 2001 From: Emanuela Mitreva Date: Tue, 7 Mar 2023 18:00:49 +0200 Subject: [PATCH 073/143] Fixes --- src/cbc_sdk/rest_api.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cbc_sdk/rest_api.py b/src/cbc_sdk/rest_api.py index 294d32ead..0408516aa 100644 --- a/src/cbc_sdk/rest_api.py +++ b/src/cbc_sdk/rest_api.py @@ -431,11 +431,11 @@ def observations_search_validation(self, query, min_backend_timestamp=None, max_ Args: query (str): A search query to be validated. - min_backend_timestamp (str): (optional) The start time for the query - max_backend_timestamp (str): (optional) The end time for the query + min_backend_timestamp (int): (optional) The start time for the query + max_backend_timestamp (int): (optional) The end time for the query Returns: - dict: A dict with status of the validation + boolean: A flag showing the status of the validation """ query_params = {"q": query} if min_backend_timestamp: From 7ad50133c11773dba358c2a3dcc2920ad94d723f Mon Sep 17 00:00:00 2001 From: Emanuela Mitreva Date: Tue, 7 Mar 2023 18:15:40 +0200 Subject: [PATCH 074/143] cherry picking --- src/cbc_sdk/rest_api.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cbc_sdk/rest_api.py b/src/cbc_sdk/rest_api.py index 19e5db067..3e99df02c 100644 --- a/src/cbc_sdk/rest_api.py +++ b/src/cbc_sdk/rest_api.py @@ -431,11 +431,11 @@ def observations_search_validation(self, query, min_backend_timestamp=None, max_ Args: query (str): A search query to be validated. - min_backend_timestamp (str): (optional) The start time for the query - max_backend_timestamp (str): (optional) The end time for the query + min_backend_timestamp (int): (optional) The start time for the query + max_backend_timestamp (int): (optional) The end time for the query Returns: - dict: A dict with status of the validation + boolean: A flag showing the status of the validation """ query_params = {"suggest.q": query} if min_backend_timestamp: From 638bc634f0910150cc4ef8b4aed83118ee95d43a Mon Sep 17 00:00:00 2001 From: Emanuela Mitreva Date: Tue, 7 Mar 2023 21:48:05 +0200 Subject: [PATCH 075/143] Fixed based on Alex's feedback --- docs/cbc_sdk.platform.rst | 8 ++ src/cbc_sdk/platform/__init__.py | 2 + .../platform/network_threat_metadata.py | 74 +++++++++++++++++++ src/cbc_sdk/platform/observations.py | 58 ++------------- .../platform/mock_network_threat_metadata.py | 9 +++ .../fixtures/platform/mock_observations.py | 60 ++++++++++++--- .../platform/test_network_threat_metadata.py | 63 ++++++++++++++++ src/tests/unit/platform/test_observations.py | 65 +++++++++------- 8 files changed, 254 insertions(+), 85 deletions(-) create mode 100644 src/cbc_sdk/platform/network_threat_metadata.py create mode 100644 src/tests/unit/fixtures/platform/mock_network_threat_metadata.py create mode 100644 src/tests/unit/platform/test_network_threat_metadata.py diff --git a/docs/cbc_sdk.platform.rst b/docs/cbc_sdk.platform.rst index 3361af62f..f3dbe84f0 100644 --- a/docs/cbc_sdk.platform.rst +++ b/docs/cbc_sdk.platform.rst @@ -52,6 +52,14 @@ cbc\_sdk.platform.jobs module :undoc-members: :show-inheritance: +cbc\_sdk.platform.network_threat_metadata module +------------------------------------------------ + +.. automodule:: cbc_sdk.platform.network_threat_metadata + :members: + :undoc-members: + :show-inheritance: + cbc\_sdk.platform.observations module ------------------------------------- diff --git a/src/cbc_sdk/platform/__init__.py b/src/cbc_sdk/platform/__init__.py index 5e82b8d43..3c3df8b0a 100644 --- a/src/cbc_sdk/platform/__init__.py +++ b/src/cbc_sdk/platform/__init__.py @@ -26,3 +26,5 @@ from cbc_sdk.platform.jobs import Job from cbc_sdk.platform.observations import Observation + +from cbc_sdk.platform.network_threat_metadata import NetworkThreatMetadata diff --git a/src/cbc_sdk/platform/network_threat_metadata.py b/src/cbc_sdk/platform/network_threat_metadata.py new file mode 100644 index 000000000..038b661bf --- /dev/null +++ b/src/cbc_sdk/platform/network_threat_metadata.py @@ -0,0 +1,74 @@ +# ******************************************************* +# 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. + +"""Model Class for NetworkThreatMetadata""" + +from cbc_sdk.base import NewBaseModel +from cbc_sdk.errors import ApiError + + +class NetworkThreatMetadata(NewBaseModel): + """Represents a NetworkThreatMetadata""" + + primary_key = "tms_rule_id" + swagger_meta_file = "platform/models/network_threat_metadata.yaml" + urlobject = "/threatmetadata/v1/orgs/{0}/detectors/{1}" + + def __init__( + self, + cb, + model_unique_id=None, + initial_data=None, + force_init=False, + full_doc=True, + ): + """ + Initialize the NetworkThreatMetadata object. + + Required Permissions: + org.xdr.metadata (READ) + + Args: + cb (CBCloudAPI): A reference to the CBCloudAPI object. + model_unique_id (Any): The unique ID for this particular instance of the model object. + initial_data (dict): Not used, retained for compatibility. + force_init (bool): False to not force object initialization. + full_doc (bool): True to mark the object as fully initialized. + + Raises: + ApiError: if model_unique_id is not provided + """ + self._info = None + if not model_unique_id: + raise ApiError("model_unique_id is required.") + + url = NetworkThreatMetadata.urlobject.format(cb.credentials.org_key, model_unique_id) + data = cb.get_object(url) + data[NetworkThreatMetadata.primary_key] = model_unique_id + + super(NetworkThreatMetadata, self).__init__( + cb, + model_unique_id=model_unique_id, + initial_data=data, + force_init=force_init, + full_doc=full_doc, + ) + + @classmethod + def _query_implementation(self, cb, **kwargs): + """ + Raises NotImplementedError, because the resource doesn't allow querying. + + Args: + cb (BaseAPI): Reference to API object used to communicate with the server. + **kwargs (dict): Not used, retained for compatibility. + """ + raise NotImplementedError("Resource does not allow query") diff --git a/src/cbc_sdk/platform/observations.py b/src/cbc_sdk/platform/observations.py index 1852e3cd8..df24a3a28 100644 --- a/src/cbc_sdk/platform/observations.py +++ b/src/cbc_sdk/platform/observations.py @@ -14,6 +14,7 @@ from cbc_sdk.base import UnrefreshableModel, NewBaseModel, FacetQuery from cbc_sdk.base import Query from cbc_sdk.errors import ApiError, TimeoutError, InvalidObjectError +from cbc_sdk.platform.network_threat_metadata import NetworkThreatMetadata import logging import time @@ -179,11 +180,17 @@ def get_network_threat_metadata(self): Returns: NetworkThreatMetadata: Get the metadata for a given detector (rule). + Raises: + ApiError: when rule_id is not returned for the Observation + Examples: >>> observation = api.select(Observation, observation_id) >>> threat_metadata = observation.get_network_threat_metadata() """ - return NetworkThreatMetadata(self._cb, self.rule_id) + try: + return NetworkThreatMetadata(self._cb, self.rule_id) + except AttributeError: + raise ApiError("No available network threat metadata.") class ObservationFacet(UnrefreshableModel): @@ -614,52 +621,3 @@ def __getitem__(self, item): else: raise AttributeError("'{0}' object has no attribute '{1}'".format(self.__class__.__name__, item)) - - -class NetworkThreatMetadata(NewBaseModel): - """Represents a NetworkThreatMetadata""" - - primary_key = "rule_id" - swagger_meta_file = "platform/models/network_threat_metadata.yaml" - urlobject = "/threatmetadata/v1/orgs/{0}/detectors/{1}" - - def __init__( - self, - cb, - model_unique_id=None, - initial_data=None, - force_init=False, - full_doc=True, - ): - """ - Initialize the NetworkThreatMetadata object. - - Required Permissions: - org.xdr.metadata (READ) - - Args: - cb (CBCloudAPI): A reference to the CBCloudAPI object. - model_unique_id (Any): The unique ID for this particular instance of the model object. - initial_data (dict): Not used, retained for compatibility. - force_init (bool): False to not force object initialization. - full_doc (bool): True to mark the object as fully initialized. - - Raises: - ApiError: if model_unique_id is not provided - """ - self._info = None - - if model_unique_id is None: - raise ApiError("model_unique_id is required.") - - url = NetworkThreatMetadata.urlobject.format(cb.credentials.org_key, model_unique_id) - data = cb.get_object(url) - data[NetworkThreatMetadata.primary_key] = model_unique_id - - super(NetworkThreatMetadata, self).__init__( - cb, - model_unique_id=model_unique_id, - initial_data=data, - force_init=force_init, - full_doc=full_doc, - ) diff --git a/src/tests/unit/fixtures/platform/mock_network_threat_metadata.py b/src/tests/unit/fixtures/platform/mock_network_threat_metadata.py new file mode 100644 index 000000000..56d9cdebe --- /dev/null +++ b/src/tests/unit/fixtures/platform/mock_network_threat_metadata.py @@ -0,0 +1,9 @@ +"""Mock responses for network threat metadata.""" + +GET_NETWORK_THREAT_METADATA_RESP = { + "detector_abstract": "QE Test signature", + "detector_goal": "QE Test signature", + "false_negatives": None, + "false_positives": None, + "threat_public_comment": "Threat class used for VMWARE NSX Testing", +} diff --git a/src/tests/unit/fixtures/platform/mock_observations.py b/src/tests/unit/fixtures/platform/mock_observations.py index 63ab328e1..296817302 100644 --- a/src/tests/unit/fixtures/platform/mock_observations.py +++ b/src/tests/unit/fixtures/platform/mock_observations.py @@ -3,6 +3,54 @@ POST_OBSERVATIONS_SEARCH_JOB_RESP = {"job_id": "08ffa932-b633-4107-ba56-8741e929e48b"} +GET_OBSERVATIONS_SEARCH_JOB_RESULTS_NO_RULE_ID_RESP = { + "approximate_unaggregated": 1, + "completed": 4, + "contacted": 4, + "num_aggregated": 1, + "num_available": 1, + "num_found": 1, + "results": [ + { + "alert_category": ["OBSERVED"], + "alert_id": None, + "backend_timestamp": "2023-02-08T03:22:59.196Z", + "device_group_id": 0, + "device_id": 17482451, + "device_name": "dev01-39x-1", + "device_policy_id": 20792247, + "device_timestamp": "2023-02-08T03:20:33.751Z", + "enriched": True, + "enriched_event_type": ["NETWORK"], + "event_description": "The script", + "event_id": "8fbccc2da75f11ed937ae3cb089984c6", + "event_network_inbound": False, + "event_network_local_ipv4": "10.203.105.21", + "event_network_location": "Santa Clara,CA,United States", + "event_network_protocol": "TCP", + "event_network_remote_ipv4": "23.44.229.234", + "event_network_remote_port": 80, + "event_type": ["netconn"], + "ingress_time": 1675826462036, + "legacy": True, + "observation_description": "The application firefox.exe invoked ", + "observation_id": "8fbccc2da75f11ed937ae3cb089984c6:be6ff259-88e3-6286-789f-74defa192d2e", + "observation_type": "CB_ANALYTICS", + "org_id": "ABCD123456", + "parent_guid": "ABCD123456-010ac2d3-00001c68-00000000-1d93b6c4d1f20ad", + "parent_pid": 7272, + "process_guid": "ABCD123456-010ac2d3-00001cf8-00000000-1d93b6c4d2b16a4", + "process_hash": [ + "9df1ec5e25919660a1b0b85d3965d55797b9aac81e028008428106c4dcda7b29" + ], + "process_name": "c:\\programdata\\mozilla-1de4eec8-1241-4177-a864-e594e8d1fb38\\updates", + "process_pid": [2000], + "process_username": ["DEV01-39X-1\\bit9qa"] + } + ], +} + + GET_OBSERVATIONS_SEARCH_JOB_RESULTS_RESP = { "approximate_unaggregated": 1, "completed": 4, @@ -46,7 +94,7 @@ "process_name": "c:\\programdata\\mozilla-1de4eec8-1241-4177-a864-e594e8d1fb38\\updates", "process_pid": [2000], "process_username": ["DEV01-39X-1\\bit9qa"], - "rule_id": "8a4b43c5-5e0a-4f7d-aa46-bd729f1989a7", + "rule_id": "8a4b43c5-5e0a-4f7d-aa46-bd729f1989a7" } ], } @@ -253,6 +301,7 @@ ], } + GET_OBSERVATIONS_SEARCH_JOB_RESULTS_RESP_ALERTS = { "approximate_unaggregated": 2, "completed": 4, @@ -450,12 +499,3 @@ "num_available": 1, "num_found": 1, } - - -GET_NETWORK_THREAT_METADATA_RESP = { - "detector_abstract": "QE Test signature", - "detector_goal": "QE Test signature", - "false_negatives": None, - "false_positives": None, - "threat_public_comment": "Threat class used for VMWARE NSX Testing", -} diff --git a/src/tests/unit/platform/test_network_threat_metadata.py b/src/tests/unit/platform/test_network_threat_metadata.py new file mode 100644 index 000000000..f180e92c3 --- /dev/null +++ b/src/tests/unit/platform/test_network_threat_metadata.py @@ -0,0 +1,63 @@ +"""Testing NetworkThreatMetadata objects of cbc_sdk.platform""" + +import pytest +import logging + +from cbc_sdk.platform.network_threat_metadata import NetworkThreatMetadata +from cbc_sdk.rest_api import CBCloudAPI +from cbc_sdk.errors import ApiError +from tests.unit.fixtures.CBCSDKMock import CBCSDKMock +from tests.unit.fixtures.platform.mock_network_threat_metadata import GET_NETWORK_THREAT_METADATA_RESP + +log = logging.basicConfig( + format="%(asctime)s %(levelname)s:%(message)s", + level=logging.DEBUG, + filename="log.txt", +) + + +@pytest.fixture(scope="function") +def cb(): + """Create CBCloudAPI singleton""" + return CBCloudAPI( + url="https://example.com", org_key="test", token="abcd/1234", ssl_verify=False + ) + + +@pytest.fixture(scope="function") +def cbcsdk_mock(monkeypatch, cb): + """Mocks CBC SDK for unit tests""" + return CBCSDKMock(monkeypatch, cb) + + +# ==================================== UNIT TESTS BELOW ==================================== + +def test_get_threat_metadata(cbcsdk_mock): + """Testing get network threat metadata""" + cbcsdk_mock.mock_request( + "GET", + "/threatmetadata/v1/orgs/test/detectors/8a4b43c5-5e0a-4f7d-aa46-bd729f1989a7", + GET_NETWORK_THREAT_METADATA_RESP, + ) + + api = cbcsdk_mock.api + threat_meta_data = api.select( + NetworkThreatMetadata, "8a4b43c5-5e0a-4f7d-aa46-bd729f1989a7" + ) + assert threat_meta_data["detector_abstract"] + assert threat_meta_data["detector_goal"] + assert threat_meta_data["threat_public_comment"] + + +def test_get_threat_metadata_without_id(cbcsdk_mock): + """Testing get network threat metadata - exception""" + api = cbcsdk_mock.api + with pytest.raises(ApiError): + api.select(NetworkThreatMetadata, "") + + +def test_get_threat_metadata_query(cbcsdk_mock): + """Testing get network threat metadata - exception""" + api = cbcsdk_mock.api + with pytest.raises(NotImplementedError): + api.select(NetworkThreatMetadata) diff --git a/src/tests/unit/platform/test_observations.py b/src/tests/unit/platform/test_observations.py index ed956cbfd..f78ca83ad 100644 --- a/src/tests/unit/platform/test_observations.py +++ b/src/tests/unit/platform/test_observations.py @@ -1,4 +1,4 @@ -"""Testing Observation objects of cbc_sdk.endpoint_standard""" +"""Testing Observation objects of cbc_sdk.platform""" import pytest import logging @@ -7,8 +7,7 @@ from cbc_sdk.platform import Observation from cbc_sdk.platform.observations import ( ObservationQuery, - ObservationFacet, - NetworkThreatMetadata, + ObservationFacet ) from cbc_sdk.rest_api import CBCloudAPI from cbc_sdk.errors import ApiError, TimeoutError @@ -23,13 +22,14 @@ GET_OBSERVATIONS_SEARCH_JOB_RESULTS_RESP_ZERO_COMP, GET_OBSERVATIONS_DETAIL_JOB_RESULTS_RESP, GET_OBSERVATIONS_SEARCH_JOB_RESULTS_RESP, + GET_OBSERVATIONS_SEARCH_JOB_RESULTS_NO_RULE_ID_RESP, POST_OBSERVATIONS_FACET_SEARCH_JOB_RESP, GET_OBSERVATIONS_FACET_SEARCH_JOB_RESULTS_RESP_1, GET_OBSERVATIONS_FACET_SEARCH_JOB_RESULTS_RESP_2, GET_OBSERVATIONS_FACET_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING, GET_OBSERVATIONS_GROUPED_RESULTS_RESP, - GET_NETWORK_THREAT_METADATA_RESP, ) +from tests.unit.fixtures.platform.mock_network_threat_metadata import GET_NETWORK_THREAT_METADATA_RESP log = logging.basicConfig( format="%(asctime)s %(levelname)s:%(message)s", @@ -994,8 +994,8 @@ def test_observation_select_group_results(cbcsdk_mock): # ---------- Network Threat Metadata -def test_observation_get_threat_metadata(cbcsdk_mock): - """Testing get network threat metadata through observation""" +def test_observation_get_threat_metadata_api_error(cbcsdk_mock): + """Testing get network threat metadata through observation - no rule_id""" cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/observations/search_jobs", @@ -1004,18 +1004,24 @@ def test_observation_get_threat_metadata(cbcsdk_mock): cbcsdk_mock.mock_request( "GET", "/api/investigate/v2/orgs/test/observations/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results", # noqa: E501 - GET_OBSERVATIONS_SEARCH_JOB_RESULTS_RESP, + GET_OBSERVATIONS_SEARCH_JOB_RESULTS_NO_RULE_ID_RESP, ) cbcsdk_mock.mock_request( "GET", "/api/investigate/v2/orgs/test/observations/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=500", # noqa: E501 - GET_OBSERVATIONS_SEARCH_JOB_RESULTS_RESP, + GET_OBSERVATIONS_SEARCH_JOB_RESULTS_NO_RULE_ID_RESP, + ) + + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/observations/detail_jobs", + POST_OBSERVATIONS_SEARCH_JOB_RESP, ) cbcsdk_mock.mock_request( "GET", - "/threatmetadata/v1/orgs/test/detectors/8a4b43c5-5e0a-4f7d-aa46-bd729f1989a7", - GET_NETWORK_THREAT_METADATA_RESP, + "/api/investigate/v2/orgs/test/observations/detail_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results", # noqa: E501 + GET_OBSERVATIONS_DETAIL_JOB_RESULTS_RESP, ) api = cbcsdk_mock.api @@ -1023,14 +1029,28 @@ def test_observation_get_threat_metadata(cbcsdk_mock): observation_id="8fbccc2da75f11ed937ae3cb089984c6:be6ff259-88e3-6286-789f-74defa192d2e" ) obs = obs_list[0] - threat_meta_data = obs.get_network_threat_metadata() - assert threat_meta_data["detector_abstract"] - assert threat_meta_data["detector_goal"] - assert threat_meta_data["threat_public_comment"] + with pytest.raises(ApiError): + obs.get_network_threat_metadata() -def test_get_threat_metadata(cbcsdk_mock): - """Testing get network threat metadata""" +def test_observation_get_threat_metadata(cbcsdk_mock): + """Testing get network threat metadata through observation""" + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/observations/search_jobs", + POST_OBSERVATIONS_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results", # noqa: E501 + GET_OBSERVATIONS_SEARCH_JOB_RESULTS_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results?start=0&rows=500", # noqa: E501 + GET_OBSERVATIONS_SEARCH_JOB_RESULTS_RESP, + ) + cbcsdk_mock.mock_request( "GET", "/threatmetadata/v1/orgs/test/detectors/8a4b43c5-5e0a-4f7d-aa46-bd729f1989a7", @@ -1038,16 +1058,11 @@ def test_get_threat_metadata(cbcsdk_mock): ) api = cbcsdk_mock.api - threat_meta_data = api.select( - NetworkThreatMetadata, "8a4b43c5-5e0a-4f7d-aa46-bd729f1989a7" + obs_list = api.select(Observation).where( + observation_id="8fbccc2da75f11ed937ae3cb089984c6:be6ff259-88e3-6286-789f-74defa192d2e" ) + obs = obs_list[0] + threat_meta_data = obs.get_network_threat_metadata() assert threat_meta_data["detector_abstract"] assert threat_meta_data["detector_goal"] assert threat_meta_data["threat_public_comment"] - - -def test_get_threat_metadata_without_id(cbcsdk_mock): - """Testing get network threat metadata - exception""" - api = cbcsdk_mock.api - with pytest.raises(ApiError): - api.select(NetworkThreatMetadata) From d7d0a5110e3d11190fc9624c3c113ef693e05978 Mon Sep 17 00:00:00 2001 From: Jasmine Clark <89797061+jclark-vmware@users.noreply.github.com> Date: Mon, 6 Mar 2023 11:22:23 -0500 Subject: [PATCH 076/143] adding example for retrieving alerts from multiple orgs --- docs/alerts.rst | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/docs/alerts.rst b/docs/alerts.rst index 8303229e4..a96161b5f 100644 --- a/docs/alerts.rst +++ b/docs/alerts.rst @@ -68,6 +68,29 @@ You can also filter on different kind of **TTPs** (*Tools Techniques Procedures* ... +Retrieving Alerts for Multiple Organizations +-------------------- + +With the example below, you can retrieve alerts for multiple organizations. + +Create a csv file with values that match the profile names in your credentials.cbc file. You can modify the code inside the for loop to fit your needs. + +.. code-block:: python + + >>> from cbc_sdk import CBCloudAPI + >>> from cbc_sdk.platform import BaseAlert + >>> import csv + >>> file = open ("myFile.csv", "r", encoding='utf-8-sig') + >>> org_list = list(csv.reader(file, delimiter=",")) + >>> file.close() + >>> for org in org_list: + ... org = ''.join(org) + ... api = CBCloudAPI(profile=org) + ... alerts = api.select(BaseAlert).set_minimum_severity(7)[:5] + ... print(alerts[0].id, alerts[0].device_os, alerts[0].device_name, alerts[0].category) + ... + + Retrieving of Carbon Black Analytics Alerts (CBAnalyticsAlert) -------------------------------------------------------------- From 66369b8a1689846778e112034632c28e56ff9ef9 Mon Sep 17 00:00:00 2001 From: Emanuela Mitreva Date: Tue, 14 Mar 2023 21:27:47 +0200 Subject: [PATCH 077/143] Add uat test + improvements based on KK discussion --- src/cbc_sdk/platform/alerts.py | 23 ++ src/cbc_sdk/platform/observations.py | 109 ++++++++- src/cbc_sdk/rest_api.py | 41 ---- src/tests/uat/observations_uat.py | 199 +++++++++++++++++ src/tests/unit/fixtures/mock_rest_api.py | 21 -- .../fixtures/platform/mock_observations.py | 25 ++- src/tests/unit/platform/test_alertsv6_api.py | 19 ++ src/tests/unit/platform/test_observations.py | 210 +++++++++++++++++- src/tests/unit/test_rest_api.py | 31 --- 9 files changed, 567 insertions(+), 111 deletions(-) create mode 100644 src/tests/uat/observations_uat.py diff --git a/src/cbc_sdk/platform/alerts.py b/src/cbc_sdk/platform/alerts.py index 0827ef132..4b993eca0 100644 --- a/src/cbc_sdk/platform/alerts.py +++ b/src/cbc_sdk/platform/alerts.py @@ -252,6 +252,29 @@ def update_threat(self, remediation=None, comment=None): """ return self._update_threat_workflow_status("OPEN", remediation, comment) + @staticmethod + def search_suggestions(cb, query): + """ + Returns suggestions for keys and field values that can be used in a search. + + Args: + cb (CBCloudAPI): A reference to the CBCloudAPI object. + query (str): A search query to use. + + Returns: + list: A list of search suggestions expressed as dict objects. + + Raises: + ApiError: if cb is not instance of CBCloudAPI + """ + from cbc_sdk.rest_api import CBCloudAPI + if not isinstance(cb, CBCloudAPI): + raise ApiError("cb argument should be instance of CBCloudAPI.") + query_params = {"suggest.q": query} + url = "/appservices/v6/orgs/{0}/alerts/search_suggestions".format(cb.credentials.org_key) + output = cb.get_object(url, query_params) + return output["suggestions"] + class WatchlistAlert(BaseAlert): """Represents watch list alerts.""" diff --git a/src/cbc_sdk/platform/observations.py b/src/cbc_sdk/platform/observations.py index df24a3a28..394ddd881 100644 --- a/src/cbc_sdk/platform/observations.py +++ b/src/cbc_sdk/platform/observations.py @@ -18,6 +18,7 @@ import logging import time +from copy import deepcopy log = logging.getLogger(__name__) @@ -26,6 +27,7 @@ class Observation(NewBaseModel): """Represents an Observation""" primary_key = "observation_id" + validation_url = "/api/investigate/v2/orgs/{}/observations/search_validation" swagger_meta_file = "platform/models/observation.yaml" def __init__( @@ -130,21 +132,53 @@ def get_details(self, timeout=0, async_mode=False): def _get_detailed_results(self): """Actual get details implementation""" - args = {"observation_ids": [self.observation_id]} - url = "/api/investigate/v2/orgs/{}/observations/detail_jobs".format( - self._cb.credentials.org_key + obj = Observation._helper_get_details( + self._cb, + observation_ids=[self.observation_id], + timeout=self._details_timeout, ) - query_start = self._cb.post_object(url, body=args) + if obj: + self._info = deepcopy(obj._info) + return self + + @staticmethod + def _helper_get_details(cb, alert_id=None, observation_ids=None, bulk=False, timeout=0): + """Helper to get observation details + + Args: + cb (CBCloudAPI): A reference to the CBCloudAPI object. + alert_id (str): An alert id to fetch associated observations + observation_ids (list): A list of observation ids to fetch + bulk (bool): Whether it is a bulk request + timeout (int): Observations details request timeout in milliseconds. + + Returns: + Observation or list(Observation): if it is a bulk operation a list, otherwise Observation + + Raises: + ApiError: if cb is not instance of CBCloudAPI + """ + from cbc_sdk.rest_api import CBCloudAPI + if not isinstance(cb, CBCloudAPI): + raise ApiError("cb argument should be instance of CBCloudAPI.") + if (alert_id and observation_ids) or not (alert_id or observation_ids): + raise ApiError("Either alert_id or observation_ids should be provided.") + elif alert_id: + args = {"alert_id": alert_id} + else: + args = {"observation_ids": observation_ids} + url = "/api/investigate/v2/orgs/{}/observations/detail_jobs".format(cb.credentials.org_key) + query_start = cb.post_object(url, body=args) job_id = query_start.json().get("job_id") timed_out = False submit_time = time.time() * 1000 while True: result_url = "/api/investigate/v2/orgs/{}/observations/detail_jobs/{}/results".format( - self._cb.credentials.org_key, + cb.credentials.org_key, job_id, ) - result = self._cb.get_object(result_url) + result = cb.get_object(result_url) contacted = result.get("contacted", 0) completed = result.get("completed", 0) log.debug("contacted = {}, completed = {}".format(contacted, completed)) @@ -153,7 +187,7 @@ def _get_detailed_results(self): time.sleep(0.5) continue if completed < contacted: - if self._details_timeout != 0 and (time.time() * 1000) - submit_time > self._details_timeout: + if timeout != 0 and (time.time() * 1000) - submit_time > timeout: timed_out = True break else: @@ -161,11 +195,12 @@ def _get_detailed_results(self): found_results = result.get("num_found", 0) # if found is 0, then no observations were found if found_results == 0: - return self + return None if total_results != 0: results = result.get("results", []) - self._info = results[0] - return self + if bulk: + return [Observation(cb, initial_data=x) for x in results] + return Observation(cb, initial_data=results[0]) time.sleep(0.5) @@ -192,6 +227,59 @@ def get_network_threat_metadata(self): except AttributeError: raise ApiError("No available network threat metadata.") + @staticmethod + def search_suggestions(cb, query, count=None): + """ + Returns suggestions for keys and field values that can be used in a search. + + Args: + cb (CBCloudAPI): A reference to the CBCloudAPI object. + query (str): A search query to use. + count (int): (optional) Number of suggestions to be returned + + Returns: + list: A list of search suggestions expressed as dict objects. + + Raises: + ApiError: if cb is not instance of CBCloudAPI + """ + from cbc_sdk.rest_api import CBCloudAPI + if not isinstance(cb, CBCloudAPI): + raise ApiError("cb argument should be instance of CBCloudAPI.") + query_params = {"suggest.q": query} + if count: + query_params["suggest.count"] = count + url = "/api/investigate/v2/orgs/{}/observations/search_suggestions".format(cb.credentials.org_key) + output = cb.get_object(url, query_params) + return output["suggestions"] + + @staticmethod + def bulk_get_details(cb, alert_id=None, observation_ids=None, timeout=0): + """Bulk get details + + Args: + cb (CBCloudAPI): A reference to the CBCloudAPI object. + alert_id (str): An alert id to fetch associated observations + observation_ids (list): A list of observation ids to fetch + timeout (int): Observations details request timeout in milliseconds. + + Returns: + list: list of Observations + + Raises: + ApiError: if cb is not instance of CBCloudAPI + """ + from cbc_sdk.rest_api import CBCloudAPI + if not isinstance(cb, CBCloudAPI): + raise ApiError("cb argument should be instance of CBCloudAPI.") + return Observation._helper_get_details( + cb, + alert_id=alert_id, + observation_ids=observation_ids, + bulk=True, + timeout=timeout + ) + class ObservationFacet(UnrefreshableModel): """Represents an observation facet retrieved.""" @@ -365,6 +453,7 @@ def _submit(self): ) args = self._get_query_parameters() + self._validate({"q": args.get("query", "")}) url = "/api/investigate/v2/orgs/{}/observations/search_jobs".format( self._cb.credentials.org_key ) diff --git a/src/cbc_sdk/rest_api.py b/src/cbc_sdk/rest_api.py index 0408516aa..12d346128 100644 --- a/src/cbc_sdk/rest_api.py +++ b/src/cbc_sdk/rest_api.py @@ -405,47 +405,6 @@ def bulk_threat_dismiss(self, threat_ids, remediation=None, comment=None): """ return self._bulk_threat_update_status(threat_ids, "DISMISSED", remediation, comment) - # ---- Observations ----- - - def observations_search_suggestions(self, query, count=None): - """ - Returns suggestions for keys and field values that can be used in a search. - - Args: - query (str): A search query to use. - count (int): (optional) Number of suggestions to be returned - - Returns: - list: A list of search suggestions expressed as dict objects. - """ - query_params = {"suggest.q": query} - if count: - query_params["suggest.count"] = count - url = "/api/investigate/v2/orgs/{}/observations/search_suggestions".format(self.credentials.org_key) - output = self.get_object(url, query_params) - return output["suggestions"] - - def observations_search_validation(self, query, min_backend_timestamp=None, max_backend_timestamp=None): - """ - Returns validation result of a query. - - Args: - query (str): A search query to be validated. - min_backend_timestamp (int): (optional) The start time for the query - max_backend_timestamp (int): (optional) The end time for the query - - Returns: - boolean: A flag showing the status of the validation - """ - query_params = {"q": query} - if min_backend_timestamp: - query_params["cb.min_backend_timestamp"] = min_backend_timestamp - if max_backend_timestamp: - query_params["cb.max_backend_timestamp"] = max_backend_timestamp - url = "/api/investigate/v2/orgs/{}/observations/search_validation".format(self.credentials.org_key) - output = self.get_object(url, query_params) - return output.get("valid", False) - # ---- Enterprise EDR def create(self, cls, data=None): diff --git a/src/tests/uat/observations_uat.py b/src/tests/uat/observations_uat.py new file mode 100644 index 000000000..5667fde7a --- /dev/null +++ b/src/tests/uat/observations_uat.py @@ -0,0 +1,199 @@ +#!/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. + +Observations: +https://developer.carbonblack.com/reference/carbon-black-cloud/platform/latest/observations-api/ +""" + +import sys +import time +import requests +from cbc_sdk.helpers import build_cli_parser, get_cb_cloud_object +from cbc_sdk.platform import Observation, NetworkThreatMetadata + +# ------------------------------ APIs ---------------------------------------------- + +# search job + grouped results +START_SEARCH_JOB = "{}api/investigate/v2/orgs/{}/observations/search_jobs" +GET_SEARCH_RESULTS = "{}api/investigate/v2/orgs/{}/observations/search_jobs/{}/results" +GET_GROUPED_RESULTS = "{}api/investigate/v2/orgs/{}/observations/search_jobs/{}/group_results" + +# detail job +START_DETAILS_JOB = "{}api/investigate/v2/orgs/{}/observations/detail_jobs" +GET_DETAILS_RESULTS = "{}api/investigate/v2/orgs/{}/observations/detail_jobs/{}/results" + +# facet job +START_FACET_JOB = "{}api/investigate/v2/orgs/{}/observations/facet_jobs" +GET_FACET_RESULTS = "{}api/investigate/v2/orgs/{}/observations/facet_jobs/{}/results" + +# others +SEARCH_SUGGESTIONS = "{}api/investigate/v2/orgs/{}/observations/search_suggestions?suggest.q={}" +SEARCH_VALIDATION = "{}api/investigate/v2/orgs/{}/observations/search_validation?{}" +GET_NETWORK_THREAT_METADATA = "{}threatmetadata/v1/orgs/{}/detectors/{}" + +# ------------------------------ Formatters ------------------------------------------ + +HEADERS = {"X-Auth-Token": "", "Content-Type": "application/json"} +ORG_KEY = "" +HOSTNAME = "" +DELIMITER = "-" +SYMBOLS = 70 +OBSERVATION_ID = 0 +SECTION_TITLES = ["Observations", "Network Threat Metadata"] +TITLES = [ + "Get Search Suggestions", + "Get Search Validations", + "Get Search Results", + "Get Search Grouped Results", + "Get Details Results", + "Get Network Threat Metadata", +] + +# ------------------------------ Helper functions ------------------------------------- + + +def get_search_results(): + """Get search results - both groupped and not grouped""" + global OBSERVATION_ID + sdata = {"query": "rule_id:* AND observation_type:TAU_INTELLIGENCE"} + gdata = {"fields": ["device_name"], "range": {}, "rows": 50} + job_id = requests.post( + START_SEARCH_JOB.format(HOSTNAME, ORG_KEY), headers=HEADERS, json=sdata + ).json()["job_id"] + time.sleep(0.5) + results = requests.get( + GET_SEARCH_RESULTS.format(HOSTNAME, ORG_KEY, job_id), headers=HEADERS + ).json() + OBSERVATION_ID = results["results"][0]["observation_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(observation_id): + """Get details results""" + ddata = {"observation_ids": [observation_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_seach_suggestions(q="device_id&suggest.count=1"): + """Get search suggestions""" + results = requests.get(SEARCH_SUGGESTIONS.format(HOSTNAME, ORG_KEY, q), headers=HEADERS) + return results.json()["suggestions"] + + +def get_seach_validation(q="*:*", mint="1641469642000", maxt="1678103242000"): + """Get search suggestions""" + query = f"q={q}&cb.min_backend_timestamp={mint}&cb.max_backend_timestamp={maxt}" + results = requests.get(SEARCH_VALIDATION.format(HOSTNAME, ORG_KEY, query), headers=HEADERS) + return results.json()["valid"] + + +def get_network_threat_metadata(rule_id): + """Get network threat metadata""" + results = requests.get( + GET_NETWORK_THREAT_METADATA.format(HOSTNAME, ORG_KEY, rule_id), headers=HEADERS + ).json() + results["tms_rule_id"] = rule_id + return results + + +# ------------------------------ 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_seach_suggestions() + sdk_result = cb.observations_search_suggestions(query="device_id", count=1) + 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_seach_validation() + sdk_result = cb.observations_search_validation( + query="*:*", + min_backend_timestamp="1641469642000", + max_backend_timestamp="1678103242000", + ) + 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(Observation).where("rule_id:* AND observation_type:TAU_INTELLIGENCE") + 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.get_group_results("device_name") for y in x.observations] + 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") + + # check get details job + obs_id = sapi_result[0]["observation_id"] + rule_id = sapi_result[0]["rule_id"] + api_result = get_details_results(obs_id) + sdk_result = cb.select(Observation, obs_id).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") + + print() + print(f"{SECTION_TITLES[1]:^70}") + print(SYMBOLS * DELIMITER) + api_result = get_network_threat_metadata(rule_id) + osdk_result = cb.select(Observation, obs_id).get_network_threat_metadata()._info + ntmsdk_result = cb.select(NetworkThreatMetadata, rule_id)._info + assert ( + api_result == osdk_result == ntmsdk_result + ), f"Test Failed Expected: {api_result} Actual: {osdk_result} other, {ntmsdk_result}" + print(TITLES[5] + "." * (SYMBOLS - len(TITLES[5]) - 2) + "OK") + + +if __name__ == "__main__": + try: + sys.exit(main()) + except KeyboardInterrupt: + print("\nInterrupted by user") diff --git a/src/tests/unit/fixtures/mock_rest_api.py b/src/tests/unit/fixtures/mock_rest_api.py index 724a93942..df0afad45 100644 --- a/src/tests/unit/fixtures/mock_rest_api.py +++ b/src/tests/unit/fixtures/mock_rest_api.py @@ -193,24 +193,3 @@ CONVERT_FEED_QUERY_RESP = {"query": "(process_guid:123) -enriched:true"} - - -OBSERVATIONS_SEARCH_VALIDATIONS_RESP = {"valid": True, "value_search_query": True} - - -OBSERVATIONS_SEARCH_SUGGESTIONS_RESP = { - "suggestions": [ - { - "required_skus_all": [], - "required_skus_some": ["threathunter", "defense"], - "term": "device_id", - "weight": 100, - }, - { - "required_skus_all": ["xdr"], - "required_skus_some": [], - "term": "netconn_remote_device_id", - "weight": 70, - }, - ] -} diff --git a/src/tests/unit/fixtures/platform/mock_observations.py b/src/tests/unit/fixtures/platform/mock_observations.py index 296817302..8dfa525c2 100644 --- a/src/tests/unit/fixtures/platform/mock_observations.py +++ b/src/tests/unit/fixtures/platform/mock_observations.py @@ -45,7 +45,7 @@ ], "process_name": "c:\\programdata\\mozilla-1de4eec8-1241-4177-a864-e594e8d1fb38\\updates", "process_pid": [2000], - "process_username": ["DEV01-39X-1\\bit9qa"] + "process_username": ["DEV01-39X-1\\bit9qa"], } ], } @@ -94,7 +94,7 @@ "process_name": "c:\\programdata\\mozilla-1de4eec8-1241-4177-a864-e594e8d1fb38\\updates", "process_pid": [2000], "process_username": ["DEV01-39X-1\\bit9qa"], - "rule_id": "8a4b43c5-5e0a-4f7d-aa46-bd729f1989a7" + "rule_id": "8a4b43c5-5e0a-4f7d-aa46-bd729f1989a7", } ], } @@ -499,3 +499,24 @@ "num_available": 1, "num_found": 1, } + + +OBSERVATIONS_SEARCH_VALIDATIONS_RESP = {"valid": True, "value_search_query": True} + + +OBSERVATIONS_SEARCH_SUGGESTIONS_RESP = { + "suggestions": [ + { + "required_skus_all": [], + "required_skus_some": ["threathunter", "defense"], + "term": "device_id", + "weight": 100, + }, + { + "required_skus_all": ["xdr"], + "required_skus_some": [], + "term": "netconn_remote_device_id", + "weight": 70, + }, + ] +} diff --git a/src/tests/unit/platform/test_alertsv6_api.py b/src/tests/unit/platform/test_alertsv6_api.py index 0b362858c..1e99464ac 100755 --- a/src/tests/unit/platform/test_alertsv6_api.py +++ b/src/tests/unit/platform/test_alertsv6_api.py @@ -51,6 +51,7 @@ GET_ALERT_NOTES, CREATE_ALERT_NOTE, ) +from tests.unit.fixtures.mock_rest_api import ALERT_SEARCH_SUGGESTIONS_RESP @pytest.fixture(scope="function") @@ -1074,3 +1075,21 @@ def test_base_alert_refresh_note(cbcsdk_mock): alert = api.select(BaseAlert, "1ba0c35f-9c01-4413-afd8-fe4f01365e35") notes = alert.notes_() assert notes[0]._refresh() is True + + +def test_alert_search_suggestions(cbcsdk_mock): + """Tests getting alert search suggestions""" + api = cbcsdk_mock.api + cbcsdk_mock.mock_request( + "GET", + "/appservices/v6/orgs/test/alerts/search_suggestions?suggest.q=", + ALERT_SEARCH_SUGGESTIONS_RESP, + ) + result = BaseAlert.search_suggestions(api, "") + assert len(result) == 20 + + +def test_alert_search_suggestions_api_error(): + """Tests getting alert search suggestions - no CBCloudAPI arg""" + with pytest.raises(ApiError): + BaseAlert.search_suggestions("", "") diff --git a/src/tests/unit/platform/test_observations.py b/src/tests/unit/platform/test_observations.py index f78ca83ad..1a05f678c 100644 --- a/src/tests/unit/platform/test_observations.py +++ b/src/tests/unit/platform/test_observations.py @@ -5,10 +5,7 @@ from cbc_sdk.base import FacetQuery from cbc_sdk.platform import Observation -from cbc_sdk.platform.observations import ( - ObservationQuery, - ObservationFacet -) +from cbc_sdk.platform.observations import ObservationQuery, ObservationFacet from cbc_sdk.rest_api import CBCloudAPI from cbc_sdk.errors import ApiError, TimeoutError from tests.unit.fixtures.CBCSDKMock import CBCSDKMock @@ -28,8 +25,12 @@ GET_OBSERVATIONS_FACET_SEARCH_JOB_RESULTS_RESP_2, GET_OBSERVATIONS_FACET_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING, GET_OBSERVATIONS_GROUPED_RESULTS_RESP, + OBSERVATIONS_SEARCH_VALIDATIONS_RESP, + OBSERVATIONS_SEARCH_SUGGESTIONS_RESP, +) +from tests.unit.fixtures.platform.mock_network_threat_metadata import ( + GET_NETWORK_THREAT_METADATA_RESP, ) -from tests.unit.fixtures.platform.mock_network_threat_metadata import GET_NETWORK_THREAT_METADATA_RESP log = logging.basicConfig( format="%(asctime)s %(levelname)s:%(message)s", @@ -57,6 +58,11 @@ def cbcsdk_mock(monkeypatch, cb): def test_observation_select_where(cbcsdk_mock): """Testing Observation Querying with select()""" + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/search_validation?q=observation_id%3A8fbccc2da75f11ed937ae3cb089984c6%5C%3Abe6ff259%5C-88e3%5C-6286%5C-789f%5C-74defa192d2e", # noqa: E501 + OBSERVATIONS_SEARCH_VALIDATIONS_RESP, + ) cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/observations/search_jobs", @@ -84,6 +90,11 @@ def test_observation_select_where(cbcsdk_mock): def test_observation_select_async(cbcsdk_mock): """Testing Observation Querying with select() - asynchronous way""" + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/search_validation?q=observation_id%3A8fbccc2da75f11ed937ae3cb089984c6%5C%3Abe6ff259%5C-88e3%5C-6286%5C-789f%5C-74defa192d2e", # noqa: E501 + OBSERVATIONS_SEARCH_VALIDATIONS_RESP, + ) cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/observations/search_jobs", @@ -115,6 +126,11 @@ def test_observation_select_async(cbcsdk_mock): def test_observation_select_by_id(cbcsdk_mock): """Testing Observation Querying with select() - asynchronous way""" + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/search_validation?q=observation_id%3A8fbccc2da75f11ed937ae3cb089984c6%5C%3Abe6ff259%5C-88e3%5C-6286%5C-789f%5C-74defa192d2e", # noqa: E501 + OBSERVATIONS_SEARCH_VALIDATIONS_RESP, + ) cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/observations/search_jobs", @@ -142,6 +158,11 @@ def test_observation_select_by_id(cbcsdk_mock): def test_observation_select_details_async(cbcsdk_mock): """Testing Observation Querying with get_details - asynchronous mode""" + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/search_validation?q=process_pid%3A2000", + OBSERVATIONS_SEARCH_VALIDATIONS_RESP, + ) cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/observations/search_jobs", @@ -225,6 +246,11 @@ def test_observations_details_timeout(cbcsdk_mock): def test_observations_select_details_sync(cbcsdk_mock): """Testing Observation Querying with get_details""" + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/search_validation?q=process_pid%3A2000", + OBSERVATIONS_SEARCH_VALIDATIONS_RESP, + ) cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/observations/search_jobs", @@ -263,6 +289,11 @@ def test_observations_select_details_sync(cbcsdk_mock): def test_observations_select_details_refresh(cbcsdk_mock): """Testing Observation Querying with get_details""" + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/search_validation?q=process_pid%3A2000", + OBSERVATIONS_SEARCH_VALIDATIONS_RESP, + ) cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/observations/search_jobs", @@ -301,6 +332,11 @@ def test_observations_select_details_refresh(cbcsdk_mock): def test_observations_select_details_sync_zero(cbcsdk_mock): """Testing Observation Querying with get_details""" + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/search_validation?q=process_pid%3A2000", + OBSERVATIONS_SEARCH_VALIDATIONS_RESP, + ) cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/observations/search_jobs", @@ -337,6 +373,11 @@ def test_observations_select_details_sync_zero(cbcsdk_mock): def test_observations_select_compound(cbcsdk_mock): """Testing Observation Querying with select() and more complex criteria""" + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/search_validation?q=process_pid%3A1000+OR+process_pid%3A1000", + OBSERVATIONS_SEARCH_VALIDATIONS_RESP, + ) cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/observations/search_jobs", @@ -344,7 +385,7 @@ def test_observations_select_compound(cbcsdk_mock): ) cbcsdk_mock.mock_request( "GET", - "/api/investigate/v2/orgs/test/observations/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results", # noqa: E501 + "/api/investigate/v2/orgs/test/observations/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results", GET_OBSERVATIONS_SEARCH_JOB_RESULTS_RESP, ) cbcsdk_mock.mock_request( @@ -362,6 +403,11 @@ def test_observations_select_compound(cbcsdk_mock): def test_observations_query_implementation(cbcsdk_mock): """Testing Observation querying with where().""" + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/search_validation?q=observation_id%3A8fbccc2da75f11ed937ae3cb089984c6%3Abe6ff259-88e3-6286-789f-74defa192d2e", # noqa: E501 + OBSERVATIONS_SEARCH_VALIDATIONS_RESP, + ) cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/observations/search_jobs", @@ -388,6 +434,11 @@ def test_observations_query_implementation(cbcsdk_mock): def test_observations_timeout(cbcsdk_mock): """Testing ObservationQuery.timeout().""" + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/search_validation?q=observation_id%3A8fbccc2da75f11ed937ae3cb089984c6%5C%3Abe6ff259%5C-88e3%5C-6286%5C-789f%5C-74defa192d2e", # noqa: E501 + OBSERVATIONS_SEARCH_VALIDATIONS_RESP, + ) api = cbcsdk_mock.api query = api.select(Observation).where("observation_id:some_id") assert query._timeout == 0 @@ -397,6 +448,11 @@ def test_observations_timeout(cbcsdk_mock): def test_observations_timeout_error(cbcsdk_mock): """Testing that a timeout in Observation querying throws a TimeoutError correctly""" + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/search_validation?q=observation_id%3A8fbccc2da75f11ed937ae3cb089984c6%3Abe6ff259-88e3-6286-789f-74defa192d2e", # noqa: E501 + OBSERVATIONS_SEARCH_VALIDATIONS_RESP, + ) cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/observations/search_jobs", @@ -471,6 +527,11 @@ def test_observations_time_range(cbcsdk_mock): def test_observations_submit(cbcsdk_mock): """Test _submit method of ObservationQuery class""" + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/search_validation?q=process_pid%3A1000", + OBSERVATIONS_SEARCH_VALIDATIONS_RESP, + ) cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/observations/search_jobs", @@ -487,6 +548,11 @@ def test_observations_submit(cbcsdk_mock): def test_observations_count(cbcsdk_mock): """Test _submit method of Observationquery class""" + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/search_validation?q=process_pid%3A1000", + OBSERVATIONS_SEARCH_VALIDATIONS_RESP, + ) cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/observations/search_jobs", @@ -511,6 +577,11 @@ def test_observations_count(cbcsdk_mock): def test_observations_search(cbcsdk_mock): """Test _search method of Observationquery class""" + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/search_validation?q=process_pid%3A2000", + OBSERVATIONS_SEARCH_VALIDATIONS_RESP, + ) cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/observations/search_jobs", @@ -537,6 +608,11 @@ def test_observations_search(cbcsdk_mock): def test_observations_still_querying(cbcsdk_mock): """Test _search method of Observationquery class""" + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/search_validation?q=process_pid%3A1000", + OBSERVATIONS_SEARCH_VALIDATIONS_RESP, + ) cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/observations/search_jobs", @@ -560,6 +636,11 @@ def test_observations_still_querying(cbcsdk_mock): def test_observations_still_querying2(cbcsdk_mock): """Test _search method of Observationquery class""" + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/search_validation?q=process_pid%3A1000", + OBSERVATIONS_SEARCH_VALIDATIONS_RESP, + ) cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/observations/search_jobs", @@ -664,6 +745,11 @@ def test_observation_facet_select_compound(cbcsdk_mock): def test_observation_facet_query_implementation(cbcsdk_mock): """Testing Observation querying with where().""" + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/search_validation?q=process_pid%3A2000", + OBSERVATIONS_SEARCH_VALIDATIONS_RESP, + ) cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/observations/facet_jobs", @@ -948,6 +1034,11 @@ def test_observation_aggregation_wrong_field(cbcsdk_mock): def test_observation_select_group_results(cbcsdk_mock): """Testing Observation Querying with select() and more complex criteria""" + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/search_validation?q=process_pid%3A2000", + OBSERVATIONS_SEARCH_VALIDATIONS_RESP, + ) cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/observations/search_jobs", @@ -980,6 +1071,7 @@ def test_observation_select_group_results(cbcsdk_mock): start=0, range_field="backend_timestamp", range_duration="-2y", + range_method="interval" ) ) # invoke get_details() on the first Observation in the list @@ -996,6 +1088,11 @@ def test_observation_select_group_results(cbcsdk_mock): def test_observation_get_threat_metadata_api_error(cbcsdk_mock): """Testing get network threat metadata through observation - no rule_id""" + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/search_validation?q=observation_id%3A8fbccc2da75f11ed937ae3cb089984c6%5C%3Abe6ff259%5C-88e3%5C-6286%5C-789f%5C-74defa192d2e", # noqa: E501 + OBSERVATIONS_SEARCH_VALIDATIONS_RESP, + ) cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/observations/search_jobs", @@ -1035,6 +1132,11 @@ def test_observation_get_threat_metadata_api_error(cbcsdk_mock): def test_observation_get_threat_metadata(cbcsdk_mock): """Testing get network threat metadata through observation""" + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/search_validation?q=observation_id%3A8fbccc2da75f11ed937ae3cb089984c6%5C%3Abe6ff259%5C-88e3%5C-6286%5C-789f%5C-74defa192d2e", # noqa: E501 + OBSERVATIONS_SEARCH_VALIDATIONS_RESP, + ) cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/observations/search_jobs", @@ -1066,3 +1168,99 @@ def test_observation_get_threat_metadata(cbcsdk_mock): assert threat_meta_data["detector_abstract"] assert threat_meta_data["detector_goal"] assert threat_meta_data["threat_public_comment"] + + +def test_observations_search_suggestions(cbcsdk_mock): + """Tests getting observations search suggestions""" + api = cbcsdk_mock.api + q = "suggest.count=10&suggest.q=device_id" + cbcsdk_mock.mock_request( + "GET", + f"/api/investigate/v2/orgs/test/observations/search_suggestions?{q}", + OBSERVATIONS_SEARCH_SUGGESTIONS_RESP, + ) + result = Observation.search_suggestions(api, "device_id", 10) + assert len(result) != 0 + + +def test_observations_search_suggestions_api_error(): + """Tests getting observations search suggestions - no CBCloudAPI arg""" + with pytest.raises(ApiError): + Observation.search_suggestions("", "device_id", 10) + + +def test_bulk_get_details_api_error(): + """Tests bulk_get_details - no CBCloudAPI arg""" + with pytest.raises(ApiError): + Observation.bulk_get_details("", alert_id="xx") + + +def test_helper_get_details_api_error(): + """Tests _helper_get_details - no CBCloudAPI arg""" + with pytest.raises(ApiError): + Observation._helper_get_details("", alert_id="xx") + + +def test_bulk_get_details_neither(cbcsdk_mock): + """Tests getting bulk_get_details - no alert_id or observation_ids""" + api = cbcsdk_mock.api + with pytest.raises(ApiError): + Observation.bulk_get_details(api) + + +def test_bulk_get_details_both(cbcsdk_mock): + """Tests getting bulk_get_details - both alert_id and observation_ids is provided""" + api = cbcsdk_mock.api + with pytest.raises(ApiError): + Observation.bulk_get_details(api, "xxx", ["xxx"]) + + +def test_bulk_get_details(cbcsdk_mock): + """Tests getting bulk_get_details with observation_ids""" + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/observations/detail_jobs", + POST_OBSERVATIONS_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/detail_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results", # noqa: E501 + GET_OBSERVATIONS_DETAIL_JOB_RESULTS_RESP, + ) + + api = cbcsdk_mock.api + results = Observation.bulk_get_details( + api, + observation_ids=[ + "c7bdd379ac2f11ed92c0b59a6de446c9:fbb78467-f63c-ac52-622a-f41c6f07d815" + ], + ) + assert len(results) == 1 + assert results[0]["device_name"] is not None + assert results[0].device_name is not None + assert results[0].enriched is True + assert results[0].process_pid[0] == 2000 + + +def test_bulk_get_details_alert_id(cbcsdk_mock): + """Tests getting bulk_get_details with alert_id""" + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/observations/detail_jobs", + POST_OBSERVATIONS_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/detail_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results", # noqa: E501 + GET_OBSERVATIONS_DETAIL_JOB_RESULTS_RESP, + ) + + api = cbcsdk_mock.api + results = Observation.bulk_get_details( + api, alert_id="fbb78467-f63c-ac52-622a-f41c6f07d815" + ) + assert len(results) == 1 + assert results[0]["device_name"] is not None + assert results[0].device_name is not None + assert results[0].enriched is True + assert results[0].process_pid[0] == 2000 diff --git a/src/tests/unit/test_rest_api.py b/src/tests/unit/test_rest_api.py index b626e05bf..84cae76fb 100644 --- a/src/tests/unit/test_rest_api.py +++ b/src/tests/unit/test_rest_api.py @@ -23,8 +23,6 @@ PROCESS_LIMITS_RESP, FETCH_PROCESS_QUERY_RESP, CONVERT_FEED_QUERY_RESP, - OBSERVATIONS_SEARCH_VALIDATIONS_RESP, - OBSERVATIONS_SEARCH_SUGGESTIONS_RESP, ) @@ -139,32 +137,3 @@ def test_convert_feed_query(cbcsdk_mock): ) result = api.convert_feed_query("id:123") assert "process_guid:123" in result - - -def test_observations_search_validations(cbcsdk_mock): - """Tests getting observations search validations""" - api = cbcsdk_mock.api - q = "?cb.max_backend_timestamp=2020-08-05T08%3A01%3A32.077Z&cb.min_backend_timestamp=" \ - "2020-08-04T08%3A01%3A32.077Z&q=device_id" - cbcsdk_mock.mock_request( - "GET", - f"/api/investigate/v2/orgs/test/observations/search_validation{q}", - OBSERVATIONS_SEARCH_VALIDATIONS_RESP, - ) - result = api.observations_search_validation( - "device_id", "2020-08-04T08:01:32.077Z", "2020-08-05T08:01:32.077Z" - ) - assert result - - -def test_observations_search_suggestions(cbcsdk_mock): - """Tests getting observations search suggestions""" - api = cbcsdk_mock.api - q = "suggest.count=10&suggest.q=device_id" - cbcsdk_mock.mock_request( - "GET", - f"/api/investigate/v2/orgs/test/observations/search_suggestions?{q}", - OBSERVATIONS_SEARCH_SUGGESTIONS_RESP, - ) - result = api.observations_search_suggestions("device_id", 10) - assert len(result) != 0 From 6dd9a37ede0accd61d6ba5176743e0ec26dc4ffd Mon Sep 17 00:00:00 2001 From: Emanuela Mitreva Date: Tue, 14 Mar 2023 23:15:51 +0200 Subject: [PATCH 078/143] replace with alternative --- src/cbc_sdk/platform/alerts.py | 3 +-- src/cbc_sdk/platform/observations.py | 9 +++------ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/cbc_sdk/platform/alerts.py b/src/cbc_sdk/platform/alerts.py index 4b993eca0..bcfb2a81e 100644 --- a/src/cbc_sdk/platform/alerts.py +++ b/src/cbc_sdk/platform/alerts.py @@ -267,8 +267,7 @@ def search_suggestions(cb, query): Raises: ApiError: if cb is not instance of CBCloudAPI """ - from cbc_sdk.rest_api import CBCloudAPI - if not isinstance(cb, CBCloudAPI): + if cb.__class__.__name__ != "CBCloudAPI": raise ApiError("cb argument should be instance of CBCloudAPI.") query_params = {"suggest.q": query} url = "/appservices/v6/orgs/{0}/alerts/search_suggestions".format(cb.credentials.org_key) diff --git a/src/cbc_sdk/platform/observations.py b/src/cbc_sdk/platform/observations.py index 394ddd881..f59afac73 100644 --- a/src/cbc_sdk/platform/observations.py +++ b/src/cbc_sdk/platform/observations.py @@ -158,8 +158,7 @@ def _helper_get_details(cb, alert_id=None, observation_ids=None, bulk=False, tim Raises: ApiError: if cb is not instance of CBCloudAPI """ - from cbc_sdk.rest_api import CBCloudAPI - if not isinstance(cb, CBCloudAPI): + if cb.__class__.__name__ != "CBCloudAPI": raise ApiError("cb argument should be instance of CBCloudAPI.") if (alert_id and observation_ids) or not (alert_id or observation_ids): raise ApiError("Either alert_id or observation_ids should be provided.") @@ -243,8 +242,7 @@ def search_suggestions(cb, query, count=None): Raises: ApiError: if cb is not instance of CBCloudAPI """ - from cbc_sdk.rest_api import CBCloudAPI - if not isinstance(cb, CBCloudAPI): + if cb.__class__.__name__ != "CBCloudAPI": raise ApiError("cb argument should be instance of CBCloudAPI.") query_params = {"suggest.q": query} if count: @@ -269,8 +267,7 @@ def bulk_get_details(cb, alert_id=None, observation_ids=None, timeout=0): Raises: ApiError: if cb is not instance of CBCloudAPI """ - from cbc_sdk.rest_api import CBCloudAPI - if not isinstance(cb, CBCloudAPI): + if cb.__class__.__name__ != "CBCloudAPI": raise ApiError("cb argument should be instance of CBCloudAPI.") return Observation._helper_get_details( cb, From c763e83e568260207efb975cadcb4b71e964bc24 Mon Sep 17 00:00:00 2001 From: Emanuela Mitreva Date: Wed, 15 Mar 2023 07:35:01 +0200 Subject: [PATCH 079/143] Update uat to match new changes --- src/cbc_sdk/platform/__init__.py | 2 +- src/tests/uat/observations_uat.py | 77 +++++++++++++++---------------- 2 files changed, 38 insertions(+), 41 deletions(-) diff --git a/src/cbc_sdk/platform/__init__.py b/src/cbc_sdk/platform/__init__.py index 3c3df8b0a..f8d32af3d 100644 --- a/src/cbc_sdk/platform/__init__.py +++ b/src/cbc_sdk/platform/__init__.py @@ -25,6 +25,6 @@ from cbc_sdk.platform.jobs import Job -from cbc_sdk.platform.observations import Observation +from cbc_sdk.platform.observations import Observation, ObservationFacet from cbc_sdk.platform.network_threat_metadata import NetworkThreatMetadata diff --git a/src/tests/uat/observations_uat.py b/src/tests/uat/observations_uat.py index 5667fde7a..3e4cb3777 100644 --- a/src/tests/uat/observations_uat.py +++ b/src/tests/uat/observations_uat.py @@ -21,7 +21,7 @@ import time import requests from cbc_sdk.helpers import build_cli_parser, get_cb_cloud_object -from cbc_sdk.platform import Observation, NetworkThreatMetadata +from cbc_sdk.platform import Observation, NetworkThreatMetadata, ObservationFacet # ------------------------------ APIs ---------------------------------------------- @@ -40,7 +40,6 @@ # others SEARCH_SUGGESTIONS = "{}api/investigate/v2/orgs/{}/observations/search_suggestions?suggest.q={}" -SEARCH_VALIDATION = "{}api/investigate/v2/orgs/{}/observations/search_validation?{}" GET_NETWORK_THREAT_METADATA = "{}threatmetadata/v1/orgs/{}/detectors/{}" # ------------------------------ Formatters ------------------------------------------ @@ -54,10 +53,10 @@ SECTION_TITLES = ["Observations", "Network Threat Metadata"] TITLES = [ "Get Search Suggestions", - "Get Search Validations", "Get Search Results", "Get Search Grouped Results", "Get Details Results", + "Get Facet Data", "Get Network Threat Metadata", ] @@ -69,13 +68,9 @@ def get_search_results(): global OBSERVATION_ID sdata = {"query": "rule_id:* AND observation_type:TAU_INTELLIGENCE"} gdata = {"fields": ["device_name"], "range": {}, "rows": 50} - job_id = requests.post( - START_SEARCH_JOB.format(HOSTNAME, ORG_KEY), headers=HEADERS, json=sdata - ).json()["job_id"] + job_id = requests.post(START_SEARCH_JOB.format(HOSTNAME, ORG_KEY), headers=HEADERS, json=sdata).json()["job_id"] time.sleep(0.5) - results = requests.get( - GET_SEARCH_RESULTS.format(HOSTNAME, ORG_KEY, job_id), headers=HEADERS - ).json() + results = requests.get(GET_SEARCH_RESULTS.format(HOSTNAME, ORG_KEY, job_id), headers=HEADERS).json() OBSERVATION_ID = results["results"][0]["observation_id"] gresults = requests.post( GET_GROUPED_RESULTS.format(HOSTNAME, ORG_KEY, job_id), @@ -88,32 +83,33 @@ def get_search_results(): def get_details_results(observation_id): """Get details results""" ddata = {"observation_ids": [observation_id]} - job_id = requests.post( - START_DETAILS_JOB.format(HOSTNAME, ORG_KEY), headers=HEADERS, json=ddata - ).json()["job_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": "rule_id:* AND observation_type:TAU_INTELLIGENCE", + "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_seach_suggestions(q="device_id&suggest.count=1"): """Get search suggestions""" results = requests.get(SEARCH_SUGGESTIONS.format(HOSTNAME, ORG_KEY, q), headers=HEADERS) return results.json()["suggestions"] -def get_seach_validation(q="*:*", mint="1641469642000", maxt="1678103242000"): - """Get search suggestions""" - query = f"q={q}&cb.min_backend_timestamp={mint}&cb.max_backend_timestamp={maxt}" - results = requests.get(SEARCH_VALIDATION.format(HOSTNAME, ORG_KEY, query), headers=HEADERS) - return results.json()["valid"] - - def get_network_threat_metadata(rule_id): """Get network threat metadata""" - results = requests.get( - GET_NETWORK_THREAT_METADATA.format(HOSTNAME, ORG_KEY, rule_id), headers=HEADERS - ).json() + results = requests.get(GET_NETWORK_THREAT_METADATA.format(HOSTNAME, ORG_KEY, rule_id), headers=HEADERS).json() results["tms_rule_id"] = rule_id return results @@ -142,42 +138,43 @@ def main(): print(SYMBOLS * DELIMITER) api_result = get_seach_suggestions() - sdk_result = cb.observations_search_suggestions(query="device_id", count=1) - 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_seach_validation() - sdk_result = cb.observations_search_validation( - query="*:*", - min_backend_timestamp="1641469642000", - max_backend_timestamp="1678103242000", - ) + sdk_result = Observation.search_suggestions(cb, query="device_id", count=1) assert api_result == sdk_result, f"Test Failed Expected: {api_result} Actual: {sdk_result}" - print(TITLES[1] + "." * (SYMBOLS - len(TITLES[1]) - 2) + "OK") + print(TITLES[0] + "." * (SYMBOLS - len(TITLES[0]) - 2) + "OK") # check get search job sapi_result, gapi_result = get_search_results() sdk_r = cb.select(Observation).where("rule_id:* AND observation_type:TAU_INTELLIGENCE") 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") + print(TITLES[1] + "." * (SYMBOLS - len(TITLES[1]) - 2) + "OK") + # check get group results sdk_result = [y._info for x in sdk_r.get_group_results("device_name") for y in x.observations] 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") + assert api_result == sdk_result, f"Test Failed Expected: {api_result} Actual: {sdk_result}" + print(TITLES[2] + "." * (SYMBOLS - len(TITLES[2]) - 2) + "OK") # check get details job obs_id = sapi_result[0]["observation_id"] rule_id = sapi_result[0]["rule_id"] api_result = get_details_results(obs_id) sdk_result = cb.select(Observation, obs_id).get_details()._info - assert ( - api_result == sdk_result - ), f"Test Failed Expected: {api_result} Actual: {sdk_result}" + assert api_result == sdk_result, f"Test Failed Expected: {api_result} Actual: {sdk_result}" + print(TITLES[3] + "." * (SYMBOLS - len(TITLES[3]) - 2) + "OK") + + # check get facet job + api_result = get_facet_results()["terms"] + xx = ( + cb.select(ObservationFacet) + .where("rule_id:* AND observation_type:TAU_INTELLIGENCE") + .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[4] + "." * (SYMBOLS - len(TITLES[4]) - 2) + "OK") print() From a7db38a25906407a62124f108bc1531288219b0d Mon Sep 17 00:00:00 2001 From: Emanuela Mitreva Date: Fri, 17 Mar 2023 09:44:40 +0200 Subject: [PATCH 080/143] Add static methods as concept --- docs/concepts.rst | 75 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/docs/concepts.rst b/docs/concepts.rst index 55a4f442e..018779673 100644 --- a/docs/concepts.rst +++ b/docs/concepts.rst @@ -513,3 +513,78 @@ Get details for all events per alert Category: ['OBSERVED'] Type: NETWORK Alert Id: ['BE084638'] + + +Static Methods +--------------------------------------------- +As of version 1.4.2 we are introducing static methods on some classes. They handle API requests that are not tied with a specific instance of the class, but are like helper methods for that class. Because those methods are static, you need to pass CBCloudAPI object as a first argument. + +Search suggestions +^^^^^^^^^^^^^^^^^^ + +:: + + # Search Suggestions for Observation + >>> from cbc_sdk import CBCloudAPI + >>> from cbc_sdk.platform import Observation + >>> api = CBCloudAPI(profile='platform') + >>> suggestions = Observation.search_suggestions(api, query="device_id", count=2) + >>> for suggestion in suggestions: + ... print(suggestion["term"], suggestion["required_skus_all"], suggestion["required_skus_some"]) + device_id [] ['threathunter', 'defense'] + netconn_remote_device_id ['xdr'] [] + + +:: + + # Search Suggestions for Alerts + >>> from cbc_sdk import CBCloudAPI + >>> from cbc_sdk.platform import BaseAlert + >>> api = CBCloudAPI(profile='platform') + >>> suggestions = BaseAlert.search_suggestions(api, query="device_id") + >>> for suggestion in suggestions: + ... print(suggestion["term"], suggestion["required_skus_some"]) + ... + device_id ['defense', 'threathunter', 'deviceControl'] + device_os ['defense', 'threathunter', 'deviceControl'] + ... + workload_name ['kubernetesSecurityRuntimeProtection'] + +Bulk Get Details +^^^^^^^^^^^^^^^^ + +:: + + # Observation get details per alert id + >>> from cbc_sdk import CBCloudAPI + >>> from cbc_sdk.platform import Observation + >>> api = CBCloudAPI(profile='platform') + >>> bulk_details = Observation.bulk_get_details(api, alert_id="4d49d171-0a11-0731-5172-d0963b77d422") + >>> for obs in bulk_details: + ... print( + ... f''' + ... Category: {obs.alert_category} + ... Type: {obs.observation_type} + ... Alert Id: {obs.alert_id} + ... ''') + Category: ['THREAT'] + Type: CB_ANALYTICS + Alert Id: ['4d49d171-0a11-0731-5172-d0963b77d422'] + + + # Observation get details per observation_ids + >>> from cbc_sdk import CBCloudAPI + >>> from cbc_sdk.platform import Observation + >>> api = CBCloudAPI(profile='platform') + >>> bulk_details = Observation.bulk_get_details(api, observation_ids=["13A5F4E5-C4BD-11ED-A7AB-005056A5B601:13a5f4e4-c4bd-11ed-a7ab-005056a5b611"]) + >>> for obs in bulk_details: + ... print( + ... f''' + ... Category: {obs.alert_category} + ... Type: {obs.observation_type} + ... Alert Id: {obs.alert_id} + ... ''') + Category: ['THREAT'] + Type: CB_ANALYTICS + Alert Id: ['4d49d171-0a11-0731-5172-d0963b77d422'] + From cadd3111049f79108e286b635409f1e319348745 Mon Sep 17 00:00:00 2001 From: Emanuela Mitreva Date: Fri, 17 Mar 2023 09:46:59 +0200 Subject: [PATCH 081/143] fix formatting --- docs/concepts.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/concepts.rst b/docs/concepts.rst index 018779673..260eda8d2 100644 --- a/docs/concepts.rst +++ b/docs/concepts.rst @@ -516,7 +516,7 @@ Get details for all events per alert Static Methods ---------------------------------------------- +-------------- As of version 1.4.2 we are introducing static methods on some classes. They handle API requests that are not tied with a specific instance of the class, but are like helper methods for that class. Because those methods are static, you need to pass CBCloudAPI object as a first argument. Search suggestions From 9aa613f9698f101c84d1936adb132b1bfb3aaf75 Mon Sep 17 00:00:00 2001 From: Emanuela Mitreva Date: Fri, 17 Mar 2023 12:40:45 +0200 Subject: [PATCH 082/143] fixes --- docs/concepts.rst | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/docs/concepts.rst b/docs/concepts.rst index 260eda8d2..ab52ba35b 100644 --- a/docs/concepts.rst +++ b/docs/concepts.rst @@ -517,7 +517,8 @@ Get details for all events per alert Static Methods -------------- -As of version 1.4.2 we are introducing static methods on some classes. They handle API requests that are not tied with a specific instance of the class, but are like helper methods for that class. Because those methods are static, you need to pass CBCloudAPI object as a first argument. + +As of version 1.4.2 we are introducing static methods on some classes. They handle API requests that are not tied with a specific resouce id, thus they cannot be instance methods, but static helper methods. And because those methods are static, you need to pass CBCloudAPI object as a first argument. Search suggestions ^^^^^^^^^^^^^^^^^^ @@ -544,18 +545,18 @@ Search suggestions >>> suggestions = BaseAlert.search_suggestions(api, query="device_id") >>> for suggestion in suggestions: ... print(suggestion["term"], suggestion["required_skus_some"]) - ... device_id ['defense', 'threathunter', 'deviceControl'] device_os ['defense', 'threathunter', 'deviceControl'] ... workload_name ['kubernetesSecurityRuntimeProtection'] + Bulk Get Details ^^^^^^^^^^^^^^^^ :: - # Observation get details per alert id + # Observations get details per alert id >>> from cbc_sdk import CBCloudAPI >>> from cbc_sdk.platform import Observation >>> api = CBCloudAPI(profile='platform') @@ -571,12 +572,13 @@ Bulk Get Details Type: CB_ANALYTICS Alert Id: ['4d49d171-0a11-0731-5172-d0963b77d422'] +:: - # Observation get details per observation_ids + # Observations get details per observation_ids >>> from cbc_sdk import CBCloudAPI >>> from cbc_sdk.platform import Observation >>> api = CBCloudAPI(profile='platform') - >>> bulk_details = Observation.bulk_get_details(api, observation_ids=["13A5F4E5-C4BD-11ED-A7AB-005056A5B601:13a5f4e4-c4bd-11ed-a7ab-005056a5b611"]) + >>> bulk_details = Observation.bulk_get_details(api, observation_ids=["13A5F4E5-C4BD-11ED-A7AB-005056A5B601:13a5f4e4-c4bd-11ed-a7ab-005056a5b611", "13A5F4E5-C4BD-11ED-A7AB-005056A5B601:13a5f4e4-c4bd-11ed-a7ab-005056a5b622"]) >>> for obs in bulk_details: ... print( ... f''' @@ -587,4 +589,8 @@ Bulk Get Details Category: ['THREAT'] Type: CB_ANALYTICS Alert Id: ['4d49d171-0a11-0731-5172-d0963b77d422'] + + Category: ['THREAT'] + Type: CB_ANALYTICS + Alert Id: ['4d49d171-0a11-0731-5172-d0963b77d411'] From a2ca7d6118fd0660101b06d1b0373976db067586 Mon Sep 17 00:00:00 2001 From: Jasmine Clark <89797061+jclark-vmware@users.noreply.github.com> Date: Fri, 17 Mar 2023 10:40:46 -0400 Subject: [PATCH 083/143] adding example for retrieving alerts from multiple orgs --- docs/alerts.rst | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/alerts.rst b/docs/alerts.rst index a96161b5f..af32da647 100644 --- a/docs/alerts.rst +++ b/docs/alerts.rst @@ -69,7 +69,7 @@ You can also filter on different kind of **TTPs** (*Tools Techniques Procedures* Retrieving Alerts for Multiple Organizations --------------------- +-------------------------------------------- With the example below, you can retrieve alerts for multiple organizations. @@ -77,18 +77,21 @@ Create a csv file with values that match the profile names in your credentials.c .. code-block:: python - >>> from cbc_sdk import CBCloudAPI + >>> from cbc_sdk import CBCloudAPI >>> from cbc_sdk.platform import BaseAlert >>> import csv - >>> file = open ("myFile.csv", "r", encoding='utf-8-sig') + >>> file = open ("data.csv", "r", encoding='utf-8-sig') >>> org_list = list(csv.reader(file, delimiter=",")) >>> file.close() >>> for org in org_list: ... org = ''.join(org) ... api = CBCloudAPI(profile=org) ... alerts = api.select(BaseAlert).set_minimum_severity(7)[:5] + ... print('Results for Org {}'.format(org)) + >>> for alert in alerts: ... print(alerts[0].id, alerts[0].device_os, alerts[0].device_name, alerts[0].category) ... + ... Retrieving of Carbon Black Analytics Alerts (CBAnalyticsAlert) From 25630092aa57b1d30db105e1a7176b544d52734a Mon Sep 17 00:00:00 2001 From: Jasmine Clark <89797061+jclark-vmware@users.noreply.github.com> Date: Fri, 17 Mar 2023 10:51:47 -0400 Subject: [PATCH 084/143] fixing alert for loop --- docs/alerts.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/alerts.rst b/docs/alerts.rst index af32da647..077b00f64 100644 --- a/docs/alerts.rst +++ b/docs/alerts.rst @@ -89,7 +89,7 @@ Create a csv file with values that match the profile names in your credentials.c ... alerts = api.select(BaseAlert).set_minimum_severity(7)[:5] ... print('Results for Org {}'.format(org)) >>> for alert in alerts: - ... print(alerts[0].id, alerts[0].device_os, alerts[0].device_name, alerts[0].category) + ... print(alert.id, alert.device_os, alert.device_name, alert.category) ... ... From 439d8f9fee4b64b2325bbb98a366a427b70c2dcf Mon Sep 17 00:00:00 2001 From: Emanuela Mitreva Date: Fri, 17 Mar 2023 16:53:34 +0200 Subject: [PATCH 085/143] Fix typo --- docs/concepts.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/concepts.rst b/docs/concepts.rst index ab52ba35b..dbe6c9deb 100644 --- a/docs/concepts.rst +++ b/docs/concepts.rst @@ -518,7 +518,7 @@ Get details for all events per alert Static Methods -------------- -As of version 1.4.2 we are introducing static methods on some classes. They handle API requests that are not tied with a specific resouce id, thus they cannot be instance methods, but static helper methods. And because those methods are static, you need to pass CBCloudAPI object as a first argument. +As of version 1.4.2 we are introducing static methods on some classes. They handle API requests that are not tied with a specific resource id, thus they cannot be instance methods, but static helper methods. And because those methods are static, you need to pass CBCloudAPI object as a first argument. Search suggestions ^^^^^^^^^^^^^^^^^^ From 1563b4ae447a7185090df817069cff5a1824f9df Mon Sep 17 00:00:00 2001 From: Emanuela Mitreva Date: Fri, 17 Mar 2023 17:39:41 +0200 Subject: [PATCH 086/143] rewording --- docs/concepts.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/concepts.rst b/docs/concepts.rst index dbe6c9deb..c9e94dd02 100644 --- a/docs/concepts.rst +++ b/docs/concepts.rst @@ -518,7 +518,7 @@ Get details for all events per alert Static Methods -------------- -As of version 1.4.2 we are introducing static methods on some classes. They handle API requests that are not tied with a specific resource id, thus they cannot be instance methods, but static helper methods. And because those methods are static, you need to pass CBCloudAPI object as a first argument. +In version 1.4.2 we introduced static methods on some classes. They handle API requests that are not tied to a specific resource id, thus they cannot be instance methods, instead static helper methods. Because those methods are static, they need a CBCloudAPI object to be passed as the first argument. Search suggestions ^^^^^^^^^^^^^^^^^^ From 3bf4072afdce576e0e839fe1c06beb47268acdc9 Mon Sep 17 00:00:00 2001 From: Alex Van Brunt Date: Fri, 17 Mar 2023 10:04:37 -0600 Subject: [PATCH 087/143] Fix amazon linux container --- docker/amazon/Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker/amazon/Dockerfile b/docker/amazon/Dockerfile index 7058be004..47b6f65bd 100644 --- a/docker/amazon/Dockerfile +++ b/docker/amazon/Dockerfile @@ -5,5 +5,7 @@ COPY . /app WORKDIR /app RUN yum -y install python3-devel +RUN yum -y install python3-pip +RUN pip3 install setuptools RUN pip3 install -r requirements.txt RUN pip3 install . From 67980b3f2623db77ca78d7f1baaf230cdb95207e Mon Sep 17 00:00:00 2001 From: Alex Van Brunt Date: Fri, 17 Mar 2023 10:06:42 -0600 Subject: [PATCH 088/143] Fix flake8 error --- src/tests/unit/test_base_api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/tests/unit/test_base_api.py b/src/tests/unit/test_base_api.py index 525bb1baa..5c90db70b 100755 --- a/src/tests/unit/test_base_api.py +++ b/src/tests/unit/test_base_api.py @@ -228,7 +228,8 @@ def test_BaseAPI_get_raw_data_returns(mox, expath, code, response, params, defau """Test the cases where get_raw_data returns a value.""" sut = BaseAPI(url='https://example.com', token='ABCDEFGH', org_key='A1B2C3D4') mox.StubOutWithMock(sut.session, 'http_request') - sut.session.http_request('GET', expath, headers={}, data=None, params=params).AndReturn(StubResponse(None, code, response)) + sut.session.http_request('GET', expath, headers={}, data=None, params=params) \ + .AndReturn(StubResponse(None, code, response)) mox.ReplayAll() rc = sut.get_raw_data('/path', params, default) assert rc == expected From 1e443a39c5e3f5b5e5124d084c614efbe39f2e82 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Tue, 7 Feb 2023 10:35:52 -0700 Subject: [PATCH 089/143] factored out PolicyRuleConfig into its own source file That file will also eventually hold core prevention and HBFW code, and the policies.py file was getting too damn big as it is. --- src/cbc_sdk/platform/__init__.py | 4 +- src/cbc_sdk/platform/policies.py | 147 +----------------- src/cbc_sdk/platform/policy_ruleconfigs.py | 164 +++++++++++++++++++++ 3 files changed, 168 insertions(+), 147 deletions(-) create mode 100644 src/cbc_sdk/platform/policy_ruleconfigs.py diff --git a/src/cbc_sdk/platform/__init__.py b/src/cbc_sdk/platform/__init__.py index 6ecbcb2a3..f43acee51 100644 --- a/src/cbc_sdk/platform/__init__.py +++ b/src/cbc_sdk/platform/__init__.py @@ -9,7 +9,9 @@ from cbc_sdk.platform.events import Event, EventFacet -from cbc_sdk.platform.policies import Policy, PolicyRule, PolicyRuleConfig +from cbc_sdk.platform.policies import Policy, PolicyRule + +from cbc_sdk.platform.policy_ruleconfigs import PolicyRuleConfig from cbc_sdk.platform.processes import (Process, ProcessFacet, AsyncProcessQuery, SummaryQuery) diff --git a/src/cbc_sdk/platform/policies.py b/src/cbc_sdk/platform/policies.py index a751620b6..96165b3af 100644 --- a/src/cbc_sdk/platform/policies.py +++ b/src/cbc_sdk/platform/policies.py @@ -14,8 +14,8 @@ """Policy implementation as part of Platform API""" import copy import json -import jsonschema from cbc_sdk.base import MutableBaseModel, BaseQuery, IterableQueryMixin, AsyncQueryMixin +from cbc_sdk.platform.policy_ruleconfigs import PolicyRuleConfig from cbc_sdk.errors import ApiError, ServerError, InvalidObjectError @@ -1257,151 +1257,6 @@ def validate(self): return True -class PolicyRuleConfig(MutableBaseModel): - """ - Represents a 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 PolicyRuleConfig, change the values of its property fields, then call its save() method. This - requires the org.policies(UPDATE) permission. - - To delete an existing PolicyRuleConfig, call its delete() method. This requires the org.policies(DELETE) permission. - - """ - primary_key = "id" - 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 PolicyRuleConfig 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(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 - if model_unique_id is None: - self.touch(True) - - def _refresh(self): - """ - Refreshes the rule configuration object from the server. - - Required Permissions: - org.policies (READ) - - Returns: - bool: True if the refresh was successful. - """ - 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 - - def _update_object(self): - """ - Updates the rule configuration object on the policy on the server. - - Required Permissions: - org.policies(UPDATE) - """ - self._parent._on_updated_rule_config(self) - - def _delete_object(self): - """ - Deletes this rule configuration object from the policy on the server. - - Required Permissions: - org.policies(DELETE) - """ - was_deleted = False - try: - self._parent._on_deleted_rule_config(self) - was_deleted = True - finally: - if was_deleted: - self._parent = None - - def get_parameter(self, name): - """ - Returns a parameter value from the rule configuration. - - Args: - name (str): The parameter name. - - Returns: - Any: The parameter value, or None if there is no value. - """ - params = self._info['parameters'] - return params.get(name, None) - - def set_parameter(self, name, value): - """ - Sets a parameter value into the rule configuration. - - Args: - name (str): The parameter name. - value (Any): The new value to be set. - """ - params = self._info['parameters'] - old_value = params.get(name, None) - if old_value != value: - if 'parameters' not in self._dirty_attributes: - self._dirty_attributes['parameters'] = params - new_params = copy.deepcopy(params) - else: - new_params = params - new_params[name] = value - self._info['parameters'] = new_params - - def validate(self): - """ - Validates this rule configuration against its constraints. - - Raises: - InvalidObjectError: If the rule object is not valid. - """ - super(PolicyRuleConfig, self).validate() - - if self._parent is not None: - # set high-level fields - valid_configs = self._parent.valid_rule_configs() - data = valid_configs.get(self._model_unique_id, {}) - self._info.update(data) - if 'inherited_from' not in self._info: - self._info['inherited_from'] = 'psc:region' - - # validate parameters - if self._parent is None: - parameter_validations = self._cb.get_policy_ruleconfig_parameter_schema(self._model_unique_id) - else: - parameter_validations = self._parent.get_ruleconfig_parameter_schema(self._model_unique_id) - my_parameters = self._info.get('parameters', {}) - try: - jsonschema.validate(instance=my_parameters, schema=parameter_validations) - except jsonschema.ValidationError as e: - raise InvalidObjectError(f"parameter error: {e.message}", e) - except jsonschema.exceptions.SchemaError as e: - raise ApiError(f"internal error: {e.message}", e) - self._info['parameters'] = my_parameters - - @property - def is_deleted(self): - """Returns True if this rule configuration object has been deleted.""" - return self._parent is None - - """Query Class""" diff --git a/src/cbc_sdk/platform/policy_ruleconfigs.py b/src/cbc_sdk/platform/policy_ruleconfigs.py new file mode 100644 index 000000000..334db916f --- /dev/null +++ b/src/cbc_sdk/platform/policy_ruleconfigs.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python3 + +# ******************************************************* +# Copyright (c) VMware, Inc. 2020-2022. 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. + +"""Policy rule configuration implementation as part of Platform API""" + +import copy +import jsonschema +from cbc_sdk.base import MutableBaseModel +from cbc_sdk.errors import ApiError, ServerError, InvalidObjectError + + +class PolicyRuleConfig(MutableBaseModel): + """ + Represents a 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 PolicyRuleConfig, change the values of its property fields, then call its save() method. This + requires the org.policies(UPDATE) permission. + + To delete an existing PolicyRuleConfig, call its delete() method. This requires the org.policies(DELETE) permission. + + """ + primary_key = "id" + 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 PolicyRuleConfig 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(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 + if model_unique_id is None: + self.touch(True) + + def _refresh(self): + """ + Refreshes the rule configuration object from the server. + + Required Permissions: + org.policies (READ) + + Returns: + bool: True if the refresh was successful. + """ + 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 + + def _update_object(self): + """ + Updates the rule configuration object on the policy on the server. + + Required Permissions: + org.policies(UPDATE) + """ + self._parent._on_updated_rule_config(self) + + def _delete_object(self): + """ + Deletes this rule configuration object from the policy on the server. + + Required Permissions: + org.policies(DELETE) + """ + was_deleted = False + try: + self._parent._on_deleted_rule_config(self) + was_deleted = True + finally: + if was_deleted: + self._parent = None + + def get_parameter(self, name): + """ + Returns a parameter value from the rule configuration. + + Args: + name (str): The parameter name. + + Returns: + Any: The parameter value, or None if there is no value. + """ + params = self._info['parameters'] + return params.get(name, None) + + def set_parameter(self, name, value): + """ + Sets a parameter value into the rule configuration. + + Args: + name (str): The parameter name. + value (Any): The new value to be set. + """ + params = self._info['parameters'] + old_value = params.get(name, None) + if old_value != value: + if 'parameters' not in self._dirty_attributes: + self._dirty_attributes['parameters'] = params + new_params = copy.deepcopy(params) + else: + new_params = params + new_params[name] = value + self._info['parameters'] = new_params + + def validate(self): + """ + Validates this rule configuration against its constraints. + + Raises: + InvalidObjectError: If the rule object is not valid. + """ + super(PolicyRuleConfig, self).validate() + + if self._parent is not None: + # set high-level fields + valid_configs = self._parent.valid_rule_configs() + data = valid_configs.get(self._model_unique_id, {}) + self._info.update(data) + if 'inherited_from' not in self._info: + self._info['inherited_from'] = 'psc:region' + + # validate parameters + if self._parent is None: + parameter_validations = self._cb.get_policy_ruleconfig_parameter_schema(self._model_unique_id) + else: + parameter_validations = self._parent.get_ruleconfig_parameter_schema(self._model_unique_id) + my_parameters = self._info.get('parameters', {}) + try: + jsonschema.validate(instance=my_parameters, schema=parameter_validations) + except jsonschema.ValidationError as e: + raise InvalidObjectError(f"parameter error: {e.message}", e) + except jsonschema.exceptions.SchemaError as e: + raise ApiError(f"internal error: {e.message}", e) + self._info['parameters'] = my_parameters + + @property + def is_deleted(self): + """Returns True if this rule configuration object has been deleted.""" + return self._parent is None From a5225931094f2f80bdd1c374f6616dcf9a8a8d89 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Tue, 7 Feb 2023 10:48:48 -0700 Subject: [PATCH 090/143] factored out tests for rule configs into their own file, too --- src/cbc_sdk/platform/policy_ruleconfigs.py | 2 +- src/tests/unit/platform/test_policies.py | 259 +-------------- .../unit/platform/test_policy_ruleconfigs.py | 301 ++++++++++++++++++ 3 files changed, 303 insertions(+), 259 deletions(-) create mode 100644 src/tests/unit/platform/test_policy_ruleconfigs.py diff --git a/src/cbc_sdk/platform/policy_ruleconfigs.py b/src/cbc_sdk/platform/policy_ruleconfigs.py index 334db916f..219c07768 100644 --- a/src/cbc_sdk/platform/policy_ruleconfigs.py +++ b/src/cbc_sdk/platform/policy_ruleconfigs.py @@ -16,7 +16,7 @@ import copy import jsonschema from cbc_sdk.base import MutableBaseModel -from cbc_sdk.errors import ApiError, ServerError, InvalidObjectError +from cbc_sdk.errors import ApiError, InvalidObjectError class PolicyRuleConfig(MutableBaseModel): diff --git a/src/tests/unit/platform/test_policies.py b/src/tests/unit/platform/test_policies.py index 27c704b54..56dfeda79 100644 --- a/src/tests/unit/platform/test_policies.py +++ b/src/tests/unit/platform/test_policies.py @@ -24,8 +24,7 @@ SUMMARY_POLICY_3, OLD_POLICY_1, FULL_POLICY_2, OLD_POLICY_2, RULE_ADD_1, RULE_ADD_2, RULE_MODIFY_1, NEW_POLICY_CONSTRUCT_1, NEW_POLICY_RETURN_1, BASIC_CONFIG_TEMPLATE_RETURN, - TEMPLATE_RETURN_BOGUS_TYPE, POLICY_CONFIG_PRESENTATION, - REPLACE_RULECONFIG, BUILD_RULECONFIG_1) + BUILD_RULECONFIG_1) logging.basicConfig(format='%(asctime)s %(levelname)s:%(message)s', level=logging.DEBUG, filename='log.txt') @@ -484,262 +483,6 @@ def test_rule_delete_is_new(cb): new_rule.delete() -@pytest.mark.parametrize("initial_data, param_schema_return, handler, message", [ - ({"id": "88b19232-7ebb-48ef-a198-2a75a282de5d", "name": "Privilege Escalation", "inherited_from": "", - "category": "core_prevention", "parameters": {"WindowsAssignmentMode": "BLOCK"}}, - BASIC_CONFIG_TEMPLATE_RETURN, does_not_raise(), None), - ({"id": "88b19232-7ebb-48ef-a198-2a75a282de5d", "name": "Privilege Escalation", "inherited_from": "", - "category": "core_prevention", "parameters": {"WindowsAssignmentMode": "BLOCK"}}, - ServerError(error_code=400, message="blah"), pytest.raises(InvalidObjectError), - "invalid rule config ID 88b19232-7ebb-48ef-a198-2a75a282de5d"), - ({"id": "88b19232-7ebb-48ef-a198-2a75a282de5d", "name": "Privilege Escalation", "inherited_from": "", - "category": "core_prevention", "parameters": {}}, - BASIC_CONFIG_TEMPLATE_RETURN, does_not_raise(), None), - ({"id": "88b19232-7ebb-48ef-a198-2a75a282de5d", "name": "Privilege Escalation", "inherited_from": "", - "category": "core_prevention", "parameters": {"WindowsAssignmentMode": "BLOCK"}}, - TEMPLATE_RETURN_BOGUS_TYPE, pytest.raises(ApiError), - "internal error: 'bogus' is not valid under any of the given schemas"), - ({"id": "88b19232-7ebb-48ef-a198-2a75a282de5d", "name": "Privilege Escalation", "inherited_from": "", - "category": "core_prevention", "parameters": {"WindowsAssignmentMode": 666}}, - BASIC_CONFIG_TEMPLATE_RETURN, pytest.raises(InvalidObjectError), - "parameter error: 666 is not of type 'string'"), - ({"id": "88b19232-7ebb-48ef-a198-2a75a282de5d", "name": "Privilege Escalation", "inherited_from": "", - "category": "core_prevention", "parameters": {"WindowsAssignmentMode": "BOGUSVALUE"}}, - BASIC_CONFIG_TEMPLATE_RETURN, pytest.raises(InvalidObjectError), - "parameter error: 'BOGUSVALUE' is not one of ['REPORT', 'BLOCK']"), -]) -def test_rule_config_validate(cbcsdk_mock, initial_data, param_schema_return, handler, message): - """Tests rule configuration validation.""" - def param_schema(uri, query_params, default): - if isinstance(param_schema_return, Exception): - raise param_schema_return - return param_schema_return - - cbcsdk_mock.mock_request('GET', f"/policyservice/v1/orgs/test/rule_configs/{initial_data['id']}/parameters/schema", - param_schema) - api = cbcsdk_mock.api - rule_config = Policy._create_rule_config(api, None, initial_data) - with handler as h: - rule_config.validate() - if message is not None: - assert h.value.args[0] == message - - -@pytest.mark.parametrize("new_data, get_id, handler, message", [ - ({"id": "88b19232-7ebb-48ef-a198-2a75a282de5d", "name": "Privilege Escalation", "inherited_from": "", - "category": "core_prevention", "parameters": {"WindowsAssignmentMode": "BLOCK"}}, - None, does_not_raise(), None), - ({"id": "88b19236-7ebb-48ef-a198-2a75a282de5d", "name": "Privilege Escalation", "inherited_from": "", - "category": "core_prevention", "parameters": {"WindowsAssignmentMode": "BLOCK"}}, - "88b19232-7ebb-48ef-a198-2a75a282de5d", pytest.raises(InvalidObjectError), - "invalid rule config ID 88b19236-7ebb-48ef-a198-2a75a282de5d"), - ({"id": "88b19232-7ebb-48ef-a198-2a75a282de5d", "name": "Privilege Escalation", "inherited_from": "", - "category": "core_prevention", "parameters": {}}, - None, does_not_raise(), None), - ({"id": "88b19232-7ebb-48ef-a198-2a75a282de5d", "name": "Privilege Escalation", "inherited_from": "", - "category": "core_prevention", "parameters": {"WindowsAssignmentMode": 666}}, - None, pytest.raises(InvalidObjectError), "parameter error: 666 is not of type 'string'"), - ({"id": "88b19232-7ebb-48ef-a198-2a75a282de5d", "name": "Privilege Escalation", "inherited_from": "", - "category": "core_prevention", "parameters": {"WindowsAssignmentMode": "BOGUSVALUE"}}, - None, pytest.raises(InvalidObjectError), "parameter error: 'BOGUSVALUE' is not one of ['REPORT', 'BLOCK']"), -]) -def test_rule_config_validate_inside_policy(cbcsdk_mock, new_data, get_id, handler, message): - """Tests rule configuration validation when it's part of a policy.""" - cbcsdk_mock.mock_request('GET', '/policyservice/v1/orgs/test/policies/65535/configs/presentation', - POLICY_CONFIG_PRESENTATION) - api = cbcsdk_mock.api - policy = Policy(api, 65535, copy.deepcopy(FULL_POLICY_1), False, True) - rule_config_id = get_id if get_id is not None else new_data['id'] - rule_config = policy.object_rule_configs[rule_config_id] - rule_config._info = copy.deepcopy(new_data) - with handler as h: - rule_config.validate() - if message is not None: - assert h.value.args[0] == message - - -def test_rule_config_refresh(cbcsdk_mock): - """Tests the rule config refresh() operation.""" - cbcsdk_mock.mock_request('GET', '/policyservice/v1/orgs/test/policies/65536', FULL_POLICY_1) - api = cbcsdk_mock.api - policy = Policy(api, 65536, FULL_POLICY_1, False, True) - rule_config = random.choice(list(policy.object_rule_configs.values())) - old_name = rule_config.name - old_category = rule_config.category - old_parameters = rule_config.parameters - rule_config.refresh() - assert rule_config.name == old_name - assert rule_config.category == old_category - assert rule_config.parameters == old_parameters - - -@pytest.mark.parametrize("give_error, handler", [ - (False, does_not_raise()), - (True, pytest.raises(ServerError)) -]) -def test_rule_config_add_by_object(cbcsdk_mock, give_error, handler): - """Tests using a PolicyRuleConfig object to add a rule configuration.""" - def on_put(url, body, **kwargs): - assert body == FULL_POLICY_1 - if give_error: - return CBCSDKMock.StubResponse("Failure", scode=404) - rc = copy.deepcopy(body) - return rc - - 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', on_put) - api = cbcsdk_mock.api - policy_data = copy.deepcopy(FULL_POLICY_1) - rule_config_data1 = [p for p in enumerate(policy_data['rule_configs']) - if p[1]['id'] == '88b19232-7ebb-48ef-a198-2a75a282de5d'] - rule_config_data = rule_config_data1[0][1] - del policy_data['rule_configs'][rule_config_data1[0][0]] - policy = Policy(api, 65536, policy_data, False, True) - rule_config_count = len(policy.object_rule_configs) - new_rule_config = Policy._create_rule_config(api, policy, rule_config_data) - new_rule_config.touch() - with handler: - new_rule_config.save() - assert len(policy.object_rule_configs) == rule_config_count + 1 - assert '88b19232-7ebb-48ef-a198-2a75a282de5d' in policy.object_rule_configs - - -def test_rule_config_add_by_base_method(cbcsdk_mock): - """Tests using the base method on Policy to add a rule.""" - def on_put(url, body, **kwargs): - assert body == FULL_POLICY_1 - return copy.deepcopy(body) - - 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', on_put) - api = cbcsdk_mock.api - policy_data = copy.deepcopy(FULL_POLICY_1) - rule_config_data1 = [p for p in enumerate(policy_data['rule_configs']) - if p[1]['id'] == '88b19232-7ebb-48ef-a198-2a75a282de5d'] - rule_config_data = rule_config_data1[0][1] - del policy_data['rule_configs'][rule_config_data1[0][0]] - policy = Policy(api, 65536, policy_data, False, True) - rule_config_count = len(policy.object_rule_configs) - policy.add_rule_config(rule_config_data) - assert len(policy.object_rule_configs) == rule_config_count + 1 - assert '88b19232-7ebb-48ef-a198-2a75a282de5d' in policy.object_rule_configs - - -def test_rule_config_modify_by_object(cbcsdk_mock): - """Tests modifying a PolicyRuleConfig object within the policy.""" - def on_put(url, body, **kwargs): - nonlocal new_data - assert body == new_data - return copy.deepcopy(body) - - 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', on_put) - api = cbcsdk_mock.api - new_data = copy.deepcopy(FULL_POLICY_1) - new_data['rule_configs'][3]['parameters']['WindowsAssignmentMode'] = 'REPORT' - policy = Policy(api, 65536, copy.deepcopy(FULL_POLICY_1), False, True) - rule_config_count = len(policy.object_rule_configs) - rule_config = policy.object_rule_configs['88b19232-7ebb-48ef-a198-2a75a282de5d'] - rule_config.set_parameter('WindowsAssignmentMode', 'REPORT') - rule_config.save() - assert len(policy.object_rule_configs) == rule_config_count - rule_config = policy.object_rule_configs['88b19232-7ebb-48ef-a198-2a75a282de5d'] - assert rule_config.get_parameter('WindowsAssignmentMode') == 'REPORT' - - -def test_rule_config_modify_by_base_method(cbcsdk_mock): - """Tests modifying a PolicyRuleConfig object using the replace_rule_config method.""" - def on_put(url, body, **kwargs): - nonlocal new_data - assert body == new_data - return copy.deepcopy(body) - - 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', on_put) - api = cbcsdk_mock.api - new_data = copy.deepcopy(FULL_POLICY_1) - new_data['rule_configs'][3]['parameters']['WindowsAssignmentMode'] = 'REPORT' - policy = Policy(api, 65536, copy.deepcopy(FULL_POLICY_1), False, True) - rule_config_count = len(policy.object_rule_configs) - policy.replace_rule_config('88b19232-7ebb-48ef-a198-2a75a282de5d', REPLACE_RULECONFIG) - assert len(policy.object_rule_configs) == rule_config_count - rule_config = policy.object_rule_configs['88b19232-7ebb-48ef-a198-2a75a282de5d'] - assert rule_config.get_parameter('WindowsAssignmentMode') == 'REPORT' - - -def test_rule_config_modify_by_base_method_invalid_id(cb): - """Tests modifying a PolicyRuleConfig object using replace_rule_config, but with an invalid ID.""" - policy = Policy(cb, 65536, copy.deepcopy(FULL_POLICY_1), False, True) - with pytest.raises(ApiError): - policy.replace_rule_config('88b19266-7ebb-48ef-a198-2a75a282de5d', REPLACE_RULECONFIG) - - -def test_rule_config_delete_by_object(cbcsdk_mock): - """Tests deleting a PolicyRuleConfig object from a policy.""" - def on_put(url, body, **kwargs): - nonlocal new_data - assert body == new_data - return copy.deepcopy(body) - - 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', on_put) - api = cbcsdk_mock.api - new_data = copy.deepcopy(FULL_POLICY_1) - rule_config_data1 = [p for p in enumerate(new_data['rule_configs']) - if p[1]['id'] == '88b19232-7ebb-48ef-a198-2a75a282de5d'] - del new_data['rule_configs'][rule_config_data1[0][0]] - policy = Policy(api, 65536, copy.deepcopy(FULL_POLICY_1), False, True) - rule_config_count = len(policy.object_rule_configs) - rule_config = policy.object_rule_configs['88b19232-7ebb-48ef-a198-2a75a282de5d'] - assert not rule_config.is_deleted - rule_config.delete() - assert len(policy.object_rule_configs) == rule_config_count - 1 - assert '88b19232-7ebb-48ef-a198-2a75a282de5d' not in policy.object_rule_configs - assert rule_config.is_deleted - - -def test_rule_config_delete_by_base_method(cbcsdk_mock): - """Tests deleting a rule configuration from a policy via the delete_rule_config method.""" - def on_put(url, body, **kwargs): - nonlocal new_data - assert body == new_data - return copy.deepcopy(body) - - 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', on_put) - api = cbcsdk_mock.api - new_data = copy.deepcopy(FULL_POLICY_1) - rule_config_data1 = [p for p in enumerate(new_data['rule_configs']) - if p[1]['id'] == '88b19232-7ebb-48ef-a198-2a75a282de5d'] - del new_data['rule_configs'][rule_config_data1[0][0]] - policy = Policy(api, 65536, copy.deepcopy(FULL_POLICY_1), False, True) - rule_config_count = len(policy.object_rule_configs) - policy.delete_rule_config('88b19232-7ebb-48ef-a198-2a75a282de5d') - assert len(policy.object_rule_configs) == rule_config_count - 1 - assert '88b19232-7ebb-48ef-a198-2a75a282de5d' not in policy.object_rule_configs - - -def test_rule_config_delete_by_base_method_nonexistent(cb): - """Tests what happens when you try to delete a nonexistent rule configuration via delete_rule_config.""" - policy = Policy(cb, 65536, copy.deepcopy(FULL_POLICY_1), False, True) - with pytest.raises(ApiError): - policy.delete_rule_config('88b19266-7ebb-48ef-a198-2a75a282de5d') - - -def test_rule_config_delete_is_new(cb): - """Tests that deleting a new PolicyRuleConfig raises an error.""" - policy = Policy(cb, 65536, copy.deepcopy(FULL_POLICY_1), False, True) - new_rule_config = PolicyRuleConfig(cb, policy, None, REPLACE_RULECONFIG, False, True) - with pytest.raises(ApiError): - new_rule_config.delete() - - def test_policy_builder_make_policy(cbcsdk_mock): """Tests using a policy builder to create a new policy.""" def on_post(uri, body, **kwargs): diff --git a/src/tests/unit/platform/test_policy_ruleconfigs.py b/src/tests/unit/platform/test_policy_ruleconfigs.py new file mode 100644 index 000000000..7637ad62d --- /dev/null +++ b/src/tests/unit/platform/test_policy_ruleconfigs.py @@ -0,0 +1,301 @@ +# ******************************************************* +# Copyright (c) VMware, Inc. 2020-2022. 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. + +"""Tests of the policy rule configurations support in the Platform API.""" + +import copy +import pytest +import logging +import random +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.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, + TEMPLATE_RETURN_BOGUS_TYPE, POLICY_CONFIG_PRESENTATION, + REPLACE_RULECONFIG) + + +logging.basicConfig(format='%(asctime)s %(levelname)s:%(message)s', level=logging.DEBUG, filename='log.txt') + + +@pytest.fixture(scope="function") +def cb(): + """Create CBCloudAPI singleton""" + return CBCloudAPI(url="https://example.com", + org_key="test", + token="abcd/1234", + ssl_verify=False) + + +@pytest.fixture(scope="function") +def cbcsdk_mock(monkeypatch, cb): + """Mocks CBC SDK for unit tests""" + return CBCSDKMock(monkeypatch, cb) + + +# ==================================== UNIT TESTS BELOW ==================================== + +@pytest.mark.parametrize("initial_data, param_schema_return, handler, message", [ + ({"id": "88b19232-7ebb-48ef-a198-2a75a282de5d", "name": "Privilege Escalation", "inherited_from": "", + "category": "core_prevention", "parameters": {"WindowsAssignmentMode": "BLOCK"}}, + BASIC_CONFIG_TEMPLATE_RETURN, does_not_raise(), None), + ({"id": "88b19232-7ebb-48ef-a198-2a75a282de5d", "name": "Privilege Escalation", "inherited_from": "", + "category": "core_prevention", "parameters": {"WindowsAssignmentMode": "BLOCK"}}, + ServerError(error_code=400, message="blah"), pytest.raises(InvalidObjectError), + "invalid rule config ID 88b19232-7ebb-48ef-a198-2a75a282de5d"), + ({"id": "88b19232-7ebb-48ef-a198-2a75a282de5d", "name": "Privilege Escalation", "inherited_from": "", + "category": "core_prevention", "parameters": {}}, + BASIC_CONFIG_TEMPLATE_RETURN, does_not_raise(), None), + ({"id": "88b19232-7ebb-48ef-a198-2a75a282de5d", "name": "Privilege Escalation", "inherited_from": "", + "category": "core_prevention", "parameters": {"WindowsAssignmentMode": "BLOCK"}}, + TEMPLATE_RETURN_BOGUS_TYPE, pytest.raises(ApiError), + "internal error: 'bogus' is not valid under any of the given schemas"), + ({"id": "88b19232-7ebb-48ef-a198-2a75a282de5d", "name": "Privilege Escalation", "inherited_from": "", + "category": "core_prevention", "parameters": {"WindowsAssignmentMode": 666}}, + BASIC_CONFIG_TEMPLATE_RETURN, pytest.raises(InvalidObjectError), + "parameter error: 666 is not of type 'string'"), + ({"id": "88b19232-7ebb-48ef-a198-2a75a282de5d", "name": "Privilege Escalation", "inherited_from": "", + "category": "core_prevention", "parameters": {"WindowsAssignmentMode": "BOGUSVALUE"}}, + BASIC_CONFIG_TEMPLATE_RETURN, pytest.raises(InvalidObjectError), + "parameter error: 'BOGUSVALUE' is not one of ['REPORT', 'BLOCK']"), +]) +def test_rule_config_validate(cbcsdk_mock, initial_data, param_schema_return, handler, message): + """Tests rule configuration validation.""" + def param_schema(uri, query_params, default): + if isinstance(param_schema_return, Exception): + raise param_schema_return + return param_schema_return + + cbcsdk_mock.mock_request('GET', f"/policyservice/v1/orgs/test/rule_configs/{initial_data['id']}/parameters/schema", + param_schema) + api = cbcsdk_mock.api + rule_config = Policy._create_rule_config(api, None, initial_data) + with handler as h: + rule_config.validate() + if message is not None: + assert h.value.args[0] == message + + +@pytest.mark.parametrize("new_data, get_id, handler, message", [ + ({"id": "88b19232-7ebb-48ef-a198-2a75a282de5d", "name": "Privilege Escalation", "inherited_from": "", + "category": "core_prevention", "parameters": {"WindowsAssignmentMode": "BLOCK"}}, + None, does_not_raise(), None), + ({"id": "88b19236-7ebb-48ef-a198-2a75a282de5d", "name": "Privilege Escalation", "inherited_from": "", + "category": "core_prevention", "parameters": {"WindowsAssignmentMode": "BLOCK"}}, + "88b19232-7ebb-48ef-a198-2a75a282de5d", pytest.raises(InvalidObjectError), + "invalid rule config ID 88b19236-7ebb-48ef-a198-2a75a282de5d"), + ({"id": "88b19232-7ebb-48ef-a198-2a75a282de5d", "name": "Privilege Escalation", "inherited_from": "", + "category": "core_prevention", "parameters": {}}, + None, does_not_raise(), None), + ({"id": "88b19232-7ebb-48ef-a198-2a75a282de5d", "name": "Privilege Escalation", "inherited_from": "", + "category": "core_prevention", "parameters": {"WindowsAssignmentMode": 666}}, + None, pytest.raises(InvalidObjectError), "parameter error: 666 is not of type 'string'"), + ({"id": "88b19232-7ebb-48ef-a198-2a75a282de5d", "name": "Privilege Escalation", "inherited_from": "", + "category": "core_prevention", "parameters": {"WindowsAssignmentMode": "BOGUSVALUE"}}, + None, pytest.raises(InvalidObjectError), "parameter error: 'BOGUSVALUE' is not one of ['REPORT', 'BLOCK']"), +]) +def test_rule_config_validate_inside_policy(cbcsdk_mock, new_data, get_id, handler, message): + """Tests rule configuration validation when it's part of a policy.""" + cbcsdk_mock.mock_request('GET', '/policyservice/v1/orgs/test/policies/65535/configs/presentation', + POLICY_CONFIG_PRESENTATION) + api = cbcsdk_mock.api + policy = Policy(api, 65535, copy.deepcopy(FULL_POLICY_1), False, True) + rule_config_id = get_id if get_id is not None else new_data['id'] + rule_config = policy.object_rule_configs[rule_config_id] + rule_config._info = copy.deepcopy(new_data) + with handler as h: + rule_config.validate() + if message is not None: + assert h.value.args[0] == message + + +def test_rule_config_refresh(cbcsdk_mock): + """Tests the rule config refresh() operation.""" + cbcsdk_mock.mock_request('GET', '/policyservice/v1/orgs/test/policies/65536', FULL_POLICY_1) + api = cbcsdk_mock.api + policy = Policy(api, 65536, FULL_POLICY_1, False, True) + rule_config = random.choice(list(policy.object_rule_configs.values())) + old_name = rule_config.name + old_category = rule_config.category + old_parameters = rule_config.parameters + rule_config.refresh() + assert rule_config.name == old_name + assert rule_config.category == old_category + assert rule_config.parameters == old_parameters + + +@pytest.mark.parametrize("give_error, handler", [ + (False, does_not_raise()), + (True, pytest.raises(ServerError)) +]) +def test_rule_config_add_by_object(cbcsdk_mock, give_error, handler): + """Tests using a PolicyRuleConfig object to add a rule configuration.""" + def on_put(url, body, **kwargs): + assert body == FULL_POLICY_1 + if give_error: + return CBCSDKMock.StubResponse("Failure", scode=404) + rc = copy.deepcopy(body) + return rc + + 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', on_put) + api = cbcsdk_mock.api + policy_data = copy.deepcopy(FULL_POLICY_1) + rule_config_data1 = [p for p in enumerate(policy_data['rule_configs']) + if p[1]['id'] == '88b19232-7ebb-48ef-a198-2a75a282de5d'] + rule_config_data = rule_config_data1[0][1] + del policy_data['rule_configs'][rule_config_data1[0][0]] + policy = Policy(api, 65536, policy_data, False, True) + rule_config_count = len(policy.object_rule_configs) + new_rule_config = Policy._create_rule_config(api, policy, rule_config_data) + new_rule_config.touch() + with handler: + new_rule_config.save() + assert len(policy.object_rule_configs) == rule_config_count + 1 + assert '88b19232-7ebb-48ef-a198-2a75a282de5d' in policy.object_rule_configs + + +def test_rule_config_add_by_base_method(cbcsdk_mock): + """Tests using the base method on Policy to add a rule.""" + def on_put(url, body, **kwargs): + assert body == FULL_POLICY_1 + return copy.deepcopy(body) + + 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', on_put) + api = cbcsdk_mock.api + policy_data = copy.deepcopy(FULL_POLICY_1) + rule_config_data1 = [p for p in enumerate(policy_data['rule_configs']) + if p[1]['id'] == '88b19232-7ebb-48ef-a198-2a75a282de5d'] + rule_config_data = rule_config_data1[0][1] + del policy_data['rule_configs'][rule_config_data1[0][0]] + policy = Policy(api, 65536, policy_data, False, True) + rule_config_count = len(policy.object_rule_configs) + policy.add_rule_config(rule_config_data) + assert len(policy.object_rule_configs) == rule_config_count + 1 + assert '88b19232-7ebb-48ef-a198-2a75a282de5d' in policy.object_rule_configs + + +def test_rule_config_modify_by_object(cbcsdk_mock): + """Tests modifying a PolicyRuleConfig object within the policy.""" + def on_put(url, body, **kwargs): + nonlocal new_data + assert body == new_data + return copy.deepcopy(body) + + 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', on_put) + api = cbcsdk_mock.api + new_data = copy.deepcopy(FULL_POLICY_1) + new_data['rule_configs'][3]['parameters']['WindowsAssignmentMode'] = 'REPORT' + policy = Policy(api, 65536, copy.deepcopy(FULL_POLICY_1), False, True) + rule_config_count = len(policy.object_rule_configs) + rule_config = policy.object_rule_configs['88b19232-7ebb-48ef-a198-2a75a282de5d'] + rule_config.set_parameter('WindowsAssignmentMode', 'REPORT') + rule_config.save() + assert len(policy.object_rule_configs) == rule_config_count + rule_config = policy.object_rule_configs['88b19232-7ebb-48ef-a198-2a75a282de5d'] + assert rule_config.get_parameter('WindowsAssignmentMode') == 'REPORT' + + +def test_rule_config_modify_by_base_method(cbcsdk_mock): + """Tests modifying a PolicyRuleConfig object using the replace_rule_config method.""" + def on_put(url, body, **kwargs): + nonlocal new_data + assert body == new_data + return copy.deepcopy(body) + + 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', on_put) + api = cbcsdk_mock.api + new_data = copy.deepcopy(FULL_POLICY_1) + new_data['rule_configs'][3]['parameters']['WindowsAssignmentMode'] = 'REPORT' + policy = Policy(api, 65536, copy.deepcopy(FULL_POLICY_1), False, True) + rule_config_count = len(policy.object_rule_configs) + policy.replace_rule_config('88b19232-7ebb-48ef-a198-2a75a282de5d', REPLACE_RULECONFIG) + assert len(policy.object_rule_configs) == rule_config_count + rule_config = policy.object_rule_configs['88b19232-7ebb-48ef-a198-2a75a282de5d'] + assert rule_config.get_parameter('WindowsAssignmentMode') == 'REPORT' + + +def test_rule_config_modify_by_base_method_invalid_id(cb): + """Tests modifying a PolicyRuleConfig object using replace_rule_config, but with an invalid ID.""" + policy = Policy(cb, 65536, copy.deepcopy(FULL_POLICY_1), False, True) + with pytest.raises(ApiError): + policy.replace_rule_config('88b19266-7ebb-48ef-a198-2a75a282de5d', REPLACE_RULECONFIG) + + +def test_rule_config_delete_by_object(cbcsdk_mock): + """Tests deleting a PolicyRuleConfig object from a policy.""" + def on_put(url, body, **kwargs): + nonlocal new_data + assert body == new_data + return copy.deepcopy(body) + + 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', on_put) + api = cbcsdk_mock.api + new_data = copy.deepcopy(FULL_POLICY_1) + rule_config_data1 = [p for p in enumerate(new_data['rule_configs']) + if p[1]['id'] == '88b19232-7ebb-48ef-a198-2a75a282de5d'] + del new_data['rule_configs'][rule_config_data1[0][0]] + policy = Policy(api, 65536, copy.deepcopy(FULL_POLICY_1), False, True) + rule_config_count = len(policy.object_rule_configs) + rule_config = policy.object_rule_configs['88b19232-7ebb-48ef-a198-2a75a282de5d'] + assert not rule_config.is_deleted + rule_config.delete() + assert len(policy.object_rule_configs) == rule_config_count - 1 + assert '88b19232-7ebb-48ef-a198-2a75a282de5d' not in policy.object_rule_configs + assert rule_config.is_deleted + + +def test_rule_config_delete_by_base_method(cbcsdk_mock): + """Tests deleting a rule configuration from a policy via the delete_rule_config method.""" + def on_put(url, body, **kwargs): + nonlocal new_data + assert body == new_data + return copy.deepcopy(body) + + 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', on_put) + api = cbcsdk_mock.api + new_data = copy.deepcopy(FULL_POLICY_1) + rule_config_data1 = [p for p in enumerate(new_data['rule_configs']) + if p[1]['id'] == '88b19232-7ebb-48ef-a198-2a75a282de5d'] + del new_data['rule_configs'][rule_config_data1[0][0]] + policy = Policy(api, 65536, copy.deepcopy(FULL_POLICY_1), False, True) + rule_config_count = len(policy.object_rule_configs) + policy.delete_rule_config('88b19232-7ebb-48ef-a198-2a75a282de5d') + assert len(policy.object_rule_configs) == rule_config_count - 1 + assert '88b19232-7ebb-48ef-a198-2a75a282de5d' not in policy.object_rule_configs + + +def test_rule_config_delete_by_base_method_nonexistent(cb): + """Tests what happens when you try to delete a nonexistent rule configuration via delete_rule_config.""" + policy = Policy(cb, 65536, copy.deepcopy(FULL_POLICY_1), False, True) + with pytest.raises(ApiError): + policy.delete_rule_config('88b19266-7ebb-48ef-a198-2a75a282de5d') + + +def test_rule_config_delete_is_new(cb): + """Tests that deleting a new PolicyRuleConfig raises an error.""" + policy = Policy(cb, 65536, copy.deepcopy(FULL_POLICY_1), False, True) + new_rule_config = PolicyRuleConfig(cb, policy, None, REPLACE_RULECONFIG, False, True) + with pytest.raises(ApiError): + new_rule_config.delete() From 9a63dc4a1ae04017c057e21fed10dda4a3fa47a1 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Wed, 8 Mar 2023 10:59:27 -0700 Subject: [PATCH 091/143] as of right now, policy_ruleconfigs is at 91%, policies is 93% --- src/cbc_sdk/platform/policies.py | 39 +--- src/cbc_sdk/platform/policy_ruleconfigs.py | 123 +++++++++- .../unit/fixtures/platform/mock_policies.py | 86 +++++-- src/tests/unit/platform/test_policies.py | 2 +- .../unit/platform/test_policy_ruleconfigs.py | 214 ++++++++---------- 5 files changed, 293 insertions(+), 171 deletions(-) diff --git a/src/cbc_sdk/platform/policies.py b/src/cbc_sdk/platform/policies.py index 96165b3af..52b6a61d4 100644 --- a/src/cbc_sdk/platform/policies.py +++ b/src/cbc_sdk/platform/policies.py @@ -14,11 +14,17 @@ """Policy implementation as part of Platform API""" import copy import json +from types import MappingProxyType from cbc_sdk.base import MutableBaseModel, BaseQuery, IterableQueryMixin, AsyncQueryMixin -from cbc_sdk.platform.policy_ruleconfigs import PolicyRuleConfig +from cbc_sdk.platform.policy_ruleconfigs import PolicyRuleConfig, CorePreventionRuleConfig from cbc_sdk.errors import ApiError, ServerError, InvalidObjectError +SPECIFIC_RULECONFIGS = MappingProxyType({ + "core_prevention": CorePreventionRuleConfig +}) + + class Policy(MutableBaseModel): """ Represents a policy within the organization. @@ -686,7 +692,8 @@ def _create_rule_config(cls, cb, parent, data): Returns: PolicyRuleConfig: The new object. """ - return PolicyRuleConfig(cb, parent, data.get("id", None), data, False, True) + newclass = SPECIFIC_RULECONFIGS.get(data.get("category", None), PolicyRuleConfig) + return newclass(cb, parent, data.get("id", None), data, False, True) def get_ruleconfig_parameter_schema(self, ruleconfig_id): """ @@ -774,10 +781,8 @@ def _on_updated_rule_config(self, rule_config): if rule_config._parent is not self: raise ApiError("internal error: updated rule configuration does not belong to this policy") existed = rule_config.id in self.object_rule_configs - old_rule_configs = dict(self.object_rule_configs) self._object_rule_configs[rule_config.id] = rule_config raw_rule_configs = self._info.get("rule_configs", []) - old_raw_rules = copy.deepcopy(raw_rule_configs) if existed: for index, raw_rule_config in enumerate(raw_rule_configs): if raw_rule_config['id'] == rule_config.id: @@ -786,18 +791,6 @@ def _on_updated_rule_config(self, rule_config): else: raw_rule_configs.append(copy.deepcopy(rule_config._info)) self._info['rule_configs'] = raw_rule_configs - self.touch() - rollback = True - try: - self.save() - rollback = False - finally: - if rollback: - self._object_rule_configs = old_rule_configs - self._info['rule_configs'] = old_raw_rules - else: - self._object_rules_need_load = True - self._object_rule_configs_need_load = True def _on_deleted_rule_config(self, rule_config): """ @@ -809,25 +802,11 @@ def _on_deleted_rule_config(self, rule_config): if rule_config._parent is not self: raise ApiError("internal error: updated rule configuration does not belong to this policy") if rule_config.id in self.object_rule_configs: - old_rule_configs = dict(self.object_rule_configs) del self._object_rule_configs[rule_config.id] else: raise ApiError("internal error: updated rule configuration does not belong to this policy") - old_raw_rule_configs = self._info.get("rule_configs", []) new_raw_rule_configs = [raw for raw in old_raw_rule_configs if raw['id'] != rule_config.id] self._info['rule_configs'] = new_raw_rule_configs - self.touch() - rollback = True - try: - self.save() - rollback = False - finally: - if rollback: - self._object_rule_configs = old_rule_configs - self._info['rule_configs'] = old_raw_rule_configs - else: - self._object_rules_need_load = True - self._object_rule_configs_need_load = True def add_rule(self, new_rule): """Adds a rule to this Policy. diff --git a/src/cbc_sdk/platform/policy_ruleconfigs.py b/src/cbc_sdk/platform/policy_ruleconfigs.py index 219c07768..2881ec7a0 100644 --- a/src/cbc_sdk/platform/policy_ruleconfigs.py +++ b/src/cbc_sdk/platform/policy_ruleconfigs.py @@ -32,6 +32,7 @@ class PolicyRuleConfig(MutableBaseModel): To delete an existing PolicyRuleConfig, call its delete() method. This requires the org.policies(DELETE) permission. """ + urlobject = "/policyservice/v1/orgs/{0}/policies" primary_key = "id" swagger_meta_file = "platform/models/policy_ruleconfig.yaml" @@ -53,6 +54,21 @@ 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. @@ -71,6 +87,10 @@ def _refresh(self): self._info = newobj._info return rc + def _update_ruleconfig(self): + """Perform the internal update of the rule configuration object.""" + raise NotImplementedError("update not defined for this category of rule configuration") + def _update_object(self): """ Updates the rule configuration object on the policy on the server. @@ -78,8 +98,14 @@ def _update_object(self): Required Permissions: org.policies(UPDATE) """ + self._update_ruleconfig() + self._full_init = True self._parent._on_updated_rule_config(self) + def _delete_ruleconfig(self): + """Perform the internal delete of the rule configuration object.""" + raise NotImplementedError("delete not defined for this category of rule configuration") + def _delete_object(self): """ Deletes this rule configuration object from the policy on the server. @@ -89,8 +115,9 @@ def _delete_object(self): """ was_deleted = False try: - self._parent._on_deleted_rule_config(self) - was_deleted = True + if self._delete_ruleconfig(): + self._parent._on_deleted_rule_config(self) + was_deleted = True finally: if was_deleted: self._parent = None @@ -162,3 +189,95 @@ def validate(self): def is_deleted(self): """Returns True if this rule configuration object has been deleted.""" return self._parent is None + + +class CorePreventionRuleConfig(PolicyRuleConfig): + """ + Represents a core prevention 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 CorePreventionRuleConfig, 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. + + """ + 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(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. + + 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] + else: + raise InvalidObjectError(f"invalid core prevention ID: {self._model_unique_id}") + return True + + def _update_ruleconfig(self): + """Perform the internal update of the rule configuration object.""" + body = [{"id": self.id, "parameters": self.parameters}] + self._cb.put_object(self._base_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}") + return False + + def get_assignment_mode(self): + """ + Returns the assignment mode of this core prevention rule configuration. + + Returns: + str: The assignment mode, either "REPORT" or "BLOCK". + """ + return self.get_parameter("WindowsAssignmentMode") + + def set_assignment_mode(self, mode): + """ + Sets the assignment mode of this core prevention rule configuration. + + Args: + mode (str): The new mode to set, either "REPORT" or "BLOCK". The default is "BLOCK". + """ + if mode not in ("REPORT", "BLOCK"): + raise ApiError(f"invalid assignment mode: {mode}") + self.set_parameter("WindowsAssignmentMode", mode) diff --git a/src/tests/unit/fixtures/platform/mock_policies.py b/src/tests/unit/fixtures/platform/mock_policies.py index 6bc74295d..7f88f6346 100644 --- a/src/tests/unit/fixtures/platform/mock_policies.py +++ b/src/tests/unit/fixtures/platform/mock_policies.py @@ -196,7 +196,7 @@ "name": "Advanced Scripting Prevention", "description": "Addresses malicious fileless and file-backed scripts that leverage native programs [...]", "inherited_from": "psc:region", - "category": "core-prevention", + "category": "core_prevention", "parameters": { "WindowsAssignmentMode": "BLOCK" } @@ -206,7 +206,7 @@ "name": "Credential Theft", "description": "Addresses threat actors obtaining credentials and relies on detecting the malicious [...]", "inherited_from": "psc:region", - "category": "core-prevention", + "category": "core_prevention", "parameters": { "WindowsAssignmentMode": "REPORT" } @@ -216,7 +216,7 @@ "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", + "category": "core_prevention", "parameters": { "WindowsAssignmentMode": "REPORT" } @@ -226,7 +226,7 @@ "name": "Privilege Escalation", "description": "Addresses behaviors that indicate a threat actor has gained elevated access via [...]", "inherited_from": "psc:region", - "category": "core-prevention", + "category": "core_prevention", "parameters": { "WindowsAssignmentMode": "BLOCK" } @@ -1442,7 +1442,7 @@ "id": "88b19232-7ebb-48ef-a198-2a75a282de5d", "name": "Privilege Escalation", "inherited_from": "", - "category": "core-prevention", + "category": "core_prevention", "parameters": { "WindowsAssignmentMode": "BLOCK" } @@ -1451,7 +1451,7 @@ "id": "ac67fa14-f6be-4df9-93f2-6de0dbd96061", "name": "Credential Theft", "inherited_from": "", - "category": "core-prevention", + "category": "core_prevention", "parameters": { "WindowsAssignmentMode": "REPORT" } @@ -1575,7 +1575,7 @@ "name": "Advanced Scripting Prevention", "description": "Addresses malicious fileless and file-backed scripts that leverage native programs [...]", "inherited_from": "psc:region", - "category": "core-prevention", + "category": "core_prevention", "parameters": { "WindowsAssignmentMode": "BLOCK" } @@ -1585,7 +1585,7 @@ "name": "Credential Theft", "description": "Addresses threat actors obtaining credentials and relies on detecting the malicious [...]", "inherited_from": "psc:region", - "category": "core-prevention", + "category": "core_prevention", "parameters": { "WindowsAssignmentMode": "REPORT" } @@ -1595,7 +1595,7 @@ "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", + "category": "core_prevention", "parameters": { "WindowsAssignmentMode": "REPORT" } @@ -1605,7 +1605,7 @@ "name": "Privilege Escalation", "description": "Addresses behaviors that indicate a threat actor has gained elevated access via [...]", "inherited_from": "psc:region", - "category": "core-prevention", + "category": "core_prevention", "parameters": { "WindowsAssignmentMode": "BLOCK" } @@ -1647,7 +1647,7 @@ "description": "Addresses malicious fileless and file-backed scripts that leverage native programs [...]", "presentation": { "name": "amsi.name", - "category": "core-prevention", + "category": "core_prevention", "description": [ "amsi.description" ], @@ -1691,7 +1691,7 @@ "description": "Addresses threat actors obtaining credentials and relies on detecting the malicious [...]", "presentation": { "name": "cred_theft.name", - "category": "core-prevention", + "category": "core_prevention", "description": [ "cred_theft.description" ], @@ -1735,7 +1735,7 @@ "description": "Addresses common and pervasive TTPs used for malicious activity as well as [...]", "presentation": { "name": "cbti.name", - "category": "core-prevention", + "category": "core_prevention", "description": [ "cbti.description" ], @@ -1779,7 +1779,7 @@ "description": "Addresses behaviors that indicate a threat actor has gained elevated access via [...]", "presentation": { "name": "privesc.name", - "category": "core-prevention", + "category": "core_prevention", "description": [ "privesc.description" ], @@ -1825,7 +1825,7 @@ "name": "Privilege Escalation", "description": "Addresses behaviors that indicate a threat actor has gained elevated access via [...]", "inherited_from": "psc:region", - "category": "core-prevention", + "category": "core_prevention", "parameters": { "WindowsAssignmentMode": "REPORT" } @@ -1835,8 +1835,62 @@ "id": "88b19232-7ebb-48ef-a198-2a75a282de5d", "name": "Privilege Escalation", "inherited_from": "", - "category": "core-prevention", + "category": "core_prevention", "parameters": { "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/platform/test_policies.py b/src/tests/unit/platform/test_policies.py index 56dfeda79..c9788185b 100644 --- a/src/tests/unit/platform/test_policies.py +++ b/src/tests/unit/platform/test_policies.py @@ -515,7 +515,7 @@ def on_post(uri, body, **kwargs): builder.set_managed_detection_response_permissions(False, True) rule_config = PolicyRuleConfig(api, None, BUILD_RULECONFIG_1['id'], BUILD_RULECONFIG_1, False, True) builder.add_rule_config_copy(rule_config) - builder.add_rule_config("ac67fa14-f6be-4df9-93f2-6de0dbd96061", "Credential Theft", "core-prevention", + builder.add_rule_config("ac67fa14-f6be-4df9-93f2-6de0dbd96061", "Credential Theft", "core_prevention", WindowsAssignmentMode='REPORT') policy = builder.build() assert policy._info == NEW_POLICY_CONSTRUCT_1 diff --git a/src/tests/unit/platform/test_policy_ruleconfigs.py b/src/tests/unit/platform/test_policy_ruleconfigs.py index 7637ad62d..ca9cd3467 100644 --- a/src/tests/unit/platform/test_policy_ruleconfigs.py +++ b/src/tests/unit/platform/test_policy_ruleconfigs.py @@ -18,11 +18,13 @@ 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.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, TEMPLATE_RETURN_BOGUS_TYPE, POLICY_CONFIG_PRESENTATION, - REPLACE_RULECONFIG) + REPLACE_RULECONFIG, CORE_PREVENTION_RETURNS, + CORE_PREVENTION_UPDATE_1) logging.basicConfig(format='%(asctime)s %(levelname)s:%(message)s', level=logging.DEBUG, filename='log.txt') @@ -124,6 +126,12 @@ def test_rule_config_refresh(cbcsdk_mock): cbcsdk_mock.mock_request('GET', '/policyservice/v1/orgs/test/policies/65536', FULL_POLICY_1) api = cbcsdk_mock.api policy = Policy(api, 65536, FULL_POLICY_1, False, True) + # Replace all rule configs with the base class for purposes of this test + cfgs = policy._info.get("rule_configs", []) + ruleconfigobjects = [PolicyRuleConfig(api, policy, cfg['id'], cfg, force_init=False, full_doc=True) for cfg in cfgs] + policy._object_rule_configs = dict([(rconf.id, rconf) for rconf in ruleconfigobjects]) + policy._object_rule_configs_need_load = False + # proceed with test rule_config = random.choice(list(policy.object_rule_configs.values())) old_name = rule_config.name old_category = rule_config.category @@ -134,22 +142,10 @@ def test_rule_config_refresh(cbcsdk_mock): assert rule_config.parameters == old_parameters -@pytest.mark.parametrize("give_error, handler", [ - (False, does_not_raise()), - (True, pytest.raises(ServerError)) -]) -def test_rule_config_add_by_object(cbcsdk_mock, give_error, handler): - """Tests using a PolicyRuleConfig object to add a rule configuration.""" - def on_put(url, body, **kwargs): - assert body == FULL_POLICY_1 - if give_error: - return CBCSDKMock.StubResponse("Failure", scode=404) - rc = copy.deepcopy(body) - return rc - +def test_rule_config_add_base_not_implemented(cbcsdk_mock): + """Verifies that adding a new BaseRuleConfig is not implemented.""" 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', on_put) api = cbcsdk_mock.api policy_data = copy.deepcopy(FULL_POLICY_1) rule_config_data1 = [p for p in enumerate(policy_data['rule_configs']) @@ -157,145 +153,119 @@ def on_put(url, body, **kwargs): rule_config_data = rule_config_data1[0][1] del policy_data['rule_configs'][rule_config_data1[0][0]] policy = Policy(api, 65536, policy_data, False, True) - rule_config_count = len(policy.object_rule_configs) - new_rule_config = Policy._create_rule_config(api, policy, rule_config_data) + new_rule_config = PolicyRuleConfig(api, policy, '88b19232-7ebb-48ef-a198-2a75a282de5d', rule_config_data, + force_init=False, full_doc=True) new_rule_config.touch() - with handler: + with pytest.raises(NotImplementedError): new_rule_config.save() - assert len(policy.object_rule_configs) == rule_config_count + 1 - assert '88b19232-7ebb-48ef-a198-2a75a282de5d' in policy.object_rule_configs - -def test_rule_config_add_by_base_method(cbcsdk_mock): - """Tests using the base method on Policy to add a rule.""" - def on_put(url, body, **kwargs): - assert body == FULL_POLICY_1 - return copy.deepcopy(body) +def test_rule_config_delete_base_not_implemented(cbcsdk_mock): + """Verifies that deleting a BaseRuleConfig is not implemented.""" 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', on_put) api = cbcsdk_mock.api - policy_data = copy.deepcopy(FULL_POLICY_1) - rule_config_data1 = [p for p in enumerate(policy_data['rule_configs']) - if p[1]['id'] == '88b19232-7ebb-48ef-a198-2a75a282de5d'] - rule_config_data = rule_config_data1[0][1] - del policy_data['rule_configs'][rule_config_data1[0][0]] - policy = Policy(api, 65536, policy_data, False, True) - rule_config_count = len(policy.object_rule_configs) - policy.add_rule_config(rule_config_data) - assert len(policy.object_rule_configs) == rule_config_count + 1 - assert '88b19232-7ebb-48ef-a198-2a75a282de5d' in policy.object_rule_configs + policy = Policy(api, 65536, copy.deepcopy(FULL_POLICY_1), False, True) + # Replace all rule configs with the base class for purposes of this test + cfgs = policy._info.get("rule_configs", []) + ruleconfigobjects = [PolicyRuleConfig(api, policy, cfg['id'], cfg, force_init=False, full_doc=True) for cfg in cfgs] + policy._object_rule_configs = dict([(rconf.id, rconf) for rconf in ruleconfigobjects]) + policy._object_rule_configs_need_load = False + # proceed with test + with pytest.raises(NotImplementedError): + policy.delete_rule_config('88b19232-7ebb-48ef-a198-2a75a282de5d') -def test_rule_config_modify_by_object(cbcsdk_mock): - """Tests modifying a PolicyRuleConfig object within the policy.""" - def on_put(url, body, **kwargs): - nonlocal new_data - assert body == new_data - return copy.deepcopy(body) +def test_rule_config_modify_by_base_method_invalid_id(cb): + """Tests modifying a PolicyRuleConfig object using replace_rule_config, but with an invalid ID.""" + policy = Policy(cb, 65536, copy.deepcopy(FULL_POLICY_1), False, True) + with pytest.raises(ApiError): + policy.replace_rule_config('88b19266-7ebb-48ef-a198-2a75a282de5d', REPLACE_RULECONFIG) - 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', on_put) - api = cbcsdk_mock.api - new_data = copy.deepcopy(FULL_POLICY_1) - new_data['rule_configs'][3]['parameters']['WindowsAssignmentMode'] = 'REPORT' - policy = Policy(api, 65536, copy.deepcopy(FULL_POLICY_1), False, True) - rule_config_count = len(policy.object_rule_configs) - rule_config = policy.object_rule_configs['88b19232-7ebb-48ef-a198-2a75a282de5d'] - rule_config.set_parameter('WindowsAssignmentMode', 'REPORT') - rule_config.save() - assert len(policy.object_rule_configs) == rule_config_count - rule_config = policy.object_rule_configs['88b19232-7ebb-48ef-a198-2a75a282de5d'] - assert rule_config.get_parameter('WindowsAssignmentMode') == 'REPORT' +def test_rule_config_delete_by_base_method_nonexistent(cb): + """Tests what happens when you try to delete a nonexistent rule configuration via delete_rule_config.""" + policy = Policy(cb, 65536, copy.deepcopy(FULL_POLICY_1), False, True) + with pytest.raises(ApiError): + policy.delete_rule_config('88b19266-7ebb-48ef-a198-2a75a282de5d') -def test_rule_config_modify_by_base_method(cbcsdk_mock): - """Tests modifying a PolicyRuleConfig object using the replace_rule_config method.""" - def on_put(url, body, **kwargs): - nonlocal new_data - assert body == new_data - return copy.deepcopy(body) - 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', on_put) +def test_rule_config_initialization_matches_categories(cbcsdk_mock): + """Tests that rule configurations are initialized with the correct classes.""" + api = cbcsdk_mock.api + policy = Policy(api, 65536, FULL_POLICY_1, False, True) + for cfg in policy.object_rule_configs.values(): + if cfg.category == "core_prevention": + assert isinstance(cfg, CorePreventionRuleConfig) + else: + assert not isinstance(cfg, CorePreventionRuleConfig) + + +def test_core_prevention_refresh(cbcsdk_mock): + """Tests the refresh operation for a CorePreventionRuleConfig.""" + cbcsdk_mock.mock_request('GET', '/policyservice/v1/orgs/test/policies/65536/rule_configs/core_prevention', + CORE_PREVENTION_RETURNS) api = cbcsdk_mock.api - new_data = copy.deepcopy(FULL_POLICY_1) - new_data['rule_configs'][3]['parameters']['WindowsAssignmentMode'] = 'REPORT' policy = Policy(api, 65536, copy.deepcopy(FULL_POLICY_1), False, True) - rule_config_count = len(policy.object_rule_configs) - policy.replace_rule_config('88b19232-7ebb-48ef-a198-2a75a282de5d', REPLACE_RULECONFIG) - assert len(policy.object_rule_configs) == rule_config_count - rule_config = policy.object_rule_configs['88b19232-7ebb-48ef-a198-2a75a282de5d'] - assert rule_config.get_parameter('WindowsAssignmentMode') == 'REPORT' + rule_config = random.choice([cfg for cfg in policy.object_rule_configs.values() + if isinstance(cfg, CorePreventionRuleConfig)]) + rule_config.refresh() -def test_rule_config_modify_by_base_method_invalid_id(cb): - """Tests modifying a PolicyRuleConfig object using replace_rule_config, but with an invalid ID.""" +def test_core_prevention_set_assignment_mode(cb): + """Tests the assignment mode setting, which uses the underlying parameter setting.""" policy = Policy(cb, 65536, copy.deepcopy(FULL_POLICY_1), False, True) + rule_config = random.choice([cfg for cfg in policy.object_rule_configs.values() + if isinstance(cfg, CorePreventionRuleConfig)]) + old_mode = rule_config.get_assignment_mode() + assert not rule_config.is_dirty() + rule_config.set_assignment_mode(old_mode) + assert not rule_config.is_dirty() + rule_config.set_assignment_mode('BLOCK' if old_mode == 'REPORT' else 'REPORT') + assert rule_config.is_dirty() with pytest.raises(ApiError): - policy.replace_rule_config('88b19266-7ebb-48ef-a198-2a75a282de5d', REPLACE_RULECONFIG) + rule_config.set_assignment_mode('BOGUSVALUE') + +def test_core_prevention_update_and_save(cbcsdk_mock): + """Tests updating the core prevention data and saving it.""" + put_called = False -def test_rule_config_delete_by_object(cbcsdk_mock): - """Tests deleting a PolicyRuleConfig object from a policy.""" def on_put(url, body, **kwargs): - nonlocal new_data - assert body == new_data + nonlocal put_called + assert body == CORE_PREVENTION_UPDATE_1 + put_called = True return copy.deepcopy(body) 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', on_put) + cbcsdk_mock.mock_request('PUT', '/policyservice/v1/orgs/test/policies/65536/rule_configs/core_prevention', on_put) api = cbcsdk_mock.api - new_data = copy.deepcopy(FULL_POLICY_1) - rule_config_data1 = [p for p in enumerate(new_data['rule_configs']) - if p[1]['id'] == '88b19232-7ebb-48ef-a198-2a75a282de5d'] - del new_data['rule_configs'][rule_config_data1[0][0]] policy = Policy(api, 65536, copy.deepcopy(FULL_POLICY_1), False, True) - rule_config_count = len(policy.object_rule_configs) - rule_config = policy.object_rule_configs['88b19232-7ebb-48ef-a198-2a75a282de5d'] - assert not rule_config.is_deleted - rule_config.delete() - assert len(policy.object_rule_configs) == rule_config_count - 1 - assert '88b19232-7ebb-48ef-a198-2a75a282de5d' not in policy.object_rule_configs - assert rule_config.is_deleted + rule_config = policy.object_rule_configs['c4ed61b3-d5aa-41a9-814f-0f277451532b'] + assert rule_config.name == 'Carbon Black Threat Intel' + assert rule_config.get_assignment_mode() == 'REPORT' + rule_config.set_assignment_mode('BLOCK') + rule_config.save() + assert put_called -def test_rule_config_delete_by_base_method(cbcsdk_mock): - """Tests deleting a rule configuration from a policy via the delete_rule_config method.""" - def on_put(url, body, **kwargs): - nonlocal new_data - assert body == new_data - return copy.deepcopy(body) +def test_core_prevention_delete(cbcsdk_mock): + """Tests delete of a core prevention 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('PUT', '/policyservice/v1/orgs/test/policies/65536', on_put) + cbcsdk_mock.mock_request('DELETE', '/policyservice/v1/orgs/test/policies/65536/rule_configs/core_prevention' + + '/c4ed61b3-d5aa-41a9-814f-0f277451532b', on_delete) api = cbcsdk_mock.api - new_data = copy.deepcopy(FULL_POLICY_1) - rule_config_data1 = [p for p in enumerate(new_data['rule_configs']) - if p[1]['id'] == '88b19232-7ebb-48ef-a198-2a75a282de5d'] - del new_data['rule_configs'][rule_config_data1[0][0]] policy = Policy(api, 65536, copy.deepcopy(FULL_POLICY_1), False, True) - rule_config_count = len(policy.object_rule_configs) - policy.delete_rule_config('88b19232-7ebb-48ef-a198-2a75a282de5d') - assert len(policy.object_rule_configs) == rule_config_count - 1 - assert '88b19232-7ebb-48ef-a198-2a75a282de5d' not in policy.object_rule_configs - - -def test_rule_config_delete_by_base_method_nonexistent(cb): - """Tests what happens when you try to delete a nonexistent rule configuration via delete_rule_config.""" - policy = Policy(cb, 65536, copy.deepcopy(FULL_POLICY_1), False, True) - with pytest.raises(ApiError): - policy.delete_rule_config('88b19266-7ebb-48ef-a198-2a75a282de5d') - - -def test_rule_config_delete_is_new(cb): - """Tests that deleting a new PolicyRuleConfig raises an error.""" - policy = Policy(cb, 65536, copy.deepcopy(FULL_POLICY_1), False, True) - new_rule_config = PolicyRuleConfig(cb, policy, None, REPLACE_RULECONFIG, False, True) - with pytest.raises(ApiError): - new_rule_config.delete() + rule_config = policy.object_rule_configs['c4ed61b3-d5aa-41a9-814f-0f277451532b'] + assert rule_config.name == 'Carbon Black Threat Intel' + rule_config.delete() + assert delete_called From d47527d2f217b47a40c5d2883dfd4ac3e1b277b3 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Thu, 9 Mar 2023 16:24:13 -0700 Subject: [PATCH 092/143] coverage now 97% for policies, 95% for policy_ruleconfigs, also deflake8'd --- src/cbc_sdk/platform/policies.py | 39 ++++++------------- src/cbc_sdk/platform/policy_ruleconfigs.py | 19 +++------ .../unit/platform/test_policy_ruleconfigs.py | 27 ++++++++++++- 3 files changed, 43 insertions(+), 42 deletions(-) diff --git a/src/cbc_sdk/platform/policies.py b/src/cbc_sdk/platform/policies.py index 52b6a61d4..6fdf57eaf 100644 --- a/src/cbc_sdk/platform/policies.py +++ b/src/cbc_sdk/platform/policies.py @@ -780,17 +780,14 @@ def _on_updated_rule_config(self, rule_config): """ if rule_config._parent is not self: raise ApiError("internal error: updated rule configuration does not belong to this policy") - existed = rule_config.id in self.object_rule_configs + if rule_config.id not in self.object_rule_configs: + raise ApiError(f"internal error: unvalid rule configuration ID {rule_config.id}") self._object_rule_configs[rule_config.id] = rule_config raw_rule_configs = self._info.get("rule_configs", []) - if existed: - for index, raw_rule_config in enumerate(raw_rule_configs): - if raw_rule_config['id'] == rule_config.id: - raw_rule_configs[index] = copy.deepcopy(rule_config._info) - break - else: - raw_rule_configs.append(copy.deepcopy(rule_config._info)) - self._info['rule_configs'] = raw_rule_configs + location = [index for (index, item) in enumerate(raw_rule_configs) if item['id'] == rule_config.id] + if location: + raw_rule_configs[location[0]] = copy.deepcopy(rule_config._info) + self._info['rule_configs'] = raw_rule_configs def _on_deleted_rule_config(self, rule_config): """ @@ -801,12 +798,11 @@ def _on_deleted_rule_config(self, rule_config): """ if rule_config._parent is not self: raise ApiError("internal error: updated rule configuration does not belong to this policy") - if rule_config.id in self.object_rule_configs: - del self._object_rule_configs[rule_config.id] - else: - raise ApiError("internal error: updated rule configuration does not belong to this policy") - new_raw_rule_configs = [raw for raw in old_raw_rule_configs if raw['id'] != rule_config.id] - self._info['rule_configs'] = new_raw_rule_configs + rconfs = self._info.get("rule_configs", []) + location = [index for (index, item) in enumerate(rconfs) if item['id'] == rule_config.id] + if location: + rconfs[location[0]] = copy.deepcopy(rule_config._info) + self._info['rule_configs'] = rconfs def add_rule(self, new_rule): """Adds a rule to this Policy. @@ -887,17 +883,6 @@ def replace_rule(self, rule_id, new_rule): else: raise ApiError(f"rule #{rule_id} not found in policy") - def add_rule_config(self, new_rule_config): - """ - Adds a rule configuration to this policy. - - Args: - new_rule_config (dict): The new rule configuration to add to this Policy. - """ - new_obj = self._create_rule_config(self._cb, self, new_rule_config) - new_obj.touch() - new_obj.save() - def delete_rule_config(self, rule_config_id): """ Deletes a rule configuration from this Policy. @@ -931,7 +916,7 @@ def replace_rule_config(self, rule_config_id, new_rule_config): new_rule_config_info['id'] = rule_config_id saved_rule_config_info = old_rule_config._info old_rule_config._info = new_rule_config_info - old_rule_config.touch() + old_rule_config.touch(True) restore_rule_config = True try: old_rule_config.save() diff --git a/src/cbc_sdk/platform/policy_ruleconfigs.py b/src/cbc_sdk/platform/policy_ruleconfigs.py index 2881ec7a0..192a5700a 100644 --- a/src/cbc_sdk/platform/policy_ruleconfigs.py +++ b/src/cbc_sdk/platform/policy_ruleconfigs.py @@ -113,14 +113,8 @@ def _delete_object(self): Required Permissions: org.policies(DELETE) """ - was_deleted = False - try: - if self._delete_ruleconfig(): - self._parent._on_deleted_rule_config(self) - was_deleted = True - finally: - if was_deleted: - self._parent = None + self._delete_ruleconfig() + self._parent._on_deleted_rule_config(self) def get_parameter(self, name): """ @@ -185,11 +179,6 @@ def validate(self): raise ApiError(f"internal error: {e.message}", e) self._info['parameters'] = my_parameters - @property - def is_deleted(self): - """Returns True if this rule configuration object has been deleted.""" - return self._parent is None - class CorePreventionRuleConfig(PolicyRuleConfig): """ @@ -205,6 +194,8 @@ class CorePreventionRuleConfig(PolicyRuleConfig): permission. """ + 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. @@ -260,7 +251,7 @@ def _update_ruleconfig(self): def _delete_ruleconfig(self): """Perform the internal delete of the rule configuration object.""" self._cb.delete_object(self._base_url() + f"/{self.id}") - return False + self._info["parameters"] = copy.deepcopy({"WindowsAssignmentMode": "BLOCK"}) # mirror server side def get_assignment_mode(self): """ diff --git a/src/tests/unit/platform/test_policy_ruleconfigs.py b/src/tests/unit/platform/test_policy_ruleconfigs.py index ca9cd3467..f6c7ff5db 100644 --- a/src/tests/unit/platform/test_policy_ruleconfigs.py +++ b/src/tests/unit/platform/test_policy_ruleconfigs.py @@ -250,6 +250,31 @@ def on_put(url, body, **kwargs): assert put_called +def test_core_prevention_update_via_replace(cbcsdk_mock): + """Tests updating the core prevention data and saving it via replace_rule_config.""" + put_called = False + + def on_put(url, body, **kwargs): + nonlocal put_called + assert body == CORE_PREVENTION_UPDATE_1 + put_called = True + return copy.deepcopy(body) + + 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/core_prevention', on_put) + api = cbcsdk_mock.api + policy = Policy(api, 65536, copy.deepcopy(FULL_POLICY_1), False, True) + rule_config = policy.object_rule_configs['c4ed61b3-d5aa-41a9-814f-0f277451532b'] + assert rule_config.name == 'Carbon Black Threat Intel' + assert rule_config.get_assignment_mode() == 'REPORT' + new_data = copy.deepcopy(rule_config._info) + new_data["parameters"]["WindowsAssignmentMode"] = "BLOCK" + policy.replace_rule_config('c4ed61b3-d5aa-41a9-814f-0f277451532b', new_data) + assert put_called + assert rule_config.get_assignment_mode() == "BLOCK" + + def test_core_prevention_delete(cbcsdk_mock): """Tests delete of a core prevention data item.""" delete_called = False @@ -262,7 +287,7 @@ def on_delete(url, body): 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/core_prevention' - + '/c4ed61b3-d5aa-41a9-814f-0f277451532b', on_delete) + '/c4ed61b3-d5aa-41a9-814f-0f277451532b', on_delete) api = cbcsdk_mock.api policy = Policy(api, 65536, copy.deepcopy(FULL_POLICY_1), False, True) rule_config = policy.object_rule_configs['c4ed61b3-d5aa-41a9-814f-0f277451532b'] From 6142b446eecffce4f24b6a2afb98515a7eff5172 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Tue, 14 Mar 2023 14:37:00 -0600 Subject: [PATCH 093/143] added additional function to Policy to get all core prevention rule configs Also changed all policy_ruleconfigs tests that used random.choice() from a list to just test EVERY policy in the list --- src/cbc_sdk/platform/policies.py | 12 ++++++ .../unit/platform/test_policy_ruleconfigs.py | 41 +++++++++---------- 2 files changed, 31 insertions(+), 22 deletions(-) diff --git a/src/cbc_sdk/platform/policies.py b/src/cbc_sdk/platform/policies.py index 6fdf57eaf..b58dbac64 100644 --- a/src/cbc_sdk/platform/policies.py +++ b/src/cbc_sdk/platform/policies.py @@ -663,6 +663,18 @@ def object_rule_configs(self): self._object_rule_configs_need_load = False return self._object_rule_configs + @property + def core_prevention_rule_configs(self): + """ + Returns a dictionary of core prevention rule configuration IDs and objects for this Policy. + + Returns: + dict: A dictionary with core prevention rule configuration IDs as keys and CorePreventionRuleConfig objects + as values. + """ + return {key: rconf for (key, rconf) in self.object_rule_configs.items() + if isinstance(rconf, CorePreventionRuleConfig)} + def valid_rule_configs(self): """ Returns a dictionary identifying all valid rule configurations for this policy. diff --git a/src/tests/unit/platform/test_policy_ruleconfigs.py b/src/tests/unit/platform/test_policy_ruleconfigs.py index f6c7ff5db..6c675c932 100644 --- a/src/tests/unit/platform/test_policy_ruleconfigs.py +++ b/src/tests/unit/platform/test_policy_ruleconfigs.py @@ -14,7 +14,6 @@ import copy import pytest import logging -import random from contextlib import ExitStack as does_not_raise from cbc_sdk.rest_api import CBCloudAPI from cbc_sdk.platform import Policy, PolicyRuleConfig @@ -132,14 +131,14 @@ def test_rule_config_refresh(cbcsdk_mock): policy._object_rule_configs = dict([(rconf.id, rconf) for rconf in ruleconfigobjects]) policy._object_rule_configs_need_load = False # proceed with test - rule_config = random.choice(list(policy.object_rule_configs.values())) - old_name = rule_config.name - old_category = rule_config.category - old_parameters = rule_config.parameters - rule_config.refresh() - assert rule_config.name == old_name - assert rule_config.category == old_category - assert rule_config.parameters == old_parameters + for rule_config in policy.object_rule_configs.values(): + old_name = rule_config.name + old_category = rule_config.category + old_parameters = rule_config.parameters + rule_config.refresh() + assert rule_config.name == old_name + assert rule_config.category == old_category + assert rule_config.parameters == old_parameters def test_rule_config_add_base_not_implemented(cbcsdk_mock): @@ -207,24 +206,22 @@ def test_core_prevention_refresh(cbcsdk_mock): CORE_PREVENTION_RETURNS) api = cbcsdk_mock.api policy = Policy(api, 65536, copy.deepcopy(FULL_POLICY_1), False, True) - rule_config = random.choice([cfg for cfg in policy.object_rule_configs.values() - if isinstance(cfg, CorePreventionRuleConfig)]) - rule_config.refresh() + for rule_config in policy.core_prevention_rule_configs.values(): + rule_config.refresh() def test_core_prevention_set_assignment_mode(cb): """Tests the assignment mode setting, which uses the underlying parameter setting.""" policy = Policy(cb, 65536, copy.deepcopy(FULL_POLICY_1), False, True) - rule_config = random.choice([cfg for cfg in policy.object_rule_configs.values() - if isinstance(cfg, CorePreventionRuleConfig)]) - old_mode = rule_config.get_assignment_mode() - assert not rule_config.is_dirty() - rule_config.set_assignment_mode(old_mode) - assert not rule_config.is_dirty() - rule_config.set_assignment_mode('BLOCK' if old_mode == 'REPORT' else 'REPORT') - assert rule_config.is_dirty() - with pytest.raises(ApiError): - rule_config.set_assignment_mode('BOGUSVALUE') + for rule_config in policy.core_prevention_rule_configs.values(): + old_mode = rule_config.get_assignment_mode() + assert not rule_config.is_dirty() + rule_config.set_assignment_mode(old_mode) + assert not rule_config.is_dirty() + rule_config.set_assignment_mode('BLOCK' if old_mode == 'REPORT' else 'REPORT') + assert rule_config.is_dirty() + with pytest.raises(ApiError): + rule_config.set_assignment_mode('BOGUSVALUE') def test_core_prevention_update_and_save(cbcsdk_mock): From 78468d7e58c2c7965cc1d6bea79a5c5e99416139 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Wed, 15 Mar 2023 12:15:49 -0600 Subject: [PATCH 094/143] added policy fixture for tests --- .../unit/platform/test_policy_ruleconfigs.py | 57 ++++++++----------- 1 file changed, 23 insertions(+), 34 deletions(-) diff --git a/src/tests/unit/platform/test_policy_ruleconfigs.py b/src/tests/unit/platform/test_policy_ruleconfigs.py index 6c675c932..f24b9d59f 100644 --- a/src/tests/unit/platform/test_policy_ruleconfigs.py +++ b/src/tests/unit/platform/test_policy_ruleconfigs.py @@ -44,6 +44,12 @@ def cbcsdk_mock(monkeypatch, cb): return CBCSDKMock(monkeypatch, cb) +@pytest.fixture(scope="function") +def policy(cb): + """Mocks a sample policy for unit tests""" + return Policy(cb, 65536, copy.deepcopy(FULL_POLICY_1), False, True) + + # ==================================== UNIT TESTS BELOW ==================================== @pytest.mark.parametrize("initial_data, param_schema_return, handler, message", [ @@ -105,12 +111,10 @@ def param_schema(uri, query_params, default): "category": "core_prevention", "parameters": {"WindowsAssignmentMode": "BOGUSVALUE"}}, None, pytest.raises(InvalidObjectError), "parameter error: 'BOGUSVALUE' is not one of ['REPORT', 'BLOCK']"), ]) -def test_rule_config_validate_inside_policy(cbcsdk_mock, new_data, get_id, handler, message): +def test_rule_config_validate_inside_policy(cbcsdk_mock, policy, new_data, get_id, handler, message): """Tests rule configuration validation when it's part of a policy.""" - cbcsdk_mock.mock_request('GET', '/policyservice/v1/orgs/test/policies/65535/configs/presentation', + cbcsdk_mock.mock_request('GET', '/policyservice/v1/orgs/test/policies/65536/configs/presentation', POLICY_CONFIG_PRESENTATION) - api = cbcsdk_mock.api - policy = Policy(api, 65535, copy.deepcopy(FULL_POLICY_1), False, True) rule_config_id = get_id if get_id is not None else new_data['id'] rule_config = policy.object_rule_configs[rule_config_id] rule_config._info = copy.deepcopy(new_data) @@ -120,14 +124,13 @@ def test_rule_config_validate_inside_policy(cbcsdk_mock, new_data, get_id, handl assert h.value.args[0] == message -def test_rule_config_refresh(cbcsdk_mock): +def test_rule_config_refresh(cbcsdk_mock, policy): """Tests the rule config refresh() operation.""" cbcsdk_mock.mock_request('GET', '/policyservice/v1/orgs/test/policies/65536', FULL_POLICY_1) - api = cbcsdk_mock.api - policy = Policy(api, 65536, FULL_POLICY_1, False, True) # Replace all rule configs with the base class for purposes of this test cfgs = policy._info.get("rule_configs", []) - ruleconfigobjects = [PolicyRuleConfig(api, policy, cfg['id'], cfg, force_init=False, full_doc=True) for cfg in cfgs] + ruleconfigobjects = [PolicyRuleConfig(cbcsdk_mock.api, policy, cfg['id'], cfg, force_init=False, full_doc=True) + for cfg in cfgs] policy._object_rule_configs = dict([(rconf.id, rconf) for rconf in ruleconfigobjects]) policy._object_rule_configs_need_load = False # proceed with test @@ -151,7 +154,7 @@ def test_rule_config_add_base_not_implemented(cbcsdk_mock): if p[1]['id'] == '88b19232-7ebb-48ef-a198-2a75a282de5d'] rule_config_data = rule_config_data1[0][1] del policy_data['rule_configs'][rule_config_data1[0][0]] - policy = Policy(api, 65536, policy_data, False, True) + policy = Policy(api, 65536, policy_data, False, True) # this policy HAS to be created here because data was altered new_rule_config = PolicyRuleConfig(api, policy, '88b19232-7ebb-48ef-a198-2a75a282de5d', rule_config_data, force_init=False, full_doc=True) new_rule_config.touch() @@ -159,15 +162,14 @@ def test_rule_config_add_base_not_implemented(cbcsdk_mock): new_rule_config.save() -def test_rule_config_delete_base_not_implemented(cbcsdk_mock): +def test_rule_config_delete_base_not_implemented(cbcsdk_mock, policy): """Verifies that deleting a BaseRuleConfig is not implemented.""" cbcsdk_mock.mock_request('GET', '/policyservice/v1/orgs/test/policies/65536/configs/presentation', POLICY_CONFIG_PRESENTATION) - api = cbcsdk_mock.api - policy = Policy(api, 65536, copy.deepcopy(FULL_POLICY_1), False, True) # Replace all rule configs with the base class for purposes of this test cfgs = policy._info.get("rule_configs", []) - ruleconfigobjects = [PolicyRuleConfig(api, policy, cfg['id'], cfg, force_init=False, full_doc=True) for cfg in cfgs] + ruleconfigobjects = [PolicyRuleConfig(cbcsdk_mock.api, policy, cfg['id'], cfg, force_init=False, full_doc=True) + for cfg in cfgs] policy._object_rule_configs = dict([(rconf.id, rconf) for rconf in ruleconfigobjects]) policy._object_rule_configs_need_load = False # proceed with test @@ -175,24 +177,20 @@ def test_rule_config_delete_base_not_implemented(cbcsdk_mock): policy.delete_rule_config('88b19232-7ebb-48ef-a198-2a75a282de5d') -def test_rule_config_modify_by_base_method_invalid_id(cb): +def test_rule_config_modify_by_base_method_invalid_id(policy): """Tests modifying a PolicyRuleConfig object using replace_rule_config, but with an invalid ID.""" - policy = Policy(cb, 65536, copy.deepcopy(FULL_POLICY_1), False, True) with pytest.raises(ApiError): policy.replace_rule_config('88b19266-7ebb-48ef-a198-2a75a282de5d', REPLACE_RULECONFIG) -def test_rule_config_delete_by_base_method_nonexistent(cb): +def test_rule_config_delete_by_base_method_nonexistent(policy): """Tests what happens when you try to delete a nonexistent rule configuration via delete_rule_config.""" - policy = Policy(cb, 65536, copy.deepcopy(FULL_POLICY_1), False, True) with pytest.raises(ApiError): policy.delete_rule_config('88b19266-7ebb-48ef-a198-2a75a282de5d') -def test_rule_config_initialization_matches_categories(cbcsdk_mock): +def test_rule_config_initialization_matches_categories(policy): """Tests that rule configurations are initialized with the correct classes.""" - api = cbcsdk_mock.api - policy = Policy(api, 65536, FULL_POLICY_1, False, True) for cfg in policy.object_rule_configs.values(): if cfg.category == "core_prevention": assert isinstance(cfg, CorePreventionRuleConfig) @@ -200,19 +198,16 @@ def test_rule_config_initialization_matches_categories(cbcsdk_mock): assert not isinstance(cfg, CorePreventionRuleConfig) -def test_core_prevention_refresh(cbcsdk_mock): +def test_core_prevention_refresh(cbcsdk_mock, policy): """Tests the refresh operation for a CorePreventionRuleConfig.""" cbcsdk_mock.mock_request('GET', '/policyservice/v1/orgs/test/policies/65536/rule_configs/core_prevention', CORE_PREVENTION_RETURNS) - api = cbcsdk_mock.api - policy = Policy(api, 65536, copy.deepcopy(FULL_POLICY_1), False, True) for rule_config in policy.core_prevention_rule_configs.values(): rule_config.refresh() -def test_core_prevention_set_assignment_mode(cb): +def test_core_prevention_set_assignment_mode(policy): """Tests the assignment mode setting, which uses the underlying parameter setting.""" - policy = Policy(cb, 65536, copy.deepcopy(FULL_POLICY_1), False, True) for rule_config in policy.core_prevention_rule_configs.values(): old_mode = rule_config.get_assignment_mode() assert not rule_config.is_dirty() @@ -224,7 +219,7 @@ def test_core_prevention_set_assignment_mode(cb): rule_config.set_assignment_mode('BOGUSVALUE') -def test_core_prevention_update_and_save(cbcsdk_mock): +def test_core_prevention_update_and_save(cbcsdk_mock, policy): """Tests updating the core prevention data and saving it.""" put_called = False @@ -237,8 +232,6 @@ def on_put(url, body, **kwargs): 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/core_prevention', on_put) - api = cbcsdk_mock.api - policy = Policy(api, 65536, copy.deepcopy(FULL_POLICY_1), False, True) rule_config = policy.object_rule_configs['c4ed61b3-d5aa-41a9-814f-0f277451532b'] assert rule_config.name == 'Carbon Black Threat Intel' assert rule_config.get_assignment_mode() == 'REPORT' @@ -247,7 +240,7 @@ def on_put(url, body, **kwargs): assert put_called -def test_core_prevention_update_via_replace(cbcsdk_mock): +def test_core_prevention_update_via_replace(cbcsdk_mock, policy): """Tests updating the core prevention data and saving it via replace_rule_config.""" put_called = False @@ -260,8 +253,6 @@ def on_put(url, body, **kwargs): 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/core_prevention', on_put) - api = cbcsdk_mock.api - policy = Policy(api, 65536, copy.deepcopy(FULL_POLICY_1), False, True) rule_config = policy.object_rule_configs['c4ed61b3-d5aa-41a9-814f-0f277451532b'] assert rule_config.name == 'Carbon Black Threat Intel' assert rule_config.get_assignment_mode() == 'REPORT' @@ -272,7 +263,7 @@ def on_put(url, body, **kwargs): assert rule_config.get_assignment_mode() == "BLOCK" -def test_core_prevention_delete(cbcsdk_mock): +def test_core_prevention_delete(cbcsdk_mock, policy): """Tests delete of a core prevention data item.""" delete_called = False @@ -285,8 +276,6 @@ def on_delete(url, body): POLICY_CONFIG_PRESENTATION) cbcsdk_mock.mock_request('DELETE', '/policyservice/v1/orgs/test/policies/65536/rule_configs/core_prevention' '/c4ed61b3-d5aa-41a9-814f-0f277451532b', on_delete) - api = cbcsdk_mock.api - policy = Policy(api, 65536, copy.deepcopy(FULL_POLICY_1), False, True) rule_config = policy.object_rule_configs['c4ed61b3-d5aa-41a9-814f-0f277451532b'] assert rule_config.name == 'Carbon Black Threat Intel' rule_config.delete() From 73ee88ac51dbf8e76352c864d08fa48d6b3b4c28 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Wed, 15 Mar 2023 12:27:56 -0600 Subject: [PATCH 095/143] added two new helper property methods and modified tests to make use of them --- src/cbc_sdk/platform/policies.py | 20 +++++++++++++++++++ .../unit/platform/test_policy_ruleconfigs.py | 12 +++++------ 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/cbc_sdk/platform/policies.py b/src/cbc_sdk/platform/policies.py index b58dbac64..f984bfa55 100644 --- a/src/cbc_sdk/platform/policies.py +++ b/src/cbc_sdk/platform/policies.py @@ -663,6 +663,16 @@ def object_rule_configs(self): self._object_rule_configs_need_load = False return self._object_rule_configs + @property + def object_rule_configs_list(self): + """ + Returns a list of rule configuration objects for this Policy. + + Returns: + list: A list of PolicyRuleConfig objects. + """ + return [rconf for rconf in self.object_rule_configs.values()] + @property def core_prevention_rule_configs(self): """ @@ -675,6 +685,16 @@ def core_prevention_rule_configs(self): return {key: rconf for (key, rconf) in self.object_rule_configs.items() if isinstance(rconf, CorePreventionRuleConfig)} + @property + def core_prevention_rule_configs_list(self): + """ + Returns a list of core prevention rule configuration objects for this Policy. + + Returns: + list: A list of CorePreventionRuleConfig objects. + """ + return [rconf for rconf in self.object_rule_configs.values() if isinstance(rconf, CorePreventionRuleConfig)] + def valid_rule_configs(self): """ Returns a dictionary identifying all valid rule configurations for this policy. diff --git a/src/tests/unit/platform/test_policy_ruleconfigs.py b/src/tests/unit/platform/test_policy_ruleconfigs.py index f24b9d59f..08d92dbcd 100644 --- a/src/tests/unit/platform/test_policy_ruleconfigs.py +++ b/src/tests/unit/platform/test_policy_ruleconfigs.py @@ -134,7 +134,7 @@ def test_rule_config_refresh(cbcsdk_mock, policy): policy._object_rule_configs = dict([(rconf.id, rconf) for rconf in ruleconfigobjects]) policy._object_rule_configs_need_load = False # proceed with test - for rule_config in policy.object_rule_configs.values(): + for rule_config in policy.object_rule_configs_list: old_name = rule_config.name old_category = rule_config.category old_parameters = rule_config.parameters @@ -202,13 +202,13 @@ def test_core_prevention_refresh(cbcsdk_mock, policy): """Tests the refresh operation for a CorePreventionRuleConfig.""" cbcsdk_mock.mock_request('GET', '/policyservice/v1/orgs/test/policies/65536/rule_configs/core_prevention', CORE_PREVENTION_RETURNS) - for rule_config in policy.core_prevention_rule_configs.values(): + for rule_config in policy.core_prevention_rule_configs_list: rule_config.refresh() def test_core_prevention_set_assignment_mode(policy): """Tests the assignment mode setting, which uses the underlying parameter setting.""" - for rule_config in policy.core_prevention_rule_configs.values(): + for rule_config in policy.core_prevention_rule_configs_list: old_mode = rule_config.get_assignment_mode() assert not rule_config.is_dirty() rule_config.set_assignment_mode(old_mode) @@ -232,7 +232,7 @@ def on_put(url, body, **kwargs): 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/core_prevention', on_put) - rule_config = policy.object_rule_configs['c4ed61b3-d5aa-41a9-814f-0f277451532b'] + rule_config = policy.core_prevention_rule_configs['c4ed61b3-d5aa-41a9-814f-0f277451532b'] assert rule_config.name == 'Carbon Black Threat Intel' assert rule_config.get_assignment_mode() == 'REPORT' rule_config.set_assignment_mode('BLOCK') @@ -253,7 +253,7 @@ def on_put(url, body, **kwargs): 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/core_prevention', on_put) - rule_config = policy.object_rule_configs['c4ed61b3-d5aa-41a9-814f-0f277451532b'] + rule_config = policy.core_prevention_rule_configs['c4ed61b3-d5aa-41a9-814f-0f277451532b'] assert rule_config.name == 'Carbon Black Threat Intel' assert rule_config.get_assignment_mode() == 'REPORT' new_data = copy.deepcopy(rule_config._info) @@ -276,7 +276,7 @@ def on_delete(url, body): POLICY_CONFIG_PRESENTATION) cbcsdk_mock.mock_request('DELETE', '/policyservice/v1/orgs/test/policies/65536/rule_configs/core_prevention' '/c4ed61b3-d5aa-41a9-814f-0f277451532b', on_delete) - rule_config = policy.object_rule_configs['c4ed61b3-d5aa-41a9-814f-0f277451532b'] + rule_config = policy.core_prevention_rule_configs['c4ed61b3-d5aa-41a9-814f-0f277451532b'] assert rule_config.name == 'Carbon Black Threat Intel' rule_config.delete() assert delete_called From 886ab6971265381b45db0c650a8428f14c204368 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Thu, 16 Mar 2023 10:50:59 -0600 Subject: [PATCH 096/143] changed date to 2023 on all files I touched in this PR (thanks Hristo) --- src/cbc_sdk/platform/policies.py | 2 +- src/cbc_sdk/platform/policy_ruleconfigs.py | 2 +- src/tests/unit/platform/test_policies.py | 2 +- src/tests/unit/platform/test_policy_ruleconfigs.py | 2 +- src/tests/unit/test_base_api.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/cbc_sdk/platform/policies.py b/src/cbc_sdk/platform/policies.py index f984bfa55..5035ed131 100644 --- a/src/cbc_sdk/platform/policies.py +++ b/src/cbc_sdk/platform/policies.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/cbc_sdk/platform/policy_ruleconfigs.py b/src/cbc_sdk/platform/policy_ruleconfigs.py index 192a5700a..965bea166 100644 --- a/src/cbc_sdk/platform/policy_ruleconfigs.py +++ b/src/cbc_sdk/platform/policy_ruleconfigs.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/tests/unit/platform/test_policies.py b/src/tests/unit/platform/test_policies.py index c9788185b..48703ff1a 100644 --- a/src/tests/unit/platform/test_policies.py +++ b/src/tests/unit/platform/test_policies.py @@ -1,5 +1,5 @@ # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/tests/unit/platform/test_policy_ruleconfigs.py b/src/tests/unit/platform/test_policy_ruleconfigs.py index 08d92dbcd..548ede8a7 100644 --- a/src/tests/unit/platform/test_policy_ruleconfigs.py +++ b/src/tests/unit/platform/test_policy_ruleconfigs.py @@ -1,5 +1,5 @@ # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/tests/unit/test_base_api.py b/src/tests/unit/test_base_api.py index 5c90db70b..5508db0b3 100755 --- a/src/tests/unit/test_base_api.py +++ b/src/tests/unit/test_base_api.py @@ -1,5 +1,5 @@ # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * From 492a8a8c98f954a0d626ac89e848b8e9834fae51 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Thu, 16 Mar 2023 10:57:14 -0600 Subject: [PATCH 097/143] added doc import for policy_ruleconfigs --- docs/cbc_sdk.platform.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/cbc_sdk.platform.rst b/docs/cbc_sdk.platform.rst index 3ee9f3df9..1e58c174c 100644 --- a/docs/cbc_sdk.platform.rst +++ b/docs/cbc_sdk.platform.rst @@ -60,6 +60,14 @@ cbc\_sdk.platform.policies module :undoc-members: :show-inheritance: +cbc\_sdk.platform.policy_ruleconfigs module +------------------------------------------- + +.. automodule:: cbc_sdk.platform.policy_ruleconfigs + :members: + :undoc-members: + :show-inheritance: + cbc\_sdk.platform.processes module ---------------------------------- From b01f1342f965525b40dde0e9db4ec79d18b460f5 Mon Sep 17 00:00:00 2001 From: Kylie Ebringer Date: Mon, 20 Feb 2023 17:35:47 -0700 Subject: [PATCH 098/143] Work in progress to add dismiss-undismiss vulnerabilit to UAT --- src/tests/uat/vulnerability_assessment_uat.py | 127 ++++++++++++++++-- 1 file changed, 119 insertions(+), 8 deletions(-) diff --git a/src/tests/uat/vulnerability_assessment_uat.py b/src/tests/uat/vulnerability_assessment_uat.py index 3031e9a36..d62567f76 100644 --- a/src/tests/uat/vulnerability_assessment_uat.py +++ b/src/tests/uat/vulnerability_assessment_uat.py @@ -29,6 +29,17 @@ - Get a list of assets affected by a specific vulnerability CVE ID. - Get vulnerability details for a specific CVE ID. +- Test Dismiss and UnDismiss Vulnerability +This is implemented differently. Instead of comparing SDK to API results it +executes the following sequence: +1. Get the status of the vulnerability and verify it is ACTIVE +2. Set it to DISMISSED +3. Retrieve and verify status +4. Set it to UNDISMISSED +5. Verify new status +The flow has been verified to be effective and the system is +returned to its initial state. + """ # Standard library imports @@ -159,6 +170,87 @@ def get_affected_devices(cve_id=None, data={}): return requests.post(url, json=data, headers=HEADERS).json() +def dismiss_then_undismiss(cb=None, cve_id=None): + """ + Dismiss and undismiss a vulnerabilty. + + Fina a vulnerablity, dismiss it, verify the new state and then undismiss it, + returning the system to the original state. + """ + # 1. Get the status of the vulnerability and verify it is ACTIVE + vulnerability_list_orig = cb.select(Vulnerability).where(cve_id) + assert len(vulnerability_list_orig) > 0, 'Vulnerability not found. {}'.format(vulnerability_list_orig) + vulnerability_orig = vulnerability_list_orig.first() + assert not vulnerability_orig.get('dismissed'), 'Vulnerability is already dismissed. {}'.format(vulnerability_orig) + # 2. Set it to DISMISSED + vulnerability_orig.perform_action('DISMISS', 'OTHER', 'SDK Testing') + # 3. Retrieve and verify status + # Should be able to do visibility and query and check existence. Not the loop. + # vulnerability_list_dismissed = cb.select(Vulnerability).set_visibility('DISMISSED').where('CVE-2013-3900') + # vulnerability_dismissed = vulnerability_list_dismissed.first() + vulnerability_list_dismissed = cb.select(Vulnerability).set_visibility('DISMISSED') + # vulnerability_list_dismissed = cb.select(Vulnerability).where('CVE-2013-3900') + got_dismissed_match = False + vulnerability_dismissed = '' + for vuln in vulnerability_list_dismissed: + # print(vuln) + if vuln.get('dismissed'): + if vuln.vuln_info.get('cve_id') == 'CVE-2013-3900': + got_dismissed_match = True + vulnerability_dismissed = vuln + assert len(vulnerability_list_dismissed) > 0, 'Vulnerability not found after dismissal. {}'. \ + format(vulnerability_list_dismissed) + # assert vulnerability_dismissed.get('dismissed'), 'Vulnerability is already dismissed'. \ + # format(vulnerability_list_dismissed) + assert got_dismissed_match, 'Vulnerability not found after dismissal. {}'. \ + format(vulnerability_list_dismissed) + # 4. Set it to UNDISMISSED + vulnerability_dismissed.perform_action('UNDISMISS', 'OTHER', 'SDK Testing - reset to initial state') + # 5. Verify new status + # vulnerability_list_undismissed = cb.select(Vulnerability).set_visibility('ACTIVE').where(cve_id) + # vulnerability_undismissed = vulnerability_list_undismissed.first() + vulnerability_list_undismissed = cb.select(Vulnerability).where('CVE-2013-3900') + got_undismissed_match = False + for vuln in vulnerability_list_dismissed: + if not vuln.get('dismissed'): + if vuln.vuln_info.get('cve_id') == 'CVE-2013-3900': + got_undismissed_match = True + + assert len(vulnerability_list_undismissed) > 0, 'Vulnerability not found after un-dismissal. {}'. \ + format(vulnerability_list_undismissed) + # assert not vulnerability_undismissed.get('dismissed'), + # 'Vulnerability did not get undismissed. {}'.format(vulnerability_list_undismissed) + assert got_undismissed_match, 'Vulnerability did not get undismissed. {}'.format( + vulnerability_list_dismissed) + + +def just_undismiss(cb, cve_id): + """ + Undismiss the vulnerability that is passed it. + + TO DO: remove before finalizing this script. Only needed during debugging. + """ + # 3. Retrieve and verify status + # Should be able to do visibility and query and check existence. Not the loop. + # vulnerability_list_dismissed = cb.select(Vulnerability).set_visibility('DISMISSED').where('CVE-2013-3900') + # vulnerability_dismissed = vulnerability_list_dismissed.first() + vulnerability_list_dismissed = cb.select(Vulnerability).set_visibility('DISMISSED') + # vulnerability_list_dismissed = cb.select(Vulnerability).where('CVE-2013-3900') + got_dismissed_match = False + vulnerability_dismissed = '' + for vuln in vulnerability_list_dismissed: + if vuln.get('dismissed'): + if vuln.vuln_info.get('cve_id') == 'CVE-2013-3900': + got_dismissed_match = True + vulnerability_dismissed = vuln + assert len(vulnerability_list_dismissed) > 0, 'Vulnerability not found after dismissal. {}'. \ + format(vulnerability_list_dismissed) + assert got_dismissed_match, 'Vulnerability not found after dismissal. {}'. \ + format(vulnerability_list_dismissed) + # 4. Set it to UNDISMISSED + vulnerability_dismissed.perform_action('UNDISMISS', 'OTHER', 'SDK Testing - reset to initial state') + + def main(): """Script entry point""" global ORG_KEY @@ -174,6 +266,7 @@ def main(): HEADERS['X-Auth-Token'] = cb.credentials.token ORG_KEY = cb.credentials.org_key HOSTNAME = cb.credentials.url + CVE_ID = 'CVE-2013-3900' print() print(18 * ' ', 'Vulnerability Organization Level') @@ -184,6 +277,14 @@ def main(): assert api_results == sdk_results._info, 'Test Failed Expected: {} Actual: {}'.\ format(api_results, sdk_results) print('Get Vulnerability Summary...........................................OK') + + print('Starting Dismiss and Undissmiss a specific CVE......................OK') + + just_undismiss(cb, CVE_ID) + + dismiss_then_undismiss(cb, CVE_ID) + print('Completed Dismiss and Undissmiss a specific CVE.....................OK') + api_results = get_vulnerability_summary(severity='LOW') sdk_results = cb.select(Vulnerability.OrgSummary).set_severity('LOW').submit() assert api_results == sdk_results._info, 'Test Failed Expected: {} Actual: {}'.\ @@ -211,11 +312,16 @@ def main(): data = { 'criteria': {'severity': {'value': 'LOW', 'operator': 'EQUALS'}}, - 'sort': [{'field': 'highest_risk_score', 'order': 'DESC'}], + # TO DO: Put this back + # removing sort until bug CWP-14873 is resolved + # 'sort': [{'field': 'highest_risk_score', 'order': 'DESC'}], 'rows': 5 } api_results = search_vulnerabilities(data=data) - query = cb.select(Vulnerability).set_severity('LOW', 'EQUALS').sort_by('highest_risk_score', 'DESC') + # TO DO: Put this back. + # removing sort until bug CWP-14873 is resolved + # query = cb.select(Vulnerability).set_severity('LOW', 'EQUALS').sort_by('highest_risk_score', 'DESC') + query = cb.select(Vulnerability).set_severity('LOW', 'EQUALS') sdk_results = [x._info for x in query[:5]] assert api_results == sdk_results, 'Test Failed Expected: {} Actual: {}'.\ format(api_results, sdk_results) @@ -225,7 +331,11 @@ def main(): print(22 * ' ', 'Device Vulnerability Level') print(SYMBOLS * DELIMITER) - device = cb.select(Device).set_status(['ACTIVE']).first() + # TO DO: check with Ema how to do this properly. First active dev + # device = cb.select(Device).set_status(['ACTIVE']).first() + device = cb.select(Device).set_deployment_type(['WORKLOAD']).first() + # device_list = cb.select(Device).set_device_ids([17481251]) + DEVICE_ID = device.id api_results = get_specific_device_summary(device_id=DEVICE_ID) @@ -238,7 +348,8 @@ def main(): api_results = get_specific_device_list(device_id=DEVICE_ID) sdk_results = [x._info for x in query] - CVE_ID = sdk_results[0]["cve_id"] + # Removed, not used + # CVE_ID = sdk_results[0]["cve_id"] assert api_results == sdk_results, 'Test Failed Expected: {} Actual: {}'.\ format(api_results, sdk_results) @@ -257,8 +368,6 @@ def main(): print(25 * ' ', 'Vulnerability Level') print(SYMBOLS * DELIMITER) - CVE_ID = 'CVE-2008-5915' - api_results = get_vulnerability(cve_id=CVE_ID) vulnerability = None try: @@ -274,7 +383,7 @@ def main(): else: sdk_results = vulnerability._info - assert api_results == sdk_results, 'Test Failed Expected: {} Actual: {}'.format(api_results, sdk_results) + assert api_results[0] == sdk_results, 'Test Failed Expected: {} Actual: {}'.format(api_results, sdk_results) print('Get vulnerability details for a specific CVE ID.....................OK') api_response = get_affected_devices(cve_id=CVE_ID, data={'os_product_id': vulnerability.os_product_id})['results'] @@ -282,7 +391,9 @@ def main(): sdk_results = [] api_results = [] for x in query: - sdk_results.append({'device_id': x.id, 'type': x.deployment_type, 'name': x.vm_name, 'host_name': x.name}) + # sdk_results.append({'device_id': x.id, 'type': x.deployment_type, 'name': x.vm_name, 'host_name': x.name}) + # confirmed through postman that host_name is not populated + sdk_results.append({'device_id': x.id, 'type': x.deployment_type, 'name': x.name, 'host_name': x.name}) for x in api_response: api_results.append({'device_id': x['device_id'], 'type': x['type'], From cd72c8359d53803e009a68e0a84cea333ce9edf8 Mon Sep 17 00:00:00 2001 From: Kylie Ebringer Date: Mon, 27 Feb 2023 11:28:19 -0700 Subject: [PATCH 099/143] changed sort order to a valid field based on info from CWP-14873 --- src/tests/uat/vulnerability_assessment_uat.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/tests/uat/vulnerability_assessment_uat.py b/src/tests/uat/vulnerability_assessment_uat.py index d62567f76..503d3f564 100644 --- a/src/tests/uat/vulnerability_assessment_uat.py +++ b/src/tests/uat/vulnerability_assessment_uat.py @@ -170,6 +170,9 @@ def get_affected_devices(cve_id=None, data={}): return requests.post(url, json=data, headers=HEADERS).json() +""" Hide and Dismiss Vulnerability """ + + def dismiss_then_undismiss(cb=None, cve_id=None): """ Dismiss and undismiss a vulnerabilty. @@ -182,8 +185,10 @@ def dismiss_then_undismiss(cb=None, cve_id=None): assert len(vulnerability_list_orig) > 0, 'Vulnerability not found. {}'.format(vulnerability_list_orig) vulnerability_orig = vulnerability_list_orig.first() assert not vulnerability_orig.get('dismissed'), 'Vulnerability is already dismissed. {}'.format(vulnerability_orig) + # 2. Set it to DISMISSED vulnerability_orig.perform_action('DISMISS', 'OTHER', 'SDK Testing') + # 3. Retrieve and verify status # Should be able to do visibility and query and check existence. Not the loop. # vulnerability_list_dismissed = cb.select(Vulnerability).set_visibility('DISMISSED').where('CVE-2013-3900') @@ -312,15 +317,11 @@ def main(): data = { 'criteria': {'severity': {'value': 'LOW', 'operator': 'EQUALS'}}, - # TO DO: Put this back - # removing sort until bug CWP-14873 is resolved - # 'sort': [{'field': 'highest_risk_score', 'order': 'DESC'}], + 'sort': [{'field': 'risk_meter_score', 'order': 'DESC'}], 'rows': 5 } api_results = search_vulnerabilities(data=data) - # TO DO: Put this back. - # removing sort until bug CWP-14873 is resolved - # query = cb.select(Vulnerability).set_severity('LOW', 'EQUALS').sort_by('highest_risk_score', 'DESC') + query = cb.select(Vulnerability).set_severity('LOW', 'EQUALS').sort_by('risk_meter_score', 'DESC') query = cb.select(Vulnerability).set_severity('LOW', 'EQUALS') sdk_results = [x._info for x in query[:5]] assert api_results == sdk_results, 'Test Failed Expected: {} Actual: {}'.\ From b57c45d5be803acc2c416b4d47c6bcaee136841e Mon Sep 17 00:00:00 2001 From: Kylie Ebringer Date: Thu, 9 Mar 2023 13:08:56 -0700 Subject: [PATCH 100/143] Vulnerability Assessment UAT and an update to the implementation Remove "ALL" - does not behave as expected. --- .../platform/vulnerability_assessment.py | 15 ++- src/tests/uat/vulnerability_assessment_uat.py | 119 +++++++----------- 2 files changed, 57 insertions(+), 77 deletions(-) diff --git a/src/cbc_sdk/platform/vulnerability_assessment.py b/src/cbc_sdk/platform/vulnerability_assessment.py index 26c1e7c33..6cdd51400 100644 --- a/src/cbc_sdk/platform/vulnerability_assessment.py +++ b/src/cbc_sdk/platform/vulnerability_assessment.py @@ -146,7 +146,12 @@ def perform_action(self, type, reason=None, notes=None): else: raise ApiError(f"Vulnerability action: {type} not supported") - return self._cb.post_object(url, body=request).json() + response = self._cb.post_object(url, body=request).json() + results = response["results"] + if len(results) > 0: + self._info["rule_id"] = results[0]["rule_id"] + + return response class AssetView(list): """Represents a list of Vulnerability for an organization.""" @@ -224,7 +229,7 @@ class VulnerabilityOrgSummaryQuery(BaseQuery): """Represents a query that is used fetch the VulnerabiltitySummary""" VALID_SEVERITY = ["CRITICAL", "IMPORTANT", "MODERATE", "LOW"] - VALID_VISIBILITY = ["DISMISSED", "ACTIVE", "ALL"] + VALID_VISIBILITY = ["DISMISSED", "ACTIVE"] def __init__(self, doc_class, cb, device=None): """ @@ -262,7 +267,7 @@ def set_visibility(self, visibility): Restricts the vulnerabilities that this query is performed on to the specified visibility Args: - visibility (str): The visibility state of the vulnerabilty. (supports ACTIVE, ALL, DISMISSED) + visibility (str): The visibility state of the vulnerabilty. (supports ACTIVE, DISMISSED) Returns: VulnerabilityOrgSummaryQuery: This instance. @@ -332,7 +337,7 @@ class VulnerabilityQuery(BaseQuery, QueryBuilderSupportMixin, "NOT_SUPPORTED", "CANCELLED", "IN_PROGRESS", "ACTIVE", "COMPLETED"] VALID_DIRECTIONS = ["ASC", "DESC"] - VALID_VISIBILITY = ["DISMISSED", "ACTIVE", "ALL"] + VALID_VISIBILITY = ["DISMISSED", "ACTIVE"] def __init__(self, doc_class, cb, device=None): """ @@ -376,7 +381,7 @@ def set_visibility(self, visibility): Restricts the vulnerabilities that this query is performed on to the specified visibility Args: - visibility (str): The visibility state of the vulnerabilty. (supports ACTIVE, ALL, DISMISSED) + visibility (str): The visibility state of the vulnerabilty. (supports ACTIVE, DISMISSED) Returns: VulnerabilityQuery: This instance. diff --git a/src/tests/uat/vulnerability_assessment_uat.py b/src/tests/uat/vulnerability_assessment_uat.py index 503d3f564..26ff76ea8 100644 --- a/src/tests/uat/vulnerability_assessment_uat.py +++ b/src/tests/uat/vulnerability_assessment_uat.py @@ -45,6 +45,7 @@ # Standard library imports import sys import requests +import time # Internal library imports from cbc_sdk.helpers import build_cli_parser, get_cb_cloud_object @@ -180,80 +181,56 @@ def dismiss_then_undismiss(cb=None, cve_id=None): Fina a vulnerablity, dismiss it, verify the new state and then undismiss it, returning the system to the original state. """ - # 1. Get the status of the vulnerability and verify it is ACTIVE - vulnerability_list_orig = cb.select(Vulnerability).where(cve_id) - assert len(vulnerability_list_orig) > 0, 'Vulnerability not found. {}'.format(vulnerability_list_orig) - vulnerability_orig = vulnerability_list_orig.first() - assert not vulnerability_orig.get('dismissed'), 'Vulnerability is already dismissed. {}'.format(vulnerability_orig) + OS_PRODUCT_ID = "292_21728" + # 1. Get the vulnerability, searching by CVE_ID and visibility is ACTIVE. + vulnerability_query = cb.select(Vulnerability).set_visibility('ACTIVE').\ + add_criteria("cve_id", "CVE-2014-4199").\ + add_criteria("os_product_id", "292_21728") + vulnerability = vulnerability_query.first() + vulnerability_list = list(vulnerability_query) + + print("printing original vulnerability item") + print(vulnerability) + assert len(vulnerability_list) > 0, \ + 'Vulnerability not found. List contained {} items for CVE ID {} and OS Product ID {}'. \ + format(len(vulnerability_list), cve_id, OS_PRODUCT_ID) # 2. Set it to DISMISSED - vulnerability_orig.perform_action('DISMISS', 'OTHER', 'SDK Testing') - + vulnerability.perform_action('DISMISS', 'OTHER', 'SDK Testing') + # need to keep this instance as it has the rule id. Waiting for change for search to include rule_id + dismissed_vulnerability = vulnerability + # dismissal_rule_id = # need to get this for later + # takes time for dismissing and undismissing to show up. + time.sleep(20) # 3. Retrieve and verify status - # Should be able to do visibility and query and check existence. Not the loop. - # vulnerability_list_dismissed = cb.select(Vulnerability).set_visibility('DISMISSED').where('CVE-2013-3900') - # vulnerability_dismissed = vulnerability_list_dismissed.first() - vulnerability_list_dismissed = cb.select(Vulnerability).set_visibility('DISMISSED') - # vulnerability_list_dismissed = cb.select(Vulnerability).where('CVE-2013-3900') - got_dismissed_match = False - vulnerability_dismissed = '' - for vuln in vulnerability_list_dismissed: - # print(vuln) - if vuln.get('dismissed'): - if vuln.vuln_info.get('cve_id') == 'CVE-2013-3900': - got_dismissed_match = True - vulnerability_dismissed = vuln - assert len(vulnerability_list_dismissed) > 0, 'Vulnerability not found after dismissal. {}'. \ - format(vulnerability_list_dismissed) - # assert vulnerability_dismissed.get('dismissed'), 'Vulnerability is already dismissed'. \ - # format(vulnerability_list_dismissed) - assert got_dismissed_match, 'Vulnerability not found after dismissal. {}'. \ - format(vulnerability_list_dismissed) + vulnerability_query = cb.select(Vulnerability).set_visibility('DISMISSED'). \ + add_criteria("cve_id", "CVE-2014-4199"). \ + add_criteria("os_product_id", "292_21728") + + vulnerability = vulnerability_query.first() + + vulnerability_list = list(vulnerability_query) + + print("printing DISMISSED vulnerability item") + print(vulnerability) + assert len(vulnerability_list) > 0, \ + 'Vulnerability not found after dismissing. List contained {} items for CVE ID {} and OS Product ID {}'. \ + format(len(vulnerability_list), cve_id, OS_PRODUCT_ID) + # 4. Set it to UNDISMISSED - vulnerability_dismissed.perform_action('UNDISMISS', 'OTHER', 'SDK Testing - reset to initial state') + dismissed_vulnerability.perform_action('UNDISMISS', 'OTHER', 'SDK Testing - reset to initial state') + # 5. Verify new status - # vulnerability_list_undismissed = cb.select(Vulnerability).set_visibility('ACTIVE').where(cve_id) - # vulnerability_undismissed = vulnerability_list_undismissed.first() - vulnerability_list_undismissed = cb.select(Vulnerability).where('CVE-2013-3900') - got_undismissed_match = False - for vuln in vulnerability_list_dismissed: - if not vuln.get('dismissed'): - if vuln.vuln_info.get('cve_id') == 'CVE-2013-3900': - got_undismissed_match = True - - assert len(vulnerability_list_undismissed) > 0, 'Vulnerability not found after un-dismissal. {}'. \ - format(vulnerability_list_undismissed) - # assert not vulnerability_undismissed.get('dismissed'), - # 'Vulnerability did not get undismissed. {}'.format(vulnerability_list_undismissed) - assert got_undismissed_match, 'Vulnerability did not get undismissed. {}'.format( - vulnerability_list_dismissed) - - -def just_undismiss(cb, cve_id): - """ - Undismiss the vulnerability that is passed it. + vulnerability_query = cb.select(Vulnerability).set_visibility('ACTIVE'). \ + add_criteria("cve_id", "CVE-2014-4199"). \ + add_criteria("os_product_id", "292_21728") + vulnerability = vulnerability_query.first() - TO DO: remove before finalizing this script. Only needed during debugging. - """ - # 3. Retrieve and verify status - # Should be able to do visibility and query and check existence. Not the loop. - # vulnerability_list_dismissed = cb.select(Vulnerability).set_visibility('DISMISSED').where('CVE-2013-3900') - # vulnerability_dismissed = vulnerability_list_dismissed.first() - vulnerability_list_dismissed = cb.select(Vulnerability).set_visibility('DISMISSED') - # vulnerability_list_dismissed = cb.select(Vulnerability).where('CVE-2013-3900') - got_dismissed_match = False - vulnerability_dismissed = '' - for vuln in vulnerability_list_dismissed: - if vuln.get('dismissed'): - if vuln.vuln_info.get('cve_id') == 'CVE-2013-3900': - got_dismissed_match = True - vulnerability_dismissed = vuln - assert len(vulnerability_list_dismissed) > 0, 'Vulnerability not found after dismissal. {}'. \ - format(vulnerability_list_dismissed) - assert got_dismissed_match, 'Vulnerability not found after dismissal. {}'. \ - format(vulnerability_list_dismissed) - # 4. Set it to UNDISMISSED - vulnerability_dismissed.perform_action('UNDISMISS', 'OTHER', 'SDK Testing - reset to initial state') + print("printing UNDISMISSED vulnerability item") + print(vulnerability) + assert len(vulnerability_list) > 0, \ + 'Vulnerability not found after undismissing. List contained {} items for CVE ID {} and OS Product ID {}'. \ + format(len(vulnerability_list), cve_id, OS_PRODUCT_ID) def main(): @@ -271,7 +248,7 @@ def main(): HEADERS['X-Auth-Token'] = cb.credentials.token ORG_KEY = cb.credentials.org_key HOSTNAME = cb.credentials.url - CVE_ID = 'CVE-2013-3900' + CVE_ID = 'CVE-2014-4199' print() print(18 * ' ', 'Vulnerability Organization Level') @@ -285,8 +262,6 @@ def main(): print('Starting Dismiss and Undissmiss a specific CVE......................OK') - just_undismiss(cb, CVE_ID) - dismiss_then_undismiss(cb, CVE_ID) print('Completed Dismiss and Undissmiss a specific CVE.....................OK') @@ -322,7 +297,7 @@ def main(): } api_results = search_vulnerabilities(data=data) query = cb.select(Vulnerability).set_severity('LOW', 'EQUALS').sort_by('risk_meter_score', 'DESC') - query = cb.select(Vulnerability).set_severity('LOW', 'EQUALS') + # query = cb.select(Vulnerability).set_severity('LOW', 'EQUALS') sdk_results = [x._info for x in query[:5]] assert api_results == sdk_results, 'Test Failed Expected: {} Actual: {}'.\ format(api_results, sdk_results) From e423adba1543a898a76e53ae237d6cefcbaa4519 Mon Sep 17 00:00:00 2001 From: Kylie Ebringer Date: Thu, 9 Mar 2023 14:18:56 -0700 Subject: [PATCH 101/143] removed redundant code. --- src/tests/uat/vulnerability_assessment_uat.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/tests/uat/vulnerability_assessment_uat.py b/src/tests/uat/vulnerability_assessment_uat.py index 26ff76ea8..81fb5d8f1 100644 --- a/src/tests/uat/vulnerability_assessment_uat.py +++ b/src/tests/uat/vulnerability_assessment_uat.py @@ -297,7 +297,6 @@ def main(): } api_results = search_vulnerabilities(data=data) query = cb.select(Vulnerability).set_severity('LOW', 'EQUALS').sort_by('risk_meter_score', 'DESC') - # query = cb.select(Vulnerability).set_severity('LOW', 'EQUALS') sdk_results = [x._info for x in query[:5]] assert api_results == sdk_results, 'Test Failed Expected: {} Actual: {}'.\ format(api_results, sdk_results) From ebd31e94d6301c817f768f4de6857d11d53a8ad7 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Fri, 17 Mar 2023 13:09:46 -0600 Subject: [PATCH 102/143] added copy.deepcopy() around GET_VULNERABILITY_RESP because tests clobbered it --- .../platform/test_vulnerability_assessment.py | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/src/tests/unit/platform/test_vulnerability_assessment.py b/src/tests/unit/platform/test_vulnerability_assessment.py index 2c2bcbf36..ec2f23ad6 100644 --- a/src/tests/unit/platform/test_vulnerability_assessment.py +++ b/src/tests/unit/platform/test_vulnerability_assessment.py @@ -12,6 +12,7 @@ # * NON-INFRINGEMENT AND FITNESS FOR A PARTICULAR PURPOSE. """Unit test code for Vulnerability Assessment""" +import copy import pytest import logging @@ -195,11 +196,11 @@ def test_get_vulnerability_by_id(cbcsdk_mock): """Tests a get vulnerabilty by cve_id.""" cbcsdk_mock.mock_request("POST", "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search", - GET_VULNERABILITY_RESP) + copy.deepcopy(GET_VULNERABILITY_RESP)) cbcsdk_mock.mock_request("POST", "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search" "?dataForExport=true", - GET_VULNERABILITY_RESP) + copy.deepcopy(GET_VULNERABILITY_RESP)) api = cbcsdk_mock.api vulnerability = Vulnerability(api, "CVE-2014-4650") assert vulnerability._model_unique_id == "CVE-2014-4650" @@ -408,11 +409,11 @@ def post_validate(url, body, **kwargs): return GET_AFFECTED_ASSETS_SPECIFIC_VULNERABILITY cbcsdk_mock.mock_request("POST", "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search", - GET_VULNERABILITY_RESP) + copy.deepcopy(GET_VULNERABILITY_RESP)) cbcsdk_mock.mock_request("POST", "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search" "?dataForExport=true", - GET_VULNERABILITY_RESP) + copy.deepcopy(GET_VULNERABILITY_RESP)) cbcsdk_mock.mock_request("POST", "/vulnerability/assessment/api/v1/orgs/test/vulnerabilities/CVE-2014-4650/devices", post_validate) @@ -434,11 +435,11 @@ def post_validate(url, body, **kwargs): return GET_AFFECTED_ASSETS_SPECIFIC_VULNERABILITY cbcsdk_mock.mock_request("POST", "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search", - GET_VULNERABILITY_RESP) + copy.deepcopy(GET_VULNERABILITY_RESP)) cbcsdk_mock.mock_request("POST", "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search" "?dataForExport=true", - GET_VULNERABILITY_RESP) + copy.deepcopy(GET_VULNERABILITY_RESP)) cbcsdk_mock.mock_request("POST", "/vulnerability/assessment/api/v1/orgs/test/vcenters/testvcenter/vulnerabilities/CVE-2014-4650/devices", # noqa: E501 post_validate) @@ -536,11 +537,11 @@ def test_dismiss_vulnerability(cbcsdk_mock): """Test dismiss vulnerability""" cbcsdk_mock.mock_request("POST", "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search", - GET_VULNERABILITY_RESP) + copy.deepcopy(GET_VULNERABILITY_RESP)) cbcsdk_mock.mock_request("POST", "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search" "?dataForExport=true", - GET_VULNERABILITY_RESP) + copy.deepcopy(GET_VULNERABILITY_RESP)) def post_action(url, body, **kwargs): assert body["criteria"]["os_product_id"]["value"] == GET_VULNERABILITY_RESP["results"][0]["os_product_id"] @@ -574,7 +575,7 @@ def test_dismiss_edit_vulnerability(cbcsdk_mock): """Test editting an already dismissed vulnerabilty""" cbcsdk_mock.mock_request("POST", "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search", - GET_VULNERABILITY_RESP) + copy.deepcopy(GET_VULNERABILITY_RESP)) cbcsdk_mock.mock_request("POST", "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search" "?dataForExport=true", @@ -614,7 +615,7 @@ def test_undismiss_vulnerability(cbcsdk_mock): """Test undismissing a dismissed vulnerability""" cbcsdk_mock.mock_request("POST", "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search", - GET_VULNERABILITY_RESP) + copy.deepcopy(GET_VULNERABILITY_RESP)) cbcsdk_mock.mock_request("POST", "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search" "?dataForExport=true", @@ -654,11 +655,11 @@ def test_undismiss_vulnerability_not_dismissed(cbcsdk_mock): """Test undismiss a vulnerability which has not be dismissed""" cbcsdk_mock.mock_request("POST", "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search", - GET_VULNERABILITY_RESP) + copy.deepcopy(GET_VULNERABILITY_RESP)) cbcsdk_mock.mock_request("POST", "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search" "?dataForExport=true", - GET_VULNERABILITY_RESP) + copy.deepcopy(GET_VULNERABILITY_RESP)) api = cbcsdk_mock.api @@ -671,11 +672,11 @@ def test_dismiss_other_vulnerability_no_notes(cbcsdk_mock): """Test dismiss vulnerability with reason OTHER and no notes""" cbcsdk_mock.mock_request("POST", "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search", - GET_VULNERABILITY_RESP) + copy.deepcopy(GET_VULNERABILITY_RESP)) cbcsdk_mock.mock_request("POST", "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search" "?dataForExport=true", - GET_VULNERABILITY_RESP) + copy.deepcopy(GET_VULNERABILITY_RESP)) api = cbcsdk_mock.api @@ -688,11 +689,11 @@ def test_invalid_action_vulnerability(cbcsdk_mock): """Test performing an invalid action on a vulnerability""" cbcsdk_mock.mock_request("POST", "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search", - GET_VULNERABILITY_RESP) + copy.deepcopy(GET_VULNERABILITY_RESP)) cbcsdk_mock.mock_request("POST", "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search" "?dataForExport=true", - GET_VULNERABILITY_RESP) + copy.deepcopy(GET_VULNERABILITY_RESP)) api = cbcsdk_mock.api @@ -706,7 +707,7 @@ def test_vulernability_visibility(cbcsdk_mock): cbcsdk_mock.mock_request("POST", "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search" "?dataForExport=true&vulnerabilityVisibility=ACTIVE", - GET_VULNERABILITY_RESP) + copy.deepcopy(GET_VULNERABILITY_RESP)) api = cbcsdk_mock.api From 4b0883bdbf63be50fc7e07d53bdecc7b1279e7ce Mon Sep 17 00:00:00 2001 From: Kylie Ebringer Date: Thu, 16 Mar 2023 21:11:40 -0600 Subject: [PATCH 103/143] Updated copyright to 2023. --- LICENSE | 2 +- bin/cbc-sdk-help.py | 2 +- bin/set-macos-keychain.py | 2 +- bin/set-windows-registry.py | 2 +- docs/conf.py | 2 +- examples/audit_remediation/manage_run.py | 2 +- examples/endpoint_standard/enriched_events_query.py | 2 +- examples/endpoint_standard/policy_operations.py | 2 +- examples/enterprise_edr/feed_operations.py | 2 +- examples/live_response/DellBiosVerification/BiosVerification.py | 2 +- examples/live_response/examplejob.py | 2 +- examples/live_response/jobrunner.py | 2 +- examples/live_response/live_response_cli.py | 2 +- examples/platform/change_role.py | 2 +- examples/platform/container_runtime_alerts.py | 2 +- examples/platform/create_user.py | 2 +- examples/platform/device_actions.py | 2 +- examples/platform/device_processes.py | 2 +- examples/platform/find_users_with_grants.py | 2 +- examples/platform/list_devices.py | 2 +- examples/platform/list_permitted_roles.py | 2 +- examples/platform/policy_service_crud_operations.py | 2 +- examples/platform/set_user_enable.py | 2 +- examples/workload/workloads_search_example.py | 2 +- src/cbc_sdk/__init__.py | 2 +- src/cbc_sdk/audit_remediation/base.py | 2 +- src/cbc_sdk/audit_remediation/differential.py | 2 +- src/cbc_sdk/base.py | 2 +- src/cbc_sdk/cache/lru.py | 2 +- src/cbc_sdk/connection.py | 2 +- src/cbc_sdk/credential_providers/aws_sm_credential_provider.py | 2 +- src/cbc_sdk/credential_providers/default.py | 2 +- src/cbc_sdk/credential_providers/environ_credential_provider.py | 2 +- src/cbc_sdk/credential_providers/file_credential_provider.py | 2 +- .../credential_providers/keychain_credential_provider.py | 2 +- .../credential_providers/registry_credential_provider.py | 2 +- src/cbc_sdk/credentials.py | 2 +- src/cbc_sdk/endpoint_standard/base.py | 2 +- src/cbc_sdk/endpoint_standard/recommendation.py | 2 +- src/cbc_sdk/endpoint_standard/usb_device_control.py | 2 +- src/cbc_sdk/enterprise_edr/threat_intelligence.py | 2 +- src/cbc_sdk/enterprise_edr/ubs.py | 2 +- src/cbc_sdk/errors.py | 2 +- src/cbc_sdk/helpers.py | 2 +- src/cbc_sdk/live_response_api.py | 2 +- src/cbc_sdk/platform/alerts.py | 2 +- src/cbc_sdk/platform/base.py | 2 +- src/cbc_sdk/platform/devices.py | 2 +- src/cbc_sdk/platform/events.py | 2 +- src/cbc_sdk/platform/grants.py | 2 +- src/cbc_sdk/platform/jobs.py | 2 +- src/cbc_sdk/platform/processes.py | 2 +- src/cbc_sdk/platform/reputation.py | 2 +- src/cbc_sdk/platform/users.py | 2 +- src/cbc_sdk/platform/vulnerability_assessment.py | 2 +- src/cbc_sdk/rest_api.py | 2 +- src/cbc_sdk/utils.py | 2 +- src/cbc_sdk/winerror.py | 2 +- src/cbc_sdk/workload/nsx_remediation.py | 2 +- src/cbc_sdk/workload/sensor_lifecycle.py | 2 +- src/cbc_sdk/workload/vm_workloads_search.py | 2 +- src/tests/uat/alerts_uat.py | 2 +- src/tests/uat/csp_oauth.py | 2 +- src/tests/uat/device_control_uat.py | 2 +- src/tests/uat/differential_analysis_uat.py | 2 +- src/tests/uat/enriched_events_uat.py | 2 +- src/tests/uat/live_response_api_async_uat.py | 2 +- src/tests/uat/live_response_api_uat.py | 2 +- src/tests/uat/nsx_remediation_uat.py | 2 +- src/tests/uat/platform_devices_uat.py | 2 +- src/tests/uat/policy_uat.py | 2 +- src/tests/uat/process_search_calls.py | 2 +- src/tests/uat/proxy_test.py | 2 +- src/tests/uat/recommendation_uat.py | 2 +- src/tests/uat/reputation_override_uat.py | 2 +- src/tests/uat/sensor_lifecycle_uat.py | 2 +- src/tests/uat/vulnerability_assessment_uat.py | 2 +- src/tests/uat/watchlist_feed_uat.py | 2 +- src/tests/uat/workloads_search_uat.py | 2 +- src/tests/unit/audit_remediation/test_differential.py | 2 +- src/tests/unit/conftest.py | 2 +- src/tests/unit/credential_providers/test_aws_secrets_manager.py | 2 +- src/tests/unit/credential_providers/test_default.py | 2 +- src/tests/unit/credential_providers/test_environ.py | 2 +- src/tests/unit/credential_providers/test_file.py | 2 +- src/tests/unit/credential_providers/test_keychain.py | 2 +- src/tests/unit/credential_providers/test_registry.py | 2 +- src/tests/unit/endpoint_standard/test_recommendation.py | 2 +- src/tests/unit/endpoint_standard/test_usb_device.py | 2 +- src/tests/unit/endpoint_standard/test_usb_device_approval.py | 2 +- src/tests/unit/endpoint_standard/test_usb_device_block.py | 2 +- src/tests/unit/fixtures/CBCSDKMock.py | 2 +- src/tests/unit/fixtures/mock_credentials.py | 2 +- src/tests/unit/fixtures/mock_rest_api.py | 2 +- src/tests/unit/platform/test_alertsv6_api.py | 2 +- src/tests/unit/platform/test_devicev6_api.py | 2 +- src/tests/unit/platform/test_platform_models.py | 2 +- src/tests/unit/platform/test_reputation_overrides.py | 2 +- src/tests/unit/platform/test_vulnerability_assessment.py | 2 +- src/tests/unit/test_connection.py | 2 +- src/tests/unit/test_credentials.py | 2 +- src/tests/unit/test_helpers.py | 2 +- src/tests/unit/test_live_response_api.py | 2 +- src/tests/unit/test_rest_api.py | 2 +- src/tests/unit/test_utils.py | 2 +- src/tests/unit/workload/test_nsx_remediation.py | 2 +- src/tests/unit/workload/test_search.py | 2 +- src/tests/unit/workload/test_sensor_lifecycle.py | 2 +- 108 files changed, 108 insertions(+), 108 deletions(-) diff --git a/LICENSE b/LICENSE index 9609965ee..d0c2f466a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020-2022 VMware Inc. +Copyright (c) 2020-2023 VMware Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/bin/cbc-sdk-help.py b/bin/cbc-sdk-help.py index d7067a135..790b38ace 100644 --- a/bin/cbc-sdk-help.py +++ b/bin/cbc-sdk-help.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/bin/set-macos-keychain.py b/bin/set-macos-keychain.py index 4e2041316..c1643d0ca 100755 --- a/bin/set-macos-keychain.py +++ b/bin/set-macos-keychain.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/bin/set-windows-registry.py b/bin/set-windows-registry.py index e877ab2cf..0b4da33fb 100755 --- a/bin/set-windows-registry.py +++ b/bin/set-windows-registry.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/docs/conf.py b/docs/conf.py index e48e925ec..d9c8d025a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -19,7 +19,7 @@ # -- Project information ----------------------------------------------------- project = 'Carbon Black Cloud Python SDK' -copyright = '2020-2022, Developer Relations' +copyright = '2020-2023 VMware Carbon Black' author = 'Developer Relations' # The full version, including alpha/beta/rc tags diff --git a/examples/audit_remediation/manage_run.py b/examples/audit_remediation/manage_run.py index e49fe8a8a..6e564185c 100755 --- a/examples/audit_remediation/manage_run.py +++ b/examples/audit_remediation/manage_run.py @@ -1,5 +1,5 @@ # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/examples/endpoint_standard/enriched_events_query.py b/examples/endpoint_standard/enriched_events_query.py index c843f0584..450b9f2f5 100755 --- a/examples/endpoint_standard/enriched_events_query.py +++ b/examples/endpoint_standard/enriched_events_query.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/examples/endpoint_standard/policy_operations.py b/examples/endpoint_standard/policy_operations.py index 3ac540eed..eb30b6e67 100755 --- a/examples/endpoint_standard/policy_operations.py +++ b/examples/endpoint_standard/policy_operations.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/examples/enterprise_edr/feed_operations.py b/examples/enterprise_edr/feed_operations.py index d7a5a9669..d990466dd 100755 --- a/examples/enterprise_edr/feed_operations.py +++ b/examples/enterprise_edr/feed_operations.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/examples/live_response/DellBiosVerification/BiosVerification.py b/examples/live_response/DellBiosVerification/BiosVerification.py index 90257f39f..b76cb5e16 100755 --- a/examples/live_response/DellBiosVerification/BiosVerification.py +++ b/examples/live_response/DellBiosVerification/BiosVerification.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # usage: BiosVerification.py [-h] [-m MACHINENAME] [-g] [-o ORGPROFILE] diff --git a/examples/live_response/examplejob.py b/examples/live_response/examplejob.py index 05cb1836d..76c269d54 100755 --- a/examples/live_response/examplejob.py +++ b/examples/live_response/examplejob.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # ******************************************************* -# Copyright (c) VMware, Inc. 2021-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2021-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/examples/live_response/jobrunner.py b/examples/live_response/jobrunner.py index 6aef4946a..3c87e7e72 100644 --- a/examples/live_response/jobrunner.py +++ b/examples/live_response/jobrunner.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # ******************************************************* -# Copyright (c) VMware, Inc. 2021-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2021-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/examples/live_response/live_response_cli.py b/examples/live_response/live_response_cli.py index e099130ea..c3fed3c2e 100755 --- a/examples/live_response/live_response_cli.py +++ b/examples/live_response/live_response_cli.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/examples/platform/change_role.py b/examples/platform/change_role.py index 3b5d2c54f..aa0649b2c 100644 --- a/examples/platform/change_role.py +++ b/examples/platform/change_role.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/examples/platform/container_runtime_alerts.py b/examples/platform/container_runtime_alerts.py index cda4f6688..f906f8e85 100644 --- a/examples/platform/container_runtime_alerts.py +++ b/examples/platform/container_runtime_alerts.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/examples/platform/create_user.py b/examples/platform/create_user.py index 389d9aaf4..d99b8e464 100644 --- a/examples/platform/create_user.py +++ b/examples/platform/create_user.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/examples/platform/device_actions.py b/examples/platform/device_actions.py index 620f91341..2882732a7 100755 --- a/examples/platform/device_actions.py +++ b/examples/platform/device_actions.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/examples/platform/device_processes.py b/examples/platform/device_processes.py index 7a88a1d01..78e88b724 100755 --- a/examples/platform/device_processes.py +++ b/examples/platform/device_processes.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/examples/platform/find_users_with_grants.py b/examples/platform/find_users_with_grants.py index a4a00e860..eb5cee6b3 100644 --- a/examples/platform/find_users_with_grants.py +++ b/examples/platform/find_users_with_grants.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/examples/platform/list_devices.py b/examples/platform/list_devices.py index 2f2dab23b..41b6ea86d 100755 --- a/examples/platform/list_devices.py +++ b/examples/platform/list_devices.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/examples/platform/list_permitted_roles.py b/examples/platform/list_permitted_roles.py index 191b45dd7..3c5e85263 100644 --- a/examples/platform/list_permitted_roles.py +++ b/examples/platform/list_permitted_roles.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/examples/platform/policy_service_crud_operations.py b/examples/platform/policy_service_crud_operations.py index 206d54235..58ffc72ad 100755 --- a/examples/platform/policy_service_crud_operations.py +++ b/examples/platform/policy_service_crud_operations.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/examples/platform/set_user_enable.py b/examples/platform/set_user_enable.py index 3b2ce0d7d..7bb99e421 100644 --- a/examples/platform/set_user_enable.py +++ b/examples/platform/set_user_enable.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/examples/workload/workloads_search_example.py b/examples/workload/workloads_search_example.py index 34692fb5f..585d31010 100755 --- a/examples/workload/workloads_search_example.py +++ b/examples/workload/workloads_search_example.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ******************************************************* -# Copyright (c) VMware, Inc. 2021-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2021-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/cbc_sdk/__init__.py b/src/cbc_sdk/__init__.py index 5d1ae70ac..fb5ebede7 100644 --- a/src/cbc_sdk/__init__.py +++ b/src/cbc_sdk/__init__.py @@ -3,7 +3,7 @@ __title__ = 'cbc_sdk' __author__ = 'Carbon Black Developer Network' __license__ = 'MIT' -__copyright__ = 'Copyright 2020-2022 VMware Carbon Black' +__copyright__ = 'Copyright 2020-2023 VMware Carbon Black' __version__ = '1.4.1' from .rest_api import CBCloudAPI diff --git a/src/cbc_sdk/audit_remediation/base.py b/src/cbc_sdk/audit_remediation/base.py index 882c7656f..0c115d39f 100644 --- a/src/cbc_sdk/audit_remediation/base.py +++ b/src/cbc_sdk/audit_remediation/base.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/cbc_sdk/audit_remediation/differential.py b/src/cbc_sdk/audit_remediation/differential.py index 785b76d8e..e0d0c4fee 100644 --- a/src/cbc_sdk/audit_remediation/differential.py +++ b/src/cbc_sdk/audit_remediation/differential.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/cbc_sdk/base.py b/src/cbc_sdk/base.py index 61814a5f5..b5a386403 100644 --- a/src/cbc_sdk/base.py +++ b/src/cbc_sdk/base.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/cbc_sdk/cache/lru.py b/src/cbc_sdk/cache/lru.py index d65be00b6..efca7ea0c 100644 --- a/src/cbc_sdk/cache/lru.py +++ b/src/cbc_sdk/cache/lru.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/cbc_sdk/connection.py b/src/cbc_sdk/connection.py index 1f6d9a18f..5e4b45b5e 100644 --- a/src/cbc_sdk/connection.py +++ b/src/cbc_sdk/connection.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/cbc_sdk/credential_providers/aws_sm_credential_provider.py b/src/cbc_sdk/credential_providers/aws_sm_credential_provider.py index d509494d8..a3ce17ca1 100644 --- a/src/cbc_sdk/credential_providers/aws_sm_credential_provider.py +++ b/src/cbc_sdk/credential_providers/aws_sm_credential_provider.py @@ -1,5 +1,5 @@ # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/cbc_sdk/credential_providers/default.py b/src/cbc_sdk/credential_providers/default.py index ead149677..aabef3596 100755 --- a/src/cbc_sdk/credential_providers/default.py +++ b/src/cbc_sdk/credential_providers/default.py @@ -1,5 +1,5 @@ # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/cbc_sdk/credential_providers/environ_credential_provider.py b/src/cbc_sdk/credential_providers/environ_credential_provider.py index ef9655972..f9609dcda 100755 --- a/src/cbc_sdk/credential_providers/environ_credential_provider.py +++ b/src/cbc_sdk/credential_providers/environ_credential_provider.py @@ -1,5 +1,5 @@ # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/cbc_sdk/credential_providers/file_credential_provider.py b/src/cbc_sdk/credential_providers/file_credential_provider.py index 2bfc5f7ef..746e7070d 100755 --- a/src/cbc_sdk/credential_providers/file_credential_provider.py +++ b/src/cbc_sdk/credential_providers/file_credential_provider.py @@ -1,5 +1,5 @@ # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/cbc_sdk/credential_providers/keychain_credential_provider.py b/src/cbc_sdk/credential_providers/keychain_credential_provider.py index dfd6c895f..f82867fc2 100644 --- a/src/cbc_sdk/credential_providers/keychain_credential_provider.py +++ b/src/cbc_sdk/credential_providers/keychain_credential_provider.py @@ -1,5 +1,5 @@ # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/cbc_sdk/credential_providers/registry_credential_provider.py b/src/cbc_sdk/credential_providers/registry_credential_provider.py index 208ba2318..a1ca9570c 100755 --- a/src/cbc_sdk/credential_providers/registry_credential_provider.py +++ b/src/cbc_sdk/credential_providers/registry_credential_provider.py @@ -1,5 +1,5 @@ # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/cbc_sdk/credentials.py b/src/cbc_sdk/credentials.py index 1c8b0b15f..863e3f804 100644 --- a/src/cbc_sdk/credentials.py +++ b/src/cbc_sdk/credentials.py @@ -1,5 +1,5 @@ # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/cbc_sdk/endpoint_standard/base.py b/src/cbc_sdk/endpoint_standard/base.py index 33b28457a..3e1f86ee8 100644 --- a/src/cbc_sdk/endpoint_standard/base.py +++ b/src/cbc_sdk/endpoint_standard/base.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/cbc_sdk/endpoint_standard/recommendation.py b/src/cbc_sdk/endpoint_standard/recommendation.py index 2bc9fdf23..2e9588f46 100644 --- a/src/cbc_sdk/endpoint_standard/recommendation.py +++ b/src/cbc_sdk/endpoint_standard/recommendation.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/cbc_sdk/endpoint_standard/usb_device_control.py b/src/cbc_sdk/endpoint_standard/usb_device_control.py index b053ec088..f6f94aed3 100755 --- a/src/cbc_sdk/endpoint_standard/usb_device_control.py +++ b/src/cbc_sdk/endpoint_standard/usb_device_control.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/cbc_sdk/enterprise_edr/threat_intelligence.py b/src/cbc_sdk/enterprise_edr/threat_intelligence.py index 8596b3da4..d36afb977 100644 --- a/src/cbc_sdk/enterprise_edr/threat_intelligence.py +++ b/src/cbc_sdk/enterprise_edr/threat_intelligence.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/cbc_sdk/enterprise_edr/ubs.py b/src/cbc_sdk/enterprise_edr/ubs.py index 5e890969d..e67189583 100644 --- a/src/cbc_sdk/enterprise_edr/ubs.py +++ b/src/cbc_sdk/enterprise_edr/ubs.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/cbc_sdk/errors.py b/src/cbc_sdk/errors.py index 732cc731d..dc312374a 100644 --- a/src/cbc_sdk/errors.py +++ b/src/cbc_sdk/errors.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/cbc_sdk/helpers.py b/src/cbc_sdk/helpers.py index 6ba9c6ab7..4bedc623a 100644 --- a/src/cbc_sdk/helpers.py +++ b/src/cbc_sdk/helpers.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/cbc_sdk/live_response_api.py b/src/cbc_sdk/live_response_api.py index fa06b87e1..5a42afa47 100644 --- a/src/cbc_sdk/live_response_api.py +++ b/src/cbc_sdk/live_response_api.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/cbc_sdk/platform/alerts.py b/src/cbc_sdk/platform/alerts.py index bcfb2a81e..72bfe4dad 100644 --- a/src/cbc_sdk/platform/alerts.py +++ b/src/cbc_sdk/platform/alerts.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/cbc_sdk/platform/base.py b/src/cbc_sdk/platform/base.py index add175542..8a3e12047 100644 --- a/src/cbc_sdk/platform/base.py +++ b/src/cbc_sdk/platform/base.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/cbc_sdk/platform/devices.py b/src/cbc_sdk/platform/devices.py index f0c15b45b..c87d0e362 100644 --- a/src/cbc_sdk/platform/devices.py +++ b/src/cbc_sdk/platform/devices.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/cbc_sdk/platform/events.py b/src/cbc_sdk/platform/events.py index 0facd59d5..1b83d9e5d 100644 --- a/src/cbc_sdk/platform/events.py +++ b/src/cbc_sdk/platform/events.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/cbc_sdk/platform/grants.py b/src/cbc_sdk/platform/grants.py index 38d03362f..dd2ea85c8 100644 --- a/src/cbc_sdk/platform/grants.py +++ b/src/cbc_sdk/platform/grants.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ******************************************************* -# Copyright (c) VMware, Inc. 2020. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/cbc_sdk/platform/jobs.py b/src/cbc_sdk/platform/jobs.py index 3ab816c8f..a8954c156 100644 --- a/src/cbc_sdk/platform/jobs.py +++ b/src/cbc_sdk/platform/jobs.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ******************************************************* -# Copyright (c) VMware, Inc. 2020. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/cbc_sdk/platform/processes.py b/src/cbc_sdk/platform/processes.py index b8796a975..25dae5005 100644 --- a/src/cbc_sdk/platform/processes.py +++ b/src/cbc_sdk/platform/processes.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/cbc_sdk/platform/reputation.py b/src/cbc_sdk/platform/reputation.py index ae543e077..ba049cc3a 100644 --- a/src/cbc_sdk/platform/reputation.py +++ b/src/cbc_sdk/platform/reputation.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/cbc_sdk/platform/users.py b/src/cbc_sdk/platform/users.py index da3c8c57d..883cb8d47 100644 --- a/src/cbc_sdk/platform/users.py +++ b/src/cbc_sdk/platform/users.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ******************************************************* -# Copyright (c) VMware, Inc. 2020. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/cbc_sdk/platform/vulnerability_assessment.py b/src/cbc_sdk/platform/vulnerability_assessment.py index 6cdd51400..2bda6bd1a 100644 --- a/src/cbc_sdk/platform/vulnerability_assessment.py +++ b/src/cbc_sdk/platform/vulnerability_assessment.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ******************************************************* -# Copyright (c) VMware, Inc. 2021-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2021-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/cbc_sdk/rest_api.py b/src/cbc_sdk/rest_api.py index 12d346128..326d64372 100644 --- a/src/cbc_sdk/rest_api.py +++ b/src/cbc_sdk/rest_api.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/cbc_sdk/utils.py b/src/cbc_sdk/utils.py index 9347ce3e5..e1ae7de5f 100755 --- a/src/cbc_sdk/utils.py +++ b/src/cbc_sdk/utils.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/cbc_sdk/winerror.py b/src/cbc_sdk/winerror.py index ec6f40895..a2d983990 100644 --- a/src/cbc_sdk/winerror.py +++ b/src/cbc_sdk/winerror.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/cbc_sdk/workload/nsx_remediation.py b/src/cbc_sdk/workload/nsx_remediation.py index a707c7947..98a65950d 100644 --- a/src/cbc_sdk/workload/nsx_remediation.py +++ b/src/cbc_sdk/workload/nsx_remediation.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/cbc_sdk/workload/sensor_lifecycle.py b/src/cbc_sdk/workload/sensor_lifecycle.py index a08bfabac..a2d964fc7 100755 --- a/src/cbc_sdk/workload/sensor_lifecycle.py +++ b/src/cbc_sdk/workload/sensor_lifecycle.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/cbc_sdk/workload/vm_workloads_search.py b/src/cbc_sdk/workload/vm_workloads_search.py index 746151cc1..1908f4c39 100644 --- a/src/cbc_sdk/workload/vm_workloads_search.py +++ b/src/cbc_sdk/workload/vm_workloads_search.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ******************************************************* -# Copyright (c) VMware, Inc. 2021-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2021-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/tests/uat/alerts_uat.py b/src/tests/uat/alerts_uat.py index 7fd403505..10e939bbe 100644 --- a/src/tests/uat/alerts_uat.py +++ b/src/tests/uat/alerts_uat.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/tests/uat/csp_oauth.py b/src/tests/uat/csp_oauth.py index 5ef5cb650..1e9017a7a 100644 --- a/src/tests/uat/csp_oauth.py +++ b/src/tests/uat/csp_oauth.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2021. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/tests/uat/device_control_uat.py b/src/tests/uat/device_control_uat.py index 916a5e59a..1c35dffb9 100644 --- a/src/tests/uat/device_control_uat.py +++ b/src/tests/uat/device_control_uat.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/tests/uat/differential_analysis_uat.py b/src/tests/uat/differential_analysis_uat.py index 5c34e5a69..757208170 100644 --- a/src/tests/uat/differential_analysis_uat.py +++ b/src/tests/uat/differential_analysis_uat.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # ******************************************************* -# Copyright (c) VMware, Inc. 2021-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2021-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/tests/uat/enriched_events_uat.py b/src/tests/uat/enriched_events_uat.py index f2a23a9ae..31ab13501 100644 --- a/src/tests/uat/enriched_events_uat.py +++ b/src/tests/uat/enriched_events_uat.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # ******************************************************* -# Copyright (c) VMware, Inc. 2021-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2021-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/tests/uat/live_response_api_async_uat.py b/src/tests/uat/live_response_api_async_uat.py index 05f6f9f09..b308cd23e 100644 --- a/src/tests/uat/live_response_api_async_uat.py +++ b/src/tests/uat/live_response_api_async_uat.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # ******************************************************* -# Copyright (c) VMware, Inc. 2021-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2021-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/tests/uat/live_response_api_uat.py b/src/tests/uat/live_response_api_uat.py index 92fb20f3f..05ec737fa 100644 --- a/src/tests/uat/live_response_api_uat.py +++ b/src/tests/uat/live_response_api_uat.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # ******************************************************* -# Copyright (c) VMware, Inc. 2021-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2021-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/tests/uat/nsx_remediation_uat.py b/src/tests/uat/nsx_remediation_uat.py index bcd3c8fae..6cb7e2d40 100644 --- a/src/tests/uat/nsx_remediation_uat.py +++ b/src/tests/uat/nsx_remediation_uat.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/tests/uat/platform_devices_uat.py b/src/tests/uat/platform_devices_uat.py index 82c053652..837ca7dc0 100755 --- a/src/tests/uat/platform_devices_uat.py +++ b/src/tests/uat/platform_devices_uat.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/tests/uat/policy_uat.py b/src/tests/uat/policy_uat.py index 7b13df377..adacb9c84 100644 --- a/src/tests/uat/policy_uat.py +++ b/src/tests/uat/policy_uat.py @@ -1,5 +1,5 @@ # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/tests/uat/process_search_calls.py b/src/tests/uat/process_search_calls.py index 465bcd741..c895e68e1 100755 --- a/src/tests/uat/process_search_calls.py +++ b/src/tests/uat/process_search_calls.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/tests/uat/proxy_test.py b/src/tests/uat/proxy_test.py index 45ba4d50d..08bb26cca 100644 --- a/src/tests/uat/proxy_test.py +++ b/src/tests/uat/proxy_test.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/tests/uat/recommendation_uat.py b/src/tests/uat/recommendation_uat.py index 378e13135..1ae3c5779 100644 --- a/src/tests/uat/recommendation_uat.py +++ b/src/tests/uat/recommendation_uat.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/tests/uat/reputation_override_uat.py b/src/tests/uat/reputation_override_uat.py index c6d2af933..ba4c1afe8 100644 --- a/src/tests/uat/reputation_override_uat.py +++ b/src/tests/uat/reputation_override_uat.py @@ -1,5 +1,5 @@ # ******************************************************* -# Copyright (c) VMware, Inc. 2021-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2021-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/tests/uat/sensor_lifecycle_uat.py b/src/tests/uat/sensor_lifecycle_uat.py index 8e093d90e..8dc17e05c 100755 --- a/src/tests/uat/sensor_lifecycle_uat.py +++ b/src/tests/uat/sensor_lifecycle_uat.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # ******************************************************* -# Copyright (c) VMware, Inc. 2021-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2021-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/tests/uat/vulnerability_assessment_uat.py b/src/tests/uat/vulnerability_assessment_uat.py index 81fb5d8f1..c57a46c60 100644 --- a/src/tests/uat/vulnerability_assessment_uat.py +++ b/src/tests/uat/vulnerability_assessment_uat.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # ******************************************************* -# Copyright (c) VMware, Inc. 2021-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2021-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/tests/uat/watchlist_feed_uat.py b/src/tests/uat/watchlist_feed_uat.py index 4f0089e34..062c2e161 100644 --- a/src/tests/uat/watchlist_feed_uat.py +++ b/src/tests/uat/watchlist_feed_uat.py @@ -1,5 +1,5 @@ # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/tests/uat/workloads_search_uat.py b/src/tests/uat/workloads_search_uat.py index 6509de41c..08cccbbcc 100755 --- a/src/tests/uat/workloads_search_uat.py +++ b/src/tests/uat/workloads_search_uat.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # ******************************************************* -# Copyright (c) VMware, Inc. 2021-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2021-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/tests/unit/audit_remediation/test_differential.py b/src/tests/unit/audit_remediation/test_differential.py index 81ff30cf2..846459e8f 100644 --- a/src/tests/unit/audit_remediation/test_differential.py +++ b/src/tests/unit/audit_remediation/test_differential.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/tests/unit/conftest.py b/src/tests/unit/conftest.py index 8db67a1e7..12bec4275 100755 --- a/src/tests/unit/conftest.py +++ b/src/tests/unit/conftest.py @@ -1,5 +1,5 @@ # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/tests/unit/credential_providers/test_aws_secrets_manager.py b/src/tests/unit/credential_providers/test_aws_secrets_manager.py index de207f65d..0dc5522b0 100644 --- a/src/tests/unit/credential_providers/test_aws_secrets_manager.py +++ b/src/tests/unit/credential_providers/test_aws_secrets_manager.py @@ -1,5 +1,5 @@ # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/tests/unit/credential_providers/test_default.py b/src/tests/unit/credential_providers/test_default.py index db3306dba..e27fa872b 100755 --- a/src/tests/unit/credential_providers/test_default.py +++ b/src/tests/unit/credential_providers/test_default.py @@ -1,5 +1,5 @@ # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/tests/unit/credential_providers/test_environ.py b/src/tests/unit/credential_providers/test_environ.py index 8536d0f5b..824321621 100755 --- a/src/tests/unit/credential_providers/test_environ.py +++ b/src/tests/unit/credential_providers/test_environ.py @@ -1,5 +1,5 @@ # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/tests/unit/credential_providers/test_file.py b/src/tests/unit/credential_providers/test_file.py index 3d5503819..519f84e14 100755 --- a/src/tests/unit/credential_providers/test_file.py +++ b/src/tests/unit/credential_providers/test_file.py @@ -1,5 +1,5 @@ # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/tests/unit/credential_providers/test_keychain.py b/src/tests/unit/credential_providers/test_keychain.py index 2936cfca0..0141860e1 100644 --- a/src/tests/unit/credential_providers/test_keychain.py +++ b/src/tests/unit/credential_providers/test_keychain.py @@ -1,5 +1,5 @@ # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/tests/unit/credential_providers/test_registry.py b/src/tests/unit/credential_providers/test_registry.py index f536e3815..2de02d601 100755 --- a/src/tests/unit/credential_providers/test_registry.py +++ b/src/tests/unit/credential_providers/test_registry.py @@ -1,5 +1,5 @@ # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/tests/unit/endpoint_standard/test_recommendation.py b/src/tests/unit/endpoint_standard/test_recommendation.py index 653218f4c..050609438 100644 --- a/src/tests/unit/endpoint_standard/test_recommendation.py +++ b/src/tests/unit/endpoint_standard/test_recommendation.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/tests/unit/endpoint_standard/test_usb_device.py b/src/tests/unit/endpoint_standard/test_usb_device.py index 97acd7cc7..9e45b1483 100755 --- a/src/tests/unit/endpoint_standard/test_usb_device.py +++ b/src/tests/unit/endpoint_standard/test_usb_device.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/tests/unit/endpoint_standard/test_usb_device_approval.py b/src/tests/unit/endpoint_standard/test_usb_device_approval.py index 71fe8961e..ad6021935 100755 --- a/src/tests/unit/endpoint_standard/test_usb_device_approval.py +++ b/src/tests/unit/endpoint_standard/test_usb_device_approval.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/tests/unit/endpoint_standard/test_usb_device_block.py b/src/tests/unit/endpoint_standard/test_usb_device_block.py index 39fbdc680..9784f1c5d 100755 --- a/src/tests/unit/endpoint_standard/test_usb_device_block.py +++ b/src/tests/unit/endpoint_standard/test_usb_device_block.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/tests/unit/fixtures/CBCSDKMock.py b/src/tests/unit/fixtures/CBCSDKMock.py index 261871072..3477b387e 100644 --- a/src/tests/unit/fixtures/CBCSDKMock.py +++ b/src/tests/unit/fixtures/CBCSDKMock.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/tests/unit/fixtures/mock_credentials.py b/src/tests/unit/fixtures/mock_credentials.py index 3e4834c4d..4e7123829 100755 --- a/src/tests/unit/fixtures/mock_credentials.py +++ b/src/tests/unit/fixtures/mock_credentials.py @@ -1,5 +1,5 @@ # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/tests/unit/fixtures/mock_rest_api.py b/src/tests/unit/fixtures/mock_rest_api.py index df0afad45..2e40270e8 100644 --- a/src/tests/unit/fixtures/mock_rest_api.py +++ b/src/tests/unit/fixtures/mock_rest_api.py @@ -1,5 +1,5 @@ # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/tests/unit/platform/test_alertsv6_api.py b/src/tests/unit/platform/test_alertsv6_api.py index 1e99464ac..3a47059c2 100755 --- a/src/tests/unit/platform/test_alertsv6_api.py +++ b/src/tests/unit/platform/test_alertsv6_api.py @@ -1,5 +1,5 @@ # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/tests/unit/platform/test_devicev6_api.py b/src/tests/unit/platform/test_devicev6_api.py index 151c58981..80fda77ce 100755 --- a/src/tests/unit/platform/test_devicev6_api.py +++ b/src/tests/unit/platform/test_devicev6_api.py @@ -1,5 +1,5 @@ # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/tests/unit/platform/test_platform_models.py b/src/tests/unit/platform/test_platform_models.py index 5c3a17e9e..63ef94ef0 100755 --- a/src/tests/unit/platform/test_platform_models.py +++ b/src/tests/unit/platform/test_platform_models.py @@ -1,5 +1,5 @@ # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/tests/unit/platform/test_reputation_overrides.py b/src/tests/unit/platform/test_reputation_overrides.py index a6359fa2b..582c42fc5 100644 --- a/src/tests/unit/platform/test_reputation_overrides.py +++ b/src/tests/unit/platform/test_reputation_overrides.py @@ -1,5 +1,5 @@ # ******************************************************* -# Copyright (c) VMware, Inc. 2021-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2021-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/tests/unit/platform/test_vulnerability_assessment.py b/src/tests/unit/platform/test_vulnerability_assessment.py index ec2f23ad6..11c41156b 100644 --- a/src/tests/unit/platform/test_vulnerability_assessment.py +++ b/src/tests/unit/platform/test_vulnerability_assessment.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ******************************************************* -# Copyright (c) VMware, Inc. 2021-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2021-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/tests/unit/test_connection.py b/src/tests/unit/test_connection.py index 1399ca79f..8678e03af 100755 --- a/src/tests/unit/test_connection.py +++ b/src/tests/unit/test_connection.py @@ -1,5 +1,5 @@ # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/tests/unit/test_credentials.py b/src/tests/unit/test_credentials.py index 8bf05aaca..ed5a81627 100755 --- a/src/tests/unit/test_credentials.py +++ b/src/tests/unit/test_credentials.py @@ -1,5 +1,5 @@ # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/tests/unit/test_helpers.py b/src/tests/unit/test_helpers.py index ad2b4c004..8aa6da7e8 100755 --- a/src/tests/unit/test_helpers.py +++ b/src/tests/unit/test_helpers.py @@ -1,5 +1,5 @@ # ******************************************************* -# Copyright (c) VMware, Inc. 2021-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2021-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/tests/unit/test_live_response_api.py b/src/tests/unit/test_live_response_api.py index 487af4f03..9a46bc2d2 100755 --- a/src/tests/unit/test_live_response_api.py +++ b/src/tests/unit/test_live_response_api.py @@ -1,5 +1,5 @@ # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/tests/unit/test_rest_api.py b/src/tests/unit/test_rest_api.py index 84cae76fb..c7ab39d22 100644 --- a/src/tests/unit/test_rest_api.py +++ b/src/tests/unit/test_rest_api.py @@ -1,5 +1,5 @@ # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/tests/unit/test_utils.py b/src/tests/unit/test_utils.py index a0af54356..3b0878ea2 100755 --- a/src/tests/unit/test_utils.py +++ b/src/tests/unit/test_utils.py @@ -1,5 +1,5 @@ # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/tests/unit/workload/test_nsx_remediation.py b/src/tests/unit/workload/test_nsx_remediation.py index 8448b69f8..80271634c 100644 --- a/src/tests/unit/workload/test_nsx_remediation.py +++ b/src/tests/unit/workload/test_nsx_remediation.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ******************************************************* -# Copyright (c) VMware, Inc. 2021-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2021-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/tests/unit/workload/test_search.py b/src/tests/unit/workload/test_search.py index e203052c5..e0f25e4e2 100755 --- a/src/tests/unit/workload/test_search.py +++ b/src/tests/unit/workload/test_search.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ******************************************************* -# Copyright (c) VMware, Inc. 2021-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2021-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * diff --git a/src/tests/unit/workload/test_sensor_lifecycle.py b/src/tests/unit/workload/test_sensor_lifecycle.py index 243ba2c9d..eef77ae6e 100755 --- a/src/tests/unit/workload/test_sensor_lifecycle.py +++ b/src/tests/unit/workload/test_sensor_lifecycle.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # ******************************************************* -# Copyright (c) VMware, Inc. 2020-2022. All Rights Reserved. +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT # ******************************************************* # * From 5433e00e6faaf87b8c4f55b6769b462caee941ab Mon Sep 17 00:00:00 2001 From: Alex Van Brunt Date: Mon, 6 Feb 2023 12:33:27 -0700 Subject: [PATCH 104/143] Fix bug in listing policy rules --- examples/platform/policy_service_crud_operations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/platform/policy_service_crud_operations.py b/examples/platform/policy_service_crud_operations.py index 58ffc72ad..0db7d02ca 100755 --- a/examples/platform/policy_service_crud_operations.py +++ b/examples/platform/policy_service_crud_operations.py @@ -71,7 +71,7 @@ def list_policies(cb, parser, args): 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:") - for r in p.rules.values(): + 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"))) From 2aaab62c90155493c9bface96bb3fe5ad384c78e Mon Sep 17 00:00:00 2001 From: Alex Van Brunt Date: Mon, 6 Feb 2023 12:55:35 -0700 Subject: [PATCH 105/143] Replace single quotes --- examples/platform/policy_service_crud_operations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/platform/policy_service_crud_operations.py b/examples/platform/policy_service_crud_operations.py index 0db7d02ca..28ac29f08 100755 --- a/examples/platform/policy_service_crud_operations.py +++ b/examples/platform/policy_service_crud_operations.py @@ -72,7 +72,7 @@ def list_policies(cb, parser, args): print(u"Policy id {0}: {1} {2}".format(p.id, p.name, "({0})".format(p.description) if p.description else "")) print("Rules:") for r in p.rules: - print(" {0}: {1} when {2} {3} is {4}".format(r.get('id'), r.get("action"), + 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"))) From 8ecf37b1ec28120dfbfeb89af12962bef7cffb3c Mon Sep 17 00:00:00 2001 From: Alex Van Brunt Date: Mon, 6 Feb 2023 17:33:25 -0700 Subject: [PATCH 106/143] Add list action to policy uat --- src/tests/uat/policy_uat.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/tests/uat/policy_uat.py b/src/tests/uat/policy_uat.py index adacb9c84..d262b540c 100644 --- a/src/tests/uat/policy_uat.py +++ b/src/tests/uat/policy_uat.py @@ -28,6 +28,9 @@ def main(): Sequence on command line is: + # 0. List the policies + $ python3 examples/platform/policy_service_crud_operations.py --profile PROFILE_NAME --verbose list + # 1. export the default policy $ python3 examples/platform/policy_service_crud_operations.py --profile PROFILE_NAME --verbose export \ --id DEFAULT_POLICY_ID From 921e56830e63f05981439b84d971c0a0d07d2174 Mon Sep 17 00:00:00 2001 From: Hristo Karagitliev Date: Tue, 7 Mar 2023 16:53:53 +0200 Subject: [PATCH 107/143] initial commit --- src/cbc_sdk/enterprise_edr/auth_events.py | 690 ++++++++++++++++++++++ 1 file changed, 690 insertions(+) create mode 100644 src/cbc_sdk/enterprise_edr/auth_events.py diff --git a/src/cbc_sdk/enterprise_edr/auth_events.py b/src/cbc_sdk/enterprise_edr/auth_events.py new file mode 100644 index 000000000..55b14aef7 --- /dev/null +++ b/src/cbc_sdk/enterprise_edr/auth_events.py @@ -0,0 +1,690 @@ +# ******************************************************* +# 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. + +"""Model and Query Classes for Auth Events""" + +from cbc_sdk.base import UnrefreshableModel, FacetQuery +from cbc_sdk.base import Query +from cbc_sdk.errors import ApiError, TimeoutError, InvalidObjectError + +import logging +import time + +log = logging.getLogger(__name__) + + +class AuthEvents(UnrefreshableModel): + """Represents an AuthEvents""" + + primary_key = "event_id" + swagger_meta_file = "enterprise_edr/models/auth_events.yaml" + + def __init__( + self, + cb, + model_unique_id=None, + initial_data=None, + force_init=False, + full_doc=False, + ): + """ + Initialize the AuthEvents object. + + Required RBAC Permissions: + org.search.events (CREATE, READ) + + Args: + cb (CBCloudAPI): A reference to the CBCloudAPI object. + model_unique_id (Any): The unique ID for this particular instance of the model object. + initial_data (dict): The data to use when initializing the model object. + force_init (bool): True to force object initialization. + full_doc (bool): False to mark the object as not fully initialized. + + Example: + >>> cb = CBCloudAPI(profile="example_profile") + >>> events = cb.select(AuthEvents).where("auth_username:SYSTEM") + >>> print(*events) + """ + self._details_timeout = 0 + self._info = None + if model_unique_id is not None and initial_data is None: + auth_events_future = ( + cb.select(AuthEvents) + .where(event_id=model_unique_id) + .execute_async() + ) + result = auth_events_future.result() + if len(result) == 1: + initial_data = result[0] + super(AuthEvents, self).__init__( + cb, + model_unique_id=model_unique_id, + initial_data=initial_data, + force_init=force_init, + full_doc=full_doc, + ) + + def _refresh(self): + """ + Refreshes the AuthEvents object from the server by getting the details. + + Required RBAC Permissions: + org.search.events (READ) + + Returns: + True if the refresh was successful. + """ + self._get_detailed_results() + return True + + @classmethod + def _query_implementation(self, cb, **kwargs): + """ + Returns the appropriate query object for this object type. + + Args: + cb (BaseAPI): Reference to API object used to communicate with the server. + **kwargs (dict): Not used, retained for compatibility. + + Returns: + Query: The query object for this Auth Event. + """ + return AuthEventsQuery(self, cb) + + def get_details(self, timeout=0, async_mode=False): + """Requests detailed results. + + Args: + timeout (int): AuthEvents details request timeout in milliseconds. + async_mode (bool): True to request details in an asynchronous manner. + + Returns: + AuthEvents: Auth Events object enriched with the details fields + + Note: + - When using asynchronous mode, this method returns a python future. + You can call result() on the future object to wait for completion and get the results. + + Examples: + >>> cb = CBCloudAPI(profile="example_profile") + + >>> event = cb.select(AuthEvents, "example-auth-event-id") + >>> print(event.get_details()) + + >>> events = cb.select(AuthEvents).where(process_pid=2000) + >>> print(events[0].get_details()) + """ + self._details_timeout = timeout + if not self.event_id: + raise ApiError( + "Trying to get auth_event details on an invalid auth_event_id" + ) + if async_mode: + return self._cb._async_submit( + lambda arg, kwarg: self._get_detailed_results() + ) + return self._get_detailed_results() + + def _get_detailed_results(self): + """Actual get details implementation""" + args = {"event_ids": [self.event_id]} + url = "/api/investigate/v2/orgs/{}/auth_events/detail_jobs".format( + self._cb.credentials.org_key + ) + query_start = self._cb.post_object(url, body=args) + job_id = query_start.json().get("job_id") + timed_out = False + submit_time = time.time() * 1000 + + while True: + result_url = "/api/investigate/v2/orgs/{}/auth_events/detail_jobs/{}/results".format( + self._cb.credentials.org_key, + job_id, + ) + result = self._cb.get_object(result_url) + contacted = result.get("contacted", 0) + completed = result.get("completed", 0) + log.debug(f"contacted = {contacted}, completed = {completed}") + + if contacted == 0: + time.sleep(0.5) + continue + if completed < contacted: + if self._details_timeout != 0 and (time.time() * 1000) - submit_time > self._details_timeout: + timed_out = True + break + else: + total_results = result.get("num_available", 0) + found_results = result.get("num_found", 0) + if found_results == 0: + return self + if total_results != 0: + results = result.get("results", []) + self._info = results[0] + return self + + time.sleep(0.5) + + if timed_out: + raise TimeoutError( + message="user-specified timeout exceeded while waiting for results" + ) + + +class AuthEventsFacet(UnrefreshableModel): + """ + Represents an AuthEvents facet retrieved. + + Example: + >>> cb = CBCloudAPI(profile="example_profile") + >>> events_facet = cb.select(AuthEventsFacet).where("auth_username:SYSTEM").add_facet_field("process_name") + >>> print(events_facet.results) + """ + + primary_key = "job_id" + swagger_meta_file = "enterprise_edr/models/auth_events_facet.yaml" + submit_url = "/api/investigate/v2/orgs/{}/auth_events/facet_jobs" + result_url = "/api/investigate/v2/orgs/{}/auth_events/facet_jobs/{}/results" + + class Terms(UnrefreshableModel): + """Represents the facet fields and values associated with an AuthEvents Facet query.""" + + def __init__(self, cb, initial_data): + """Initialize an AuthEventsFacet Terms object with initial_data.""" + super(AuthEventsFacet.Terms, self).__init__( + cb, + model_unique_id=None, + initial_data=initial_data, + force_init=False, + full_doc=True, + ) + self._facets = {} + for facet_term_data in initial_data: + field = facet_term_data["field"] + values = facet_term_data["values"] + self._facets[field] = values + + @property + def facets(self): + """Returns the terms' facets for this result.""" + return self._facets + + @property + def fields(self): + """Returns the terms facets' fields for this result.""" + return [field for field in self._facets] + + class Ranges(UnrefreshableModel): + """Represents the range (bucketed) facet fields and values associated with an AuthEvents Facet query.""" + + def __init__(self, cb, initial_data): + """Initialize an AuthEventsFacet Ranges object with initial_data.""" + super(AuthEventsFacet.Ranges, self).__init__( + cb, + model_unique_id=None, + initial_data=initial_data, + force_init=False, + full_doc=True, + ) + self._facets = {} + for facet_range_data in initial_data: + field = facet_range_data["field"] + values = facet_range_data["values"] + self._facets[field] = values + + @property + def facets(self): + """Returns the reified `AuthEventsFacet.Terms._facets` for this result.""" + return self._facets + + @property + def fields(self): + """Returns the ranges fields for this result.""" + return [field for field in self._facets] + + @classmethod + def _query_implementation(self, cb, **kwargs): + # This will emulate a synchronous auth_event facet query, for now. + return FacetQuery(self, cb) + + def __init__(self, cb, model_unique_id, initial_data): + """Initialize the Terms object with initial data.""" + super(AuthEventsFacet, self).__init__( + cb, + model_unique_id=model_unique_id, + initial_data=initial_data, + force_init=False, + full_doc=True, + ) + self._terms = AuthEventsFacet.Terms(cb, initial_data=initial_data["terms"]) + self._ranges = AuthEventsFacet.Ranges(cb, initial_data=initial_data["ranges"]) + + @property + def terms_(self): + """Returns the reified `AuthEventsFacet.Terms` for this result.""" + return self._terms + + @property + def ranges_(self): + """Returns the reified `AuthEventsFacet.Ranges` for this result.""" + return self._ranges + + +class AuthEventsGroup: + """Represents AuthEventsGroup""" + def __init__(self, cb, initial_data=None): + """ + Initialize AuthEventsGroup object + + Args: + cb (CBCloudAPI): A reference to the CBCloudAPI object. + initial_data (dict): The data to use when initializing the model object. + + Notes: + The constructed object will have the following data: + - group_start_timestamp + - group_end_timestamp + - group_key + - group_value + """ + if not initial_data: + raise InvalidObjectError("Cannot create object without initial data") + self._info = initial_data + self._cb = cb + self.auth_events = [AuthEvents(cb, initial_data=x) for x in initial_data.get("results", [])] + + def __getattr__(self, item): + """ + Return an attribute of this object. + + Args: + item (str): Name of the attribute to be returned. + + Returns: + Any: The returned attribute value. + + Raises: + AttributeError: If the object has no such attribute. + """ + try: + super(AuthEventsGroup, self).__getattribute__(item) + except AttributeError: + pass # fall through to the rest of the logic... + + # try looking up via self._info, if we already have it. + if item in self._info: + return self._info[item] + raise AttributeError("'{0}' object has no attribute '{1}'".format(self.__class__.__name__, + item)) + + def __getitem__(self, item): + """ + Return an attribute of this object. + + Args: + item (str): Name of the attribute to be returned. + + Returns: + Any: The returned attribute value. + + Raises: + AttributeError: If the object has no such attribute. + """ + try: + super(AuthEventsGroup, self).__getattribute__(item) + except AttributeError: + pass # fall through to the rest of the logic... + + # try looking up via self._info, if we already have it. + if item in self._info: + return self._info[item] + raise AttributeError("'{0}' object has no attribute '{1}'".format(self.__class__.__name__, + item)) + + +class AuthEventsQuery(Query): + """Represents the query logic for an AuthEvents query. + + This class specializes `Query` to handle the particulars of Auth Events querying. + """ + + VALID_GROUP_FIELDS = ( + "auth_domain_name", "auth_event_action", "auth_remote_port", + "auth_username", "backend_timestamp", "childproc_count", + "crossproc_count", "device_group_id", "device_id", + "device_name", "device_policy_id", "device_timestamp", + "event_id", "filemod_count", "ingress_time", + "modload_count", "netconn_count", "org_id", + "parent_guid", "parent_pid", "process_guid", + "process_hash", "process_name", "process_pid", + "process_username", "regmod_count", "scriptload_count", + "windows_event_id" + ) + + def __init__(self, doc_class, cb): + """ + Initialize the AuthEventsQuery object. + + Args: + doc_class (class): The class of the model this query returns. + cb (CBCloudAPI): A reference to the CBCloudAPI object. + + Example: + >>> cb = CBCloudAPI(profile="example_profile") + >>> events = cb.select(AuthEvents).where("auth_username:SYSTEM") + >>> print(*events) + """ + super(AuthEventsQuery, self).__init__(doc_class, cb) + self._default_args["rows"] = self._batch_size + self._query_token = None + self._timeout = 0 + self._timed_out = False + + def or_(self, **kwargs): + """ + :meth:`or_` criteria are explicitly provided to AuthEvents queries. + + This method overrides the base class in order to provide or_() functionality rather than raising an exception. + + Example: + >>> cb = CBCloudAPI(profile="example_profile") + >>> events = cb.select(AuthEvents).where(process_name="chrome.exe").or_(process_name="firefox.exe") + >>> print(*events) + """ + self._query_builder.or_(None, **kwargs) + return self + + def set_rows(self, rows): + """ + Sets the 'rows' query body parameter to the 'start search' API call, determining how many rows to request. + + Args: + rows (int): How many rows to request. + + Returns: + Query: AuthEventsQuery object + + Example: + >>> cb = CBCloudAPI(profile="example_profile") + >>> events = cb.select(AuthEvents).where(process_name="chrome.exe").set_rows(5) + >>> print(*events) + """ + if not isinstance(rows, int): + raise ApiError(f"Rows must be an integer. {rows} is a {type(rows)}.") + if rows > 10000: + raise ApiError("Maximum allowed value for rows is 10000") + super(AuthEventsQuery, self).set_rows(rows) + return self + + def timeout(self, msecs): + """Sets the timeout on a Auth Event query. + + Arguments: + msecs (int): Timeout duration, in milliseconds. + + Returns: + Query (AuthEventsQuery): The Query object with new milliseconds + parameter. + + Example: + >>> cb = CBCloudAPI(profile="example_profile") + >>> events = cb.select(AuthEvents).where(process_name="chrome.exe").timeout(5000) + >>> print(*events) + """ + self._timeout = msecs + return self + + def _submit(self): + """Submit the search job""" + if self._query_token: + raise ApiError( + "Query already submitted: token {0}".format(self._query_token) + ) + + args = self._get_query_parameters() + url = "/api/investigate/v2/orgs/{}/auth_events/search_jobs".format( + self._cb.credentials.org_key + ) + query_start = self._cb.post_object(url, body=args) + self._query_token = query_start.json().get("job_id") + self._timed_out = False + self._submit_time = time.time() * 1000 + + def _still_querying(self): + """Check whether there are still records to be collected.""" + if not self._query_token: + self._submit() + + results_url = ( + "/api/investigate/v2/orgs/{}/auth_events/search_jobs/{}/results".format( + self._cb.credentials.org_key, + self._query_token, + ) + ) + result = self._cb.get_object(results_url) + contacted = result.get("contacted", 0) + completed = result.get("completed", 0) + log.debug("contacted = {}, completed = {}".format(contacted, completed)) + + if contacted == 0: + return True + if completed < contacted: + if self._timeout != 0 and (time.time() * 1000) - self._submit_time > self._timeout: + self._timed_out = True + return False + return True + + return False + + def _count(self): + """Returns the number of records.""" + if self._count_valid: + return self._total_results + + while self._still_querying(): + time.sleep(0.5) + + if self._timed_out: + raise TimeoutError( + message="user-specified timeout exceeded while waiting for results" + ) + + result_url = ( + "/api/investigate/v2/orgs/{}/auth_events/search_jobs/{}/results".format( + self._cb.credentials.org_key, + self._query_token, + ) + ) + result = self._cb.get_object(result_url) + + self._total_results = result.get("num_available", 0) + self._count_valid = True + + return self._total_results + + def _search(self, start=0, rows=0): + """Start a search job and get the results.""" + if not self._query_token: + self._submit() + + while self._still_querying(): + time.sleep(0.5) + + if self._timed_out: + raise TimeoutError( + message="user-specified timeout exceeded while waiting for results" + ) + + log.debug("Pulling results, timed_out={}".format(self._timed_out)) + + current = start + rows_fetched = 0 + still_fetching = True + query_parameters = {} + result_url_template = ( + "/api/investigate/v2/orgs/{}/auth_events/search_jobs/{}/results".format( + self._cb.credentials.org_key, self._query_token + ) + ) + + while still_fetching: + result_url = "{}?start={}&rows={}".format( + result_url_template, current, self._batch_size + ) + result = self._cb.get_object(result_url, query_parameters=query_parameters) + results = result.get("results", []) + + self._total_results = result.get("num_available", 0) + self._count_valid = True + + for item in results: + yield item + current += 1 + rows_fetched += 1 + + if rows and rows_fetched >= rows: + still_fetching = False + break + + if current >= self._total_results: + still_fetching = False + + log.debug("current: {}, total_results: {}".format(current, self._total_results)) + + def group_results( + self, + fields, + max_events_per_group=None, + rows=500, + start=None, + range_duration=None, + range_field=None, + range_method=None + ): + """ + Get group results grouped by provided fields. + + Args: + fields (str / list): field or fields by which to perform the grouping + max_events_per_group (int): Maximum number of events in a group, if not provided, all events will be returned + rows (int): Number of rows to request, can be paginated + start (int): First row to use for pagination + ranges (dict): dict with information about duration, field, method + + Returns: + dict: grouped results + + Examples: + >>> cb = CBCloudAPI(profile="example_profile") + >>> groups = set(cb.select(AuthEvents).where(process_pid=2000).group_results("device_name")) + >>> for group in groups: + >>> print(group._info) + """ + if not isinstance(fields, list) and not isinstance(fields, str): + raise ApiError("Fields should be either a single field or list of fields") + + if isinstance(fields, str): + fields = [fields] + + if not all((gf in AuthEventsQuery.VALID_GROUP_FIELDS) for gf in fields): + raise ApiError("One or more invalid aggregation fields") + + if not self._query_token: + self._submit() + + result_url = "/api/investigate/v2/orgs/{}/auth_events/search_jobs/{}/group_results".format( + self._cb.credentials.org_key, + self._query_token, + ) + + # construct the group results body, required ones are fields and rows + data = dict(fields=fields, rows=rows) + if max_events_per_group is not None: + data["max_events_per_group"] = max_events_per_group + if range_duration or range_field or range_method: + data["range"] = {} + if range_method: + data["range"]["method"] = range_method + if range_duration: + data["range"]["duration"] = range_duration + if range_field: + data["range"]["field"] = range_field + if start is not None: + data["start"] = start + + still_fetching = True + while still_fetching: + result = self._cb.post_object(result_url, data).json() + contacted = result.get("contacted", 0) + completed = result.get("completed", 0) + if contacted < completed: + time.sleep(0.5) + continue + still_fetching = False + + for group in result.get("group_results", []): + yield AuthEventsGroup(self._cb, initial_data=group) + + def get_auth_events_descriptions(self): + """ + Returns descriptions and status messages of Auth Events. + + Returns: + dict: Descriptions and status messages of Auth Events as dict objects. + Example: + >>> cb = CBCloudAPI(profile="example_profile") + >>> descriptions = cb.select(AuthEvents).get_auth_events_descriptions() + >>> print(descriptions) + """ + url = "/api/investigate/v2/orgs/{}/auth_events/descriptions".format(self._cb.credentials.org_key) + + return self._cb.get_object(url) + + def search_suggestions(self, query, count=None): + """ + Returns suggestions for keys and field values that can be used in a search. + + Args: + query (str): A search query to use. + count (int): (optional) Number of suggestions to be returned + + Returns: + list: A list of search suggestions expressed as dict objects. + Example: + >>> cb = CBCloudAPI(profile="example_profile") + >>> suggestions = cb.select(AuthEvents).search_suggestions('auth') + >>> print(suggestions) + """ + query_params = {"suggest.q": query} + if count: + query_params["suggest.count"] = count + url = "/api/investigate/v2/orgs/{}/auth_events/search_suggestions".format(self._cb.credentials.org_key) + output = self._cb.get_object(url, query_params) + return output["suggestions"] + + def search_validation(self, query): + """ + Returns validation result of a query. + + Args: + query (str): A search query to be validated. + + Returns: + bool: Status of the validation + Example: + >>> cb = CBCloudAPI(profile="example_profile") + >>> validation = cb.select(AuthEvents).search_validation('auth_username:Administrator') + >>> print(validation) + """ + query_params = {"q": query} + url = "/api/investigate/v2/orgs/{}/auth_events/search_validation".format(self._cb.credentials.org_key) + output = self._cb.get_object(url, query_params) + return output.get("valid", False) From e13e1f36dacbe2a1370ad1eb31cd60dded3e32a9 Mon Sep 17 00:00:00 2001 From: Hristo Karagitliev Date: Tue, 7 Mar 2023 16:54:54 +0200 Subject: [PATCH 108/143] initial commit --- src/cbc_sdk/enterprise_edr/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/cbc_sdk/enterprise_edr/__init__.py b/src/cbc_sdk/enterprise_edr/__init__.py index 209faa7a8..0b9b4da37 100644 --- a/src/cbc_sdk/enterprise_edr/__init__.py +++ b/src/cbc_sdk/enterprise_edr/__init__.py @@ -7,3 +7,4 @@ WatchlistQuery) from cbc_sdk.enterprise_edr.ubs import Binary, Downloads +from cbc_sdk.enterprise_edr.auth_events import AuthEvents, AuthEventsFacet From 1891dfc3a69ac23ad677fe41800dce220a819531 Mon Sep 17 00:00:00 2001 From: Hristo Karagitliev Date: Tue, 7 Mar 2023 18:19:32 +0200 Subject: [PATCH 109/143] flake8 --- src/cbc_sdk/enterprise_edr/auth_events.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cbc_sdk/enterprise_edr/auth_events.py b/src/cbc_sdk/enterprise_edr/auth_events.py index 55b14aef7..485e69e37 100644 --- a/src/cbc_sdk/enterprise_edr/auth_events.py +++ b/src/cbc_sdk/enterprise_edr/auth_events.py @@ -323,7 +323,7 @@ def __getattr__(self, item): if item in self._info: return self._info[item] raise AttributeError("'{0}' object has no attribute '{1}'".format(self.__class__.__name__, - item)) + item)) def __getitem__(self, item): """ @@ -347,7 +347,7 @@ def __getitem__(self, item): if item in self._info: return self._info[item] raise AttributeError("'{0}' object has no attribute '{1}'".format(self.__class__.__name__, - item)) + item)) class AuthEventsQuery(Query): @@ -574,7 +574,7 @@ def group_results( Args: fields (str / list): field or fields by which to perform the grouping - max_events_per_group (int): Maximum number of events in a group, if not provided, all events will be returned + max_events_per_group (int): Maximum number of events in a group, if not provided all events will be returned rows (int): Number of rows to request, can be paginated start (int): First row to use for pagination ranges (dict): dict with information about duration, field, method From 5b9ac59d0ba53ce8ca9f418c9c62df369b5dd6c4 Mon Sep 17 00:00:00 2001 From: Hristo Karagitliev Date: Wed, 8 Mar 2023 09:24:31 +0200 Subject: [PATCH 110/143] change AuthEvents base model --- src/cbc_sdk/enterprise_edr/auth_events.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cbc_sdk/enterprise_edr/auth_events.py b/src/cbc_sdk/enterprise_edr/auth_events.py index 485e69e37..0ff97c85d 100644 --- a/src/cbc_sdk/enterprise_edr/auth_events.py +++ b/src/cbc_sdk/enterprise_edr/auth_events.py @@ -11,7 +11,7 @@ """Model and Query Classes for Auth Events""" -from cbc_sdk.base import UnrefreshableModel, FacetQuery +from cbc_sdk.base import UnrefreshableModel, NewBaseModel, FacetQuery from cbc_sdk.base import Query from cbc_sdk.errors import ApiError, TimeoutError, InvalidObjectError @@ -21,7 +21,7 @@ log = logging.getLogger(__name__) -class AuthEvents(UnrefreshableModel): +class AuthEvents(NewBaseModel): """Represents an AuthEvents""" primary_key = "event_id" From eeeb778d0cb3efed74ea83ddedb65603577f958a Mon Sep 17 00:00:00 2001 From: Hristo Karagitliev Date: Thu, 9 Mar 2023 11:38:41 +0200 Subject: [PATCH 111/143] add unit tests --- .../unit/enterprise_edr/test_auth_events.py | 952 ++++++++++++++++++ .../enterprise_edr/mock_auth_events.py | 470 +++++++++ 2 files changed, 1422 insertions(+) create mode 100644 src/tests/unit/enterprise_edr/test_auth_events.py create mode 100644 src/tests/unit/fixtures/enterprise_edr/mock_auth_events.py diff --git a/src/tests/unit/enterprise_edr/test_auth_events.py b/src/tests/unit/enterprise_edr/test_auth_events.py new file mode 100644 index 000000000..5717ad971 --- /dev/null +++ b/src/tests/unit/enterprise_edr/test_auth_events.py @@ -0,0 +1,952 @@ +"""Testing AuthEvents objects of cbc_sdk.enterprise_edr""" + +import pytest +import logging + +from cbc_sdk.base import FacetQuery +from cbc_sdk.enterprise_edr import AuthEvents +from cbc_sdk.enterprise_edr.auth_events import AuthEventsQuery, AuthEventsFacet +from cbc_sdk.rest_api import CBCloudAPI +from cbc_sdk.errors import ApiError, TimeoutError +from tests.unit.fixtures.CBCSDKMock import CBCSDKMock + +from tests.unit.fixtures.enterprise_edr.mock_auth_events import ( + POST_AUTH_EVENTS_SEARCH_JOB_RESP, + GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP, + GET_AUTH_EVENTS_DETAIL_JOB_RESULTS_RESP, + GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP_ZERO_COMP, + GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_ZERO, + GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING, + GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP_2, + GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP_0, + POST_AUTH_EVENTS_FACET_SEARCH_JOB_RESP, + GET_AUTH_EVENTS_FACET_SEARCH_JOB_RESULTS_RESP_2, + GET_AUTH_EVENTS_FACET_SEARCH_JOB_RESULTS_RESP_1, + GET_AUTH_EVENTS_FACET_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING, + GET_AUTH_EVENTS_GROUPED_RESULTS_RESP, + # GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP, +) + +log = logging.basicConfig( + format="%(asctime)s %(levelname)s:%(message)s", + level=logging.DEBUG, + filename="log.txt", +) + + +@pytest.fixture(scope="function") +def cb(): + """Create CBCloudAPI singleton""" + return CBCloudAPI( + url="https://example.com", org_key="test", token="abcd/1234", ssl_verify=False + ) + + +@pytest.fixture(scope="function") +def cbcsdk_mock(monkeypatch, cb): + """Mocks CBC SDK for unit tests""" + return CBCSDKMock(monkeypatch, cb) + + +# ==================================== UNIT TESTS BELOW ==================================== + + +def test_auth_events_select_where(cbcsdk_mock): + """Testing AuthEvents Querying with select()""" + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/auth_events/search_jobs", + POST_AUTH_EVENTS_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/auth_events/search_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 + GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/auth_events/search_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results?start=0&rows=500", # noqa: E501 + GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP, + ) + + api = cbcsdk_mock.api + events_list = api.select(AuthEvents).where("auth_username:SYSTEM") + for event in events_list: + assert event.device_name is not None + + +def test_auth_events_select_async(cbcsdk_mock): + """Testing AuthEvents Querying with select() - asynchronous way""" + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/auth_events/search_jobs", + POST_AUTH_EVENTS_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/auth_events/search_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 + GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/auth_events/search_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results?start=0&rows=500", # noqa: E501 + GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP, + ) + + api = cbcsdk_mock.api + events_list = api.select(AuthEvents).where(event_id="DA9E269E-421D-469D-A212-9062888A02F4").execute_async() + for event in events_list.result(): + assert event["device_name"] is not None + + +def test_auth_events_select_by_id(cbcsdk_mock): + """Testing AuthEvents Querying with select() - asynchronous way""" + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/auth_events/search_jobs", + POST_AUTH_EVENTS_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/auth_events/search_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 + GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/auth_events/search_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results?start=0&rows=500", # noqa: E501 + GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP, + ) + + api = cbcsdk_mock.api + auth_events = api.select(AuthEvents, "DA9E269E-421D-469D-A212-9062888A02F4") + assert auth_events["device_name"] is not None + + +def test_auth_events_select_details_async(cbcsdk_mock): + """Testing AuthEvents Querying with get_details - asynchronous mode""" + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/auth_events/search_jobs", + POST_AUTH_EVENTS_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/auth_events/search_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 + GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/auth_events/search_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results?start=0&rows=500", # noqa: E501 + GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP, + ) + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/auth_events/detail_jobs", + POST_AUTH_EVENTS_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/auth_events/detail_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 + GET_AUTH_EVENTS_DETAIL_JOB_RESULTS_RESP, + ) + + api = cbcsdk_mock.api + # events_list = api.select(AuthEvents).where(process_pid=2000) + events_list = api.select(AuthEvents).where(event_id="DA9E269E-421D-469D-A212-9062888A02F4") + events = events_list[0] + details = events.get_details(async_mode=True, timeout=500) + results = details.result() + assert results.device_name is not None + assert events._details_timeout == 500 + assert results.process_pid[0] == 764 + assert results["device_name"] is not None + assert results["process_pid"][0] == 764 + + +def test_auth_events_details_only(cbcsdk_mock): + """Testing AuthEvents with get_details - just the get_details REST API calls""" + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/auth_events/detail_jobs", + POST_AUTH_EVENTS_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/auth_events/detail_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 + GET_AUTH_EVENTS_DETAIL_JOB_RESULTS_RESP, + ) + + api = cbcsdk_mock.api + events = AuthEvents(api, initial_data={"event_id": "D06DC822-B25E-4162-A5A7-6166BFA9B8DF"}) + results = events._get_detailed_results() + assert results._info["device_name"] is not None + assert results._info["process_pid"][0] == 764 + + +def test_auth_events_details_timeout(cbcsdk_mock): + """Testing AuthEvents get_details() timeout handling""" + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/auth_events/detail_jobs", + POST_AUTH_EVENTS_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/auth_events/detail_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 + GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP_ZERO_COMP, + ) + + api = cbcsdk_mock.api + events = AuthEvents(api, initial_data={"event_id": "D06DC822-B25E-4162-A5A7-6166BFA9B8DF"}) + events._details_timeout = 1 + with pytest.raises(TimeoutError): + events._get_detailed_results() + + +def test_auth_events_select_details_sync(cbcsdk_mock): + """Testing AuthEvents Querying with get_details""" + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/auth_events/search_jobs", + POST_AUTH_EVENTS_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/auth_events/search_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 + GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/auth_events/search_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results?start=0&rows=500", # noqa: E501 + GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP, + ) + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/auth_events/detail_jobs", + POST_AUTH_EVENTS_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/auth_events/detail_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 + GET_AUTH_EVENTS_DETAIL_JOB_RESULTS_RESP, + ) + + api = cbcsdk_mock.api + events_list = api.select(AuthEvents).where(process_pid=2000) + events = events_list[0] + results = events.get_details() + assert results["device_name"] is not None + assert results.device_name is not None + assert results.process_pid[0] == 764 + + +def test_auth_events_select_details_refresh(cbcsdk_mock): + """Testing AuthEvents Querying with get_details""" + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/auth_events/search_jobs", + POST_AUTH_EVENTS_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/auth_events/search_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 + GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/auth_events/search_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results?start=0&rows=500", # noqa: E501 + GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP, + ) + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/auth_events/detail_jobs", + POST_AUTH_EVENTS_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/auth_events/detail_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 + GET_AUTH_EVENTS_DETAIL_JOB_RESULTS_RESP, + ) + + api = cbcsdk_mock.api + events_list = api.select(AuthEvents).where(event_id="DA9E269E-421D-469D-A212-9062888A02F4") + events = events_list[0] + assert events.device_name is not None + assert events.process_pid[0] == 776 + + +def test_auth_events_select_details_sync_zero(cbcsdk_mock): + """Testing AuthEvents Querying with get_details""" + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/auth_events/search_jobs", + POST_AUTH_EVENTS_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/auth_events/search_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 + GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/auth_events/search_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results?start=0&rows=500", # noqa: E501 + GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP, + ) + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/auth_events/detail_jobs", + POST_AUTH_EVENTS_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/auth_events/detail_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 + GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_ZERO, + ) + + api = cbcsdk_mock.api + events_list = api.select(AuthEvents).where(process_pid=2000) + events = events_list[0] + results = events.get_details() + assert results["device_name"] is not None + assert results.get("alert_id") == [] + + +def test_auth_events_select_compound(cbcsdk_mock): + """Testing AuthEvents Querying with select() and more complex criteria""" + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/auth_events/search_jobs", + POST_AUTH_EVENTS_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/auth_events/search_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 + GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/auth_events/search_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results?start=0&rows=500", # noqa: E501 + GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP, + ) + + api = cbcsdk_mock.api + events_list = api.select(AuthEvents).where(process_pid=776).or_(parent_pid=608) + for events in events_list: + assert events.device_name is not None + + +def test_auth_events_query_implementation(cbcsdk_mock): + """Testing AuthEvents querying with where().""" + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/auth_events/search_jobs", + POST_AUTH_EVENTS_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/auth_events/search_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 + GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/auth_events/search_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results?start=0&rows=500", # noqa: E501 + GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP, + ) + api = cbcsdk_mock.api + event_id = ( + "DA9E269E-421D-469D-A212-9062888A02F4" + ) + events_list = api.select(AuthEvents).where(f"event_id:{event_id}") + assert isinstance(events_list, AuthEventsQuery) + assert events_list[0].event_id == event_id + + +def test_auth_events_timeout(cbcsdk_mock): + """Testing AuthEventsQuery.timeout().""" + api = cbcsdk_mock.api + query = api.select(AuthEvents).where("event_id:some_id") + assert query._timeout == 0 + query.timeout(msecs=500) + assert query._timeout == 500 + + +def test_auth_events_timeout_error(cbcsdk_mock): + """Testing that a timeout in AuthEvents querying throws a TimeoutError correctly""" + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/auth_events/search_jobs", + POST_AUTH_EVENTS_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/auth_events/search_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 + GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING, + ) + + api = cbcsdk_mock.api + events_list = (api.select(AuthEvents).where("event_id:DA9E269E-421D-469D-A212-9062888A02F4").timeout(1)) + with pytest.raises(TimeoutError): + list(events_list) + events_list = (api.select(AuthEvents).where("event_id:DA9E269E-421D-469D-A212-9062888A02F4").timeout(1)) + with pytest.raises(TimeoutError): + events_list._count() + + +def test_auth_events_query_sort(cbcsdk_mock): + """Testing AuthEvents results sort.""" + api = cbcsdk_mock.api + events_list = ( + api.select(AuthEvents) + .where(process_pid=1000) + .or_(process_pid=1000) + .sort_by("process_pid", direction="DESC") + ) + assert events_list._sort_by == [{"field": "process_pid", "order": "DESC"}] + + +def test_auth_events_rows(cbcsdk_mock): + """Testing AuthEvents results sort.""" + api = cbcsdk_mock.api + events_list = api.select(AuthEvents).where(process_pid=1000).set_rows(1500) + assert events_list._batch_size == 1500 + with pytest.raises(ApiError) as ex: + api.select(AuthEvents).where(process_pid=1000).set_rows("alabala") + assert "Rows must be an integer." in str(ex) + with pytest.raises(ApiError) as ex: + api.select(AuthEvents).where(process_pid=1000).set_rows(10001) + assert "Maximum allowed value for rows is 10000" in str(ex) + + +def test_auth_events_time_range(cbcsdk_mock): + """Testing AuthEvents results sort.""" + api = cbcsdk_mock.api + events_list = ( + api.select(AuthEvents) + .where(process_pid=1000) + .set_time_range( + start="2020-10-10T20:34:07Z", end="2020-10-20T20:34:07Z", window="-1d" + ) + ) + assert events_list._time_range["start"] == "2020-10-10T20:34:07Z" + assert events_list._time_range["end"] == "2020-10-20T20:34:07Z" + assert events_list._time_range["window"] == "-1d" + + +def test_auth_events_submit(cbcsdk_mock): + """Test _submit method of AuthEventsQuery class""" + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/auth_events/search_jobs", + POST_AUTH_EVENTS_SEARCH_JOB_RESP, + ) + api = cbcsdk_mock.api + events_list = api.select(AuthEvents).where(process_pid=1000) + events_list._submit() + assert events_list._query_token == "62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs" + with pytest.raises(ApiError) as ex: + events_list._submit() + assert "Query already submitted: token" in str(ex) + + +def test_auth_events_count(cbcsdk_mock): + """Test _submit method of AuthEventsquery class""" + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/auth_events/search_jobs", + POST_AUTH_EVENTS_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/auth_events/search_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 + GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP_2, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/auth_events/search_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 + GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP_2, + ) + + api = cbcsdk_mock.api + events_list = api.select(AuthEvents).where(process_pid=1000) + events_list._count() + assert events_list._count() == 198 + + +def test_auth_events_search(cbcsdk_mock): + """Test _search method of AuthEventsquery class""" + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/auth_events/search_jobs", + POST_AUTH_EVENTS_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/auth_events/search_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 + GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP_2, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/auth_events/search_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results?start=0&rows=500", # noqa: E501 + GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP_2, + ) + + api = cbcsdk_mock.api + events_list = api.select(AuthEvents).where(process_pid=828) + events_list._search() + assert events_list[0].process_pid[0] == 828 + events_list._search(start=1) + assert events_list[0].process_pid[0] == 828 + + +def test_auth_events_still_querying(cbcsdk_mock): + """Test _search method of AuthEventsquery class""" + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/auth_events/search_jobs", + POST_AUTH_EVENTS_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/auth_events/search_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 + GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP_0, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/auth_events/search_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results?start=0&rows=500", # noqa: E501 + GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING, + ) + + api = cbcsdk_mock.api + events_list = api.select(AuthEvents).where(process_pid=1000) + assert events_list._still_querying() is True + + +def test_auth_events_still_querying2(cbcsdk_mock): + """Test _search method of AuthEventsquery class""" + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/auth_events/search_jobs", + POST_AUTH_EVENTS_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/auth_events/search_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 + GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP_ZERO_COMP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/auth_events/search_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results?start=0&rows=500", # noqa: E501 + GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING, + ) + + api = cbcsdk_mock.api + events_list = api.select(AuthEvents).where(process_pid=1000) + assert events_list._still_querying() is True + + +# --------------------- AuthEventsFacet -------------------------------------- + + +def test_auth_events_facet_select_where(cbcsdk_mock): + """Testing AuthEvents Querying with select()""" + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/auth_events/facet_jobs", + POST_AUTH_EVENTS_FACET_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/auth_events/facet_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 + GET_AUTH_EVENTS_FACET_SEARCH_JOB_RESULTS_RESP_2, + ) + + api = cbcsdk_mock.api + auth_events = ( + api.select(AuthEventsFacet) + .where(process_name="chrome.exe") + .add_facet_field("process_name") + ) + event = auth_events.results + assert event.terms is not None + assert event.ranges is not None + assert event.ranges == [] + assert event.terms[0]["field"] == "process_name" + + +def test_auth_events_facet_select_async(cbcsdk_mock): + """Testing AuthEvents Querying with select()""" + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/auth_events/facet_jobs", + POST_AUTH_EVENTS_FACET_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/auth_events/facet_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 + GET_AUTH_EVENTS_FACET_SEARCH_JOB_RESULTS_RESP_2, + ) + + api = cbcsdk_mock.api + future = ( + api.select(AuthEventsFacet) + .where(process_name="chrome.exe") + .add_facet_field("process_name") + .execute_async() + ) + event = future.result() + assert event.terms is not None + assert event.ranges is not None + assert event.ranges == [] + assert event.terms[0]["field"] == "process_name" + + +def test_auth_events_facet_select_compound(cbcsdk_mock): + """Testing AuthEvents Querying with select() and more complex criteria""" + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/auth_events/facet_jobs", + POST_AUTH_EVENTS_FACET_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/auth_events/facet_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 + GET_AUTH_EVENTS_FACET_SEARCH_JOB_RESULTS_RESP_2, + ) + + api = cbcsdk_mock.api + auth_events = ( + api.select(AuthEventsFacet) + .where(process_name="chrome.exe") + .or_(process_name="firefox.exe") + .add_facet_field("process_name") + ) + event = auth_events.results + assert event.terms_.fields == ["process_name"] + assert event.ranges == [] + + +def test_auth_events_facet_query_implementation(cbcsdk_mock): + """Testing AuthEvents querying with where().""" + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/auth_events/facet_jobs", + POST_AUTH_EVENTS_FACET_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/auth_events/facet_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 + GET_AUTH_EVENTS_FACET_SEARCH_JOB_RESULTS_RESP_1, + ) + + api = cbcsdk_mock.api + field = "process_name" + auth_events = ( + api.select(AuthEventsFacet) + .where(process_name="test") + .add_facet_field("process_name") + ) + assert isinstance(auth_events, FacetQuery) + event = auth_events.results + assert event.terms[0]["field"] == field + assert event.terms_.facets["process_name"] is not None + assert event.terms_.fields[0] == "process_name" + assert event.ranges_.facets is not None + assert event.ranges_.fields[0] == "device_timestamp" + assert isinstance(event._query_implementation(api), FacetQuery) + + +def test_auth_events_facet_timeout(cbcsdk_mock): + """Testing AuthEventsQuery.timeout().""" + api = cbcsdk_mock.api + query = ( + api.select(AuthEventsFacet) + .where("process_name:some_name") + .add_facet_field("process_name") + ) + assert query._timeout == 0 + query.timeout(msecs=500) + assert query._timeout == 500 + + +def test_auth_events_facet_timeout_error(cbcsdk_mock): + """Testing that a timeout in AuthEventsQuery throws the right TimeoutError.""" + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/auth_events/facet_jobs", + POST_AUTH_EVENTS_FACET_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/auth_events/facet_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 + GET_AUTH_EVENTS_FACET_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING, + ) + + api = cbcsdk_mock.api + query = ( + api.select(AuthEventsFacet) + .where("process_name:some_name") + .add_facet_field("process_name") + .timeout(1) + ) + with pytest.raises(TimeoutError): + query.results() + query = ( + api.select(AuthEventsFacet) + .where("process_name:some_name") + .add_facet_field("process_name") + .timeout(1) + ) + with pytest.raises(TimeoutError): + query._count() + + +def test_auth_events_facet_query_add_range(cbcsdk_mock): + """Testing AuthEvents results sort.""" + api = cbcsdk_mock.api + range = ({"bucket_size": 30, "start": "0D", "end": "20D", "field": "something"},) + auth_events = ( + api.select(AuthEventsFacet) + .where(process_pid=1000) + .add_range(range) + .add_facet_field("process_name") + ) + assert auth_events._ranges[0]["bucket_size"] == 30 + assert auth_events._ranges[0]["start"] == "0D" + assert auth_events._ranges[0]["end"] == "20D" + assert auth_events._ranges[0]["field"] == "something" + + +def test_auth_events_facet_query_check_range(cbcsdk_mock): + """Testing AuthEvents results sort.""" + api = cbcsdk_mock.api + range = ({"bucket_size": [], "start": "0D", "end": "20D", "field": "something"},) + with pytest.raises(ApiError): + api.select(AuthEventsFacet).where(process_pid=1000).add_range( + range + ).add_facet_field("process_name") + + range = ({"bucket_size": 30, "start": [], "end": "20D", "field": "something"},) + with pytest.raises(ApiError): + api.select(AuthEventsFacet).where(process_pid=1000).add_range( + range + ).add_facet_field("process_name") + + range = ({"bucket_size": 30, "start": "0D", "end": [], "field": "something"},) + with pytest.raises(ApiError): + api.select(AuthEventsFacet).where(process_pid=1000).add_range( + range + ).add_facet_field("process_name") + + range = ({"bucket_size": 30, "start": "0D", "end": "20D", "field": []},) + with pytest.raises(ApiError): + api.select(AuthEventsFacet).where(process_pid=1000).add_range( + range + ).add_facet_field("process_name") + + +def test_auth_events_facet_query_add_facet_field(cbcsdk_mock): + """Testing AuthEvents results sort.""" + api = cbcsdk_mock.api + auth_events = ( + api.select(AuthEventsFacet) + .where(process_pid=1000) + .add_facet_field("process_name") + ) + assert auth_events._facet_fields[0] == "process_name" + + +def test_auth_events_facet_query_add_facet_fields(cbcsdk_mock): + """Testing AuthEvents results sort.""" + api = cbcsdk_mock.api + auth_events = ( + api.select(AuthEventsFacet) + .where(process_pid=1000) + .add_facet_field(["process_name", "process_pid"]) + ) + assert "process_pid" in auth_events._facet_fields + assert "process_name" in auth_events._facet_fields + + +def test_auth_events_facet_query_add_facet_invalid_fields(cbcsdk_mock): + """Testing AuthEvents results sort.""" + api = cbcsdk_mock.api + with pytest.raises(TypeError): + api.select(AuthEventsFacet).where(process_pid=1000).add_facet_field(1337) + + +def test_auth_events_facet_limit(cbcsdk_mock): + """Testing AuthEvents results limit.""" + api = cbcsdk_mock.api + auth_events = ( + api.select(AuthEventsFacet) + .where(process_pid=1000) + .limit(123) + .add_facet_field("process_name") + ) + assert auth_events._limit == 123 + + +def test_auth_events_facet_time_range(cbcsdk_mock): + """Testing AuthEvents results range.""" + api = cbcsdk_mock.api + auth_events = ( + api.select(AuthEventsFacet) + .where(process_pid=1000) + .set_time_range( + start="2020-10-10T20:34:07Z", end="2020-10-20T20:34:07Z", window="-1d" + ) + .add_facet_field("process_name") + ) + assert auth_events._time_range["start"] == "2020-10-10T20:34:07Z" + assert auth_events._time_range["end"] == "2020-10-20T20:34:07Z" + assert auth_events._time_range["window"] == "-1d" + + +def test_auth_events_facet_submit(cbcsdk_mock): + """Test _submit method of AuthEventsQuery class""" + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/auth_events/facet_jobs", + POST_AUTH_EVENTS_FACET_SEARCH_JOB_RESP, + ) + api = cbcsdk_mock.api + auth_events = ( + api.select(AuthEventsFacet) + .where(process_pid=1000) + .add_facet_field("process_name") + ) + auth_events._submit() + assert auth_events._query_token == "62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs" + + +def test_auth_events_facet_count(cbcsdk_mock): + """Test _submit method of AuthEventsQuery class""" + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/auth_events/facet_jobs", + POST_AUTH_EVENTS_FACET_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/auth_events/facet_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 + GET_AUTH_EVENTS_FACET_SEARCH_JOB_RESULTS_RESP_1, + ) + + api = cbcsdk_mock.api + auth_events = ( + api.select(AuthEventsFacet) + .where(process_pid=1000) + .add_facet_field("process_name") + ) + auth_events._count() + assert auth_events._count() == 116 + + +def test_auth_events_search(cbcsdk_mock): + """Test _search method of AuthEventsQuery class""" + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/auth_events/facet_jobs", + POST_AUTH_EVENTS_FACET_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/auth_events/facet_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 + GET_AUTH_EVENTS_FACET_SEARCH_JOB_RESULTS_RESP_2, + ) + + api = cbcsdk_mock.api + auth_events = ( + api.select(AuthEventsFacet) + .where(process_pid=1000) + .add_facet_field("process_name") + ) + future = auth_events.execute_async() + result = future.result() + assert result.terms is not None + assert len(result.ranges) == 0 + assert result.terms[0]["field"] == "process_name" + + +def test_auth_events_search_async(cbcsdk_mock): + """Test _search method of AuthEventsQuery class""" + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/auth_events/facet_jobs", + POST_AUTH_EVENTS_FACET_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/auth_events/facet_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 + GET_AUTH_EVENTS_FACET_SEARCH_JOB_RESULTS_RESP_2, + ) + + api = cbcsdk_mock.api + auth_events = ( + api.select(AuthEventsFacet) + .where(process_pid=1000) + .add_facet_field("process_name") + ) + future = auth_events.execute_async() + result = future.result() + assert result.terms is not None + assert len(result.ranges) == 0 + assert result.terms[0]["field"] == "process_name" + + +def test_auth_events_aggregation_wrong_field(cbcsdk_mock): + """Testing passing wrong aggregation_field""" + api = cbcsdk_mock.api + with pytest.raises(ApiError): + for i in ( + api.select(AuthEvents) + .where(process_pid=2000) + .group_results("wrong_field") + ): + print(i) + with pytest.raises(ApiError): + for i in api.select(AuthEvents).where(process_pid=2000).group_results(1): + print(i) + + +def test_auth_events_select_group_results(cbcsdk_mock): + """Testing AuthEvents Querying with select() and more complex criteria""" + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/auth_events/search_jobs", + POST_AUTH_EVENTS_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/auth_events/search_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/group_results", # noqa: E501 + GET_AUTH_EVENTS_GROUPED_RESULTS_RESP, + ) + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/auth_events/detail_jobs", + POST_AUTH_EVENTS_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/auth_events/detail_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 + GET_AUTH_EVENTS_DETAIL_JOB_RESULTS_RESP, + ) + + api = cbcsdk_mock.api + event_groups = list( + api.select(AuthEvents) + .where(process_pid=2000) + .group_results( + "device_name", + max_events_per_group=10, + rows=5, + start=0, + range_field="backend_timestamp", + range_duration="-2y" + ) + ) + # invoke get_details() on the first AuthEvents in the list + event_groups[0].auth_events[0].get_details() + assert event_groups[0].group_key is not None + assert event_groups[0]["group_key"] is not None + assert event_groups[0].auth_events[0]["process_pid"][0] == 764 diff --git a/src/tests/unit/fixtures/enterprise_edr/mock_auth_events.py b/src/tests/unit/fixtures/enterprise_edr/mock_auth_events.py new file mode 100644 index 000000000..fcb4a9baa --- /dev/null +++ b/src/tests/unit/fixtures/enterprise_edr/mock_auth_events.py @@ -0,0 +1,470 @@ +"""Mock responses for Auth Events queries.""" + +POST_AUTH_EVENTS_SEARCH_JOB_RESP = {"job_id": "62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs"} + +GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP = { + "results": [ + { + "auth_domain_name": "NT AUTHORITY", + "auth_event_action": "LOGON_SUCCESS", + "auth_remote_device": "-", + "auth_remote_port": 0, + "auth_username": "SYSTEM", + "backend_timestamp": "2023-01-13T17:19:01.013Z", + "childproc_count": 0, + "crossproc_count": 48, + "device_group_id": 0, + "device_id": 17686136, + "device_name": "test_name", + "device_policy_id": 20622246, + "device_timestamp": "2023-01-13T17:17:45.322Z", + "event_id": "DA9E269E-421D-469D-A212-9062888A02F4", + "filemod_count": 3, + "ingress_time": 1673630293265, + "modload_count": 1, + "netconn_count": 35, + "org_id": "ABCD1234", + "parent_guid": "ABCD1234-010dde78-00000260-00000000-1d9275de5e5b262", + "parent_pid": 608, + "process_guid": "ABCD1234-010dde78-00000308-00000000-1d9275de6169dd7", + "process_hash": [ + "15a556def233f112d127025ab51ac2d3", + "362ab9743ff5d0f95831306a780fc3e418990f535013c80212dd85cb88ef7427" + ], + "process_name": "c:\\windows\\system32\\lsass.exe", + "process_pid": [ + 776 + ], + "process_username": [ + "NT AUTHORITY\\SYSTEM" + ], + "regmod_count": 11, + "scriptload_count": 0, + "windows_event_id": 4624 + } + ], + "num_found": 1, + "num_available": 1, + "approximate_unaggregated": 1, + "num_aggregated": 1, + "contacted": 4, + "completed": 4 +} + + +GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP_0 = { + "results": [], + "num_found": 0, + "num_available": 0, + "approximate_unaggregated": 0, + "num_aggregated": 0, + "contacted": 0, + "completed": 0 +} + + +GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP_ZERO_COMP = { + "results": [], + "num_found": 0, + "num_available": 0, + "approximate_unaggregated": 0, + "num_aggregated": 0, + "contacted": 242, + "completed": 0 +} + + +GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_ZERO = { + "results": [], + "num_found": 0, + "num_available": 0, + "approximate_unaggregated": 0, + "num_aggregated": 0, + "contacted": 242, + "completed": 242 +} + + +# GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP_2 = { +# "num_found": 808, +# "num_available": 52, +# "contacted": 6, +# "completed": 6, +# "results": [ +# { +# "alert_category": ["OBSERVED"], +# "alert_id": None, +# "backend_timestamp": "2023-02-08T03:22:59.196Z", +# "device_group_id": 0, +# "device_id": 17482451, +# "device_name": "dev01-39x-1", +# "device_policy_id": 20792247, +# "device_timestamp": "2023-02-08T03:20:33.751Z", +# "enriched": True, +# "enriched_event_type": ["NETWORK"], +# "event_description": "The script", +# "event_id": "8fbccc2da75f11ed937ae3cb089984c6", +# "event_network_inbound": False, +# "event_network_local_ipv4": "10.203.105.21", +# "event_network_location": "Santa Clara,CA,United States", +# "event_network_protocol": "TCP", +# "event_network_remote_ipv4": "23.44.229.234", +# "event_network_remote_port": 80, +# "event_type": ["netconn"], +# "ingress_time": 1675826462036, +# "legacy": True, +# "observation_description": "The application firefox.exe invoked ", +# "observation_id": "8fbccc2da75f11ed937ae3cb089984c6:be6ff259-88e3-6286-789f-74defa192d2e", +# "observation_type": "CB_ANALYTICS", +# "org_id": "ABCD123456", +# "parent_guid": "ABCD123456-010ac2d3-00001c68-00000000-1d93b6c4d1f20ad", +# "parent_pid": 7272, +# "process_guid": "ABCD123456-010ac2d3-00001cf8-00000000-1d93b6c4d2b16a4", +# "process_hash": [ +# "9df1ec5e25919660a1b0b85d3965d55797b9aac81e028008428106c4dcda7b29" +# ], +# "process_name": "c:\\programdata\\mozilla-1de4eec8-1241-4177-a864-e594e8d1fb38\\updates", +# "process_pid": [2000], +# "process_username": ["DEV01-39X-1\\bit9qa"], +# }, +# { +# "alert_category": ["OBSERVED"], +# "alert_id": None, +# "backend_timestamp": "2023-02-08T03:22:59.196Z", +# "device_group_id": 0, +# "device_id": 17482451, +# "device_name": "dev01-39x-1", +# "device_policy_id": 20792247, +# "device_timestamp": "2023-02-08T03:20:33.751Z", +# "enriched": True, +# "enriched_event_type": ["NETWORK"], +# "event_description": "The script", +# "event_id": "8fbccc2da75f11ed937ae3cb089984c6", +# "event_network_inbound": False, +# "event_network_local_ipv4": "10.203.105.21", +# "event_network_location": "Santa Clara,CA,United States", +# "event_network_protocol": "TCP", +# "event_network_remote_ipv4": "23.44.229.234", +# "event_network_remote_port": 80, +# "event_type": ["netconn"], +# "ingress_time": 1675826462036, +# "legacy": True, +# "observation_description": "The application firefox.exe invoked ", +# "observation_id": "8fbccc2da75f11ed937ae3cb089984c6:be6ff259-88e3-6286-789f-74defa192d2e", +# "observation_type": "CB_ANALYTICS", +# "org_id": "ABCD123456", +# "parent_guid": "ABCD123456-010ac2d3-00001c68-00000000-1d93b6c4d1f20ad", +# "parent_pid": 7272, +# "process_guid": "ABCD123456-010ac2d3-00001cf8-00000000-1d93b6c4d2b16a4", +# "process_hash": [ +# "9df1ec5e25919660a1b0b85d3965d55797b9aac81e028008428106c4dcda7b29" +# ], +# "process_name": "c:\\programdata\\mozilla-1de4eec8-1241-4177-a864-e594e8d1fb38\\updates", +# "process_pid": [2000], +# "process_username": ["DEV01-39X-1\\bit9qa"], +# }, +# ], +# } +GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP_2 = { + "results": [ + { + "auth_domain_name": "NT AUTHORITY", + "auth_event_action": "LOGOFF_SUCCESS", + "auth_remote_port": 0, + "auth_username": "SYSTEM", + "backend_timestamp": "2023-03-08T08:18:52.654Z", + "childproc_count": 0, + "crossproc_count": 1859, + "device_group_id": 0, + "device_id": 18101914, + "device_name": "richm\\win11", + "device_policy_id": 20886205, + "device_timestamp": "2023-03-08T08:15:03.090Z", + "event_id": "9D137450-6428-446E-8C23-F0C526156A0C", + "filemod_count": 33, + "ingress_time": 1678263444460, + "modload_count": 7, + "netconn_count": 113, + "org_id": "ABCD1234", + "parent_guid": "ABCD1234-0114369a-000002ac-00000000-1d94538fb2b061e", + "parent_pid": 684, + "process_guid": "ABCD1234-0114369a-0000033c-00000000-1d94538fb4eea6c", + "process_hash": [ + "c0ba0caebf823de8f2ebf49eea9cc5e5", + "c72b9e35e307fefe59bacc3c65842e93b963f6c3732934061857cc773d6e2e5b" + ], + "process_name": "c:\\windows\\system32\\lsass.exe", + "process_pid": [ + 828 + ], + "process_username": [ + "NT AUTHORITY\\SYSTEM" + ], + "regmod_count": 42, + "scriptload_count": 0, + "windows_event_id": 4634 + }, + { + "auth_domain_name": "NT AUTHORITY", + "auth_event_action": "PRIVILEGES_GRANTED", + "auth_remote_port": 0, + "auth_username": "SYSTEM", + "backend_timestamp": "2023-03-08T08:18:52.654Z", + "childproc_count": 0, + "crossproc_count": 1859, + "device_group_id": 0, + "device_id": 18101914, + "device_name": "richm\\win11", + "device_policy_id": 20886205, + "device_timestamp": "2023-03-08T08:15:03.082Z", + "event_id": "D5A08829-041E-401E-9C14-F8FDFBC2EE63", + "filemod_count": 33, + "ingress_time": 1678263444460, + "modload_count": 7, + "netconn_count": 113, + "org_id": "ABCD1234", + "parent_guid": "ABCD1234-0114369a-000002ac-00000000-1d94538fb2b061e", + "parent_pid": 684, + "process_guid": "ABCD1234-0114369a-0000033c-00000000-1d94538fb4eea6c", + "process_hash": [ + "c0ba0caebf823de8f2ebf49eea9cc5e5", + "c72b9e35e307fefe59bacc3c65842e93b963f6c3732934061857cc773d6e2e5b" + ], + "process_name": "c:\\windows\\system32\\lsass.exe", + "process_pid": [ + 828 + ], + "process_username": [ + "NT AUTHORITY\\SYSTEM" + ], + "regmod_count": 42, + "scriptload_count": 0, + "windows_event_id": 4672 + } + ], + "num_found": 198, + "num_available": 198, + "approximate_unaggregated": 198, + "num_aggregated": 198, + "contacted": 241, + "completed": 241 +} + + +GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING = { + "num_found": 808, + "num_available": 1, + "contacted": 6, + "completed": 0, + "results": [], +} + + +GET_AUTH_EVENTS_DETAIL_JOB_RESULTS_RESP = { + "results": [ + { + "auth_cleartext_credentials_logon": False, + "auth_daemon_logon": False, + "auth_domain_name": "NT AUTHORITY", + "auth_elevated_token_logon": False, + "auth_event_action": "LOGON_SUCCESS", + "auth_failed_logon_count": 0, + "auth_impersonation_level": "IMPERSONATION_INVALID", + "auth_interactive_logon": False, + "auth_key_length": 0, + "auth_logon_id": "00000000-000003E7", + "auth_logon_type": 5, + "auth_package": "Negotiate", + "auth_remote_device": "-", + "auth_remote_logon": False, + "auth_remote_port": 0, + "auth_restricted_admin_logon": False, + "auth_user_id": "S-1-5-18", + "auth_username": "SYSTEM", + "auth_virtual_account_logon": False, + "backend_timestamp": "2023-02-23T14:31:09.058Z", + "childproc_count": 0, + "crossproc_count": 0, + "device_external_ip": "66.170.98.188", + "device_group_id": 0, + "device_id": 17853466, + "device_installed_by": "No user", + "device_internal_ip": "10.52.4.52", + "device_location": "UNKNOWN", + "device_name": "cbawtd\\w10cbws2thtplt", + "device_os": "WINDOWS", + "device_os_version": "Windows 10 x64", + "device_policy": "raj-test-monitor", + "device_policy_id": 20622246, + "device_sensor_version": "3.9.1.2451", + "device_target_priority": "MEDIUM", + "device_timestamp": "2023-02-23T14:29:03.588Z", + "document_guid": "19F5ah7QR8mTUjdqRvXm0w", + "event_id": "D06DC822-B25E-4162-A5A7-6166BFA9B8DF", + "event_report_code": "SUB_RPT_NONE", + "filemod_count": 0, + "ingress_time": 1677162610331, + "modload_count": 0, + "netconn_count": 0, + "org_id": "ABCD1234", + "parent_cmdline": "wininit.exe", + "parent_cmdline_length": 11, + "parent_effective_reputation": "LOCAL_WHITE", + "parent_effective_reputation_source": "IGNORE", + "parent_guid": "ABCD1234-01106c1a-0000025c-00000000-1d942ef2b31029a", + "parent_hash": [ + "9ef51c8ad595c5e2a123c06ad39fccd7", + "268ca325c8f12e68b6728ff24d6536030aab6e05603d0179033b1e51d8476d86" + ], + "parent_name": "c:\\windows\\system32\\wininit.exe", + "parent_pid": 604, + "parent_reputation": "TRUSTED_WHITE_LIST", + "process_cmdline": [ + "C:\\Windows\\system32\\lsass.exe" + ], + "process_cmdline_length": [ + 29 + ], + "process_effective_reputation": "LOCAL_WHITE", + "process_effective_reputation_source": "IGNORE", + "process_elevated": True, + "process_guid": "ABCD1234-01106c1a-000002fc-00000000-1d942ef2b618b15", + "process_hash": [ + "15a556def233f112d127025ab51ac2d3", + "362ab9743ff5d0f95831306a780fc3e418990f535013c80212dd85cb88ef7427" + ], + "process_integrity_level": "SYSTEM", + "process_name": "c:\\windows\\system32\\lsass.exe", + "process_pid": [ + 764 + ], + "process_reputation": "TRUSTED_WHITE_LIST", + "process_sha256": "362ab9743ff5d0f95831306a780fc3e418990f535013c80212dd85cb88ef7427", + "process_start_time": "2023-02-17T16:44:57.657Z", + "process_username": [ + "NT AUTHORITY\\SYSTEM" + ], + "regmod_count": 0, + "scriptload_count": 0, + "windows_event_id": 4624 + } + ], + "num_found": 1, + "num_available": 1, + "approximate_unaggregated": 1, + "num_aggregated": 1, + "contacted": 13, + "completed": 13 +} + + +"""Mocks for observations facet query testing.""" + + +POST_AUTH_EVENTS_FACET_SEARCH_JOB_RESP = { + "job_id": "62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs" +} + + +GET_AUTH_EVENTS_FACET_SEARCH_JOB_RESULTS_RESP_1 = { + "ranges": [ + { + "start": "2020-08-04T08:01:32.077Z", + "end": "2020-08-05T08:01:32.077Z", + "bucket_size": "+1HOUR", + "field": "device_timestamp", + "values": [{"total": 456, "name": "2020-08-04T08:01:32.077Z"}], + } + ], + "terms": [ + { + "values": [{"total": 116, "id": "chrome.exe", "name": "chrome.exe"}], + "field": "process_name", + } + ], + "num_found": 116, + "contacted": 34, + "completed": 34, +} + + +GET_AUTH_EVENTS_FACET_SEARCH_JOB_RESULTS_RESP_2 = { + "ranges": [], + "terms": [ + { + "values": [{"total": 116, "id": "chrome.exe", "name": "chrome.exe"}], + "field": "process_name", + } + ], + "num_found": 116, + "contacted": 34, + "completed": 34, +} + + +GET_AUTH_EVENTS_FACET_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING = { + "ranges": [], + "terms": [], + "num_found": 0, + "contacted": 34, + "completed": 0, +} + + +GET_AUTH_EVENTS_GROUPED_RESULTS_RESP = { + "group_results": [ + { + "group_key": "auth_username", + "group_value": "SYSTEM", + "group_start_timestamp": "2023-02-23T14:29:03.588Z", + "group_end_timestamp": "2023-03-07T11:11:21.593Z", + "results": [ + { + "auth_domain_name": "NT AUTHORITY", + "auth_event_action": "LOGOFF_SUCCESS", + "auth_remote_port": 0, + "auth_username": "SYSTEM", + "backend_timestamp": "2023-03-07T11:20:02.046Z", + "childproc_count": 0, + "crossproc_count": 1724, + "device_group_id": 0, + "device_id": 18101914, + "device_name": "richm\\win11", + "device_policy_id": 20886205, + "device_timestamp": "2023-03-07T11:11:21.593Z", + "event_id": "E8F7A1F9-72FC-4C5D-B8D2-113647B30D87", + "filemod_count": 31, + "ingress_time": 1678187557319, + "modload_count": 7, + "netconn_count": 112, + "org_id": "ABCD1234", + "parent_guid": "ABCD1234-0114369a-000002ac-00000000-1d94538fb2b061e", + "parent_pid": 684, + "process_guid": "ABCD1234-0114369a-0000033c-00000000-1d94538fb4eea6c", + "process_hash": [ + "c0ba0caebf823de8f2ebf49eea9cc5e5", + "c72b9e35e307fefe59bacc3c65842e93b963f6c3732934061857cc773d6e2e5b" + ], + "process_name": "c:\\windows\\system32\\lsass.exe", + "process_pid": [ + 828 + ], + "process_username": [ + "NT AUTHORITY\\SYSTEM" + ], + "regmod_count": 39, + "scriptload_count": 0, + "windows_event_id": 4634 + }, + ], + "total_events": 174 + } + ], + "num_found": 174, + "num_available": 174, + "groups_num_available": 1, + "approximate_unaggregated": 174, + "num_aggregated": 174, + "contacted": 169, + "completed": 169 +} From 1588e0ecf37aa431005263cefad96ca380d42b19 Mon Sep 17 00:00:00 2001 From: Hristo Karagitliev Date: Fri, 10 Mar 2023 11:41:23 +0200 Subject: [PATCH 112/143] add rtd --- docs/cbc_sdk.enterprise_edr.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/cbc_sdk.enterprise_edr.rst b/docs/cbc_sdk.enterprise_edr.rst index e19382eaf..daa95d77f 100644 --- a/docs/cbc_sdk.enterprise_edr.rst +++ b/docs/cbc_sdk.enterprise_edr.rst @@ -4,6 +4,14 @@ Enterprise EDR Submodules ---------- +cbc\_sdk.enterprise\_edr.auth\_events module +-------------------------------------------- + +.. automodule:: cbc_sdk.enterprise_edr.auth_events + :members: + :undoc-members: + :show-inheritance: + cbc\_sdk.enterprise\_edr.threat\_intelligence module ---------------------------------------------------- From 28b304bef5c7f3d9999b54b7524db5efdf00aaec Mon Sep 17 00:00:00 2001 From: Hristo Karagitliev Date: Fri, 10 Mar 2023 11:51:46 +0200 Subject: [PATCH 113/143] add group example in class --- src/cbc_sdk/enterprise_edr/auth_events.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/cbc_sdk/enterprise_edr/auth_events.py b/src/cbc_sdk/enterprise_edr/auth_events.py index 0ff97c85d..42c1c55c4 100644 --- a/src/cbc_sdk/enterprise_edr/auth_events.py +++ b/src/cbc_sdk/enterprise_edr/auth_events.py @@ -294,6 +294,11 @@ def __init__(self, cb, initial_data=None): - group_end_timestamp - group_key - group_value + Example: + >>> cb = CBCloudAPI(profile="example_profile") + >>> groups = set(cb.select(AuthEvents).where(process_pid=2000).group_results("device_name")) + >>> for group in groups: + >>> print(group._info) """ if not initial_data: raise InvalidObjectError("Cannot create object without initial data") From c9835d4b5a6092ad36d17fbc4eb6ac5a905db139 Mon Sep 17 00:00:00 2001 From: Hristo Karagitliev Date: Tue, 14 Mar 2023 12:38:08 +0200 Subject: [PATCH 114/143] add tests for suggestions and validations --- .../unit/enterprise_edr/test_auth_events.py | 28 +- .../enterprise_edr/mock_auth_events.py | 277 +++++++++++++----- 2 files changed, 223 insertions(+), 82 deletions(-) diff --git a/src/tests/unit/enterprise_edr/test_auth_events.py b/src/tests/unit/enterprise_edr/test_auth_events.py index 5717ad971..7a091bad7 100644 --- a/src/tests/unit/enterprise_edr/test_auth_events.py +++ b/src/tests/unit/enterprise_edr/test_auth_events.py @@ -24,7 +24,8 @@ GET_AUTH_EVENTS_FACET_SEARCH_JOB_RESULTS_RESP_1, GET_AUTH_EVENTS_FACET_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING, GET_AUTH_EVENTS_GROUPED_RESULTS_RESP, - # GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP, + AUTH_EVENTS_SEARCH_VALIDATIONS_RESP, + AUTH_EVENTS_SEARCH_SUGGESTIONS_RESP ) log = logging.basicConfig( @@ -950,3 +951,28 @@ def test_auth_events_select_group_results(cbcsdk_mock): assert event_groups[0].group_key is not None assert event_groups[0]["group_key"] is not None assert event_groups[0].auth_events[0]["process_pid"][0] == 764 + +def test_auth_events_search_validations(cbcsdk_mock): + """Tests getting auth_events search validations""" + api = cbcsdk_mock.api + q = 'q=auth_username' + cbcsdk_mock.mock_request( + "GET", + f"/api/investigate/v2/orgs/test/auth_events/search_validation?{q}", + AUTH_EVENTS_SEARCH_VALIDATIONS_RESP, + ) + result = api.select(AuthEvents).search_validation('auth_username') + assert result is True + +def test_auth_events_search_suggestions(cbcsdk_mock): + """Tests getting auth_events search suggestions""" + api = cbcsdk_mock.api + q = "suggest.q=auth" + cbcsdk_mock.mock_request( + "GET", + f"/api/investigate/v2/orgs/test/auth_events/search_suggestions?{q}", + AUTH_EVENTS_SEARCH_SUGGESTIONS_RESP, + ) + result = api.select(AuthEvents).search_suggestions('auth') + + assert len(result) != 0 diff --git a/src/tests/unit/fixtures/enterprise_edr/mock_auth_events.py b/src/tests/unit/fixtures/enterprise_edr/mock_auth_events.py index fcb4a9baa..fc1990f63 100644 --- a/src/tests/unit/fixtures/enterprise_edr/mock_auth_events.py +++ b/src/tests/unit/fixtures/enterprise_edr/mock_auth_events.py @@ -85,86 +85,6 @@ } -# GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP_2 = { -# "num_found": 808, -# "num_available": 52, -# "contacted": 6, -# "completed": 6, -# "results": [ -# { -# "alert_category": ["OBSERVED"], -# "alert_id": None, -# "backend_timestamp": "2023-02-08T03:22:59.196Z", -# "device_group_id": 0, -# "device_id": 17482451, -# "device_name": "dev01-39x-1", -# "device_policy_id": 20792247, -# "device_timestamp": "2023-02-08T03:20:33.751Z", -# "enriched": True, -# "enriched_event_type": ["NETWORK"], -# "event_description": "The script", -# "event_id": "8fbccc2da75f11ed937ae3cb089984c6", -# "event_network_inbound": False, -# "event_network_local_ipv4": "10.203.105.21", -# "event_network_location": "Santa Clara,CA,United States", -# "event_network_protocol": "TCP", -# "event_network_remote_ipv4": "23.44.229.234", -# "event_network_remote_port": 80, -# "event_type": ["netconn"], -# "ingress_time": 1675826462036, -# "legacy": True, -# "observation_description": "The application firefox.exe invoked ", -# "observation_id": "8fbccc2da75f11ed937ae3cb089984c6:be6ff259-88e3-6286-789f-74defa192d2e", -# "observation_type": "CB_ANALYTICS", -# "org_id": "ABCD123456", -# "parent_guid": "ABCD123456-010ac2d3-00001c68-00000000-1d93b6c4d1f20ad", -# "parent_pid": 7272, -# "process_guid": "ABCD123456-010ac2d3-00001cf8-00000000-1d93b6c4d2b16a4", -# "process_hash": [ -# "9df1ec5e25919660a1b0b85d3965d55797b9aac81e028008428106c4dcda7b29" -# ], -# "process_name": "c:\\programdata\\mozilla-1de4eec8-1241-4177-a864-e594e8d1fb38\\updates", -# "process_pid": [2000], -# "process_username": ["DEV01-39X-1\\bit9qa"], -# }, -# { -# "alert_category": ["OBSERVED"], -# "alert_id": None, -# "backend_timestamp": "2023-02-08T03:22:59.196Z", -# "device_group_id": 0, -# "device_id": 17482451, -# "device_name": "dev01-39x-1", -# "device_policy_id": 20792247, -# "device_timestamp": "2023-02-08T03:20:33.751Z", -# "enriched": True, -# "enriched_event_type": ["NETWORK"], -# "event_description": "The script", -# "event_id": "8fbccc2da75f11ed937ae3cb089984c6", -# "event_network_inbound": False, -# "event_network_local_ipv4": "10.203.105.21", -# "event_network_location": "Santa Clara,CA,United States", -# "event_network_protocol": "TCP", -# "event_network_remote_ipv4": "23.44.229.234", -# "event_network_remote_port": 80, -# "event_type": ["netconn"], -# "ingress_time": 1675826462036, -# "legacy": True, -# "observation_description": "The application firefox.exe invoked ", -# "observation_id": "8fbccc2da75f11ed937ae3cb089984c6:be6ff259-88e3-6286-789f-74defa192d2e", -# "observation_type": "CB_ANALYTICS", -# "org_id": "ABCD123456", -# "parent_guid": "ABCD123456-010ac2d3-00001c68-00000000-1d93b6c4d1f20ad", -# "parent_pid": 7272, -# "process_guid": "ABCD123456-010ac2d3-00001cf8-00000000-1d93b6c4d2b16a4", -# "process_hash": [ -# "9df1ec5e25919660a1b0b85d3965d55797b9aac81e028008428106c4dcda7b29" -# ], -# "process_name": "c:\\programdata\\mozilla-1de4eec8-1241-4177-a864-e594e8d1fb38\\updates", -# "process_pid": [2000], -# "process_username": ["DEV01-39X-1\\bit9qa"], -# }, -# ], -# } GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP_2 = { "results": [ { @@ -358,7 +278,7 @@ } -"""Mocks for observations facet query testing.""" +"""Mocks for auth events facet query testing.""" POST_AUTH_EVENTS_FACET_SEARCH_JOB_RESP = { @@ -468,3 +388,198 @@ "contacted": 169, "completed": 169 } + +AUTH_EVENTS_SEARCH_SUGGESTIONS_RESP = { + "suggestions": [ + { + "term": "auth_cleartext_credentials_logon", + "weight": 300, + "required_skus_all": [ + "auth" + ], + "required_skus_some": [] + }, + { + "term": "auth_daemon_logon", + "weight": 300, + "required_skus_all": [ + "auth" + ], + "required_skus_some": [] + }, + { + "term": "auth_domain_name", + "weight": 300, + "required_skus_all": [ + "auth" + ], + "required_skus_some": [] + }, + { + "term": "auth_elevated_token_logon", + "weight": 300, + "required_skus_all": [ + "auth" + ], + "required_skus_some": [] + }, + { + "term": "auth_event_action", + "weight": 300, + "required_skus_all": [ + "auth" + ], + "required_skus_some": [] + }, + { + "term": "auth_failed_logon_count", + "weight": 300, + "required_skus_all": [ + "auth" + ], + "required_skus_some": [] + }, + { + "term": "auth_failure_status", + "weight": 300, + "required_skus_all": [ + "auth" + ], + "required_skus_some": [] + }, + { + "term": "auth_failure_sub_status", + "weight": 300, + "required_skus_all": [ + "auth" + ], + "required_skus_some": [] + }, + { + "term": "auth_interactive_logon", + "weight": 300, + "required_skus_all": [ + "auth" + ], + "required_skus_some": [] + }, + { + "term": "auth_logon_id", + "weight": 300, + "required_skus_all": [ + "auth" + ], + "required_skus_some": [] + }, + { + "term": "auth_logon_type", + "weight": 300, + "required_skus_all": [ + "auth" + ], + "required_skus_some": [] + }, + { + "term": "auth_privileges", + "weight": 300, + "required_skus_all": [ + "auth" + ], + "required_skus_some": [] + }, + { + "term": "auth_remote_device", + "weight": 300, + "required_skus_all": [ + "auth" + ], + "required_skus_some": [] + }, + { + "term": "auth_remote_ipv4", + "weight": 300, + "required_skus_all": [ + "auth" + ], + "required_skus_some": [] + }, + { + "term": "auth_remote_ipv6", + "weight": 300, + "required_skus_all": [ + "auth" + ], + "required_skus_some": [] + }, + { + "term": "auth_remote_location", + "weight": 300, + "required_skus_all": [ + "auth" + ], + "required_skus_some": [] + }, + { + "term": "auth_remote_logon", + "weight": 300, + "required_skus_all": [ + "auth" + ], + "required_skus_some": [] + }, + { + "term": "auth_remote_port", + "weight": 300, + "required_skus_all": [ + "auth" + ], + "required_skus_some": [] + }, + { + "term": "auth_restricted_admin_logon", + "weight": 300, + "required_skus_all": [ + "auth" + ], + "required_skus_some": [] + }, + { + "term": "auth_user_id", + "weight": 300, + "required_skus_all": [ + "auth" + ], + "required_skus_some": [] + }, + { + "term": "auth_user_principal_name", + "weight": 300, + "required_skus_all": [ + "auth" + ], + "required_skus_some": [] + }, + { + "term": "auth_username", + "weight": 300, + "required_skus_all": [ + "auth" + ], + "required_skus_some": [] + }, + { + "term": "auth_virtual_account_logon", + "weight": 300, + "required_skus_all": [ + "auth" + ], + "required_skus_some": [] + } + ] +} + + +AUTH_EVENTS_SEARCH_VALIDATIONS_RESP = { + "valid": True, + "value_search_query": True +} From 707db8be7ea3e4189172b479edf10017943605d1 Mon Sep 17 00:00:00 2001 From: Hristo Karagitliev Date: Tue, 14 Mar 2023 12:29:42 +0200 Subject: [PATCH 115/143] add tests for suggestions and validations --- .../enterprise_edr/models/auth_events.yaml | 82 +++++++++++++++++++ .../enterprise_edr/mock_auth_events.py | 1 - 2 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 src/cbc_sdk/enterprise_edr/models/auth_events.yaml diff --git a/src/cbc_sdk/enterprise_edr/models/auth_events.yaml b/src/cbc_sdk/enterprise_edr/models/auth_events.yaml new file mode 100644 index 000000000..7cddf5b18 --- /dev/null +++ b/src/cbc_sdk/enterprise_edr/models/auth_events.yaml @@ -0,0 +1,82 @@ +type: object +properties: + alert_category: + type: array + items: + type: string + alert_id: + type: array + items: + type: string + backend_timestamp: + type: string + device_group_id: + type: integer + device_id: + type: integer + device_name: + type: string + device_policy: + type: string + device_policy_id: + type: integer + device_timestamp: + type: string + enriched: + type: boolean + enriched_event_type: + type: string + event_description: + type: string + event_id: + type: string + event_network_inbound: + type: boolean + event_network_local_ipv4: + type: string + event_network_location: + type: string + event_network_protocol: + type: string + event_network_remote_ipv4: + type: string + event_network_remote_port: + type: integer + event_type: + type: array + items: + type: string + ingress_time: + type: integer + legacy: + type: boolean + observation_description: + type: string + observation_id: + type: string + job_id: + type: string + observation_type: + type: string + org_id: + type: string + parent_guid: + type: string + parent_pid: + type: integer + process_guid: + type: string + process_hash: + type: array + items: + type: string + process_name: + type: string + process_pid: + type: array + items: + type: integer + process_username: + type: array + items: + type: string diff --git a/src/tests/unit/fixtures/enterprise_edr/mock_auth_events.py b/src/tests/unit/fixtures/enterprise_edr/mock_auth_events.py index fc1990f63..ac290f044 100644 --- a/src/tests/unit/fixtures/enterprise_edr/mock_auth_events.py +++ b/src/tests/unit/fixtures/enterprise_edr/mock_auth_events.py @@ -578,7 +578,6 @@ ] } - AUTH_EVENTS_SEARCH_VALIDATIONS_RESP = { "valid": True, "value_search_query": True From 5470f49bdd651155330614f344d5197f8672daae Mon Sep 17 00:00:00 2001 From: Hristo Karagitliev <74309356+hkaragitliev-cb@users.noreply.github.com> Date: Tue, 14 Mar 2023 12:40:07 +0200 Subject: [PATCH 116/143] Delete auth_events.yaml --- .../enterprise_edr/models/auth_events.yaml | 82 ------------------- 1 file changed, 82 deletions(-) delete mode 100644 src/cbc_sdk/enterprise_edr/models/auth_events.yaml diff --git a/src/cbc_sdk/enterprise_edr/models/auth_events.yaml b/src/cbc_sdk/enterprise_edr/models/auth_events.yaml deleted file mode 100644 index 7cddf5b18..000000000 --- a/src/cbc_sdk/enterprise_edr/models/auth_events.yaml +++ /dev/null @@ -1,82 +0,0 @@ -type: object -properties: - alert_category: - type: array - items: - type: string - alert_id: - type: array - items: - type: string - backend_timestamp: - type: string - device_group_id: - type: integer - device_id: - type: integer - device_name: - type: string - device_policy: - type: string - device_policy_id: - type: integer - device_timestamp: - type: string - enriched: - type: boolean - enriched_event_type: - type: string - event_description: - type: string - event_id: - type: string - event_network_inbound: - type: boolean - event_network_local_ipv4: - type: string - event_network_location: - type: string - event_network_protocol: - type: string - event_network_remote_ipv4: - type: string - event_network_remote_port: - type: integer - event_type: - type: array - items: - type: string - ingress_time: - type: integer - legacy: - type: boolean - observation_description: - type: string - observation_id: - type: string - job_id: - type: string - observation_type: - type: string - org_id: - type: string - parent_guid: - type: string - parent_pid: - type: integer - process_guid: - type: string - process_hash: - type: array - items: - type: string - process_name: - type: string - process_pid: - type: array - items: - type: integer - process_username: - type: array - items: - type: string From 8d3c254bd1f333ea9cac534ddafd3f91f35d338d Mon Sep 17 00:00:00 2001 From: Hristo Karagitliev Date: Tue, 14 Mar 2023 13:43:31 +0200 Subject: [PATCH 117/143] rename AuthEvents class to AuthEvent --- src/cbc_sdk/enterprise_edr/__init__.py | 2 +- src/cbc_sdk/enterprise_edr/auth_events.py | 104 ++-- .../unit/enterprise_edr/test_auth_events.py | 460 +++++++++--------- .../enterprise_edr/mock_auth_events.py | 30 +- 4 files changed, 298 insertions(+), 298 deletions(-) diff --git a/src/cbc_sdk/enterprise_edr/__init__.py b/src/cbc_sdk/enterprise_edr/__init__.py index 0b9b4da37..5d3c006a4 100644 --- a/src/cbc_sdk/enterprise_edr/__init__.py +++ b/src/cbc_sdk/enterprise_edr/__init__.py @@ -7,4 +7,4 @@ WatchlistQuery) from cbc_sdk.enterprise_edr.ubs import Binary, Downloads -from cbc_sdk.enterprise_edr.auth_events import AuthEvents, AuthEventsFacet +from cbc_sdk.enterprise_edr.auth_events import AuthEvent, AuthEventFacet diff --git a/src/cbc_sdk/enterprise_edr/auth_events.py b/src/cbc_sdk/enterprise_edr/auth_events.py index 42c1c55c4..c19803ed8 100644 --- a/src/cbc_sdk/enterprise_edr/auth_events.py +++ b/src/cbc_sdk/enterprise_edr/auth_events.py @@ -21,8 +21,8 @@ log = logging.getLogger(__name__) -class AuthEvents(NewBaseModel): - """Represents an AuthEvents""" +class AuthEvent(NewBaseModel): + """Represents an AuthEvent""" primary_key = "event_id" swagger_meta_file = "enterprise_edr/models/auth_events.yaml" @@ -36,7 +36,7 @@ def __init__( full_doc=False, ): """ - Initialize the AuthEvents object. + Initialize the AuthEvent object. Required RBAC Permissions: org.search.events (CREATE, READ) @@ -50,21 +50,21 @@ def __init__( Example: >>> cb = CBCloudAPI(profile="example_profile") - >>> events = cb.select(AuthEvents).where("auth_username:SYSTEM") + >>> events = cb.select(AuthEvent).where("auth_username:SYSTEM") >>> print(*events) """ self._details_timeout = 0 self._info = None if model_unique_id is not None and initial_data is None: auth_events_future = ( - cb.select(AuthEvents) + cb.select(AuthEvent) .where(event_id=model_unique_id) .execute_async() ) result = auth_events_future.result() if len(result) == 1: initial_data = result[0] - super(AuthEvents, self).__init__( + super(AuthEvent, self).__init__( cb, model_unique_id=model_unique_id, initial_data=initial_data, @@ -74,7 +74,7 @@ def __init__( def _refresh(self): """ - Refreshes the AuthEvents object from the server by getting the details. + Refreshes the AuthEvent object from the server by getting the details. Required RBAC Permissions: org.search.events (READ) @@ -97,17 +97,17 @@ def _query_implementation(self, cb, **kwargs): Returns: Query: The query object for this Auth Event. """ - return AuthEventsQuery(self, cb) + return AuthEventQuery(self, cb) def get_details(self, timeout=0, async_mode=False): """Requests detailed results. Args: - timeout (int): AuthEvents details request timeout in milliseconds. + timeout (int): AuthEvent details request timeout in milliseconds. async_mode (bool): True to request details in an asynchronous manner. Returns: - AuthEvents: Auth Events object enriched with the details fields + AuthEvent: Auth Events object enriched with the details fields Note: - When using asynchronous mode, this method returns a python future. @@ -116,10 +116,10 @@ def get_details(self, timeout=0, async_mode=False): Examples: >>> cb = CBCloudAPI(profile="example_profile") - >>> event = cb.select(AuthEvents, "example-auth-event-id") + >>> event = cb.select(AuthEvent, "example-auth-event-id") >>> print(event.get_details()) - >>> events = cb.select(AuthEvents).where(process_pid=2000) + >>> events = cb.select(AuthEvent).where(process_pid=2000) >>> print(events[0].get_details()) """ self._details_timeout = timeout @@ -179,13 +179,13 @@ def _get_detailed_results(self): ) -class AuthEventsFacet(UnrefreshableModel): +class AuthEventFacet(UnrefreshableModel): """ - Represents an AuthEvents facet retrieved. + Represents an AuthEvent facet retrieved. Example: >>> cb = CBCloudAPI(profile="example_profile") - >>> events_facet = cb.select(AuthEventsFacet).where("auth_username:SYSTEM").add_facet_field("process_name") + >>> events_facet = cb.select(AuthEventFacet).where("auth_username:SYSTEM").add_facet_field("process_name") >>> print(events_facet.results) """ @@ -195,11 +195,11 @@ class AuthEventsFacet(UnrefreshableModel): result_url = "/api/investigate/v2/orgs/{}/auth_events/facet_jobs/{}/results" class Terms(UnrefreshableModel): - """Represents the facet fields and values associated with an AuthEvents Facet query.""" + """Represents the facet fields and values associated with an AuthEvent Facet query.""" def __init__(self, cb, initial_data): - """Initialize an AuthEventsFacet Terms object with initial_data.""" - super(AuthEventsFacet.Terms, self).__init__( + """Initialize an AuthEventFacet Terms object with initial_data.""" + super(AuthEventFacet.Terms, self).__init__( cb, model_unique_id=None, initial_data=initial_data, @@ -223,11 +223,11 @@ def fields(self): return [field for field in self._facets] class Ranges(UnrefreshableModel): - """Represents the range (bucketed) facet fields and values associated with an AuthEvents Facet query.""" + """Represents the range (bucketed) facet fields and values associated with an AuthEvent Facet query.""" def __init__(self, cb, initial_data): - """Initialize an AuthEventsFacet Ranges object with initial_data.""" - super(AuthEventsFacet.Ranges, self).__init__( + """Initialize an AuthEventFacet Ranges object with initial_data.""" + super(AuthEventFacet.Ranges, self).__init__( cb, model_unique_id=None, initial_data=initial_data, @@ -242,7 +242,7 @@ def __init__(self, cb, initial_data): @property def facets(self): - """Returns the reified `AuthEventsFacet.Terms._facets` for this result.""" + """Returns the reified `AuthEventFacet.Terms._facets` for this result.""" return self._facets @property @@ -257,32 +257,32 @@ def _query_implementation(self, cb, **kwargs): def __init__(self, cb, model_unique_id, initial_data): """Initialize the Terms object with initial data.""" - super(AuthEventsFacet, self).__init__( + super(AuthEventFacet, self).__init__( cb, model_unique_id=model_unique_id, initial_data=initial_data, force_init=False, full_doc=True, ) - self._terms = AuthEventsFacet.Terms(cb, initial_data=initial_data["terms"]) - self._ranges = AuthEventsFacet.Ranges(cb, initial_data=initial_data["ranges"]) + self._terms = AuthEventFacet.Terms(cb, initial_data=initial_data["terms"]) + self._ranges = AuthEventFacet.Ranges(cb, initial_data=initial_data["ranges"]) @property def terms_(self): - """Returns the reified `AuthEventsFacet.Terms` for this result.""" + """Returns the reified `AuthEventFacet.Terms` for this result.""" return self._terms @property def ranges_(self): - """Returns the reified `AuthEventsFacet.Ranges` for this result.""" + """Returns the reified `AuthEventFacet.Ranges` for this result.""" return self._ranges -class AuthEventsGroup: - """Represents AuthEventsGroup""" +class AuthEventGroup: + """Represents AuthEventGroup""" def __init__(self, cb, initial_data=None): """ - Initialize AuthEventsGroup object + Initialize AuthEventGroup object Args: cb (CBCloudAPI): A reference to the CBCloudAPI object. @@ -296,7 +296,7 @@ def __init__(self, cb, initial_data=None): - group_value Example: >>> cb = CBCloudAPI(profile="example_profile") - >>> groups = set(cb.select(AuthEvents).where(process_pid=2000).group_results("device_name")) + >>> groups = set(cb.select(AuthEvent).where(process_pid=2000).group_results("device_name")) >>> for group in groups: >>> print(group._info) """ @@ -304,7 +304,7 @@ def __init__(self, cb, initial_data=None): raise InvalidObjectError("Cannot create object without initial data") self._info = initial_data self._cb = cb - self.auth_events = [AuthEvents(cb, initial_data=x) for x in initial_data.get("results", [])] + self.auth_events = [AuthEvent(cb, initial_data=x) for x in initial_data.get("results", [])] def __getattr__(self, item): """ @@ -320,7 +320,7 @@ def __getattr__(self, item): AttributeError: If the object has no such attribute. """ try: - super(AuthEventsGroup, self).__getattribute__(item) + super(AuthEventGroup, self).__getattribute__(item) except AttributeError: pass # fall through to the rest of the logic... @@ -344,7 +344,7 @@ def __getitem__(self, item): AttributeError: If the object has no such attribute. """ try: - super(AuthEventsGroup, self).__getattribute__(item) + super(AuthEventGroup, self).__getattribute__(item) except AttributeError: pass # fall through to the rest of the logic... @@ -355,8 +355,8 @@ def __getitem__(self, item): item)) -class AuthEventsQuery(Query): - """Represents the query logic for an AuthEvents query. +class AuthEventQuery(Query): + """Represents the query logic for an AuthEvent query. This class specializes `Query` to handle the particulars of Auth Events querying. """ @@ -376,7 +376,7 @@ class AuthEventsQuery(Query): def __init__(self, doc_class, cb): """ - Initialize the AuthEventsQuery object. + Initialize the AuthEventQuery object. Args: doc_class (class): The class of the model this query returns. @@ -384,10 +384,10 @@ def __init__(self, doc_class, cb): Example: >>> cb = CBCloudAPI(profile="example_profile") - >>> events = cb.select(AuthEvents).where("auth_username:SYSTEM") + >>> events = cb.select(AuthEvent).where("auth_username:SYSTEM") >>> print(*events) """ - super(AuthEventsQuery, self).__init__(doc_class, cb) + super(AuthEventQuery, self).__init__(doc_class, cb) self._default_args["rows"] = self._batch_size self._query_token = None self._timeout = 0 @@ -395,13 +395,13 @@ def __init__(self, doc_class, cb): def or_(self, **kwargs): """ - :meth:`or_` criteria are explicitly provided to AuthEvents queries. + :meth:`or_` criteria are explicitly provided to AuthEvent queries. This method overrides the base class in order to provide or_() functionality rather than raising an exception. Example: >>> cb = CBCloudAPI(profile="example_profile") - >>> events = cb.select(AuthEvents).where(process_name="chrome.exe").or_(process_name="firefox.exe") + >>> events = cb.select(AuthEvent).where(process_name="chrome.exe").or_(process_name="firefox.exe") >>> print(*events) """ self._query_builder.or_(None, **kwargs) @@ -415,18 +415,18 @@ def set_rows(self, rows): rows (int): How many rows to request. Returns: - Query: AuthEventsQuery object + Query: AuthEventQuery object Example: >>> cb = CBCloudAPI(profile="example_profile") - >>> events = cb.select(AuthEvents).where(process_name="chrome.exe").set_rows(5) + >>> events = cb.select(AuthEvent).where(process_name="chrome.exe").set_rows(5) >>> print(*events) """ if not isinstance(rows, int): raise ApiError(f"Rows must be an integer. {rows} is a {type(rows)}.") if rows > 10000: raise ApiError("Maximum allowed value for rows is 10000") - super(AuthEventsQuery, self).set_rows(rows) + super(AuthEventQuery, self).set_rows(rows) return self def timeout(self, msecs): @@ -436,12 +436,12 @@ def timeout(self, msecs): msecs (int): Timeout duration, in milliseconds. Returns: - Query (AuthEventsQuery): The Query object with new milliseconds + Query (AuthEventQuery): The Query object with new milliseconds parameter. Example: >>> cb = CBCloudAPI(profile="example_profile") - >>> events = cb.select(AuthEvents).where(process_name="chrome.exe").timeout(5000) + >>> events = cb.select(AuthEvent).where(process_name="chrome.exe").timeout(5000) >>> print(*events) """ self._timeout = msecs @@ -589,7 +589,7 @@ def group_results( Examples: >>> cb = CBCloudAPI(profile="example_profile") - >>> groups = set(cb.select(AuthEvents).where(process_pid=2000).group_results("device_name")) + >>> groups = set(cb.select(AuthEvent).where(process_pid=2000).group_results("device_name")) >>> for group in groups: >>> print(group._info) """ @@ -599,7 +599,7 @@ def group_results( if isinstance(fields, str): fields = [fields] - if not all((gf in AuthEventsQuery.VALID_GROUP_FIELDS) for gf in fields): + if not all((gf in AuthEventQuery.VALID_GROUP_FIELDS) for gf in fields): raise ApiError("One or more invalid aggregation fields") if not self._query_token: @@ -636,7 +636,7 @@ def group_results( still_fetching = False for group in result.get("group_results", []): - yield AuthEventsGroup(self._cb, initial_data=group) + yield AuthEventGroup(self._cb, initial_data=group) def get_auth_events_descriptions(self): """ @@ -646,7 +646,7 @@ def get_auth_events_descriptions(self): dict: Descriptions and status messages of Auth Events as dict objects. Example: >>> cb = CBCloudAPI(profile="example_profile") - >>> descriptions = cb.select(AuthEvents).get_auth_events_descriptions() + >>> descriptions = cb.select(AuthEvent).get_auth_events_descriptions() >>> print(descriptions) """ url = "/api/investigate/v2/orgs/{}/auth_events/descriptions".format(self._cb.credentials.org_key) @@ -665,7 +665,7 @@ def search_suggestions(self, query, count=None): list: A list of search suggestions expressed as dict objects. Example: >>> cb = CBCloudAPI(profile="example_profile") - >>> suggestions = cb.select(AuthEvents).search_suggestions('auth') + >>> suggestions = cb.select(AuthEvent).search_suggestions('auth') >>> print(suggestions) """ query_params = {"suggest.q": query} @@ -686,7 +686,7 @@ def search_validation(self, query): bool: Status of the validation Example: >>> cb = CBCloudAPI(profile="example_profile") - >>> validation = cb.select(AuthEvents).search_validation('auth_username:Administrator') + >>> validation = cb.select(AuthEvent).search_validation('auth_username:Administrator') >>> print(validation) """ query_params = {"q": query} diff --git a/src/tests/unit/enterprise_edr/test_auth_events.py b/src/tests/unit/enterprise_edr/test_auth_events.py index 7a091bad7..22efadcd1 100644 --- a/src/tests/unit/enterprise_edr/test_auth_events.py +++ b/src/tests/unit/enterprise_edr/test_auth_events.py @@ -1,31 +1,31 @@ -"""Testing AuthEvents objects of cbc_sdk.enterprise_edr""" +"""Testing AuthEvent objects of cbc_sdk.enterprise_edr""" import pytest import logging from cbc_sdk.base import FacetQuery -from cbc_sdk.enterprise_edr import AuthEvents -from cbc_sdk.enterprise_edr.auth_events import AuthEventsQuery, AuthEventsFacet +from cbc_sdk.enterprise_edr import AuthEvent +from cbc_sdk.enterprise_edr.auth_events import AuthEventQuery, AuthEventFacet from cbc_sdk.rest_api import CBCloudAPI from cbc_sdk.errors import ApiError, TimeoutError from tests.unit.fixtures.CBCSDKMock import CBCSDKMock from tests.unit.fixtures.enterprise_edr.mock_auth_events import ( - POST_AUTH_EVENTS_SEARCH_JOB_RESP, - GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP, - GET_AUTH_EVENTS_DETAIL_JOB_RESULTS_RESP, - GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP_ZERO_COMP, - GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_ZERO, - GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING, - GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP_2, - GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP_0, - POST_AUTH_EVENTS_FACET_SEARCH_JOB_RESP, - GET_AUTH_EVENTS_FACET_SEARCH_JOB_RESULTS_RESP_2, - GET_AUTH_EVENTS_FACET_SEARCH_JOB_RESULTS_RESP_1, - GET_AUTH_EVENTS_FACET_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING, - GET_AUTH_EVENTS_GROUPED_RESULTS_RESP, - AUTH_EVENTS_SEARCH_VALIDATIONS_RESP, - AUTH_EVENTS_SEARCH_SUGGESTIONS_RESP + POST_AUTH_EVENT_SEARCH_JOB_RESP, + GET_AUTH_EVENT_SEARCH_JOB_RESULTS_RESP, + GET_AUTH_EVENT_DETAIL_JOB_RESULTS_RESP, + GET_AUTH_EVENT_SEARCH_JOB_RESULTS_RESP_ZERO_COMP, + GET_AUTH_EVENT_SEARCH_JOB_RESULTS_ZERO, + GET_AUTH_EVENT_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING, + GET_AUTH_EVENT_SEARCH_JOB_RESULTS_RESP_2, + GET_AUTH_EVENT_SEARCH_JOB_RESULTS_RESP_0, + POST_AUTH_EVENT_FACET_SEARCH_JOB_RESP, + GET_AUTH_EVENT_FACET_SEARCH_JOB_RESULTS_RESP_2, + GET_AUTH_EVENT_FACET_SEARCH_JOB_RESULTS_RESP_1, + GET_AUTH_EVENT_FACET_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING, + GET_AUTH_EVENT_GROUPED_RESULTS_RESP, + AUTH_EVENT_SEARCH_VALIDATIONS_RESP, + AUTH_EVENT_SEARCH_SUGGESTIONS_RESP ) log = logging.basicConfig( @@ -52,108 +52,108 @@ def cbcsdk_mock(monkeypatch, cb): # ==================================== UNIT TESTS BELOW ==================================== -def test_auth_events_select_where(cbcsdk_mock): - """Testing AuthEvents Querying with select()""" +def test_auth_event_select_where(cbcsdk_mock): + """Testing AuthEvent Querying with select()""" cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/auth_events/search_jobs", - POST_AUTH_EVENTS_SEARCH_JOB_RESP, + POST_AUTH_EVENT_SEARCH_JOB_RESP, ) cbcsdk_mock.mock_request( "GET", "/api/investigate/v2/orgs/test/auth_events/search_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 - GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP, + GET_AUTH_EVENT_SEARCH_JOB_RESULTS_RESP, ) cbcsdk_mock.mock_request( "GET", "/api/investigate/v2/orgs/test/auth_events/search_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results?start=0&rows=500", # noqa: E501 - GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP, + GET_AUTH_EVENT_SEARCH_JOB_RESULTS_RESP, ) api = cbcsdk_mock.api - events_list = api.select(AuthEvents).where("auth_username:SYSTEM") + events_list = api.select(AuthEvent).where("auth_username:SYSTEM") for event in events_list: assert event.device_name is not None -def test_auth_events_select_async(cbcsdk_mock): - """Testing AuthEvents Querying with select() - asynchronous way""" +def test_auth_event_select_async(cbcsdk_mock): + """Testing AuthEvent Querying with select() - asynchronous way""" cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/auth_events/search_jobs", - POST_AUTH_EVENTS_SEARCH_JOB_RESP, + POST_AUTH_EVENT_SEARCH_JOB_RESP, ) cbcsdk_mock.mock_request( "GET", "/api/investigate/v2/orgs/test/auth_events/search_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 - GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP, + GET_AUTH_EVENT_SEARCH_JOB_RESULTS_RESP, ) cbcsdk_mock.mock_request( "GET", "/api/investigate/v2/orgs/test/auth_events/search_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results?start=0&rows=500", # noqa: E501 - GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP, + GET_AUTH_EVENT_SEARCH_JOB_RESULTS_RESP, ) api = cbcsdk_mock.api - events_list = api.select(AuthEvents).where(event_id="DA9E269E-421D-469D-A212-9062888A02F4").execute_async() + events_list = api.select(AuthEvent).where(event_id="DA9E269E-421D-469D-A212-9062888A02F4").execute_async() for event in events_list.result(): assert event["device_name"] is not None -def test_auth_events_select_by_id(cbcsdk_mock): - """Testing AuthEvents Querying with select() - asynchronous way""" +def test_auth_event_select_by_id(cbcsdk_mock): + """Testing AuthEvent Querying with select() - asynchronous way""" cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/auth_events/search_jobs", - POST_AUTH_EVENTS_SEARCH_JOB_RESP, + POST_AUTH_EVENT_SEARCH_JOB_RESP, ) cbcsdk_mock.mock_request( "GET", "/api/investigate/v2/orgs/test/auth_events/search_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 - GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP, + GET_AUTH_EVENT_SEARCH_JOB_RESULTS_RESP, ) cbcsdk_mock.mock_request( "GET", "/api/investigate/v2/orgs/test/auth_events/search_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results?start=0&rows=500", # noqa: E501 - GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP, + GET_AUTH_EVENT_SEARCH_JOB_RESULTS_RESP, ) api = cbcsdk_mock.api - auth_events = api.select(AuthEvents, "DA9E269E-421D-469D-A212-9062888A02F4") + auth_events = api.select(AuthEvent, "DA9E269E-421D-469D-A212-9062888A02F4") assert auth_events["device_name"] is not None -def test_auth_events_select_details_async(cbcsdk_mock): - """Testing AuthEvents Querying with get_details - asynchronous mode""" +def test_auth_event_select_details_async(cbcsdk_mock): + """Testing AuthEvent Querying with get_details - asynchronous mode""" cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/auth_events/search_jobs", - POST_AUTH_EVENTS_SEARCH_JOB_RESP, + POST_AUTH_EVENT_SEARCH_JOB_RESP, ) cbcsdk_mock.mock_request( "GET", "/api/investigate/v2/orgs/test/auth_events/search_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 - GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP, + GET_AUTH_EVENT_SEARCH_JOB_RESULTS_RESP, ) cbcsdk_mock.mock_request( "GET", "/api/investigate/v2/orgs/test/auth_events/search_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results?start=0&rows=500", # noqa: E501 - GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP, + GET_AUTH_EVENT_SEARCH_JOB_RESULTS_RESP, ) cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/auth_events/detail_jobs", - POST_AUTH_EVENTS_SEARCH_JOB_RESP, + POST_AUTH_EVENT_SEARCH_JOB_RESP, ) cbcsdk_mock.mock_request( "GET", "/api/investigate/v2/orgs/test/auth_events/detail_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 - GET_AUTH_EVENTS_DETAIL_JOB_RESULTS_RESP, + GET_AUTH_EVENT_DETAIL_JOB_RESULTS_RESP, ) api = cbcsdk_mock.api - # events_list = api.select(AuthEvents).where(process_pid=2000) - events_list = api.select(AuthEvents).where(event_id="DA9E269E-421D-469D-A212-9062888A02F4") + # events_list = api.select(AuthEvent).where(process_pid=2000) + events_list = api.select(AuthEvent).where(event_id="DA9E269E-421D-469D-A212-9062888A02F4") events = events_list[0] details = events.get_details(async_mode=True, timeout=500) results = details.result() @@ -164,76 +164,76 @@ def test_auth_events_select_details_async(cbcsdk_mock): assert results["process_pid"][0] == 764 -def test_auth_events_details_only(cbcsdk_mock): - """Testing AuthEvents with get_details - just the get_details REST API calls""" +def test_auth_event_details_only(cbcsdk_mock): + """Testing AuthEvent with get_details - just the get_details REST API calls""" cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/auth_events/detail_jobs", - POST_AUTH_EVENTS_SEARCH_JOB_RESP, + POST_AUTH_EVENT_SEARCH_JOB_RESP, ) cbcsdk_mock.mock_request( "GET", "/api/investigate/v2/orgs/test/auth_events/detail_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 - GET_AUTH_EVENTS_DETAIL_JOB_RESULTS_RESP, + GET_AUTH_EVENT_DETAIL_JOB_RESULTS_RESP, ) api = cbcsdk_mock.api - events = AuthEvents(api, initial_data={"event_id": "D06DC822-B25E-4162-A5A7-6166BFA9B8DF"}) + events = AuthEvent(api, initial_data={"event_id": "D06DC822-B25E-4162-A5A7-6166BFA9B8DF"}) results = events._get_detailed_results() assert results._info["device_name"] is not None assert results._info["process_pid"][0] == 764 -def test_auth_events_details_timeout(cbcsdk_mock): - """Testing AuthEvents get_details() timeout handling""" +def test_auth_event_details_timeout(cbcsdk_mock): + """Testing AuthEvent get_details() timeout handling""" cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/auth_events/detail_jobs", - POST_AUTH_EVENTS_SEARCH_JOB_RESP, + POST_AUTH_EVENT_SEARCH_JOB_RESP, ) cbcsdk_mock.mock_request( "GET", "/api/investigate/v2/orgs/test/auth_events/detail_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 - GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP_ZERO_COMP, + GET_AUTH_EVENT_SEARCH_JOB_RESULTS_RESP_ZERO_COMP, ) api = cbcsdk_mock.api - events = AuthEvents(api, initial_data={"event_id": "D06DC822-B25E-4162-A5A7-6166BFA9B8DF"}) + events = AuthEvent(api, initial_data={"event_id": "D06DC822-B25E-4162-A5A7-6166BFA9B8DF"}) events._details_timeout = 1 with pytest.raises(TimeoutError): events._get_detailed_results() -def test_auth_events_select_details_sync(cbcsdk_mock): - """Testing AuthEvents Querying with get_details""" +def test_auth_event_select_details_sync(cbcsdk_mock): + """Testing AuthEvent Querying with get_details""" cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/auth_events/search_jobs", - POST_AUTH_EVENTS_SEARCH_JOB_RESP, + POST_AUTH_EVENT_SEARCH_JOB_RESP, ) cbcsdk_mock.mock_request( "GET", "/api/investigate/v2/orgs/test/auth_events/search_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 - GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP, + GET_AUTH_EVENT_SEARCH_JOB_RESULTS_RESP, ) cbcsdk_mock.mock_request( "GET", "/api/investigate/v2/orgs/test/auth_events/search_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results?start=0&rows=500", # noqa: E501 - GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP, + GET_AUTH_EVENT_SEARCH_JOB_RESULTS_RESP, ) cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/auth_events/detail_jobs", - POST_AUTH_EVENTS_SEARCH_JOB_RESP, + POST_AUTH_EVENT_SEARCH_JOB_RESP, ) cbcsdk_mock.mock_request( "GET", "/api/investigate/v2/orgs/test/auth_events/detail_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 - GET_AUTH_EVENTS_DETAIL_JOB_RESULTS_RESP, + GET_AUTH_EVENT_DETAIL_JOB_RESULTS_RESP, ) api = cbcsdk_mock.api - events_list = api.select(AuthEvents).where(process_pid=2000) + events_list = api.select(AuthEvent).where(process_pid=2000) events = events_list[0] results = events.get_details() assert results["device_name"] is not None @@ -241,163 +241,163 @@ def test_auth_events_select_details_sync(cbcsdk_mock): assert results.process_pid[0] == 764 -def test_auth_events_select_details_refresh(cbcsdk_mock): - """Testing AuthEvents Querying with get_details""" +def test_auth_event_select_details_refresh(cbcsdk_mock): + """Testing AuthEvent Querying with get_details""" cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/auth_events/search_jobs", - POST_AUTH_EVENTS_SEARCH_JOB_RESP, + POST_AUTH_EVENT_SEARCH_JOB_RESP, ) cbcsdk_mock.mock_request( "GET", "/api/investigate/v2/orgs/test/auth_events/search_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 - GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP, + GET_AUTH_EVENT_SEARCH_JOB_RESULTS_RESP, ) cbcsdk_mock.mock_request( "GET", "/api/investigate/v2/orgs/test/auth_events/search_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results?start=0&rows=500", # noqa: E501 - GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP, + GET_AUTH_EVENT_SEARCH_JOB_RESULTS_RESP, ) cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/auth_events/detail_jobs", - POST_AUTH_EVENTS_SEARCH_JOB_RESP, + POST_AUTH_EVENT_SEARCH_JOB_RESP, ) cbcsdk_mock.mock_request( "GET", "/api/investigate/v2/orgs/test/auth_events/detail_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 - GET_AUTH_EVENTS_DETAIL_JOB_RESULTS_RESP, + GET_AUTH_EVENT_DETAIL_JOB_RESULTS_RESP, ) api = cbcsdk_mock.api - events_list = api.select(AuthEvents).where(event_id="DA9E269E-421D-469D-A212-9062888A02F4") + events_list = api.select(AuthEvent).where(event_id="DA9E269E-421D-469D-A212-9062888A02F4") events = events_list[0] assert events.device_name is not None assert events.process_pid[0] == 776 -def test_auth_events_select_details_sync_zero(cbcsdk_mock): - """Testing AuthEvents Querying with get_details""" +def test_auth_event_select_details_sync_zero(cbcsdk_mock): + """Testing AuthEvent Querying with get_details""" cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/auth_events/search_jobs", - POST_AUTH_EVENTS_SEARCH_JOB_RESP, + POST_AUTH_EVENT_SEARCH_JOB_RESP, ) cbcsdk_mock.mock_request( "GET", "/api/investigate/v2/orgs/test/auth_events/search_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 - GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP, + GET_AUTH_EVENT_SEARCH_JOB_RESULTS_RESP, ) cbcsdk_mock.mock_request( "GET", "/api/investigate/v2/orgs/test/auth_events/search_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results?start=0&rows=500", # noqa: E501 - GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP, + GET_AUTH_EVENT_SEARCH_JOB_RESULTS_RESP, ) cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/auth_events/detail_jobs", - POST_AUTH_EVENTS_SEARCH_JOB_RESP, + POST_AUTH_EVENT_SEARCH_JOB_RESP, ) cbcsdk_mock.mock_request( "GET", "/api/investigate/v2/orgs/test/auth_events/detail_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 - GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_ZERO, + GET_AUTH_EVENT_SEARCH_JOB_RESULTS_ZERO, ) api = cbcsdk_mock.api - events_list = api.select(AuthEvents).where(process_pid=2000) + events_list = api.select(AuthEvent).where(process_pid=2000) events = events_list[0] results = events.get_details() assert results["device_name"] is not None assert results.get("alert_id") == [] -def test_auth_events_select_compound(cbcsdk_mock): - """Testing AuthEvents Querying with select() and more complex criteria""" +def test_auth_event_select_compound(cbcsdk_mock): + """Testing AuthEvent Querying with select() and more complex criteria""" cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/auth_events/search_jobs", - POST_AUTH_EVENTS_SEARCH_JOB_RESP, + POST_AUTH_EVENT_SEARCH_JOB_RESP, ) cbcsdk_mock.mock_request( "GET", "/api/investigate/v2/orgs/test/auth_events/search_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 - GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP, + GET_AUTH_EVENT_SEARCH_JOB_RESULTS_RESP, ) cbcsdk_mock.mock_request( "GET", "/api/investigate/v2/orgs/test/auth_events/search_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results?start=0&rows=500", # noqa: E501 - GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP, + GET_AUTH_EVENT_SEARCH_JOB_RESULTS_RESP, ) api = cbcsdk_mock.api - events_list = api.select(AuthEvents).where(process_pid=776).or_(parent_pid=608) + events_list = api.select(AuthEvent).where(process_pid=776).or_(parent_pid=608) for events in events_list: assert events.device_name is not None -def test_auth_events_query_implementation(cbcsdk_mock): - """Testing AuthEvents querying with where().""" +def test_auth_event_query_implementation(cbcsdk_mock): + """Testing AuthEvent querying with where().""" cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/auth_events/search_jobs", - POST_AUTH_EVENTS_SEARCH_JOB_RESP, + POST_AUTH_EVENT_SEARCH_JOB_RESP, ) cbcsdk_mock.mock_request( "GET", "/api/investigate/v2/orgs/test/auth_events/search_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 - GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP, + GET_AUTH_EVENT_SEARCH_JOB_RESULTS_RESP, ) cbcsdk_mock.mock_request( "GET", "/api/investigate/v2/orgs/test/auth_events/search_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results?start=0&rows=500", # noqa: E501 - GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP, + GET_AUTH_EVENT_SEARCH_JOB_RESULTS_RESP, ) api = cbcsdk_mock.api event_id = ( "DA9E269E-421D-469D-A212-9062888A02F4" ) - events_list = api.select(AuthEvents).where(f"event_id:{event_id}") - assert isinstance(events_list, AuthEventsQuery) + events_list = api.select(AuthEvent).where(f"event_id:{event_id}") + assert isinstance(events_list, AuthEventQuery) assert events_list[0].event_id == event_id -def test_auth_events_timeout(cbcsdk_mock): - """Testing AuthEventsQuery.timeout().""" +def test_auth_event_timeout(cbcsdk_mock): + """Testing AuthEventQuery.timeout().""" api = cbcsdk_mock.api - query = api.select(AuthEvents).where("event_id:some_id") + query = api.select(AuthEvent).where("event_id:some_id") assert query._timeout == 0 query.timeout(msecs=500) assert query._timeout == 500 -def test_auth_events_timeout_error(cbcsdk_mock): - """Testing that a timeout in AuthEvents querying throws a TimeoutError correctly""" +def test_auth_event_timeout_error(cbcsdk_mock): + """Testing that a timeout in AuthEvent querying throws a TimeoutError correctly""" cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/auth_events/search_jobs", - POST_AUTH_EVENTS_SEARCH_JOB_RESP, + POST_AUTH_EVENT_SEARCH_JOB_RESP, ) cbcsdk_mock.mock_request( "GET", "/api/investigate/v2/orgs/test/auth_events/search_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 - GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING, + GET_AUTH_EVENT_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING, ) api = cbcsdk_mock.api - events_list = (api.select(AuthEvents).where("event_id:DA9E269E-421D-469D-A212-9062888A02F4").timeout(1)) + events_list = (api.select(AuthEvent).where("event_id:DA9E269E-421D-469D-A212-9062888A02F4").timeout(1)) with pytest.raises(TimeoutError): list(events_list) - events_list = (api.select(AuthEvents).where("event_id:DA9E269E-421D-469D-A212-9062888A02F4").timeout(1)) + events_list = (api.select(AuthEvent).where("event_id:DA9E269E-421D-469D-A212-9062888A02F4").timeout(1)) with pytest.raises(TimeoutError): events_list._count() -def test_auth_events_query_sort(cbcsdk_mock): - """Testing AuthEvents results sort.""" +def test_auth_event_query_sort(cbcsdk_mock): + """Testing AuthEvent results sort.""" api = cbcsdk_mock.api events_list = ( - api.select(AuthEvents) + api.select(AuthEvent) .where(process_pid=1000) .or_(process_pid=1000) .sort_by("process_pid", direction="DESC") @@ -405,24 +405,24 @@ def test_auth_events_query_sort(cbcsdk_mock): assert events_list._sort_by == [{"field": "process_pid", "order": "DESC"}] -def test_auth_events_rows(cbcsdk_mock): - """Testing AuthEvents results sort.""" +def test_auth_event_rows(cbcsdk_mock): + """Testing AuthEvent results sort.""" api = cbcsdk_mock.api - events_list = api.select(AuthEvents).where(process_pid=1000).set_rows(1500) + events_list = api.select(AuthEvent).where(process_pid=1000).set_rows(1500) assert events_list._batch_size == 1500 with pytest.raises(ApiError) as ex: - api.select(AuthEvents).where(process_pid=1000).set_rows("alabala") + api.select(AuthEvent).where(process_pid=1000).set_rows("alabala") assert "Rows must be an integer." in str(ex) with pytest.raises(ApiError) as ex: - api.select(AuthEvents).where(process_pid=1000).set_rows(10001) + api.select(AuthEvent).where(process_pid=1000).set_rows(10001) assert "Maximum allowed value for rows is 10000" in str(ex) -def test_auth_events_time_range(cbcsdk_mock): - """Testing AuthEvents results sort.""" +def test_auth_event_time_range(cbcsdk_mock): + """Testing AuthEvent results sort.""" api = cbcsdk_mock.api events_list = ( - api.select(AuthEvents) + api.select(AuthEvent) .where(process_pid=1000) .set_time_range( start="2020-10-10T20:34:07Z", end="2020-10-20T20:34:07Z", window="-1d" @@ -433,15 +433,15 @@ def test_auth_events_time_range(cbcsdk_mock): assert events_list._time_range["window"] == "-1d" -def test_auth_events_submit(cbcsdk_mock): - """Test _submit method of AuthEventsQuery class""" +def test_auth_event_submit(cbcsdk_mock): + """Test _submit method of AuthEventQuery class""" cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/auth_events/search_jobs", - POST_AUTH_EVENTS_SEARCH_JOB_RESP, + POST_AUTH_EVENT_SEARCH_JOB_RESP, ) api = cbcsdk_mock.api - events_list = api.select(AuthEvents).where(process_pid=1000) + events_list = api.select(AuthEvent).where(process_pid=1000) events_list._submit() assert events_list._query_token == "62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs" with pytest.raises(ApiError) as ex: @@ -449,121 +449,121 @@ def test_auth_events_submit(cbcsdk_mock): assert "Query already submitted: token" in str(ex) -def test_auth_events_count(cbcsdk_mock): - """Test _submit method of AuthEventsquery class""" +def test_auth_event_count(cbcsdk_mock): + """Test _submit method of AuthEventquery class""" cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/auth_events/search_jobs", - POST_AUTH_EVENTS_SEARCH_JOB_RESP, + POST_AUTH_EVENT_SEARCH_JOB_RESP, ) cbcsdk_mock.mock_request( "GET", "/api/investigate/v2/orgs/test/auth_events/search_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 - GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP_2, + GET_AUTH_EVENT_SEARCH_JOB_RESULTS_RESP_2, ) cbcsdk_mock.mock_request( "GET", "/api/investigate/v2/orgs/test/auth_events/search_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 - GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP_2, + GET_AUTH_EVENT_SEARCH_JOB_RESULTS_RESP_2, ) api = cbcsdk_mock.api - events_list = api.select(AuthEvents).where(process_pid=1000) + events_list = api.select(AuthEvent).where(process_pid=1000) events_list._count() assert events_list._count() == 198 -def test_auth_events_search(cbcsdk_mock): - """Test _search method of AuthEventsquery class""" +def test_auth_event_search(cbcsdk_mock): + """Test _search method of AuthEventquery class""" cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/auth_events/search_jobs", - POST_AUTH_EVENTS_SEARCH_JOB_RESP, + POST_AUTH_EVENT_SEARCH_JOB_RESP, ) cbcsdk_mock.mock_request( "GET", "/api/investigate/v2/orgs/test/auth_events/search_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 - GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP_2, + GET_AUTH_EVENT_SEARCH_JOB_RESULTS_RESP_2, ) cbcsdk_mock.mock_request( "GET", "/api/investigate/v2/orgs/test/auth_events/search_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results?start=0&rows=500", # noqa: E501 - GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP_2, + GET_AUTH_EVENT_SEARCH_JOB_RESULTS_RESP_2, ) api = cbcsdk_mock.api - events_list = api.select(AuthEvents).where(process_pid=828) + events_list = api.select(AuthEvent).where(process_pid=828) events_list._search() assert events_list[0].process_pid[0] == 828 events_list._search(start=1) assert events_list[0].process_pid[0] == 828 -def test_auth_events_still_querying(cbcsdk_mock): - """Test _search method of AuthEventsquery class""" +def test_auth_event_still_querying(cbcsdk_mock): + """Test _search method of AuthEventquery class""" cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/auth_events/search_jobs", - POST_AUTH_EVENTS_SEARCH_JOB_RESP, + POST_AUTH_EVENT_SEARCH_JOB_RESP, ) cbcsdk_mock.mock_request( "GET", "/api/investigate/v2/orgs/test/auth_events/search_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 - GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP_0, + GET_AUTH_EVENT_SEARCH_JOB_RESULTS_RESP_0, ) cbcsdk_mock.mock_request( "GET", "/api/investigate/v2/orgs/test/auth_events/search_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results?start=0&rows=500", # noqa: E501 - GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING, + GET_AUTH_EVENT_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING, ) api = cbcsdk_mock.api - events_list = api.select(AuthEvents).where(process_pid=1000) + events_list = api.select(AuthEvent).where(process_pid=1000) assert events_list._still_querying() is True -def test_auth_events_still_querying2(cbcsdk_mock): - """Test _search method of AuthEventsquery class""" +def test_auth_event_still_querying2(cbcsdk_mock): + """Test _search method of AuthEventquery class""" cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/auth_events/search_jobs", - POST_AUTH_EVENTS_SEARCH_JOB_RESP, + POST_AUTH_EVENT_SEARCH_JOB_RESP, ) cbcsdk_mock.mock_request( "GET", "/api/investigate/v2/orgs/test/auth_events/search_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 - GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP_ZERO_COMP, + GET_AUTH_EVENT_SEARCH_JOB_RESULTS_RESP_ZERO_COMP, ) cbcsdk_mock.mock_request( "GET", "/api/investigate/v2/orgs/test/auth_events/search_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results?start=0&rows=500", # noqa: E501 - GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING, + GET_AUTH_EVENT_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING, ) api = cbcsdk_mock.api - events_list = api.select(AuthEvents).where(process_pid=1000) + events_list = api.select(AuthEvent).where(process_pid=1000) assert events_list._still_querying() is True -# --------------------- AuthEventsFacet -------------------------------------- +# --------------------- AuthEventFacet -------------------------------------- -def test_auth_events_facet_select_where(cbcsdk_mock): - """Testing AuthEvents Querying with select()""" +def test_auth_event_facet_select_where(cbcsdk_mock): + """Testing AuthEvent Querying with select()""" cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/auth_events/facet_jobs", - POST_AUTH_EVENTS_FACET_SEARCH_JOB_RESP, + POST_AUTH_EVENT_FACET_SEARCH_JOB_RESP, ) cbcsdk_mock.mock_request( "GET", "/api/investigate/v2/orgs/test/auth_events/facet_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 - GET_AUTH_EVENTS_FACET_SEARCH_JOB_RESULTS_RESP_2, + GET_AUTH_EVENT_FACET_SEARCH_JOB_RESULTS_RESP_2, ) api = cbcsdk_mock.api auth_events = ( - api.select(AuthEventsFacet) + api.select(AuthEventFacet) .where(process_name="chrome.exe") .add_facet_field("process_name") ) @@ -574,22 +574,22 @@ def test_auth_events_facet_select_where(cbcsdk_mock): assert event.terms[0]["field"] == "process_name" -def test_auth_events_facet_select_async(cbcsdk_mock): - """Testing AuthEvents Querying with select()""" +def test_auth_event_facet_select_async(cbcsdk_mock): + """Testing AuthEvent Querying with select()""" cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/auth_events/facet_jobs", - POST_AUTH_EVENTS_FACET_SEARCH_JOB_RESP, + POST_AUTH_EVENT_FACET_SEARCH_JOB_RESP, ) cbcsdk_mock.mock_request( "GET", "/api/investigate/v2/orgs/test/auth_events/facet_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 - GET_AUTH_EVENTS_FACET_SEARCH_JOB_RESULTS_RESP_2, + GET_AUTH_EVENT_FACET_SEARCH_JOB_RESULTS_RESP_2, ) api = cbcsdk_mock.api future = ( - api.select(AuthEventsFacet) + api.select(AuthEventFacet) .where(process_name="chrome.exe") .add_facet_field("process_name") .execute_async() @@ -601,22 +601,22 @@ def test_auth_events_facet_select_async(cbcsdk_mock): assert event.terms[0]["field"] == "process_name" -def test_auth_events_facet_select_compound(cbcsdk_mock): - """Testing AuthEvents Querying with select() and more complex criteria""" +def test_auth_event_facet_select_compound(cbcsdk_mock): + """Testing AuthEvent Querying with select() and more complex criteria""" cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/auth_events/facet_jobs", - POST_AUTH_EVENTS_FACET_SEARCH_JOB_RESP, + POST_AUTH_EVENT_FACET_SEARCH_JOB_RESP, ) cbcsdk_mock.mock_request( "GET", "/api/investigate/v2/orgs/test/auth_events/facet_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 - GET_AUTH_EVENTS_FACET_SEARCH_JOB_RESULTS_RESP_2, + GET_AUTH_EVENT_FACET_SEARCH_JOB_RESULTS_RESP_2, ) api = cbcsdk_mock.api auth_events = ( - api.select(AuthEventsFacet) + api.select(AuthEventFacet) .where(process_name="chrome.exe") .or_(process_name="firefox.exe") .add_facet_field("process_name") @@ -626,23 +626,23 @@ def test_auth_events_facet_select_compound(cbcsdk_mock): assert event.ranges == [] -def test_auth_events_facet_query_implementation(cbcsdk_mock): - """Testing AuthEvents querying with where().""" +def test_auth_event_facet_query_implementation(cbcsdk_mock): + """Testing AuthEvent querying with where().""" cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/auth_events/facet_jobs", - POST_AUTH_EVENTS_FACET_SEARCH_JOB_RESP, + POST_AUTH_EVENT_FACET_SEARCH_JOB_RESP, ) cbcsdk_mock.mock_request( "GET", "/api/investigate/v2/orgs/test/auth_events/facet_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 - GET_AUTH_EVENTS_FACET_SEARCH_JOB_RESULTS_RESP_1, + GET_AUTH_EVENT_FACET_SEARCH_JOB_RESULTS_RESP_1, ) api = cbcsdk_mock.api field = "process_name" auth_events = ( - api.select(AuthEventsFacet) + api.select(AuthEventFacet) .where(process_name="test") .add_facet_field("process_name") ) @@ -656,11 +656,11 @@ def test_auth_events_facet_query_implementation(cbcsdk_mock): assert isinstance(event._query_implementation(api), FacetQuery) -def test_auth_events_facet_timeout(cbcsdk_mock): - """Testing AuthEventsQuery.timeout().""" +def test_auth_event_facet_timeout(cbcsdk_mock): + """Testing AuthEventQuery.timeout().""" api = cbcsdk_mock.api query = ( - api.select(AuthEventsFacet) + api.select(AuthEventFacet) .where("process_name:some_name") .add_facet_field("process_name") ) @@ -669,22 +669,22 @@ def test_auth_events_facet_timeout(cbcsdk_mock): assert query._timeout == 500 -def test_auth_events_facet_timeout_error(cbcsdk_mock): - """Testing that a timeout in AuthEventsQuery throws the right TimeoutError.""" +def test_auth_event_facet_timeout_error(cbcsdk_mock): + """Testing that a timeout in AuthEventQuery throws the right TimeoutError.""" cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/auth_events/facet_jobs", - POST_AUTH_EVENTS_FACET_SEARCH_JOB_RESP, + POST_AUTH_EVENT_FACET_SEARCH_JOB_RESP, ) cbcsdk_mock.mock_request( "GET", "/api/investigate/v2/orgs/test/auth_events/facet_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 - GET_AUTH_EVENTS_FACET_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING, + GET_AUTH_EVENT_FACET_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING, ) api = cbcsdk_mock.api query = ( - api.select(AuthEventsFacet) + api.select(AuthEventFacet) .where("process_name:some_name") .add_facet_field("process_name") .timeout(1) @@ -692,7 +692,7 @@ def test_auth_events_facet_timeout_error(cbcsdk_mock): with pytest.raises(TimeoutError): query.results() query = ( - api.select(AuthEventsFacet) + api.select(AuthEventFacet) .where("process_name:some_name") .add_facet_field("process_name") .timeout(1) @@ -701,12 +701,12 @@ def test_auth_events_facet_timeout_error(cbcsdk_mock): query._count() -def test_auth_events_facet_query_add_range(cbcsdk_mock): - """Testing AuthEvents results sort.""" +def test_auth_event_facet_query_add_range(cbcsdk_mock): + """Testing AuthEvent results sort.""" api = cbcsdk_mock.api range = ({"bucket_size": 30, "start": "0D", "end": "20D", "field": "something"},) auth_events = ( - api.select(AuthEventsFacet) + api.select(AuthEventFacet) .where(process_pid=1000) .add_range(range) .add_facet_field("process_name") @@ -717,50 +717,50 @@ def test_auth_events_facet_query_add_range(cbcsdk_mock): assert auth_events._ranges[0]["field"] == "something" -def test_auth_events_facet_query_check_range(cbcsdk_mock): - """Testing AuthEvents results sort.""" +def test_auth_event_facet_query_check_range(cbcsdk_mock): + """Testing AuthEvent results sort.""" api = cbcsdk_mock.api range = ({"bucket_size": [], "start": "0D", "end": "20D", "field": "something"},) with pytest.raises(ApiError): - api.select(AuthEventsFacet).where(process_pid=1000).add_range( + api.select(AuthEventFacet).where(process_pid=1000).add_range( range ).add_facet_field("process_name") range = ({"bucket_size": 30, "start": [], "end": "20D", "field": "something"},) with pytest.raises(ApiError): - api.select(AuthEventsFacet).where(process_pid=1000).add_range( + api.select(AuthEventFacet).where(process_pid=1000).add_range( range ).add_facet_field("process_name") range = ({"bucket_size": 30, "start": "0D", "end": [], "field": "something"},) with pytest.raises(ApiError): - api.select(AuthEventsFacet).where(process_pid=1000).add_range( + api.select(AuthEventFacet).where(process_pid=1000).add_range( range ).add_facet_field("process_name") range = ({"bucket_size": 30, "start": "0D", "end": "20D", "field": []},) with pytest.raises(ApiError): - api.select(AuthEventsFacet).where(process_pid=1000).add_range( + api.select(AuthEventFacet).where(process_pid=1000).add_range( range ).add_facet_field("process_name") -def test_auth_events_facet_query_add_facet_field(cbcsdk_mock): - """Testing AuthEvents results sort.""" +def test_auth_event_facet_query_add_facet_field(cbcsdk_mock): + """Testing AuthEvent results sort.""" api = cbcsdk_mock.api auth_events = ( - api.select(AuthEventsFacet) + api.select(AuthEventFacet) .where(process_pid=1000) .add_facet_field("process_name") ) assert auth_events._facet_fields[0] == "process_name" -def test_auth_events_facet_query_add_facet_fields(cbcsdk_mock): - """Testing AuthEvents results sort.""" +def test_auth_event_facet_query_add_facet_fields(cbcsdk_mock): + """Testing AuthEvent results sort.""" api = cbcsdk_mock.api auth_events = ( - api.select(AuthEventsFacet) + api.select(AuthEventFacet) .where(process_pid=1000) .add_facet_field(["process_name", "process_pid"]) ) @@ -768,18 +768,18 @@ def test_auth_events_facet_query_add_facet_fields(cbcsdk_mock): assert "process_name" in auth_events._facet_fields -def test_auth_events_facet_query_add_facet_invalid_fields(cbcsdk_mock): - """Testing AuthEvents results sort.""" +def test_auth_event_facet_query_add_facet_invalid_fields(cbcsdk_mock): + """Testing AuthEvent results sort.""" api = cbcsdk_mock.api with pytest.raises(TypeError): - api.select(AuthEventsFacet).where(process_pid=1000).add_facet_field(1337) + api.select(AuthEventFacet).where(process_pid=1000).add_facet_field(1337) -def test_auth_events_facet_limit(cbcsdk_mock): - """Testing AuthEvents results limit.""" +def test_auth_event_facet_limit(cbcsdk_mock): + """Testing AuthEvent results limit.""" api = cbcsdk_mock.api auth_events = ( - api.select(AuthEventsFacet) + api.select(AuthEventFacet) .where(process_pid=1000) .limit(123) .add_facet_field("process_name") @@ -787,11 +787,11 @@ def test_auth_events_facet_limit(cbcsdk_mock): assert auth_events._limit == 123 -def test_auth_events_facet_time_range(cbcsdk_mock): - """Testing AuthEvents results range.""" +def test_auth_event_facet_time_range(cbcsdk_mock): + """Testing AuthEvent results range.""" api = cbcsdk_mock.api auth_events = ( - api.select(AuthEventsFacet) + api.select(AuthEventFacet) .where(process_pid=1000) .set_time_range( start="2020-10-10T20:34:07Z", end="2020-10-20T20:34:07Z", window="-1d" @@ -803,16 +803,16 @@ def test_auth_events_facet_time_range(cbcsdk_mock): assert auth_events._time_range["window"] == "-1d" -def test_auth_events_facet_submit(cbcsdk_mock): - """Test _submit method of AuthEventsQuery class""" +def test_auth_event_facet_submit(cbcsdk_mock): + """Test _submit method of AuthEventQuery class""" cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/auth_events/facet_jobs", - POST_AUTH_EVENTS_FACET_SEARCH_JOB_RESP, + POST_AUTH_EVENT_FACET_SEARCH_JOB_RESP, ) api = cbcsdk_mock.api auth_events = ( - api.select(AuthEventsFacet) + api.select(AuthEventFacet) .where(process_pid=1000) .add_facet_field("process_name") ) @@ -820,22 +820,22 @@ def test_auth_events_facet_submit(cbcsdk_mock): assert auth_events._query_token == "62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs" -def test_auth_events_facet_count(cbcsdk_mock): - """Test _submit method of AuthEventsQuery class""" +def test_auth_event_facet_count(cbcsdk_mock): + """Test _submit method of AuthEventQuery class""" cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/auth_events/facet_jobs", - POST_AUTH_EVENTS_FACET_SEARCH_JOB_RESP, + POST_AUTH_EVENT_FACET_SEARCH_JOB_RESP, ) cbcsdk_mock.mock_request( "GET", "/api/investigate/v2/orgs/test/auth_events/facet_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 - GET_AUTH_EVENTS_FACET_SEARCH_JOB_RESULTS_RESP_1, + GET_AUTH_EVENT_FACET_SEARCH_JOB_RESULTS_RESP_1, ) api = cbcsdk_mock.api auth_events = ( - api.select(AuthEventsFacet) + api.select(AuthEventFacet) .where(process_pid=1000) .add_facet_field("process_name") ) @@ -843,22 +843,22 @@ def test_auth_events_facet_count(cbcsdk_mock): assert auth_events._count() == 116 -def test_auth_events_search(cbcsdk_mock): - """Test _search method of AuthEventsQuery class""" +def test_auth_event_search(cbcsdk_mock): + """Test _search method of AuthEventQuery class""" cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/auth_events/facet_jobs", - POST_AUTH_EVENTS_FACET_SEARCH_JOB_RESP, + POST_AUTH_EVENT_FACET_SEARCH_JOB_RESP, ) cbcsdk_mock.mock_request( "GET", "/api/investigate/v2/orgs/test/auth_events/facet_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 - GET_AUTH_EVENTS_FACET_SEARCH_JOB_RESULTS_RESP_2, + GET_AUTH_EVENT_FACET_SEARCH_JOB_RESULTS_RESP_2, ) api = cbcsdk_mock.api auth_events = ( - api.select(AuthEventsFacet) + api.select(AuthEventFacet) .where(process_pid=1000) .add_facet_field("process_name") ) @@ -869,22 +869,22 @@ def test_auth_events_search(cbcsdk_mock): assert result.terms[0]["field"] == "process_name" -def test_auth_events_search_async(cbcsdk_mock): - """Test _search method of AuthEventsQuery class""" +def test_auth_event_search_async(cbcsdk_mock): + """Test _search method of AuthEventQuery class""" cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/auth_events/facet_jobs", - POST_AUTH_EVENTS_FACET_SEARCH_JOB_RESP, + POST_AUTH_EVENT_FACET_SEARCH_JOB_RESP, ) cbcsdk_mock.mock_request( "GET", "/api/investigate/v2/orgs/test/auth_events/facet_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 - GET_AUTH_EVENTS_FACET_SEARCH_JOB_RESULTS_RESP_2, + GET_AUTH_EVENT_FACET_SEARCH_JOB_RESULTS_RESP_2, ) api = cbcsdk_mock.api auth_events = ( - api.select(AuthEventsFacet) + api.select(AuthEventFacet) .where(process_pid=1000) .add_facet_field("process_name") ) @@ -895,47 +895,47 @@ def test_auth_events_search_async(cbcsdk_mock): assert result.terms[0]["field"] == "process_name" -def test_auth_events_aggregation_wrong_field(cbcsdk_mock): +def test_auth_event_aggregation_wrong_field(cbcsdk_mock): """Testing passing wrong aggregation_field""" api = cbcsdk_mock.api with pytest.raises(ApiError): for i in ( - api.select(AuthEvents) + api.select(AuthEvent) .where(process_pid=2000) .group_results("wrong_field") ): print(i) with pytest.raises(ApiError): - for i in api.select(AuthEvents).where(process_pid=2000).group_results(1): + for i in api.select(AuthEvent).where(process_pid=2000).group_results(1): print(i) -def test_auth_events_select_group_results(cbcsdk_mock): - """Testing AuthEvents Querying with select() and more complex criteria""" +def test_auth_event_select_group_results(cbcsdk_mock): + """Testing AuthEvent Querying with select() and more complex criteria""" cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/auth_events/search_jobs", - POST_AUTH_EVENTS_SEARCH_JOB_RESP, + POST_AUTH_EVENT_SEARCH_JOB_RESP, ) cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/auth_events/search_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/group_results", # noqa: E501 - GET_AUTH_EVENTS_GROUPED_RESULTS_RESP, + GET_AUTH_EVENT_GROUPED_RESULTS_RESP, ) cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/auth_events/detail_jobs", - POST_AUTH_EVENTS_SEARCH_JOB_RESP, + POST_AUTH_EVENT_SEARCH_JOB_RESP, ) cbcsdk_mock.mock_request( "GET", "/api/investigate/v2/orgs/test/auth_events/detail_jobs/62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs/results", # noqa: E501 - GET_AUTH_EVENTS_DETAIL_JOB_RESULTS_RESP, + GET_AUTH_EVENT_DETAIL_JOB_RESULTS_RESP, ) api = cbcsdk_mock.api event_groups = list( - api.select(AuthEvents) + api.select(AuthEvent) .where(process_pid=2000) .group_results( "device_name", @@ -946,33 +946,33 @@ def test_auth_events_select_group_results(cbcsdk_mock): range_duration="-2y" ) ) - # invoke get_details() on the first AuthEvents in the list + # invoke get_details() on the first AuthEvent in the list event_groups[0].auth_events[0].get_details() assert event_groups[0].group_key is not None assert event_groups[0]["group_key"] is not None assert event_groups[0].auth_events[0]["process_pid"][0] == 764 -def test_auth_events_search_validations(cbcsdk_mock): +def test_auth_event_search_validations(cbcsdk_mock): """Tests getting auth_events search validations""" api = cbcsdk_mock.api q = 'q=auth_username' cbcsdk_mock.mock_request( "GET", f"/api/investigate/v2/orgs/test/auth_events/search_validation?{q}", - AUTH_EVENTS_SEARCH_VALIDATIONS_RESP, + AUTH_EVENT_SEARCH_VALIDATIONS_RESP, ) - result = api.select(AuthEvents).search_validation('auth_username') + result = api.select(AuthEvent).search_validation('auth_username') assert result is True -def test_auth_events_search_suggestions(cbcsdk_mock): +def test_auth_event_search_suggestions(cbcsdk_mock): """Tests getting auth_events search suggestions""" api = cbcsdk_mock.api q = "suggest.q=auth" cbcsdk_mock.mock_request( "GET", f"/api/investigate/v2/orgs/test/auth_events/search_suggestions?{q}", - AUTH_EVENTS_SEARCH_SUGGESTIONS_RESP, + AUTH_EVENT_SEARCH_SUGGESTIONS_RESP, ) - result = api.select(AuthEvents).search_suggestions('auth') + result = api.select(AuthEvent).search_suggestions('auth') assert len(result) != 0 diff --git a/src/tests/unit/fixtures/enterprise_edr/mock_auth_events.py b/src/tests/unit/fixtures/enterprise_edr/mock_auth_events.py index ac290f044..03c9680cc 100644 --- a/src/tests/unit/fixtures/enterprise_edr/mock_auth_events.py +++ b/src/tests/unit/fixtures/enterprise_edr/mock_auth_events.py @@ -1,8 +1,8 @@ """Mock responses for Auth Events queries.""" -POST_AUTH_EVENTS_SEARCH_JOB_RESP = {"job_id": "62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs"} +POST_AUTH_EVENT_SEARCH_JOB_RESP = {"job_id": "62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs"} -GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP = { +GET_AUTH_EVENT_SEARCH_JOB_RESULTS_RESP = { "results": [ { "auth_domain_name": "NT AUTHORITY", @@ -52,7 +52,7 @@ } -GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP_0 = { +GET_AUTH_EVENT_SEARCH_JOB_RESULTS_RESP_0 = { "results": [], "num_found": 0, "num_available": 0, @@ -63,7 +63,7 @@ } -GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP_ZERO_COMP = { +GET_AUTH_EVENT_SEARCH_JOB_RESULTS_RESP_ZERO_COMP = { "results": [], "num_found": 0, "num_available": 0, @@ -74,7 +74,7 @@ } -GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_ZERO = { +GET_AUTH_EVENT_SEARCH_JOB_RESULTS_ZERO = { "results": [], "num_found": 0, "num_available": 0, @@ -85,7 +85,7 @@ } -GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP_2 = { +GET_AUTH_EVENT_SEARCH_JOB_RESULTS_RESP_2 = { "results": [ { "auth_domain_name": "NT AUTHORITY", @@ -171,7 +171,7 @@ } -GET_AUTH_EVENTS_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING = { +GET_AUTH_EVENT_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING = { "num_found": 808, "num_available": 1, "contacted": 6, @@ -180,7 +180,7 @@ } -GET_AUTH_EVENTS_DETAIL_JOB_RESULTS_RESP = { +GET_AUTH_EVENT_DETAIL_JOB_RESULTS_RESP = { "results": [ { "auth_cleartext_credentials_logon": False, @@ -281,12 +281,12 @@ """Mocks for auth events facet query testing.""" -POST_AUTH_EVENTS_FACET_SEARCH_JOB_RESP = { +POST_AUTH_EVENT_FACET_SEARCH_JOB_RESP = { "job_id": "62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs" } -GET_AUTH_EVENTS_FACET_SEARCH_JOB_RESULTS_RESP_1 = { +GET_AUTH_EVENT_FACET_SEARCH_JOB_RESULTS_RESP_1 = { "ranges": [ { "start": "2020-08-04T08:01:32.077Z", @@ -308,7 +308,7 @@ } -GET_AUTH_EVENTS_FACET_SEARCH_JOB_RESULTS_RESP_2 = { +GET_AUTH_EVENT_FACET_SEARCH_JOB_RESULTS_RESP_2 = { "ranges": [], "terms": [ { @@ -322,7 +322,7 @@ } -GET_AUTH_EVENTS_FACET_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING = { +GET_AUTH_EVENT_FACET_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING = { "ranges": [], "terms": [], "num_found": 0, @@ -331,7 +331,7 @@ } -GET_AUTH_EVENTS_GROUPED_RESULTS_RESP = { +GET_AUTH_EVENT_GROUPED_RESULTS_RESP = { "group_results": [ { "group_key": "auth_username", @@ -389,7 +389,7 @@ "completed": 169 } -AUTH_EVENTS_SEARCH_SUGGESTIONS_RESP = { +AUTH_EVENT_SEARCH_SUGGESTIONS_RESP = { "suggestions": [ { "term": "auth_cleartext_credentials_logon", @@ -578,7 +578,7 @@ ] } -AUTH_EVENTS_SEARCH_VALIDATIONS_RESP = { +AUTH_EVENT_SEARCH_VALIDATIONS_RESP = { "valid": True, "value_search_query": True } From f33b45b797e1cf1f8fbab1d1fa5887d1890c4f69 Mon Sep 17 00:00:00 2001 From: Hristo Karagitliev Date: Tue, 14 Mar 2023 15:36:18 +0200 Subject: [PATCH 118/143] flake8 fixes --- .../unit/enterprise_edr/test_auth_events.py | 4 +- .../enterprise_edr/mock_auth_events.py | 901 ++++++++---------- 2 files changed, 417 insertions(+), 488 deletions(-) diff --git a/src/tests/unit/enterprise_edr/test_auth_events.py b/src/tests/unit/enterprise_edr/test_auth_events.py index 22efadcd1..b3ff5b2b1 100644 --- a/src/tests/unit/enterprise_edr/test_auth_events.py +++ b/src/tests/unit/enterprise_edr/test_auth_events.py @@ -843,7 +843,7 @@ def test_auth_event_facet_count(cbcsdk_mock): assert auth_events._count() == 116 -def test_auth_event_search(cbcsdk_mock): +def test_auth_event_search_facet(cbcsdk_mock): """Test _search method of AuthEventQuery class""" cbcsdk_mock.mock_request( "POST", @@ -952,6 +952,7 @@ def test_auth_event_select_group_results(cbcsdk_mock): assert event_groups[0]["group_key"] is not None assert event_groups[0].auth_events[0]["process_pid"][0] == 764 + def test_auth_event_search_validations(cbcsdk_mock): """Tests getting auth_events search validations""" api = cbcsdk_mock.api @@ -964,6 +965,7 @@ def test_auth_event_search_validations(cbcsdk_mock): result = api.select(AuthEvent).search_validation('auth_username') assert result is True + def test_auth_event_search_suggestions(cbcsdk_mock): """Tests getting auth_events search suggestions""" api = cbcsdk_mock.api diff --git a/src/tests/unit/fixtures/enterprise_edr/mock_auth_events.py b/src/tests/unit/fixtures/enterprise_edr/mock_auth_events.py index 03c9680cc..4c21b9cbe 100644 --- a/src/tests/unit/fixtures/enterprise_edr/mock_auth_events.py +++ b/src/tests/unit/fixtures/enterprise_edr/mock_auth_events.py @@ -3,171 +3,159 @@ POST_AUTH_EVENT_SEARCH_JOB_RESP = {"job_id": "62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs"} GET_AUTH_EVENT_SEARCH_JOB_RESULTS_RESP = { - "results": [ - { - "auth_domain_name": "NT AUTHORITY", - "auth_event_action": "LOGON_SUCCESS", - "auth_remote_device": "-", - "auth_remote_port": 0, - "auth_username": "SYSTEM", - "backend_timestamp": "2023-01-13T17:19:01.013Z", - "childproc_count": 0, - "crossproc_count": 48, - "device_group_id": 0, - "device_id": 17686136, - "device_name": "test_name", - "device_policy_id": 20622246, - "device_timestamp": "2023-01-13T17:17:45.322Z", - "event_id": "DA9E269E-421D-469D-A212-9062888A02F4", - "filemod_count": 3, - "ingress_time": 1673630293265, - "modload_count": 1, - "netconn_count": 35, - "org_id": "ABCD1234", - "parent_guid": "ABCD1234-010dde78-00000260-00000000-1d9275de5e5b262", - "parent_pid": 608, - "process_guid": "ABCD1234-010dde78-00000308-00000000-1d9275de6169dd7", - "process_hash": [ - "15a556def233f112d127025ab51ac2d3", - "362ab9743ff5d0f95831306a780fc3e418990f535013c80212dd85cb88ef7427" - ], - "process_name": "c:\\windows\\system32\\lsass.exe", - "process_pid": [ - 776 - ], - "process_username": [ - "NT AUTHORITY\\SYSTEM" - ], - "regmod_count": 11, - "scriptload_count": 0, - "windows_event_id": 4624 - } - ], - "num_found": 1, - "num_available": 1, - "approximate_unaggregated": 1, - "num_aggregated": 1, - "contacted": 4, - "completed": 4 + "results": [ + { + "auth_domain_name": "NT AUTHORITY", + "auth_event_action": "LOGON_SUCCESS", + "auth_remote_device": "-", + "auth_remote_port": 0, + "auth_username": "SYSTEM", + "backend_timestamp": "2023-01-13T17:19:01.013Z", + "childproc_count": 0, + "crossproc_count": 48, + "device_group_id": 0, + "device_id": 17686136, + "device_name": "test_name", + "device_policy_id": 20622246, + "device_timestamp": "2023-01-13T17:17:45.322Z", + "event_id": "DA9E269E-421D-469D-A212-9062888A02F4", + "filemod_count": 3, + "ingress_time": 1673630293265, + "modload_count": 1, + "netconn_count": 35, + "org_id": "ABCD1234", + "parent_guid": "ABCD1234-010dde78-00000260-00000000-1d9275de5e5b262", + "parent_pid": 608, + "process_guid": "ABCD1234-010dde78-00000308-00000000-1d9275de6169dd7", + "process_hash": [ + "15a556def233f112d127025ab51ac2d3", + "362ab9743ff5d0f95831306a780fc3e418990f535013c80212dd85cb88ef7427", + ], + "process_name": "c:\\windows\\system32\\lsass.exe", + "process_pid": [776], + "process_username": ["NT AUTHORITY\\SYSTEM"], + "regmod_count": 11, + "scriptload_count": 0, + "windows_event_id": 4624, + } + ], + "num_found": 1, + "num_available": 1, + "approximate_unaggregated": 1, + "num_aggregated": 1, + "contacted": 4, + "completed": 4, } GET_AUTH_EVENT_SEARCH_JOB_RESULTS_RESP_0 = { - "results": [], - "num_found": 0, - "num_available": 0, - "approximate_unaggregated": 0, - "num_aggregated": 0, - "contacted": 0, - "completed": 0 + "results": [], + "num_found": 0, + "num_available": 0, + "approximate_unaggregated": 0, + "num_aggregated": 0, + "contacted": 0, + "completed": 0, } GET_AUTH_EVENT_SEARCH_JOB_RESULTS_RESP_ZERO_COMP = { - "results": [], - "num_found": 0, - "num_available": 0, - "approximate_unaggregated": 0, - "num_aggregated": 0, - "contacted": 242, - "completed": 0 + "results": [], + "num_found": 0, + "num_available": 0, + "approximate_unaggregated": 0, + "num_aggregated": 0, + "contacted": 242, + "completed": 0, } GET_AUTH_EVENT_SEARCH_JOB_RESULTS_ZERO = { - "results": [], - "num_found": 0, - "num_available": 0, - "approximate_unaggregated": 0, - "num_aggregated": 0, - "contacted": 242, - "completed": 242 + "results": [], + "num_found": 0, + "num_available": 0, + "approximate_unaggregated": 0, + "num_aggregated": 0, + "contacted": 242, + "completed": 242, } GET_AUTH_EVENT_SEARCH_JOB_RESULTS_RESP_2 = { - "results": [ - { - "auth_domain_name": "NT AUTHORITY", - "auth_event_action": "LOGOFF_SUCCESS", - "auth_remote_port": 0, - "auth_username": "SYSTEM", - "backend_timestamp": "2023-03-08T08:18:52.654Z", - "childproc_count": 0, - "crossproc_count": 1859, - "device_group_id": 0, - "device_id": 18101914, - "device_name": "richm\\win11", - "device_policy_id": 20886205, - "device_timestamp": "2023-03-08T08:15:03.090Z", - "event_id": "9D137450-6428-446E-8C23-F0C526156A0C", - "filemod_count": 33, - "ingress_time": 1678263444460, - "modload_count": 7, - "netconn_count": 113, - "org_id": "ABCD1234", - "parent_guid": "ABCD1234-0114369a-000002ac-00000000-1d94538fb2b061e", - "parent_pid": 684, - "process_guid": "ABCD1234-0114369a-0000033c-00000000-1d94538fb4eea6c", - "process_hash": [ - "c0ba0caebf823de8f2ebf49eea9cc5e5", - "c72b9e35e307fefe59bacc3c65842e93b963f6c3732934061857cc773d6e2e5b" - ], - "process_name": "c:\\windows\\system32\\lsass.exe", - "process_pid": [ - 828 - ], - "process_username": [ - "NT AUTHORITY\\SYSTEM" - ], - "regmod_count": 42, - "scriptload_count": 0, - "windows_event_id": 4634 - }, - { - "auth_domain_name": "NT AUTHORITY", - "auth_event_action": "PRIVILEGES_GRANTED", - "auth_remote_port": 0, - "auth_username": "SYSTEM", - "backend_timestamp": "2023-03-08T08:18:52.654Z", - "childproc_count": 0, - "crossproc_count": 1859, - "device_group_id": 0, - "device_id": 18101914, - "device_name": "richm\\win11", - "device_policy_id": 20886205, - "device_timestamp": "2023-03-08T08:15:03.082Z", - "event_id": "D5A08829-041E-401E-9C14-F8FDFBC2EE63", - "filemod_count": 33, - "ingress_time": 1678263444460, - "modload_count": 7, - "netconn_count": 113, - "org_id": "ABCD1234", - "parent_guid": "ABCD1234-0114369a-000002ac-00000000-1d94538fb2b061e", - "parent_pid": 684, - "process_guid": "ABCD1234-0114369a-0000033c-00000000-1d94538fb4eea6c", - "process_hash": [ - "c0ba0caebf823de8f2ebf49eea9cc5e5", - "c72b9e35e307fefe59bacc3c65842e93b963f6c3732934061857cc773d6e2e5b" - ], - "process_name": "c:\\windows\\system32\\lsass.exe", - "process_pid": [ - 828 - ], - "process_username": [ - "NT AUTHORITY\\SYSTEM" - ], - "regmod_count": 42, - "scriptload_count": 0, - "windows_event_id": 4672 - } - ], - "num_found": 198, - "num_available": 198, - "approximate_unaggregated": 198, - "num_aggregated": 198, - "contacted": 241, - "completed": 241 + "results": [ + { + "auth_domain_name": "NT AUTHORITY", + "auth_event_action": "LOGOFF_SUCCESS", + "auth_remote_port": 0, + "auth_username": "SYSTEM", + "backend_timestamp": "2023-03-08T08:18:52.654Z", + "childproc_count": 0, + "crossproc_count": 1859, + "device_group_id": 0, + "device_id": 18101914, + "device_name": "richm\\win11", + "device_policy_id": 20886205, + "device_timestamp": "2023-03-08T08:15:03.090Z", + "event_id": "9D137450-6428-446E-8C23-F0C526156A0C", + "filemod_count": 33, + "ingress_time": 1678263444460, + "modload_count": 7, + "netconn_count": 113, + "org_id": "ABCD1234", + "parent_guid": "ABCD1234-0114369a-000002ac-00000000-1d94538fb2b061e", + "parent_pid": 684, + "process_guid": "ABCD1234-0114369a-0000033c-00000000-1d94538fb4eea6c", + "process_hash": [ + "c0ba0caebf823de8f2ebf49eea9cc5e5", + "c72b9e35e307fefe59bacc3c65842e93b963f6c3732934061857cc773d6e2e5b", + ], + "process_name": "c:\\windows\\system32\\lsass.exe", + "process_pid": [828], + "process_username": ["NT AUTHORITY\\SYSTEM"], + "regmod_count": 42, + "scriptload_count": 0, + "windows_event_id": 4634, + }, + { + "auth_domain_name": "NT AUTHORITY", + "auth_event_action": "PRIVILEGES_GRANTED", + "auth_remote_port": 0, + "auth_username": "SYSTEM", + "backend_timestamp": "2023-03-08T08:18:52.654Z", + "childproc_count": 0, + "crossproc_count": 1859, + "device_group_id": 0, + "device_id": 18101914, + "device_name": "richm\\win11", + "device_policy_id": 20886205, + "device_timestamp": "2023-03-08T08:15:03.082Z", + "event_id": "D5A08829-041E-401E-9C14-F8FDFBC2EE63", + "filemod_count": 33, + "ingress_time": 1678263444460, + "modload_count": 7, + "netconn_count": 113, + "org_id": "ABCD1234", + "parent_guid": "ABCD1234-0114369a-000002ac-00000000-1d94538fb2b061e", + "parent_pid": 684, + "process_guid": "ABCD1234-0114369a-0000033c-00000000-1d94538fb4eea6c", + "process_hash": [ + "c0ba0caebf823de8f2ebf49eea9cc5e5", + "c72b9e35e307fefe59bacc3c65842e93b963f6c3732934061857cc773d6e2e5b", + ], + "process_name": "c:\\windows\\system32\\lsass.exe", + "process_pid": [828], + "process_username": ["NT AUTHORITY\\SYSTEM"], + "regmod_count": 42, + "scriptload_count": 0, + "windows_event_id": 4672, + }, + ], + "num_found": 198, + "num_available": 198, + "approximate_unaggregated": 198, + "num_aggregated": 198, + "contacted": 241, + "completed": 241, } @@ -181,100 +169,92 @@ GET_AUTH_EVENT_DETAIL_JOB_RESULTS_RESP = { - "results": [ - { - "auth_cleartext_credentials_logon": False, - "auth_daemon_logon": False, - "auth_domain_name": "NT AUTHORITY", - "auth_elevated_token_logon": False, - "auth_event_action": "LOGON_SUCCESS", - "auth_failed_logon_count": 0, - "auth_impersonation_level": "IMPERSONATION_INVALID", - "auth_interactive_logon": False, - "auth_key_length": 0, - "auth_logon_id": "00000000-000003E7", - "auth_logon_type": 5, - "auth_package": "Negotiate", - "auth_remote_device": "-", - "auth_remote_logon": False, - "auth_remote_port": 0, - "auth_restricted_admin_logon": False, - "auth_user_id": "S-1-5-18", - "auth_username": "SYSTEM", - "auth_virtual_account_logon": False, - "backend_timestamp": "2023-02-23T14:31:09.058Z", - "childproc_count": 0, - "crossproc_count": 0, - "device_external_ip": "66.170.98.188", - "device_group_id": 0, - "device_id": 17853466, - "device_installed_by": "No user", - "device_internal_ip": "10.52.4.52", - "device_location": "UNKNOWN", - "device_name": "cbawtd\\w10cbws2thtplt", - "device_os": "WINDOWS", - "device_os_version": "Windows 10 x64", - "device_policy": "raj-test-monitor", - "device_policy_id": 20622246, - "device_sensor_version": "3.9.1.2451", - "device_target_priority": "MEDIUM", - "device_timestamp": "2023-02-23T14:29:03.588Z", - "document_guid": "19F5ah7QR8mTUjdqRvXm0w", - "event_id": "D06DC822-B25E-4162-A5A7-6166BFA9B8DF", - "event_report_code": "SUB_RPT_NONE", - "filemod_count": 0, - "ingress_time": 1677162610331, - "modload_count": 0, - "netconn_count": 0, - "org_id": "ABCD1234", - "parent_cmdline": "wininit.exe", - "parent_cmdline_length": 11, - "parent_effective_reputation": "LOCAL_WHITE", - "parent_effective_reputation_source": "IGNORE", - "parent_guid": "ABCD1234-01106c1a-0000025c-00000000-1d942ef2b31029a", - "parent_hash": [ - "9ef51c8ad595c5e2a123c06ad39fccd7", - "268ca325c8f12e68b6728ff24d6536030aab6e05603d0179033b1e51d8476d86" - ], - "parent_name": "c:\\windows\\system32\\wininit.exe", - "parent_pid": 604, - "parent_reputation": "TRUSTED_WHITE_LIST", - "process_cmdline": [ - "C:\\Windows\\system32\\lsass.exe" - ], - "process_cmdline_length": [ - 29 - ], - "process_effective_reputation": "LOCAL_WHITE", - "process_effective_reputation_source": "IGNORE", - "process_elevated": True, - "process_guid": "ABCD1234-01106c1a-000002fc-00000000-1d942ef2b618b15", - "process_hash": [ - "15a556def233f112d127025ab51ac2d3", - "362ab9743ff5d0f95831306a780fc3e418990f535013c80212dd85cb88ef7427" - ], - "process_integrity_level": "SYSTEM", - "process_name": "c:\\windows\\system32\\lsass.exe", - "process_pid": [ - 764 - ], - "process_reputation": "TRUSTED_WHITE_LIST", - "process_sha256": "362ab9743ff5d0f95831306a780fc3e418990f535013c80212dd85cb88ef7427", - "process_start_time": "2023-02-17T16:44:57.657Z", - "process_username": [ - "NT AUTHORITY\\SYSTEM" - ], - "regmod_count": 0, - "scriptload_count": 0, - "windows_event_id": 4624 - } - ], - "num_found": 1, - "num_available": 1, - "approximate_unaggregated": 1, - "num_aggregated": 1, - "contacted": 13, - "completed": 13 + "results": [ + { + "auth_cleartext_credentials_logon": False, + "auth_daemon_logon": False, + "auth_domain_name": "NT AUTHORITY", + "auth_elevated_token_logon": False, + "auth_event_action": "LOGON_SUCCESS", + "auth_failed_logon_count": 0, + "auth_impersonation_level": "IMPERSONATION_INVALID", + "auth_interactive_logon": False, + "auth_key_length": 0, + "auth_logon_id": "00000000-000003E7", + "auth_logon_type": 5, + "auth_package": "Negotiate", + "auth_remote_device": "-", + "auth_remote_logon": False, + "auth_remote_port": 0, + "auth_restricted_admin_logon": False, + "auth_user_id": "S-1-5-18", + "auth_username": "SYSTEM", + "auth_virtual_account_logon": False, + "backend_timestamp": "2023-02-23T14:31:09.058Z", + "childproc_count": 0, + "crossproc_count": 0, + "device_external_ip": "66.170.98.188", + "device_group_id": 0, + "device_id": 17853466, + "device_installed_by": "No user", + "device_internal_ip": "10.52.4.52", + "device_location": "UNKNOWN", + "device_name": "cbawtd\\w10cbws2thtplt", + "device_os": "WINDOWS", + "device_os_version": "Windows 10 x64", + "device_policy": "raj-test-monitor", + "device_policy_id": 20622246, + "device_sensor_version": "3.9.1.2451", + "device_target_priority": "MEDIUM", + "device_timestamp": "2023-02-23T14:29:03.588Z", + "document_guid": "19F5ah7QR8mTUjdqRvXm0w", + "event_id": "D06DC822-B25E-4162-A5A7-6166BFA9B8DF", + "event_report_code": "SUB_RPT_NONE", + "filemod_count": 0, + "ingress_time": 1677162610331, + "modload_count": 0, + "netconn_count": 0, + "org_id": "ABCD1234", + "parent_cmdline": "wininit.exe", + "parent_cmdline_length": 11, + "parent_effective_reputation": "LOCAL_WHITE", + "parent_effective_reputation_source": "IGNORE", + "parent_guid": "ABCD1234-01106c1a-0000025c-00000000-1d942ef2b31029a", + "parent_hash": [ + "9ef51c8ad595c5e2a123c06ad39fccd7", + "268ca325c8f12e68b6728ff24d6536030aab6e05603d0179033b1e51d8476d86", + ], + "parent_name": "c:\\windows\\system32\\wininit.exe", + "parent_pid": 604, + "parent_reputation": "TRUSTED_WHITE_LIST", + "process_cmdline": ["C:\\Windows\\system32\\lsass.exe"], + "process_cmdline_length": [29], + "process_effective_reputation": "LOCAL_WHITE", + "process_effective_reputation_source": "IGNORE", + "process_elevated": True, + "process_guid": "ABCD1234-01106c1a-000002fc-00000000-1d942ef2b618b15", + "process_hash": [ + "15a556def233f112d127025ab51ac2d3", + "362ab9743ff5d0f95831306a780fc3e418990f535013c80212dd85cb88ef7427", + ], + "process_integrity_level": "SYSTEM", + "process_name": "c:\\windows\\system32\\lsass.exe", + "process_pid": [764], + "process_reputation": "TRUSTED_WHITE_LIST", + "process_sha256": "362ab9743ff5d0f95831306a780fc3e418990f535013c80212dd85cb88ef7427", + "process_start_time": "2023-02-17T16:44:57.657Z", + "process_username": ["NT AUTHORITY\\SYSTEM"], + "regmod_count": 0, + "scriptload_count": 0, + "windows_event_id": 4624, + } + ], + "num_found": 1, + "num_available": 1, + "approximate_unaggregated": 1, + "num_aggregated": 1, + "contacted": 13, + "completed": 13, } @@ -332,253 +312,200 @@ GET_AUTH_EVENT_GROUPED_RESULTS_RESP = { - "group_results": [ - { - "group_key": "auth_username", - "group_value": "SYSTEM", - "group_start_timestamp": "2023-02-23T14:29:03.588Z", - "group_end_timestamp": "2023-03-07T11:11:21.593Z", - "results": [ + "group_results": [ { - "auth_domain_name": "NT AUTHORITY", - "auth_event_action": "LOGOFF_SUCCESS", - "auth_remote_port": 0, - "auth_username": "SYSTEM", - "backend_timestamp": "2023-03-07T11:20:02.046Z", - "childproc_count": 0, - "crossproc_count": 1724, - "device_group_id": 0, - "device_id": 18101914, - "device_name": "richm\\win11", - "device_policy_id": 20886205, - "device_timestamp": "2023-03-07T11:11:21.593Z", - "event_id": "E8F7A1F9-72FC-4C5D-B8D2-113647B30D87", - "filemod_count": 31, - "ingress_time": 1678187557319, - "modload_count": 7, - "netconn_count": 112, - "org_id": "ABCD1234", - "parent_guid": "ABCD1234-0114369a-000002ac-00000000-1d94538fb2b061e", - "parent_pid": 684, - "process_guid": "ABCD1234-0114369a-0000033c-00000000-1d94538fb4eea6c", - "process_hash": [ - "c0ba0caebf823de8f2ebf49eea9cc5e5", - "c72b9e35e307fefe59bacc3c65842e93b963f6c3732934061857cc773d6e2e5b" - ], - "process_name": "c:\\windows\\system32\\lsass.exe", - "process_pid": [ - 828 - ], - "process_username": [ - "NT AUTHORITY\\SYSTEM" - ], - "regmod_count": 39, - "scriptload_count": 0, - "windows_event_id": 4634 - }, - ], - "total_events": 174 - } - ], - "num_found": 174, - "num_available": 174, - "groups_num_available": 1, - "approximate_unaggregated": 174, - "num_aggregated": 174, - "contacted": 169, - "completed": 169 + "group_key": "auth_username", + "group_value": "SYSTEM", + "group_start_timestamp": "2023-02-23T14:29:03.588Z", + "group_end_timestamp": "2023-03-07T11:11:21.593Z", + "results": [ + { + "auth_domain_name": "NT AUTHORITY", + "auth_event_action": "LOGOFF_SUCCESS", + "auth_remote_port": 0, + "auth_username": "SYSTEM", + "backend_timestamp": "2023-03-07T11:20:02.046Z", + "childproc_count": 0, + "crossproc_count": 1724, + "device_group_id": 0, + "device_id": 18101914, + "device_name": "richm\\win11", + "device_policy_id": 20886205, + "device_timestamp": "2023-03-07T11:11:21.593Z", + "event_id": "E8F7A1F9-72FC-4C5D-B8D2-113647B30D87", + "filemod_count": 31, + "ingress_time": 1678187557319, + "modload_count": 7, + "netconn_count": 112, + "org_id": "ABCD1234", + "parent_guid": "ABCD1234-0114369a-000002ac-00000000-1d94538fb2b061e", + "parent_pid": 684, + "process_guid": "ABCD1234-0114369a-0000033c-00000000-1d94538fb4eea6c", + "process_hash": [ + "c0ba0caebf823de8f2ebf49eea9cc5e5", + "c72b9e35e307fefe59bacc3c65842e93b963f6c3732934061857cc773d6e2e5b", + ], + "process_name": "c:\\windows\\system32\\lsass.exe", + "process_pid": [828], + "process_username": ["NT AUTHORITY\\SYSTEM"], + "regmod_count": 39, + "scriptload_count": 0, + "windows_event_id": 4634, + }, + ], + "total_events": 174, + } + ], + "num_found": 174, + "num_available": 174, + "groups_num_available": 1, + "approximate_unaggregated": 174, + "num_aggregated": 174, + "contacted": 169, + "completed": 169, } AUTH_EVENT_SEARCH_SUGGESTIONS_RESP = { - "suggestions": [ - { - "term": "auth_cleartext_credentials_logon", - "weight": 300, - "required_skus_all": [ - "auth" - ], - "required_skus_some": [] - }, - { - "term": "auth_daemon_logon", - "weight": 300, - "required_skus_all": [ - "auth" - ], - "required_skus_some": [] - }, - { - "term": "auth_domain_name", - "weight": 300, - "required_skus_all": [ - "auth" - ], - "required_skus_some": [] - }, - { - "term": "auth_elevated_token_logon", - "weight": 300, - "required_skus_all": [ - "auth" - ], - "required_skus_some": [] - }, - { - "term": "auth_event_action", - "weight": 300, - "required_skus_all": [ - "auth" - ], - "required_skus_some": [] - }, - { - "term": "auth_failed_logon_count", - "weight": 300, - "required_skus_all": [ - "auth" - ], - "required_skus_some": [] - }, - { - "term": "auth_failure_status", - "weight": 300, - "required_skus_all": [ - "auth" - ], - "required_skus_some": [] - }, - { - "term": "auth_failure_sub_status", - "weight": 300, - "required_skus_all": [ - "auth" - ], - "required_skus_some": [] - }, - { - "term": "auth_interactive_logon", - "weight": 300, - "required_skus_all": [ - "auth" - ], - "required_skus_some": [] - }, - { - "term": "auth_logon_id", - "weight": 300, - "required_skus_all": [ - "auth" - ], - "required_skus_some": [] - }, - { - "term": "auth_logon_type", - "weight": 300, - "required_skus_all": [ - "auth" - ], - "required_skus_some": [] - }, - { - "term": "auth_privileges", - "weight": 300, - "required_skus_all": [ - "auth" - ], - "required_skus_some": [] - }, - { - "term": "auth_remote_device", - "weight": 300, - "required_skus_all": [ - "auth" - ], - "required_skus_some": [] - }, - { - "term": "auth_remote_ipv4", - "weight": 300, - "required_skus_all": [ - "auth" - ], - "required_skus_some": [] - }, - { - "term": "auth_remote_ipv6", - "weight": 300, - "required_skus_all": [ - "auth" - ], - "required_skus_some": [] - }, - { - "term": "auth_remote_location", - "weight": 300, - "required_skus_all": [ - "auth" - ], - "required_skus_some": [] - }, - { - "term": "auth_remote_logon", - "weight": 300, - "required_skus_all": [ - "auth" - ], - "required_skus_some": [] - }, - { - "term": "auth_remote_port", - "weight": 300, - "required_skus_all": [ - "auth" - ], - "required_skus_some": [] - }, - { - "term": "auth_restricted_admin_logon", - "weight": 300, - "required_skus_all": [ - "auth" - ], - "required_skus_some": [] - }, - { - "term": "auth_user_id", - "weight": 300, - "required_skus_all": [ - "auth" - ], - "required_skus_some": [] - }, - { - "term": "auth_user_principal_name", - "weight": 300, - "required_skus_all": [ - "auth" - ], - "required_skus_some": [] - }, - { - "term": "auth_username", - "weight": 300, - "required_skus_all": [ - "auth" - ], - "required_skus_some": [] - }, - { - "term": "auth_virtual_account_logon", - "weight": 300, - "required_skus_all": [ - "auth" - ], - "required_skus_some": [] - } - ] + "suggestions": [ + { + "term": "auth_cleartext_credentials_logon", + "weight": 300, + "required_skus_all": ["auth"], + "required_skus_some": [], + }, + { + "term": "auth_daemon_logon", + "weight": 300, + "required_skus_all": ["auth"], + "required_skus_some": [], + }, + { + "term": "auth_domain_name", + "weight": 300, + "required_skus_all": ["auth"], + "required_skus_some": [], + }, + { + "term": "auth_elevated_token_logon", + "weight": 300, + "required_skus_all": ["auth"], + "required_skus_some": [], + }, + { + "term": "auth_event_action", + "weight": 300, + "required_skus_all": ["auth"], + "required_skus_some": [], + }, + { + "term": "auth_failed_logon_count", + "weight": 300, + "required_skus_all": ["auth"], + "required_skus_some": [], + }, + { + "term": "auth_failure_status", + "weight": 300, + "required_skus_all": ["auth"], + "required_skus_some": [], + }, + { + "term": "auth_failure_sub_status", + "weight": 300, + "required_skus_all": ["auth"], + "required_skus_some": [], + }, + { + "term": "auth_interactive_logon", + "weight": 300, + "required_skus_all": ["auth"], + "required_skus_some": [], + }, + { + "term": "auth_logon_id", + "weight": 300, + "required_skus_all": ["auth"], + "required_skus_some": [], + }, + { + "term": "auth_logon_type", + "weight": 300, + "required_skus_all": ["auth"], + "required_skus_some": [], + }, + { + "term": "auth_privileges", + "weight": 300, + "required_skus_all": ["auth"], + "required_skus_some": [], + }, + { + "term": "auth_remote_device", + "weight": 300, + "required_skus_all": ["auth"], + "required_skus_some": [], + }, + { + "term": "auth_remote_ipv4", + "weight": 300, + "required_skus_all": ["auth"], + "required_skus_some": [], + }, + { + "term": "auth_remote_ipv6", + "weight": 300, + "required_skus_all": ["auth"], + "required_skus_some": [], + }, + { + "term": "auth_remote_location", + "weight": 300, + "required_skus_all": ["auth"], + "required_skus_some": [], + }, + { + "term": "auth_remote_logon", + "weight": 300, + "required_skus_all": ["auth"], + "required_skus_some": [], + }, + { + "term": "auth_remote_port", + "weight": 300, + "required_skus_all": ["auth"], + "required_skus_some": [], + }, + { + "term": "auth_restricted_admin_logon", + "weight": 300, + "required_skus_all": ["auth"], + "required_skus_some": [], + }, + { + "term": "auth_user_id", + "weight": 300, + "required_skus_all": ["auth"], + "required_skus_some": [], + }, + { + "term": "auth_user_principal_name", + "weight": 300, + "required_skus_all": ["auth"], + "required_skus_some": [], + }, + { + "term": "auth_username", + "weight": 300, + "required_skus_all": ["auth"], + "required_skus_some": [], + }, + { + "term": "auth_virtual_account_logon", + "weight": 300, + "required_skus_all": ["auth"], + "required_skus_some": [], + }, + ] } -AUTH_EVENT_SEARCH_VALIDATIONS_RESP = { - "valid": True, - "value_search_query": True -} +AUTH_EVENT_SEARCH_VALIDATIONS_RESP = {"valid": True, "value_search_query": True} From 8eaa3521ce1c9b378d14f1b3f6f2b53714d0af22 Mon Sep 17 00:00:00 2001 From: Hristo Karagitliev Date: Tue, 14 Mar 2023 15:57:14 +0200 Subject: [PATCH 119/143] auth event models --- .../enterprise_edr/models/auth_events.yaml | 66 +++++++++++++++++++ .../models/auth_events_facet.yaml | 61 +++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 src/cbc_sdk/enterprise_edr/models/auth_events.yaml create mode 100644 src/cbc_sdk/enterprise_edr/models/auth_events_facet.yaml diff --git a/src/cbc_sdk/enterprise_edr/models/auth_events.yaml b/src/cbc_sdk/enterprise_edr/models/auth_events.yaml new file mode 100644 index 000000000..01b5df97f --- /dev/null +++ b/src/cbc_sdk/enterprise_edr/models/auth_events.yaml @@ -0,0 +1,66 @@ +type: object +properties: + auth_domain_name: + type: string + auth_event_action: + type: string + auth_remote_device: + type: string + auth_remote_port: + type: integer + auth_username: + type: string + backend_timestamp: + type: string + childproc_count: + type: integer + crossproc_count: + type: integer + device_group_id: + type: integer + device_id: + type: integer + device_name: + type: string + device_policy_id: + type: integer + device_timestamp: + type: string + event_id: + type: string + filemod_count: + type: integer + ingress_time: + type: integer + modload_count: + type: integer + netconn_count: + type: integer + org_id: + type: string + parent_guid: + type: string + parent_pid: + type: integer + process_guid: + type: string + process_hash: + type: array + items: + type: string + process_name: + type: string + process_pid: + type: array + items: + type: integer + process_username: + type: array + items: + type: string + regmod_count: + type: integer + scriptload_count: + type: integer + windows_event_id: + type: integer diff --git a/src/cbc_sdk/enterprise_edr/models/auth_events_facet.yaml b/src/cbc_sdk/enterprise_edr/models/auth_events_facet.yaml new file mode 100644 index 000000000..92e4301d2 --- /dev/null +++ b/src/cbc_sdk/enterprise_edr/models/auth_events_facet.yaml @@ -0,0 +1,61 @@ +type: object +properties: + terms: + type: array + description: Contains the Auth Event Facet search results + items: + field: + type: string + description: The name of the field being summarized + values: + type: array + items: + type: object + properties: + total: + type: integer + format: int32 + description: The total number of times this value appears in the query output + id: + type: string + description: The ID of the value being enumerated + name: + type: string + description: The name of the value being enumerated + ranges: + type: array + description: Groupings for search result properties that are ISO 8601 timestamps or numbers + items: + bucket_size: + type: string + description: How large of a bucket to group results in. If grouping an ISO 8601 property, use a string like '-3DAYS' + start: + oneOf: + - type: integer + - type: string + description: What value to begin grouping at + end: + type: string + description: What value to end grouping at + field: + type: string + description: The name of the field being grouped + values: + type: array + description: The result values of the field being grouped + items: + name: + type: string + description: The name of the value being enumerated + total: + type: integer + description: The total number of times this value appears in the query bucket output + num_found: + type: integer + descrption: The total number of results of the query + contacted: + type: integer + description: The number of searchers contacted for this query + completed: + type: integer + description: The number of searchers that have reported their results From 35fcfa1d45cf0839e34f1122fb798123d34bdb96 Mon Sep 17 00:00:00 2001 From: Hristo Karagitliev Date: Tue, 14 Mar 2023 16:36:36 +0200 Subject: [PATCH 120/143] test fix --- src/tests/unit/enterprise_edr/test_auth_events.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/tests/unit/enterprise_edr/test_auth_events.py b/src/tests/unit/enterprise_edr/test_auth_events.py index b3ff5b2b1..6b15a6d31 100644 --- a/src/tests/unit/enterprise_edr/test_auth_events.py +++ b/src/tests/unit/enterprise_edr/test_auth_events.py @@ -309,7 +309,6 @@ def test_auth_event_select_details_sync_zero(cbcsdk_mock): events = events_list[0] results = events.get_details() assert results["device_name"] is not None - assert results.get("alert_id") == [] def test_auth_event_select_compound(cbcsdk_mock): From 5ba0fd43352fb3285b59464566741814840f742e Mon Sep 17 00:00:00 2001 From: Hristo Karagitliev Date: Tue, 14 Mar 2023 19:50:43 +0200 Subject: [PATCH 121/143] convert support method to static class methods --- src/cbc_sdk/enterprise_edr/auth_events.py | 117 +++++++++--------- .../unit/enterprise_edr/test_auth_events.py | 4 +- 2 files changed, 63 insertions(+), 58 deletions(-) diff --git a/src/cbc_sdk/enterprise_edr/auth_events.py b/src/cbc_sdk/enterprise_edr/auth_events.py index c19803ed8..a142f3e2d 100644 --- a/src/cbc_sdk/enterprise_edr/auth_events.py +++ b/src/cbc_sdk/enterprise_edr/auth_events.py @@ -179,6 +179,67 @@ def _get_detailed_results(self): ) + @staticmethod + def get_auth_events_descriptions(cb): + """ + Returns descriptions and status messages of Auth Events. + + Returns: + dict: Descriptions and status messages of Auth Events as dict objects. + Example: + >>> cb = CBCloudAPI(profile="example_profile") + >>> descriptions = AuthEvent.get_auth_events_descriptions(cb) + >>> print(descriptions) + """ + url = "/api/investigate/v2/orgs/{}/auth_events/descriptions".format(cb.credentials.org_key) + + return cb.get_object(url) + + @staticmethod + def search_suggestions(cb, query, count=None): + """ + Returns suggestions for keys and field values that can be used in a search. + + Args: + cb + query (str): A search query to use. + count (int): (optional) Number of suggestions to be returned + + Returns: + list: A list of search suggestions expressed as dict objects. + Example: + >>> cb = CBCloudAPI(profile="example_profile") + >>> suggestions = AuthEvent.search_suggestions(cb, 'auth') + >>> print(suggestions) + """ + query_params = {"suggest.q": query} + if count: + query_params["suggest.count"] = count + url = "/api/investigate/v2/orgs/{}/auth_events/search_suggestions".format(cb.credentials.org_key) + output = cb.get_object(url, query_params) + return output["suggestions"] + + @staticmethod + def search_validation(cb, query): + """ + Returns validation result of a query. + + Args: + query (str): A search query to be validated. + + Returns: + bool: Status of the validation + Example: + >>> cb = CBCloudAPI(profile="example_profile") + >>> validation = AuthEvent.search_validation(cb, 'auth_username:Administrator') + >>> print(validation) + """ + query_params = {"q": query} + url = "/api/investigate/v2/orgs/{}/auth_events/search_validation".format(cb.credentials.org_key) + output = cb.get_object(url, query_params) + return output.get("valid", False) + + class AuthEventFacet(UnrefreshableModel): """ Represents an AuthEvent facet retrieved. @@ -637,59 +698,3 @@ def group_results( for group in result.get("group_results", []): yield AuthEventGroup(self._cb, initial_data=group) - - def get_auth_events_descriptions(self): - """ - Returns descriptions and status messages of Auth Events. - - Returns: - dict: Descriptions and status messages of Auth Events as dict objects. - Example: - >>> cb = CBCloudAPI(profile="example_profile") - >>> descriptions = cb.select(AuthEvent).get_auth_events_descriptions() - >>> print(descriptions) - """ - url = "/api/investigate/v2/orgs/{}/auth_events/descriptions".format(self._cb.credentials.org_key) - - return self._cb.get_object(url) - - def search_suggestions(self, query, count=None): - """ - Returns suggestions for keys and field values that can be used in a search. - - Args: - query (str): A search query to use. - count (int): (optional) Number of suggestions to be returned - - Returns: - list: A list of search suggestions expressed as dict objects. - Example: - >>> cb = CBCloudAPI(profile="example_profile") - >>> suggestions = cb.select(AuthEvent).search_suggestions('auth') - >>> print(suggestions) - """ - query_params = {"suggest.q": query} - if count: - query_params["suggest.count"] = count - url = "/api/investigate/v2/orgs/{}/auth_events/search_suggestions".format(self._cb.credentials.org_key) - output = self._cb.get_object(url, query_params) - return output["suggestions"] - - def search_validation(self, query): - """ - Returns validation result of a query. - - Args: - query (str): A search query to be validated. - - Returns: - bool: Status of the validation - Example: - >>> cb = CBCloudAPI(profile="example_profile") - >>> validation = cb.select(AuthEvent).search_validation('auth_username:Administrator') - >>> print(validation) - """ - query_params = {"q": query} - url = "/api/investigate/v2/orgs/{}/auth_events/search_validation".format(self._cb.credentials.org_key) - output = self._cb.get_object(url, query_params) - return output.get("valid", False) diff --git a/src/tests/unit/enterprise_edr/test_auth_events.py b/src/tests/unit/enterprise_edr/test_auth_events.py index 6b15a6d31..f6e1f2805 100644 --- a/src/tests/unit/enterprise_edr/test_auth_events.py +++ b/src/tests/unit/enterprise_edr/test_auth_events.py @@ -961,7 +961,7 @@ def test_auth_event_search_validations(cbcsdk_mock): f"/api/investigate/v2/orgs/test/auth_events/search_validation?{q}", AUTH_EVENT_SEARCH_VALIDATIONS_RESP, ) - result = api.select(AuthEvent).search_validation('auth_username') + result = AuthEvent.search_validation(api, 'auth_username') assert result is True @@ -974,6 +974,6 @@ def test_auth_event_search_suggestions(cbcsdk_mock): f"/api/investigate/v2/orgs/test/auth_events/search_suggestions?{q}", AUTH_EVENT_SEARCH_SUGGESTIONS_RESP, ) - result = api.select(AuthEvent).search_suggestions('auth') + result = AuthEvent.search_suggestions(api, 'auth') assert len(result) != 0 From c94dc1ed3722b21bdff86f9cff96e081ecbfb469 Mon Sep 17 00:00:00 2001 From: Hristo Karagitliev Date: Wed, 15 Mar 2023 14:53:18 +0200 Subject: [PATCH 122/143] create static methods and fix tests --- src/cbc_sdk/enterprise_edr/auth_events.py | 128 ++++++++++++++---- .../unit/enterprise_edr/test_auth_events.py | 108 +++++++++++++-- 2 files changed, 195 insertions(+), 41 deletions(-) diff --git a/src/cbc_sdk/enterprise_edr/auth_events.py b/src/cbc_sdk/enterprise_edr/auth_events.py index a142f3e2d..a2a5ee1d8 100644 --- a/src/cbc_sdk/enterprise_edr/auth_events.py +++ b/src/cbc_sdk/enterprise_edr/auth_events.py @@ -17,6 +17,7 @@ import logging import time +from copy import deepcopy log = logging.getLogger(__name__) @@ -25,6 +26,7 @@ class AuthEvent(NewBaseModel): """Represents an AuthEvent""" primary_key = "event_id" + validation_url = "/api/investigate/v2/orgs/{}/auth_events/search_validation" swagger_meta_file = "enterprise_edr/models/auth_events.yaml" def __init__( @@ -116,9 +118,6 @@ def get_details(self, timeout=0, async_mode=False): Examples: >>> cb = CBCloudAPI(profile="example_profile") - >>> event = cb.select(AuthEvent, "example-auth-event-id") - >>> print(event.get_details()) - >>> events = cb.select(AuthEvent).where(process_pid=2000) >>> print(events[0].get_details()) """ @@ -135,41 +134,75 @@ def get_details(self, timeout=0, async_mode=False): def _get_detailed_results(self): """Actual get details implementation""" - args = {"event_ids": [self.event_id]} - url = "/api/investigate/v2/orgs/{}/auth_events/detail_jobs".format( - self._cb.credentials.org_key + obj = AuthEvent._helper_get_details( + self._cb, + event_ids=[self.event_id], + timeout=self._details_timeout, ) - query_start = self._cb.post_object(url, body=args) + if obj: + self._info = deepcopy(obj._info) + return self + + @staticmethod + def _helper_get_details(cb, alert_id=None, event_ids=None, bulk=False, timeout=0): + """ + Helper to get auth_event details + + Args: + cb (CBCloudAPI): A reference to the CBCloudAPI object. + alert_id (str): An alert id to fetch associated auth_events + event_ids (list): A list of auth_event ids to fetch + bulk (bool): Whether it is a bulk request + timeout (int): AuthEvents details request timeout in milliseconds. + + Returns: + AuthEvent or list(AuthEvent): if it is a bulk operation a list, otherwise AuthEvent + + Raises: + ApiError: if cb is not instance of CBCloudAPI + """ + if cb.__class__.__name__ != "CBCloudAPI": + raise ApiError("cb argument should be instance of CBCloudAPI.") + if (alert_id and event_ids) or not (alert_id or event_ids): + raise ApiError("Either alert_id or event_ids should be provided.") + elif alert_id: + args = {"alert_id": alert_id} + else: + args = {"event_ids": event_ids} + url = "/api/investigate/v2/orgs/{}/auth_events/detail_jobs".format(cb.credentials.org_key) + query_start = cb.post_object(url, body=args) job_id = query_start.json().get("job_id") timed_out = False submit_time = time.time() * 1000 while True: result_url = "/api/investigate/v2/orgs/{}/auth_events/detail_jobs/{}/results".format( - self._cb.credentials.org_key, + cb.credentials.org_key, job_id, ) - result = self._cb.get_object(result_url) + result = cb.get_object(result_url) contacted = result.get("contacted", 0) completed = result.get("completed", 0) - log.debug(f"contacted = {contacted}, completed = {completed}") + log.debug("contacted = {}, completed = {}".format(contacted, completed)) if contacted == 0: time.sleep(0.5) continue if completed < contacted: - if self._details_timeout != 0 and (time.time() * 1000) - submit_time > self._details_timeout: + if timeout != 0 and (time.time() * 1000) - submit_time > timeout: timed_out = True break else: total_results = result.get("num_available", 0) found_results = result.get("num_found", 0) + # if found is 0, then no auth_events were found if found_results == 0: - return self + return None if total_results != 0: results = result.get("results", []) - self._info = results[0] - return self + if bulk: + return [AuthEvent(cb, initial_data=x) for x in results] + return AuthEvent(cb, initial_data=results[0]) time.sleep(0.5) @@ -178,19 +211,35 @@ def _get_detailed_results(self): message="user-specified timeout exceeded while waiting for results" ) - @staticmethod def get_auth_events_descriptions(cb): """ Returns descriptions and status messages of Auth Events. + Args: + cb (CBCloudAPI): A reference to the CBCloudAPI object. + Returns: dict: Descriptions and status messages of Auth Events as dict objects. + + Raises: + ApiError: if cb is not instance of CBCloudAPI + Example: >>> cb = CBCloudAPI(profile="example_profile") >>> descriptions = AuthEvent.get_auth_events_descriptions(cb) >>> print(descriptions) """ + try: + type(cb) + except TypeError: + pass + if cb.__class__.__name__ != "CBCloudAPI": + message = "cb argument should be instance of CBCloudAPI." + message += "\nExample:\ncb = CBCloudAPI(profile='example_profile')" + message += "\ndescriptions = AuthEvent.get_auth_events_descriptions(cb)" + raise ApiError(message) + url = "/api/investigate/v2/orgs/{}/auth_events/descriptions".format(cb.credentials.org_key) return cb.get_object(url) @@ -201,17 +250,27 @@ def search_suggestions(cb, query, count=None): Returns suggestions for keys and field values that can be used in a search. Args: - cb + cb (CBCloudAPI): A reference to the CBCloudAPI object. query (str): A search query to use. count (int): (optional) Number of suggestions to be returned Returns: list: A list of search suggestions expressed as dict objects. + + Raises: + ApiError: if cb is not instance of CBCloudAPI + Example: >>> cb = CBCloudAPI(profile="example_profile") >>> suggestions = AuthEvent.search_suggestions(cb, 'auth') >>> print(suggestions) """ + if cb.__class__.__name__ != "CBCloudAPI": + message = "cb argument should be instance of CBCloudAPI." + message += "\nExample:\ncb = CBCloudAPI(profile='example_profile')" + message += "\nsuggestions = AuthEvent.search_suggestions(cb, 'example-value')" + raise ApiError(message) + query_params = {"suggest.q": query} if count: query_params["suggest.count"] = count @@ -220,24 +279,38 @@ def search_suggestions(cb, query, count=None): return output["suggestions"] @staticmethod - def search_validation(cb, query): - """ - Returns validation result of a query. + def bulk_get_details(cb, alert_id=None, event_ids=None, timeout=0): + """Bulk get details Args: - query (str): A search query to be validated. + cb (CBCloudAPI): A reference to the CBCloudAPI object. + alert_id (str): An alert id to fetch associated events + event_ids (list): A list of event ids to fetch + timeout (int): AuthEvent details request timeout in milliseconds. Returns: - bool: Status of the validation + list: list of Auth Events + Example: >>> cb = CBCloudAPI(profile="example_profile") - >>> validation = AuthEvent.search_validation(cb, 'auth_username:Administrator') - >>> print(validation) + >>> bulk_details = AuthEvent.bulk_get_details(cb, event_ids=['example-value']) + >>> print(bulk_details) + + Raises: + ApiError: if cb is not instance of CBCloudAPI """ - query_params = {"q": query} - url = "/api/investigate/v2/orgs/{}/auth_events/search_validation".format(cb.credentials.org_key) - output = cb.get_object(url, query_params) - return output.get("valid", False) + if cb.__class__.__name__ != "CBCloudAPI": + message = "cb argument should be instance of CBCloudAPI." + message += "\nExample:\ncb = CBCloudAPI(profile='example_profile')" + message += "\nvalidation = AuthEvent.bulk_get_details(cb, alert_id='example-value')" + raise ApiError(message) + return AuthEvent._helper_get_details( + cb, + alert_id=alert_id, + event_ids=event_ids, + bulk=True, + timeout=timeout + ) class AuthEventFacet(UnrefreshableModel): @@ -516,6 +589,7 @@ def _submit(self): ) args = self._get_query_parameters() + self._validate({"q": args.get("query", "")}) url = "/api/investigate/v2/orgs/{}/auth_events/search_jobs".format( self._cb.credentials.org_key ) diff --git a/src/tests/unit/enterprise_edr/test_auth_events.py b/src/tests/unit/enterprise_edr/test_auth_events.py index f6e1f2805..610ae9844 100644 --- a/src/tests/unit/enterprise_edr/test_auth_events.py +++ b/src/tests/unit/enterprise_edr/test_auth_events.py @@ -25,7 +25,7 @@ GET_AUTH_EVENT_FACET_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING, GET_AUTH_EVENT_GROUPED_RESULTS_RESP, AUTH_EVENT_SEARCH_VALIDATIONS_RESP, - AUTH_EVENT_SEARCH_SUGGESTIONS_RESP + AUTH_EVENT_SEARCH_SUGGESTIONS_RESP, ) log = logging.basicConfig( @@ -54,6 +54,11 @@ def cbcsdk_mock(monkeypatch, cb): def test_auth_event_select_where(cbcsdk_mock): """Testing AuthEvent Querying with select()""" + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/auth_events/search_validation?q=auth_username%3ASYSTEM", # noqa: E501 + AUTH_EVENT_SEARCH_VALIDATIONS_RESP, + ) cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/auth_events/search_jobs", @@ -78,6 +83,11 @@ def test_auth_event_select_where(cbcsdk_mock): def test_auth_event_select_async(cbcsdk_mock): """Testing AuthEvent Querying with select() - asynchronous way""" + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/auth_events/search_validation?q=event_id%3ADA9E269E%5C-421D%5C-469D%5C-A212%5C-9062888A02F4", # noqa: E501 + AUTH_EVENT_SEARCH_VALIDATIONS_RESP, + ) cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/auth_events/search_jobs", @@ -102,6 +112,11 @@ def test_auth_event_select_async(cbcsdk_mock): def test_auth_event_select_by_id(cbcsdk_mock): """Testing AuthEvent Querying with select() - asynchronous way""" + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/auth_events/search_validation?q=event_id%3ADA9E269E%5C-421D%5C-469D%5C-A212%5C-9062888A02F4", # noqa: E501 + AUTH_EVENT_SEARCH_VALIDATIONS_RESP, + ) cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/auth_events/search_jobs", @@ -125,6 +140,11 @@ def test_auth_event_select_by_id(cbcsdk_mock): def test_auth_event_select_details_async(cbcsdk_mock): """Testing AuthEvent Querying with get_details - asynchronous mode""" + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/auth_events/search_validation?q=event_id%3ADA9E269E%5C-421D%5C-469D%5C-A212%5C-9062888A02F4", # noqa: E501 + AUTH_EVENT_SEARCH_VALIDATIONS_RESP, + ) cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/auth_events/search_jobs", @@ -206,6 +226,11 @@ def test_auth_event_details_timeout(cbcsdk_mock): def test_auth_event_select_details_sync(cbcsdk_mock): """Testing AuthEvent Querying with get_details""" + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/auth_events/search_validation?q=process_pid%3A2000", # noqa: E501 + AUTH_EVENT_SEARCH_VALIDATIONS_RESP, + ) cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/auth_events/search_jobs", @@ -243,6 +268,11 @@ def test_auth_event_select_details_sync(cbcsdk_mock): def test_auth_event_select_details_refresh(cbcsdk_mock): """Testing AuthEvent Querying with get_details""" + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/auth_events/search_validation?q=event_id%3ADA9E269E%5C-421D%5C-469D%5C-A212%5C-9062888A02F4", # noqa: E501 + AUTH_EVENT_SEARCH_VALIDATIONS_RESP, + ) cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/auth_events/search_jobs", @@ -278,6 +308,11 @@ def test_auth_event_select_details_refresh(cbcsdk_mock): def test_auth_event_select_details_sync_zero(cbcsdk_mock): """Testing AuthEvent Querying with get_details""" + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/auth_events/search_validation?q=process_pid%3A2000", # noqa: E501 + AUTH_EVENT_SEARCH_VALIDATIONS_RESP, + ) cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/auth_events/search_jobs", @@ -313,6 +348,11 @@ def test_auth_event_select_details_sync_zero(cbcsdk_mock): def test_auth_event_select_compound(cbcsdk_mock): """Testing AuthEvent Querying with select() and more complex criteria""" + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/auth_events/search_validation?q=process_pid%3A776+OR+parent_pid%3A608", # noqa: E501 + AUTH_EVENT_SEARCH_VALIDATIONS_RESP, + ) cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/auth_events/search_jobs", @@ -337,6 +377,12 @@ def test_auth_event_select_compound(cbcsdk_mock): def test_auth_event_query_implementation(cbcsdk_mock): """Testing AuthEvent querying with where().""" + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/auth_events/search_validation?q=event_id%3ADA9E269E-421D-469D-A212-9062888A02F4", # noqa: E501 + AUTH_EVENT_SEARCH_VALIDATIONS_RESP, + ) + cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/auth_events/search_jobs", @@ -372,6 +418,11 @@ def test_auth_event_timeout(cbcsdk_mock): def test_auth_event_timeout_error(cbcsdk_mock): """Testing that a timeout in AuthEvent querying throws a TimeoutError correctly""" + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/auth_events/search_validation?q=event_id%3ADA9E269E-421D-469D-A212-9062888A02F4", # noqa: E501 + AUTH_EVENT_SEARCH_VALIDATIONS_RESP, + ) cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/auth_events/search_jobs", @@ -434,6 +485,11 @@ def test_auth_event_time_range(cbcsdk_mock): def test_auth_event_submit(cbcsdk_mock): """Test _submit method of AuthEventQuery class""" + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/auth_events/search_validation?q=process_pid%3A1000", # noqa: E501 + AUTH_EVENT_SEARCH_VALIDATIONS_RESP, + ) cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/auth_events/search_jobs", @@ -450,6 +506,11 @@ def test_auth_event_submit(cbcsdk_mock): def test_auth_event_count(cbcsdk_mock): """Test _submit method of AuthEventquery class""" + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/auth_events/search_validation?q=process_pid%3A1000", # noqa: E501 + AUTH_EVENT_SEARCH_VALIDATIONS_RESP, + ) cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/auth_events/search_jobs", @@ -474,6 +535,11 @@ def test_auth_event_count(cbcsdk_mock): def test_auth_event_search(cbcsdk_mock): """Test _search method of AuthEventquery class""" + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/auth_events/search_validation?q=process_pid%3A828", # noqa: E501 + AUTH_EVENT_SEARCH_VALIDATIONS_RESP, + ) cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/auth_events/search_jobs", @@ -500,6 +566,11 @@ def test_auth_event_search(cbcsdk_mock): def test_auth_event_still_querying(cbcsdk_mock): """Test _search method of AuthEventquery class""" + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/auth_events/search_validation?q=process_pid%3A1000", # noqa: E501 + AUTH_EVENT_SEARCH_VALIDATIONS_RESP, + ) cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/auth_events/search_jobs", @@ -523,6 +594,11 @@ def test_auth_event_still_querying(cbcsdk_mock): def test_auth_event_still_querying2(cbcsdk_mock): """Test _search method of AuthEventquery class""" + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/auth_events/search_validation?q=process_pid%3A1000", # noqa: E501 + AUTH_EVENT_SEARCH_VALIDATIONS_RESP, + ) cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/auth_events/search_jobs", @@ -911,6 +987,11 @@ def test_auth_event_aggregation_wrong_field(cbcsdk_mock): def test_auth_event_select_group_results(cbcsdk_mock): """Testing AuthEvent Querying with select() and more complex criteria""" + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/auth_events/search_validation?q=process_pid%3A2000", # noqa: E501 + AUTH_EVENT_SEARCH_VALIDATIONS_RESP, + ) cbcsdk_mock.mock_request( "POST", "/api/investigate/v2/orgs/test/auth_events/search_jobs", @@ -952,19 +1033,6 @@ def test_auth_event_select_group_results(cbcsdk_mock): assert event_groups[0].auth_events[0]["process_pid"][0] == 764 -def test_auth_event_search_validations(cbcsdk_mock): - """Tests getting auth_events search validations""" - api = cbcsdk_mock.api - q = 'q=auth_username' - cbcsdk_mock.mock_request( - "GET", - f"/api/investigate/v2/orgs/test/auth_events/search_validation?{q}", - AUTH_EVENT_SEARCH_VALIDATIONS_RESP, - ) - result = AuthEvent.search_validation(api, 'auth_username') - assert result is True - - def test_auth_event_search_suggestions(cbcsdk_mock): """Tests getting auth_events search suggestions""" api = cbcsdk_mock.api @@ -977,3 +1045,15 @@ def test_auth_event_search_suggestions(cbcsdk_mock): result = AuthEvent.search_suggestions(api, 'auth') assert len(result) != 0 + + +def test_auth_event_descriptions_api_error(): + """Tests getting auth event descriptions error - no CBCloudAPI arg""" + with pytest.raises(ApiError): + AuthEvent.get_auth_events_descriptions("") + + +def test_auth_event_bulk_get_details_api_error(): + """Tests getting auth event get bulk details error - no CBCloudAPI arg""" + with pytest.raises(ApiError): + AuthEvent.bulk_get_details("", "device_id", 10) From 3a58e45efbd716f7b1da57202df48730d605be47 Mon Sep 17 00:00:00 2001 From: Hristo Karagitliev Date: Wed, 15 Mar 2023 16:41:41 +0200 Subject: [PATCH 123/143] remove dead code --- src/cbc_sdk/enterprise_edr/auth_events.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/cbc_sdk/enterprise_edr/auth_events.py b/src/cbc_sdk/enterprise_edr/auth_events.py index a2a5ee1d8..4334b70bd 100644 --- a/src/cbc_sdk/enterprise_edr/auth_events.py +++ b/src/cbc_sdk/enterprise_edr/auth_events.py @@ -230,10 +230,6 @@ def get_auth_events_descriptions(cb): >>> descriptions = AuthEvent.get_auth_events_descriptions(cb) >>> print(descriptions) """ - try: - type(cb) - except TypeError: - pass if cb.__class__.__name__ != "CBCloudAPI": message = "cb argument should be instance of CBCloudAPI." message += "\nExample:\ncb = CBCloudAPI(profile='example_profile')" From 9161918a79258433342076bd2e385aa067e9fae1 Mon Sep 17 00:00:00 2001 From: Jasmine Clark <89797061+jclark-vmware@users.noreply.github.com> Date: Mon, 27 Feb 2023 11:27:02 -0500 Subject: [PATCH 124/143] empty list parameter info --- docs/alerts.rst | 9 +++++++++ src/cbc_sdk/platform/alerts.py | 3 +++ 2 files changed, 12 insertions(+) diff --git a/docs/alerts.rst b/docs/alerts.rst index 8303229e4..d334eef2e 100644 --- a/docs/alerts.rst +++ b/docs/alerts.rst @@ -48,6 +48,15 @@ for more complex searches. The example below will search with a solr query searc 9d327888- WINDOWS WINDOWS-TEST THREAT aab3c640- WINDOWS WINDOWS-TEST THREAT +.. tip:: + When filtering by fields that take a list parameter, an empty list will be treated as a wildcard and match everything. + +Ex: Returns all types + +.. code-block:: python + + >>> alerts = (cb.select(BaseAlert).set_types([])) + .. tip:: More information about the ``solrq`` can be found in the their `documentation `_. diff --git a/src/cbc_sdk/platform/alerts.py b/src/cbc_sdk/platform/alerts.py index 72bfe4dad..2bfb6c5d5 100644 --- a/src/cbc_sdk/platform/alerts.py +++ b/src/cbc_sdk/platform/alerts.py @@ -950,6 +950,9 @@ def set_types(self, alerttypes): Returns: BaseAlertSearchQuery: This instance. + + Note: - When filtering by fields that take a list parameter, an empty list will be treated as a wildcard and + match everything. """ if not all((t in BaseAlertSearchQuery.VALID_ALERT_TYPES) for t in alerttypes): raise ApiError("One or more invalid alert type values") From 508d7bd3f29e8f5c7edfaf162a2923469ee07552 Mon Sep 17 00:00:00 2001 From: Jasmine Clark <89797061+jclark-vmware@users.noreply.github.com> Date: Fri, 17 Mar 2023 10:48:45 -0400 Subject: [PATCH 125/143] changing empty list sample to use list() --- docs/alerts.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/alerts.rst b/docs/alerts.rst index d334eef2e..1ddca1acd 100644 --- a/docs/alerts.rst +++ b/docs/alerts.rst @@ -55,7 +55,7 @@ Ex: Returns all types .. code-block:: python - >>> alerts = (cb.select(BaseAlert).set_types([])) + >>> alerts = list(cb.select(BaseAlert).set_types([])) .. tip:: More information about the ``solrq`` can be found in the From 933caf16c8b80a7559007d2441c4ae2e6e78107c Mon Sep 17 00:00:00 2001 From: Jasmine Clark <89797061+jclark-vmware@users.noreply.github.com> Date: Mon, 6 Mar 2023 11:22:23 -0500 Subject: [PATCH 126/143] adding example for retrieving alerts from multiple orgs --- docs/alerts.rst | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/docs/alerts.rst b/docs/alerts.rst index 1ddca1acd..ba5f18f99 100644 --- a/docs/alerts.rst +++ b/docs/alerts.rst @@ -77,6 +77,29 @@ You can also filter on different kind of **TTPs** (*Tools Techniques Procedures* ... +Retrieving Alerts for Multiple Organizations +-------------------- + +With the example below, you can retrieve alerts for multiple organizations. + +Create a csv file with values that match the profile names in your credentials.cbc file. You can modify the code inside the for loop to fit your needs. + +.. code-block:: python + + >>> from cbc_sdk import CBCloudAPI + >>> from cbc_sdk.platform import BaseAlert + >>> import csv + >>> file = open ("myFile.csv", "r", encoding='utf-8-sig') + >>> org_list = list(csv.reader(file, delimiter=",")) + >>> file.close() + >>> for org in org_list: + ... org = ''.join(org) + ... api = CBCloudAPI(profile=org) + ... alerts = api.select(BaseAlert).set_minimum_severity(7)[:5] + ... print(alerts[0].id, alerts[0].device_os, alerts[0].device_name, alerts[0].category) + ... + + Retrieving of Carbon Black Analytics Alerts (CBAnalyticsAlert) -------------------------------------------------------------- From 94aed3f1c767f42dde29a599dc6562ea1e066d48 Mon Sep 17 00:00:00 2001 From: Jasmine Clark <89797061+jclark-vmware@users.noreply.github.com> Date: Fri, 17 Mar 2023 10:40:46 -0400 Subject: [PATCH 127/143] adding example for retrieving alerts from multiple orgs --- docs/alerts.rst | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/alerts.rst b/docs/alerts.rst index ba5f18f99..083fd07c4 100644 --- a/docs/alerts.rst +++ b/docs/alerts.rst @@ -78,7 +78,7 @@ You can also filter on different kind of **TTPs** (*Tools Techniques Procedures* Retrieving Alerts for Multiple Organizations --------------------- +-------------------------------------------- With the example below, you can retrieve alerts for multiple organizations. @@ -86,18 +86,21 @@ Create a csv file with values that match the profile names in your credentials.c .. code-block:: python - >>> from cbc_sdk import CBCloudAPI + >>> from cbc_sdk import CBCloudAPI >>> from cbc_sdk.platform import BaseAlert >>> import csv - >>> file = open ("myFile.csv", "r", encoding='utf-8-sig') + >>> file = open ("data.csv", "r", encoding='utf-8-sig') >>> org_list = list(csv.reader(file, delimiter=",")) >>> file.close() >>> for org in org_list: ... org = ''.join(org) ... api = CBCloudAPI(profile=org) ... alerts = api.select(BaseAlert).set_minimum_severity(7)[:5] + ... print('Results for Org {}'.format(org)) + >>> for alert in alerts: ... print(alerts[0].id, alerts[0].device_os, alerts[0].device_name, alerts[0].category) ... + ... Retrieving of Carbon Black Analytics Alerts (CBAnalyticsAlert) From b003e06f1431dfe2d323dbadc44423c363f79d95 Mon Sep 17 00:00:00 2001 From: Jasmine Clark <89797061+jclark-vmware@users.noreply.github.com> Date: Fri, 17 Mar 2023 10:51:47 -0400 Subject: [PATCH 128/143] fixing alert for loop --- docs/alerts.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/alerts.rst b/docs/alerts.rst index 083fd07c4..5850c2183 100644 --- a/docs/alerts.rst +++ b/docs/alerts.rst @@ -98,7 +98,7 @@ Create a csv file with values that match the profile names in your credentials.c ... alerts = api.select(BaseAlert).set_minimum_severity(7)[:5] ... print('Results for Org {}'.format(org)) >>> for alert in alerts: - ... print(alerts[0].id, alerts[0].device_os, alerts[0].device_name, alerts[0].category) + ... print(alert.id, alert.device_os, alert.device_name, alert.category) ... ... From a1bde71d5e50c5932e623289abf95fa11ec09157 Mon Sep 17 00:00:00 2001 From: Jasmine Clark <89797061+jclark-vmware@users.noreply.github.com> Date: Mon, 20 Mar 2023 12:33:43 -0400 Subject: [PATCH 129/143] adding second example for inputting orgs directly into script --- docs/alerts.rst | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/docs/alerts.rst b/docs/alerts.rst index 5850c2183..82bd6f2aa 100644 --- a/docs/alerts.rst +++ b/docs/alerts.rst @@ -82,11 +82,25 @@ Retrieving Alerts for Multiple Organizations With the example below, you can retrieve alerts for multiple organizations. -Create a csv file with values that match the profile names in your credentials.cbc file. You can modify the code inside the for loop to fit your needs. - .. code-block:: python - >>> from cbc_sdk import CBCloudAPI + >>> from cbc_sdk import CBCloudAPI + >>> from cbc_sdk.platform import BaseAlert + >>> org_list = ["org1", "org2"] + >>> for org in org_list: + ... org = ''.join(org) + ... api = CBCloudAPI(profile=org) + ... alerts = api.select(BaseAlert).set_minimum_severity(7)[:5] + ... print('Results for Org {}'.format(org)) + >>> for alert in alerts: + ... print(alert.id, alert.device_os, alert.device_name, alert.category) + ... + ... + + +You can also read from a csv file with values that match the profile names in your credentials.cbc file. + + >>> from cbc_sdk import CBCloudAPI >>> from cbc_sdk.platform import BaseAlert >>> import csv >>> file = open ("data.csv", "r", encoding='utf-8-sig') @@ -102,7 +116,6 @@ Create a csv file with values that match the profile names in your credentials.c ... ... - Retrieving of Carbon Black Analytics Alerts (CBAnalyticsAlert) -------------------------------------------------------------- From 4bd25c5773ed8038776946b0829da8b24aebac72 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Mon, 20 Mar 2023 12:07:15 -0600 Subject: [PATCH 130/143] updated docs autogenerated RST files; updated setup.py; changed RHEL Dockerfile --- docker/rhel/Dockerfile | 2 +- docs/cbc_sdk.audit_remediation.rst | 4 ++-- 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 ++-- setup.py | 1 + 9 files changed, 29 insertions(+), 30 deletions(-) diff --git a/docker/rhel/Dockerfile b/docker/rhel/Dockerfile index 671e6c8b5..0cfaba38a 100644 --- a/docker/rhel/Dockerfile +++ b/docker/rhel/Dockerfile @@ -6,5 +6,5 @@ WORKDIR /app RUN dnf install -y redhat-rpm-config gcc libffi-devel python38-devel openssl-devel RUN pip3 install --upgrade pip -RUN pip3 install -r requirements.txt +#RUN pip3 install -r requirements.txt RUN pip3 install . diff --git a/docs/cbc_sdk.audit_remediation.rst b/docs/cbc_sdk.audit_remediation.rst index 9a8d33e97..d1837e809 100644 --- a/docs/cbc_sdk.audit_remediation.rst +++ b/docs/cbc_sdk.audit_remediation.rst @@ -1,5 +1,5 @@ -Audit and Remediation -===================== +cbc\_sdk.audit\_remediation package +=================================== Submodules ---------- diff --git a/docs/cbc_sdk.credential_providers.rst b/docs/cbc_sdk.credential_providers.rst index 9df90f9e3..802acc9d9 100644 --- a/docs/cbc_sdk.credential_providers.rst +++ b/docs/cbc_sdk.credential_providers.rst @@ -1,9 +1,17 @@ -Credential Providers -==================== +cbc\_sdk.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..3c7b7fa06 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. +cbc\_sdk.endpoint\_standard package +=================================== Submodules ---------- diff --git a/docs/cbc_sdk.enterprise_edr.rst b/docs/cbc_sdk.enterprise_edr.rst index daa95d77f..7ef51e288 100644 --- a/docs/cbc_sdk.enterprise_edr.rst +++ b/docs/cbc_sdk.enterprise_edr.rst @@ -1,5 +1,5 @@ -Enterprise EDR -============== +cbc\_sdk.enterprise\_edr package +================================ Submodules ---------- diff --git a/docs/cbc_sdk.platform.rst b/docs/cbc_sdk.platform.rst index 779720adb..1930c665d 100644 --- a/docs/cbc_sdk.platform.rst +++ b/docs/cbc_sdk.platform.rst @@ -1,5 +1,5 @@ -Platform -======== +cbc\_sdk.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..19e56e983 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..32e2923a8 100644 --- a/docs/cbc_sdk.workload.rst +++ b/docs/cbc_sdk.workload.rst @@ -1,11 +1,11 @@ -Workload +cbc\_sdk.workload package ========================= Submodules ---------- cbc\_sdk.workload.nsx\_remediation module ------------------------------------------- +----------------------------------------- .. automodule:: cbc_sdk.workload.nsx_remediation :members: diff --git a/setup.py b/setup.py index c2d866a0f..0f478ce29 100644 --- a/setup.py +++ b/setup.py @@ -22,6 +22,7 @@ 'schema', 'solrq', 'validators', + 'jsonschema', "keyring;platform_system=='Darwin'", 'boto3' ] From 202d54b4fbbc34e2e670108fe48e1c7a03e67c99 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Mon, 20 Mar 2023 12:17:21 -0600 Subject: [PATCH 131/143] added all "dev dependenciew" from requirements.txt to setup.py "tests_requires" --- setup.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 0f478ce29..79e2673f2 100644 --- a/setup.py +++ b/setup.py @@ -28,8 +28,15 @@ ] tests_requires = [ - 'pytest', - 'pymox' + 'pytest==7.2.1', + 'pymox==0.7.8', + 'coverage==6.5.0', + 'coveralls==3.3.1', + 'flake8==5.0.4', + 'flake8-colors==0.1.9', + 'flake8-docstrings==1.7.0', + 'pre-commit>=2.15.0', + 'requests-mock==1.10.0' ] if sys.version_info < (3, 0): From aeef1d6806037f3c59d0f57a74a1e8265ef96aad Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Mon, 20 Mar 2023 12:52:50 -0600 Subject: [PATCH 132/143] changed dockerfile command for pip3 install --- docker/rhel/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/rhel/Dockerfile b/docker/rhel/Dockerfile index 0cfaba38a..3b3d2b13d 100644 --- a/docker/rhel/Dockerfile +++ b/docker/rhel/Dockerfile @@ -7,4 +7,4 @@ WORKDIR /app RUN dnf install -y redhat-rpm-config gcc libffi-devel python38-devel openssl-devel RUN pip3 install --upgrade pip #RUN pip3 install -r requirements.txt -RUN pip3 install . +RUN pip3 install .[test] From 7910685f628fd0f0db1881ae33eed2a7f9963880 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Mon, 20 Mar 2023 13:12:32 -0600 Subject: [PATCH 133/143] tried other alternative command --- docker/rhel/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/rhel/Dockerfile b/docker/rhel/Dockerfile index 3b3d2b13d..093203ca5 100644 --- a/docker/rhel/Dockerfile +++ b/docker/rhel/Dockerfile @@ -7,4 +7,4 @@ WORKDIR /app RUN dnf install -y redhat-rpm-config gcc libffi-devel python38-devel openssl-devel RUN pip3 install --upgrade pip #RUN pip3 install -r requirements.txt -RUN pip3 install .[test] +RUN python3 setup.py test From 93c48cc1b6bc2936013706b8a0d7f90b0392d3c9 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Mon, 20 Mar 2023 13:50:16 -0600 Subject: [PATCH 134/143] trying a different variable def in setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 79e2673f2..d0081d40e 100644 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ 'boto3' ] -tests_requires = [ +extra_requires = [ 'pytest==7.2.1', 'pymox==0.7.8', 'coverage==6.5.0', From 99e716370acc1b246d86b6c5b0af90336eeda248 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Mon, 20 Mar 2023 14:00:06 -0600 Subject: [PATCH 135/143] last modification suggested by Alex, use extras_require --- docker/rhel/Dockerfile | 2 +- setup.py | 26 ++++++++++++++------------ 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/docker/rhel/Dockerfile b/docker/rhel/Dockerfile index 093203ca5..3b3d2b13d 100644 --- a/docker/rhel/Dockerfile +++ b/docker/rhel/Dockerfile @@ -7,4 +7,4 @@ WORKDIR /app RUN dnf install -y redhat-rpm-config gcc libffi-devel python38-devel openssl-devel RUN pip3 install --upgrade pip #RUN pip3 install -r requirements.txt -RUN python3 setup.py test +RUN pip3 install .[test] diff --git a/setup.py b/setup.py index d0081d40e..ab211d729 100644 --- a/setup.py +++ b/setup.py @@ -27,17 +27,19 @@ 'boto3' ] -extra_requires = [ - 'pytest==7.2.1', - 'pymox==0.7.8', - 'coverage==6.5.0', - 'coveralls==3.3.1', - 'flake8==5.0.4', - 'flake8-colors==0.1.9', - 'flake8-docstrings==1.7.0', - 'pre-commit>=2.15.0', - 'requests-mock==1.10.0' -] +extras_require = { + "test": [ + 'pytest==7.2.1', + 'pymox==0.7.8', + 'coverage==6.5.0', + 'coveralls==3.3.1', + 'flake8==5.0.4', + 'flake8-colors==0.1.9', + 'flake8-docstrings==1.7.0', + 'pre-commit>=2.15.0', + 'requests-mock==1.10.0' + ] +} if sys.version_info < (3, 0): install_requires.extend(['futures']) @@ -64,7 +66,7 @@ def read(fname): zip_safe=False, platforms='any', install_requires=install_requires, - tests_requires=tests_requires, + extras_require=extras_require, classifiers=[ 'Environment :: Web Environment', 'Intended Audience :: Developers', From 000fa5c3b97dd8109ba86c559172c143159cf495 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Mon, 20 Mar 2023 14:23:12 -0600 Subject: [PATCH 136/143] repair the b0rked .rst files --- docs/cbc_sdk.audit_remediation.rst | 4 ++-- 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 ++-- 7 files changed, 29 insertions(+), 27 deletions(-) diff --git a/docs/cbc_sdk.audit_remediation.rst b/docs/cbc_sdk.audit_remediation.rst index d1837e809..9a8d33e97 100644 --- a/docs/cbc_sdk.audit_remediation.rst +++ b/docs/cbc_sdk.audit_remediation.rst @@ -1,5 +1,5 @@ -cbc\_sdk.audit\_remediation package -=================================== +Audit and Remediation +===================== Submodules ---------- diff --git a/docs/cbc_sdk.credential_providers.rst b/docs/cbc_sdk.credential_providers.rst index 802acc9d9..9df90f9e3 100644 --- a/docs/cbc_sdk.credential_providers.rst +++ b/docs/cbc_sdk.credential_providers.rst @@ -1,17 +1,9 @@ -cbc\_sdk.credential\_providers package -====================================== +Credential Providers +==================== 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 3c7b7fa06..0102f08ff 100644 --- a/docs/cbc_sdk.endpoint_standard.rst +++ b/docs/cbc_sdk.endpoint_standard.rst @@ -1,5 +1,15 @@ -cbc\_sdk.endpoint\_standard package -=================================== +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. Submodules ---------- diff --git a/docs/cbc_sdk.enterprise_edr.rst b/docs/cbc_sdk.enterprise_edr.rst index 7ef51e288..daa95d77f 100644 --- a/docs/cbc_sdk.enterprise_edr.rst +++ b/docs/cbc_sdk.enterprise_edr.rst @@ -1,5 +1,5 @@ -cbc\_sdk.enterprise\_edr package -================================ +Enterprise EDR +============== Submodules ---------- diff --git a/docs/cbc_sdk.platform.rst b/docs/cbc_sdk.platform.rst index 1930c665d..779720adb 100644 --- a/docs/cbc_sdk.platform.rst +++ b/docs/cbc_sdk.platform.rst @@ -1,5 +1,5 @@ -cbc\_sdk.platform package -========================= +Platform +======== 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 19e56e983..93195737e 100644 --- a/docs/cbc_sdk.rst +++ b/docs/cbc_sdk.rst @@ -1,5 +1,5 @@ -cbc\_sdk package -================ +CBC SDK +======= Subpackages ----------- diff --git a/docs/cbc_sdk.workload.rst b/docs/cbc_sdk.workload.rst index 32e2923a8..b9ca8793d 100644 --- a/docs/cbc_sdk.workload.rst +++ b/docs/cbc_sdk.workload.rst @@ -1,11 +1,11 @@ -cbc\_sdk.workload package +Workload ========================= Submodules ---------- cbc\_sdk.workload.nsx\_remediation module ------------------------------------------ +------------------------------------------ .. automodule:: cbc_sdk.workload.nsx_remediation :members: From f67f28640835a315575b0bd34e1cbb6e1911b196 Mon Sep 17 00:00:00 2001 From: Alex Van Brunt Date: Mon, 20 Mar 2023 14:35:16 -0600 Subject: [PATCH 137/143] Remove unused run command --- docker/rhel/Dockerfile | 1 - 1 file changed, 1 deletion(-) diff --git a/docker/rhel/Dockerfile b/docker/rhel/Dockerfile index 3b3d2b13d..c7cfb8409 100644 --- a/docker/rhel/Dockerfile +++ b/docker/rhel/Dockerfile @@ -6,5 +6,4 @@ WORKDIR /app RUN dnf install -y redhat-rpm-config gcc libffi-devel python38-devel openssl-devel RUN pip3 install --upgrade pip -#RUN pip3 install -r requirements.txt RUN pip3 install .[test] From ffd18117bcab4d867bf7aa2e100544ed3510f075 Mon Sep 17 00:00:00 2001 From: Alex Van Brunt Date: Mon, 20 Mar 2023 15:09:04 -0600 Subject: [PATCH 138/143] Add all requirements to docs --- docs/requirements.txt | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/docs/requirements.txt b/docs/requirements.txt index 167651600..e5258e16d 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -2,3 +2,25 @@ sphinxcontrib-apidoc sphinx-copybutton==0.4.0 pygments + +# Package dependencies +requests +pyyaml +python-dateutil +schema +solrq +validators +jsonschema +keyring;platform_system=='Darwin' +boto3 + +# Dev dependencies +pytest==7.2.1 +pymox==0.7.8 +coverage==6.5.0 +coveralls==3.3.1 +flake8==5.0.4 +flake8-colors==0.1.9 +flake8-docstrings==1.7.0 +pre-commit>=2.15.0 +requests-mock==1.10.0 From cfbce13be5fa986b9572b700da592beb0a89a3d3 Mon Sep 17 00:00:00 2001 From: Alex Van Brunt Date: Mon, 20 Mar 2023 15:17:01 -0600 Subject: [PATCH 139/143] Reduce added packages for docs --- docs/requirements.txt | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index e5258e16d..450d69485 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -3,24 +3,6 @@ sphinxcontrib-apidoc sphinx-copybutton==0.4.0 pygments -# Package dependencies -requests -pyyaml -python-dateutil -schema -solrq -validators +# Broken dependencies (Need pre-installed to prevent build failure) jsonschema -keyring;platform_system=='Darwin' -boto3 - -# Dev dependencies -pytest==7.2.1 -pymox==0.7.8 -coverage==6.5.0 -coveralls==3.3.1 -flake8==5.0.4 -flake8-colors==0.1.9 -flake8-docstrings==1.7.0 -pre-commit>=2.15.0 -requests-mock==1.10.0 +keyring From 96569e65e5e8fc4b2b181dc28687283d4f498562 Mon Sep 17 00:00:00 2001 From: Alex Van Brunt Date: Mon, 20 Mar 2023 15:22:45 -0600 Subject: [PATCH 140/143] Add PR status for doc builder to readme.md --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 4fa9512ec..2eac6fd70 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,10 @@ The documentation is built in `docs/_build/html`. `No module named 'cbc_sdk'`. If so, set your `PYTHONPATH` to include the `src/` subdirectory of the SDK project directory before running `make html`, or the equivalent command `sphinx-build -M html . _build`. +#### Pull-Requests + +The webhook with readthedocs will create a build of the branch and report on the status of the build to the GitHub pull request + #### Using Docker Build the documentation by running: From b2bf9139fb6041394d136aa141a87861be716765 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Mon, 20 Mar 2023 15:25:06 -0600 Subject: [PATCH 141/143] version number updates for 1.4.2 --- README.md | 5 +++-- VERSION | 2 +- docs/changelog.rst | 29 +++++++++++++++++++++++++++++ docs/conf.py | 2 +- src/cbc_sdk/__init__.py | 2 +- 5 files changed, 35 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 2eac6fd70..38fea5985 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # VMware Carbon Black Cloud Python SDK -**Latest Version:** 1.4.1 +**Latest Version:** 1.4.2
-**Release Date:** October 21, 2022 +**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) @@ -51,6 +51,7 @@ At least one Carbon Black Cloud product is required to use this SDK: - python-dateutil - schema - solrq +- jsonschema - validators - keyring (for MacOS) diff --git a/VERSION b/VERSION index 347f5833e..9df886c42 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.4.1 +1.4.2 diff --git a/docs/changelog.rst b/docs/changelog.rst index 62841736b..4f068b3eb 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,5 +1,34 @@ Changelog ================================ +CBC SDK 1.4.2 - Released (TBD) +----------------------------------------- + +New Features: + +* Policy Rule Configurations - allows users to make adjustments to Carbon Black-defined rules. +* Core Prevention Rule Configurations - controls settings for core prevention rules as supplied by Carbon Black. +* Observations - search through data supplied by all sensors to find enriched events matching search criteria. +* Auth Events - visibility into authentication events on Windows endpoints. + +Updates: + +* Remove use of v1 status URL from process search, which now depends entirely on v2 operations. +* Vulnerabilities can now be dismissed and undismissed, and have dismissals edited. + +Bug Fixes: + +* User creation: raise error if the API object is not passed as the first parameter to ``User.create()``. +* Live Response: pass failed session exception back up to the ``WorkItem`` future objects. +* Improved query string parameter handling in API calls. + +Documentation: + +* New example script showing how to retrieve container alerts. +* New example script allows exporting users with grant and role information. +* Bug fixed in ``policy_service_crud_operations.py`` example script affecting iteration over rules. +* Update clarifying alert filtering by fields that take an empty list. +* Sample script added for retrieving alerts for multiple organizations. + CBC SDK 1.4.1 - Released October 21, 2022 ----------------------------------------- diff --git a/docs/conf.py b/docs/conf.py index d9c8d025a..7dc18811d 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.1' +release = '1.4.2' # -- General configuration --------------------------------------------------- diff --git a/src/cbc_sdk/__init__.py b/src/cbc_sdk/__init__.py index fb5ebede7..caab01932 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.1' +__version__ = '1.4.2' from .rest_api import CBCloudAPI from .cache import lru From 1f536a79eb055e4da038f20c1882461b308439bf Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Tue, 21 Mar 2023 09:45:43 -0600 Subject: [PATCH 142/143] reword "Observations" in changelog --- docs/changelog.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 4f068b3eb..b83ad9a05 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -7,7 +7,8 @@ New Features: * Policy Rule Configurations - allows users to make adjustments to Carbon Black-defined rules. * Core Prevention Rule Configurations - controls settings for core prevention rules as supplied by Carbon Black. -* Observations - search through data supplied by all sensors to find enriched events matching search criteria. +* Observations - search through all the noteworthy, searchable activity that was reported by your organization’s + sensors. * Auth Events - visibility into authentication events on Windows endpoints. Updates: From f87bef90efc218e65801e5c7938658e0b5aefc18 Mon Sep 17 00:00:00 2001 From: Amy Bowersox Date: Wed, 22 Mar 2023 10:32:11 -0600 Subject: [PATCH 143/143] update 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 38fea5985..c899fb553 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ **Latest Version:** 1.4.2
-**Release Date:** TBD +**Release Date:** March 22, 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 b83ad9a05..9f1a4cc06 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,7 +1,7 @@ Changelog ================================ -CBC SDK 1.4.2 - Released (TBD) ------------------------------------------ +CBC SDK 1.4.2 - Released March 22, 2023 +--------------------------------------- New Features: