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/README.md b/README.md index 4fa9512ec..c899fb553 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:** 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) @@ -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) @@ -126,6 +127,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: 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/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/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 . diff --git a/docker/rhel/Dockerfile b/docker/rhel/Dockerfile index 671e6c8b5..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 . +RUN pip3 install .[test] diff --git a/docs/alerts.rst b/docs/alerts.rst index 8303229e4..82bd6f2aa 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 = list(cb.select(BaseAlert).set_types([])) + .. tip:: More information about the ``solrq`` can be found in the their `documentation `_. @@ -68,6 +77,45 @@ 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. + +.. code-block:: python + + >>> 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') + >>> 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(alert.id, alert.device_os, alert.device_name, alert.category) + ... + ... + Retrieving of Carbon Black Analytics Alerts (CBAnalyticsAlert) -------------------------------------------------------------- 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 ---------------------------------------------------- diff --git a/docs/cbc_sdk.platform.rst b/docs/cbc_sdk.platform.rst index 3ee9f3df9..779720adb 100644 --- a/docs/cbc_sdk.platform.rst +++ b/docs/cbc_sdk.platform.rst @@ -52,6 +52,22 @@ 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 +------------------------------------- + +.. automodule:: cbc_sdk.platform.observations + :members: + :undoc-members: + :show-inheritance: + cbc\_sdk.platform.policies module ---------------------------------- @@ -60,6 +76,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 ---------------------------------- diff --git a/docs/changelog.rst b/docs/changelog.rst index 72bd7ceff..9f1a4cc06 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,5 +1,35 @@ Changelog ================================ +CBC SDK 1.4.2 - Released March 22, 2023 +--------------------------------------- + +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 all the noteworthy, searchable activity that was reported by your organization’s + sensors. +* 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 ----------------------------------------- @@ -77,7 +107,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/concepts.rst b/docs/concepts.rst index 55a4f442e..c9e94dd02 100644 --- a/docs/concepts.rst +++ b/docs/concepts.rst @@ -513,3 +513,84 @@ Get details for all events per alert Category: ['OBSERVED'] Type: NETWORK Alert Id: ['BE084638'] + + +Static Methods +-------------- + +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 +^^^^^^^^^^^^^^^^^^ + +:: + + # 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 +^^^^^^^^^^^^^^^^ + +:: + + # Observations 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'] + +:: + + # 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", "13A5F4E5-C4BD-11ED-A7AB-005056A5B601:13a5f4e4-c4bd-11ed-a7ab-005056a5b622"]) + >>> 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'] + + Category: ['THREAT'] + Type: CB_ANALYTICS + Alert Id: ['4d49d171-0a11-0731-5172-d0963b77d411'] + diff --git a/docs/conf.py b/docs/conf.py index e48e925ec..7dc18811d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -19,11 +19,11 @@ # -- 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 -release = '1.4.1' +release = '1.4.2' # -- General configuration --------------------------------------------------- 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/docs/requirements.txt b/docs/requirements.txt index 167651600..450d69485 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -2,3 +2,7 @@ sphinxcontrib-apidoc sphinx-copybutton==0.4.0 pygments + +# Broken dependencies (Need pre-installed to prevent build failure) +jsonschema +keyring 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== 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 06625ce6c..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 # ******************************************************* # * @@ -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/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 new file mode 100644 index 000000000..f906f8e85 --- /dev/null +++ b/examples/platform/container_runtime_alerts.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python +# ******************************************************* +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. +# SPDX-License-Identifier: MIT +# ******************************************************* +# * +# * DISCLAIMER. THIS PROGRAM IS PROVIDED TO YOU "AS IS" WITHOUT +# * WARRANTIES OR CONDITIONS OF ANY KIND, WHETHER ORAL OR WRITTEN, +# * EXPRESS OR IMPLIED. THE AUTHOR SPECIFICALLY DISCLAIMS ANY IMPLIED +# * WARRANTIES OR CONDITIONS OF MERCHANTABILITY, SATISFACTORY QUALITY, +# * NON-INFRINGEMENT AND FITNESS FOR A PARTICULAR PURPOSE. + +""" +Example script 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) 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/export_users_grants.py b/examples/platform/export_users_grants.py new file mode 100644 index 000000000..6bf4f747f --- /dev/null +++ b/examples/platform/export_users_grants.py @@ -0,0 +1,165 @@ +#!/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 +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: + profile_roles = profile.get("roles", []) + 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: + profiles = [copy.deepcopy(profile) for profile in grant.profiles] + 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) + 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 + + +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.") + 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} + 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] + + # 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 + + # extract data to JSON format + 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(list(data_list), indent=4)) + else: + print(json.dumps(list(data_list), indent=4)) + return 0 + + # handle CSV output from here + 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') + 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 + + +if __name__ == "__main__": + sys.exit(main()) 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..28ac29f08 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 # ******************************************************* # * @@ -71,8 +71,8 @@ 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(): - print(" {0}: {1} when {2} {3} is {4}".format(r.get('id'), r.get("action"), + 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"))) 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/requirements.txt b/requirements.txt index 5b6c8480a..1138b55d4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,16 +5,17 @@ python-dateutil schema solrq validators +jsonschema 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==5.0.4 +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 diff --git a/setup.py b/setup.py index c2d866a0f..ab211d729 100644 --- a/setup.py +++ b/setup.py @@ -22,14 +22,24 @@ 'schema', 'solrq', 'validators', + 'jsonschema', "keyring;platform_system=='Darwin'", 'boto3' ] -tests_requires = [ - 'pytest', - 'pymox' -] +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']) @@ -56,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', diff --git a/src/cbc_sdk/__init__.py b/src/cbc_sdk/__init__.py index 5d1ae70ac..caab01932 100644 --- a/src/cbc_sdk/__init__.py +++ b/src/cbc_sdk/__init__.py @@ -3,8 +3,8 @@ __title__ = 'cbc_sdk' __author__ = 'Carbon Black Developer Network' __license__ = 'MIT' -__copyright__ = 'Copyright 2020-2022 VMware Carbon Black' -__version__ = '1.4.1' +__copyright__ = 'Copyright 2020-2023 VMware Carbon Black' +__version__ = '1.4.2' from .rest_api import CBCloudAPI from .cache import lru diff --git a/src/cbc_sdk/audit_remediation/base.py b/src/cbc_sdk/audit_remediation/base.py index 2a0308c22..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 # ******************************************************* # * @@ -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..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 # ******************************************************* # * @@ -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/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 73ac9f414..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 # ******************************************************* # * @@ -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/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 b615b719c..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 # ******************************************************* # * @@ -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, ) @@ -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=0".format( self._cb.credentials.org_key, self._query_token, ) 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 e74e3c00d..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 # ******************************************************* # * @@ -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/enterprise_edr/__init__.py b/src/cbc_sdk/enterprise_edr/__init__.py index 209faa7a8..5d3c006a4 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 AuthEvent, AuthEventFacet 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..4334b70bd --- /dev/null +++ b/src/cbc_sdk/enterprise_edr/auth_events.py @@ -0,0 +1,770 @@ +# ******************************************************* +# 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, NewBaseModel, FacetQuery +from cbc_sdk.base import Query +from cbc_sdk.errors import ApiError, TimeoutError, InvalidObjectError + +import logging +import time +from copy import deepcopy + +log = logging.getLogger(__name__) + + +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__( + self, + cb, + model_unique_id=None, + initial_data=None, + force_init=False, + full_doc=False, + ): + """ + Initialize the AuthEvent 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(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(AuthEvent) + .where(event_id=model_unique_id) + .execute_async() + ) + result = auth_events_future.result() + if len(result) == 1: + initial_data = result[0] + super(AuthEvent, 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 AuthEvent 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 AuthEventQuery(self, cb) + + def get_details(self, timeout=0, async_mode=False): + """Requests detailed results. + + Args: + timeout (int): AuthEvent details request timeout in milliseconds. + async_mode (bool): True to request details in an asynchronous manner. + + Returns: + AuthEvent: 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") + + >>> events = cb.select(AuthEvent).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""" + obj = AuthEvent._helper_get_details( + self._cb, + event_ids=[self.event_id], + timeout=self._details_timeout, + ) + 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( + cb.credentials.org_key, + job_id, + ) + result = 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 completed < contacted: + 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 None + if total_results != 0: + results = result.get("results", []) + if bulk: + return [AuthEvent(cb, initial_data=x) for x in results] + return AuthEvent(cb, initial_data=results[0]) + + time.sleep(0.5) + + if timed_out: + raise TimeoutError( + 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) + """ + 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) + + @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 + + 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 + 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 bulk_get_details(cb, alert_id=None, event_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 events + event_ids (list): A list of event ids to fetch + timeout (int): AuthEvent details request timeout in milliseconds. + + Returns: + list: list of Auth Events + + Example: + >>> cb = CBCloudAPI(profile="example_profile") + >>> bulk_details = AuthEvent.bulk_get_details(cb, event_ids=['example-value']) + >>> print(bulk_details) + + Raises: + ApiError: if cb is not instance of CBCloudAPI + """ + 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): + """ + Represents an AuthEvent facet retrieved. + + Example: + >>> cb = CBCloudAPI(profile="example_profile") + >>> events_facet = cb.select(AuthEventFacet).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 AuthEvent Facet query.""" + + def __init__(self, cb, initial_data): + """Initialize an AuthEventFacet Terms object with initial_data.""" + super(AuthEventFacet.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 AuthEvent Facet query.""" + + def __init__(self, cb, initial_data): + """Initialize an AuthEventFacet Ranges object with initial_data.""" + super(AuthEventFacet.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 `AuthEventFacet.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(AuthEventFacet, self).__init__( + cb, + model_unique_id=model_unique_id, + initial_data=initial_data, + force_init=False, + full_doc=True, + ) + 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 `AuthEventFacet.Terms` for this result.""" + return self._terms + + @property + def ranges_(self): + """Returns the reified `AuthEventFacet.Ranges` for this result.""" + return self._ranges + + +class AuthEventGroup: + """Represents AuthEventGroup""" + def __init__(self, cb, initial_data=None): + """ + Initialize AuthEventGroup 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 + Example: + >>> cb = CBCloudAPI(profile="example_profile") + >>> groups = set(cb.select(AuthEvent).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") + self._info = initial_data + self._cb = cb + self.auth_events = [AuthEvent(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(AuthEventGroup, 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(AuthEventGroup, 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 AuthEventQuery(Query): + """Represents the query logic for an AuthEvent 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 AuthEventQuery 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(AuthEvent).where("auth_username:SYSTEM") + >>> print(*events) + """ + super(AuthEventQuery, 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 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(AuthEvent).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: AuthEventQuery object + + Example: + >>> cb = CBCloudAPI(profile="example_profile") + >>> 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(AuthEventQuery, 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 (AuthEventQuery): The Query object with new milliseconds + parameter. + + Example: + >>> cb = CBCloudAPI(profile="example_profile") + >>> events = cb.select(AuthEvent).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() + self._validate({"q": args.get("query", "")}) + 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(AuthEvent).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 AuthEventQuery.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 AuthEventGroup(self._cb, initial_data=group) 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 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 004a7d1b6..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 # ******************************************************* # * @@ -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) @@ -1022,7 +1024,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 +1033,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 +1096,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 +1242,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/cbc_sdk/platform/__init__.py b/src/cbc_sdk/platform/__init__.py index af3224ab1..0079993de 100644 --- a/src/cbc_sdk/platform/__init__.py +++ b/src/cbc_sdk/platform/__init__.py @@ -11,6 +11,8 @@ 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) @@ -24,3 +26,7 @@ from cbc_sdk.platform.vulnerability_assessment import Vulnerability from cbc_sdk.platform.jobs import Job + +from cbc_sdk.platform.observations import Observation, ObservationFacet + +from cbc_sdk.platform.network_threat_metadata import NetworkThreatMetadata diff --git a/src/cbc_sdk/platform/alerts.py b/src/cbc_sdk/platform/alerts.py index 0827ef132..2bfb6c5d5 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 # ******************************************************* # * @@ -252,6 +252,28 @@ 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 + """ + 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) + output = cb.get_object(url, query_params) + return output["suggestions"] + class WatchlistAlert(BaseAlert): """Represents watch list alerts.""" @@ -928,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") 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/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 new file mode 100644 index 000000000..3a5a96abe --- /dev/null +++ b/src/cbc_sdk/platform/models/observation.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 + 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 + rule_id: + type: string \ No newline at end of file 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 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/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 new file mode 100644 index 000000000..f59afac73 --- /dev/null +++ b/src/cbc_sdk/platform/observations.py @@ -0,0 +1,709 @@ +# ******************************************************* +# 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, 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 +from copy import deepcopy + +log = logging.getLogger(__name__) + + +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__( + 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): 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() + ) + 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 _refresh(self): + """ + Refreshes the observation object from the server by getting the details. + + 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. + + Args: + 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. + + 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" + ) + 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 get details implementation""" + obj = Observation._helper_get_details( + self._cb, + observation_ids=[self.observation_id], + timeout=self._details_timeout, + ) + 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 + """ + 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.") + 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( + cb.credentials.org_key, + job_id, + ) + result = 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 completed < contacted: + 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 observations were found + if found_results == 0: + return None + if total_results != 0: + results = result.get("results", []) + if bulk: + return [Observation(cb, initial_data=x) for x in results] + return Observation(cb, initial_data=results[0]) + + time.sleep(0.5) + + if timed_out: + raise TimeoutError( + 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). + + 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() + """ + try: + return NetworkThreatMetadata(self._cb, self.rule_id) + 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 + """ + if cb.__class__.__name__ != "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 + """ + if cb.__class__.__name__ != "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.""" + + 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): + """ + 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 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. + + 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)}.") + 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 observation 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): + """Submit the search job""" + if self._query_token: + raise ApiError( + "Query already submitted: token {0}".format(self._query_token) + ) + + 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 + ) + 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/{}/observations/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/{}/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): + """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/{}/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, + 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: + >>> 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") + + if isinstance(fields, str): + fields = [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: + self._submit() + + result_url = "/api/investigate/v2/orgs/{}/observations/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 + else: + still_fetching = False + + for group in result.get("group_results", []): + 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. + + 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.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/cbc_sdk/platform/policies.py b/src/cbc_sdk/platform/policies.py index 6ceb2dcef..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 # ******************************************************* # * @@ -14,10 +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, CorePreventionRuleConfig from cbc_sdk.errors import ApiError, ServerError, InvalidObjectError +SPECIFIC_RULECONFIGS = MappingProxyType({ + "core_prevention": CorePreventionRuleConfig +}) + + class Policy(MutableBaseModel): """ Represents a policy within the organization. @@ -72,6 +79,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: @@ -100,10 +110,10 @@ 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 = [] def set_name(self, name): """ @@ -408,6 +418,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 = Policy._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. @@ -449,6 +473,44 @@ 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) + self._add_rule_config(ruleconfigdata) + return self + + 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. + + Returns: + PolicyBuilder: This object. + + Raises: + InvalidObjectError: If the rule configuration data passed in is not valid. + """ + ruleconfigdata = {"id": config_id, "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. @@ -498,6 +560,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): @@ -512,6 +575,8 @@ def _subobject(self, name): """ if name == 'rules': return list(self.object_rules.values()) + if name == 'rule_configs': + return list(self.object_rule_configs.values()) return super(Policy, self)._subobject(name) @classmethod @@ -555,6 +620,7 @@ def _refresh(self): """ rc = super(Policy, self)._refresh() self._object_rules_need_load = True + self._object_rule_configs_need_load = True return rc @property @@ -582,6 +648,124 @@ def object_rules(self): self._object_rules_need_load = False return self._object_rules + @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", []) + 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 + + @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): + """ + 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)} + + @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. + + Returns: + dict: A dictionary mapping string ID values (UUIDs) to dicts containing entries for name, description, + and category. + """ + 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 self._ruleconfig_presentation.items()} + + @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. + """ + 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): + """ + 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 (a JSON schema). + + Raises: + InvalidObjectError: If the rule configuration ID is not valid. + """ + 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", []) + # 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 {'type': 'object', 'properties': schema} + else: + return self._cb.get_policy_ruleconfig_parameter_schema(ruleconfig_id) + def _on_updated_rule(self, rule): """ Called when a rule object is added or updated. @@ -619,6 +803,39 @@ 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): + """ + 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") + 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", []) + 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): + """ + 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") + 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. @@ -650,6 +867,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): @@ -697,6 +915,53 @@ def replace_rule(self, rule_id, new_rule): else: raise ApiError(f"rule #{rule_id} not found in policy") + 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() + else: + 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) + 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(True) + 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 def priorityLevel(self): """Returns the priority level of this policy (compatibility method).""" @@ -842,6 +1107,8 @@ def policy(self, oldpolicy): newpolicy["rules"] = copy.deepcopy(oldpolicy["rules"]) self._info = newpolicy + # --- END policy v1 compatibility methods --- + @classmethod def create(cls, cb): """ diff --git a/src/cbc_sdk/platform/policy_ruleconfigs.py b/src/cbc_sdk/platform/policy_ruleconfigs.py new file mode 100644 index 000000000..965bea166 --- /dev/null +++ b/src/cbc_sdk/platform/policy_ruleconfigs.py @@ -0,0 +1,274 @@ +#!/usr/bin/env python3 + +# ******************************************************* +# 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. + +"""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, 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. + + """ + urlobject = "/policyservice/v1/orgs/{0}/policies" + 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 _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. + + 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_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. + + 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. + + Required Permissions: + org.policies(DELETE) + """ + self._delete_ruleconfig() + self._parent._on_deleted_rule_config(self) + + 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 + + +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. + + """ + swagger_meta_file = "platform/models/policy_ruleconfig.yaml" + + def __init__(self, cb, parent, model_unique_id=None, initial_data=None, force_init=False, full_doc=False): + """ + Initialize the CorePreventionRuleConfig object. + + Args: + cb (BaseAPI): Reference to API object used to communicate with the server. + parent (Policy): The "parent" policy of this rule configuration. + model_unique_id (str): ID of the rule configuration. + initial_data (dict): Initial data used to populate the rule configuration. + force_init (bool): If True, forces the object to be refreshed after constructing. Default False. + full_doc (bool): If True, object is considered "fully" initialized. Default False. + """ + super(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}") + self._info["parameters"] = copy.deepcopy({"WindowsAssignmentMode": "BLOCK"}) # mirror server side + + 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/cbc_sdk/platform/processes.py b/src/cbc_sdk/platform/processes.py index 8a6bf7f86..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 # ******************************************************* # * @@ -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] @@ -384,7 +387,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, ) @@ -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" @@ -658,7 +667,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=0".format( self._cb.credentials.org_key, self._query_token, ) @@ -850,7 +859,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 +883,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 +897,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/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 a75bc6d8e..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 # ******************************************************* # * @@ -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/cbc_sdk/platform/vulnerability_assessment.py b/src/cbc_sdk/platform/vulnerability_assessment.py index 19ad2a506..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 # ******************************************************* # * @@ -104,6 +104,55 @@ def _query_implementation(cls, cb, **kwargs): """ return VulnerabilityQuery(cls, cb) + def perform_action(self, type, reason=None, notes=None): + """ + Take an action 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") + + 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.""" urlobject = "/vulnerability/assessment/api/v1/orgs/{}" @@ -132,7 +181,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 +229,7 @@ class VulnerabilityOrgSummaryQuery(BaseQuery): """Represents a query that is used fetch the VulnerabiltitySummary""" VALID_SEVERITY = ["CRITICAL", "IMPORTANT", "MODERATE", "LOW"] + VALID_VISIBILITY = ["DISMISSED", "ACTIVE"] def __init__(self, doc_class, cb, device=None): """ @@ -196,6 +246,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 +262,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, DISMISSED) + + Returns: + VulnerabilityOrgSummaryQuery: 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 +311,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,6 +337,7 @@ class VulnerabilityQuery(BaseQuery, QueryBuilderSupportMixin, "NOT_SUPPORTED", "CANCELLED", "IN_PROGRESS", "ACTIVE", "COMPLETED"] VALID_DIRECTIONS = ["ASC", "DESC"] + VALID_VISIBILITY = ["DISMISSED", "ACTIVE"] def __init__(self, doc_class, cb, device=None): """ @@ -290,6 +360,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): """ @@ -305,6 +376,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, 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. @@ -320,6 +406,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. @@ -538,6 +640,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. @@ -595,6 +698,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): @@ -672,6 +778,38 @@ 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. + + 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. + """ + 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. @@ -815,6 +953,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""" @@ -832,6 +992,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: + AffectedAssetQuery: 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/cbc_sdk/rest_api.py b/src/cbc_sdk/rest_api.py index 82784dea6..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 # ******************************************************* # * @@ -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,25 @@ 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 (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: + return self.get_object(url) + except ServerError: + raise InvalidObjectError(f"invalid rule config ID {ruleconfig_id}") diff --git a/src/cbc_sdk/utils.py b/src/cbc_sdk/utils.py index 9763d59d3..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 # ******************************************************* # * @@ -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/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/observations_uat.py b/src/tests/uat/observations_uat.py new file mode 100644 index 000000000..3e4cb3777 --- /dev/null +++ b/src/tests/uat/observations_uat.py @@ -0,0 +1,196 @@ +#!/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, ObservationFacet + +# ------------------------------ 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={}" +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 Results", + "Get Search Grouped Results", + "Get Details Results", + "Get Facet Data", + "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_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_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 = 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[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[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[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}" + 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() + 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/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..d262b540c 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 # ******************************************************* # * @@ -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 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 3031e9a36..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 # ******************************************************* # * @@ -29,11 +29,23 @@ - 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 import sys import requests +import time # Internal library imports from cbc_sdk.helpers import build_cli_parser, get_cb_cloud_object @@ -159,6 +171,68 @@ 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. + + Fina a vulnerablity, dismiss it, verify the new state and then undismiss it, + returning the system to the original state. + """ + 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.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 + 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 + dismissed_vulnerability.perform_action('UNDISMISS', 'OTHER', 'SDK Testing - reset to initial state') + + # 5. Verify new status + 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() + + 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(): """Script entry point""" global ORG_KEY @@ -174,6 +248,7 @@ def main(): HEADERS['X-Auth-Token'] = cb.credentials.token ORG_KEY = cb.credentials.org_key HOSTNAME = cb.credentials.url + CVE_ID = 'CVE-2014-4199' print() print(18 * ' ', 'Vulnerability Organization Level') @@ -184,6 +259,12 @@ 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') + + 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 +292,11 @@ def main(): data = { 'criteria': {'severity': {'value': 'LOW', 'operator': 'EQUALS'}}, - 'sort': [{'field': 'highest_risk_score', 'order': 'DESC'}], + 'sort': [{'field': 'risk_meter_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') + query = cb.select(Vulnerability).set_severity('LOW', 'EQUALS').sort_by('risk_meter_score', 'DESC') 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 +306,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 +323,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 +343,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 +358,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 +366,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'], 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/base/test_base_models.py b/src/tests/unit/base/test_base_models.py index f3c28c4c6..872b4f27d 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=0"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" - "2c292717-80ed-4f0d-845f-779e09470920/results"), + "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/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_endpoint_standard_enriched_events.py b/src/tests/unit/endpoint_standard/test_endpoint_standard_enriched_events.py index 6cd916bfa..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 @@ -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=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", # 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=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", # 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=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", # 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=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", # 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=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", # 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=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", # 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=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", # 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,10 +281,10 @@ 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/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=0", # noqa: E501 GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING) api = cbcsdk_mock.api @@ -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=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 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=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 + "/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=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", # 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=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", # 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/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/enterprise_edr/test_auth_events.py b/src/tests/unit/enterprise_edr/test_auth_events.py new file mode 100644 index 000000000..610ae9844 --- /dev/null +++ b/src/tests/unit/enterprise_edr/test_auth_events.py @@ -0,0 +1,1059 @@ +"""Testing AuthEvent objects of cbc_sdk.enterprise_edr""" + +import pytest +import logging + +from cbc_sdk.base import FacetQuery +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_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( + 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_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", + 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_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_EVENT_SEARCH_JOB_RESULTS_RESP, + ) + + api = cbcsdk_mock.api + events_list = api.select(AuthEvent).where("auth_username:SYSTEM") + for event in events_list: + assert event.device_name is not None + + +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", + 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_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_EVENT_SEARCH_JOB_RESULTS_RESP, + ) + + api = cbcsdk_mock.api + 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_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", + 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_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_EVENT_SEARCH_JOB_RESULTS_RESP, + ) + + api = cbcsdk_mock.api + auth_events = api.select(AuthEvent, "DA9E269E-421D-469D-A212-9062888A02F4") + assert auth_events["device_name"] is not None + + +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", + 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_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_EVENT_SEARCH_JOB_RESULTS_RESP, + ) + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/auth_events/detail_jobs", + 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_EVENT_DETAIL_JOB_RESULTS_RESP, + ) + + api = cbcsdk_mock.api + # 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() + 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_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_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_EVENT_DETAIL_JOB_RESULTS_RESP, + ) + + api = cbcsdk_mock.api + 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_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_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_EVENT_SEARCH_JOB_RESULTS_RESP_ZERO_COMP, + ) + + api = cbcsdk_mock.api + 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_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", + 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_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_EVENT_SEARCH_JOB_RESULTS_RESP, + ) + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/auth_events/detail_jobs", + 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_EVENT_DETAIL_JOB_RESULTS_RESP, + ) + + api = cbcsdk_mock.api + 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.device_name is not None + assert results.process_pid[0] == 764 + + +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", + 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_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_EVENT_SEARCH_JOB_RESULTS_RESP, + ) + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/auth_events/detail_jobs", + 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_EVENT_DETAIL_JOB_RESULTS_RESP, + ) + + api = cbcsdk_mock.api + 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_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", + 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_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_EVENT_SEARCH_JOB_RESULTS_RESP, + ) + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/auth_events/detail_jobs", + 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_EVENT_SEARCH_JOB_RESULTS_ZERO, + ) + + api = cbcsdk_mock.api + events_list = api.select(AuthEvent).where(process_pid=2000) + events = events_list[0] + results = events.get_details() + assert results["device_name"] is not None + + +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", + 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_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_EVENT_SEARCH_JOB_RESULTS_RESP, + ) + + api = cbcsdk_mock.api + 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_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", + 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_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_EVENT_SEARCH_JOB_RESULTS_RESP, + ) + api = cbcsdk_mock.api + event_id = ( + "DA9E269E-421D-469D-A212-9062888A02F4" + ) + 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_event_timeout(cbcsdk_mock): + """Testing AuthEventQuery.timeout().""" + api = cbcsdk_mock.api + query = api.select(AuthEvent).where("event_id:some_id") + assert query._timeout == 0 + query.timeout(msecs=500) + assert query._timeout == 500 + + +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", + 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_EVENT_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING, + ) + + api = cbcsdk_mock.api + 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(AuthEvent).where("event_id:DA9E269E-421D-469D-A212-9062888A02F4").timeout(1)) + with pytest.raises(TimeoutError): + events_list._count() + + +def test_auth_event_query_sort(cbcsdk_mock): + """Testing AuthEvent results sort.""" + api = cbcsdk_mock.api + events_list = ( + api.select(AuthEvent) + .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_event_rows(cbcsdk_mock): + """Testing AuthEvent results sort.""" + api = cbcsdk_mock.api + 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(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(AuthEvent).where(process_pid=1000).set_rows(10001) + assert "Maximum allowed value for rows is 10000" in str(ex) + + +def test_auth_event_time_range(cbcsdk_mock): + """Testing AuthEvent results sort.""" + api = cbcsdk_mock.api + events_list = ( + api.select(AuthEvent) + .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_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", + POST_AUTH_EVENT_SEARCH_JOB_RESP, + ) + api = cbcsdk_mock.api + 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: + events_list._submit() + assert "Query already submitted: token" in str(ex) + + +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", + 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_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_EVENT_SEARCH_JOB_RESULTS_RESP_2, + ) + + api = cbcsdk_mock.api + events_list = api.select(AuthEvent).where(process_pid=1000) + events_list._count() + assert events_list._count() == 198 + + +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", + 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_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_EVENT_SEARCH_JOB_RESULTS_RESP_2, + ) + + api = cbcsdk_mock.api + 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_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", + 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_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_EVENT_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING, + ) + + api = cbcsdk_mock.api + events_list = api.select(AuthEvent).where(process_pid=1000) + assert events_list._still_querying() is True + + +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", + 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_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_EVENT_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING, + ) + + api = cbcsdk_mock.api + events_list = api.select(AuthEvent).where(process_pid=1000) + assert events_list._still_querying() is True + + +# --------------------- AuthEventFacet -------------------------------------- + + +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_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_EVENT_FACET_SEARCH_JOB_RESULTS_RESP_2, + ) + + api = cbcsdk_mock.api + auth_events = ( + api.select(AuthEventFacet) + .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_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_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_EVENT_FACET_SEARCH_JOB_RESULTS_RESP_2, + ) + + api = cbcsdk_mock.api + future = ( + api.select(AuthEventFacet) + .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_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_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_EVENT_FACET_SEARCH_JOB_RESULTS_RESP_2, + ) + + api = cbcsdk_mock.api + auth_events = ( + api.select(AuthEventFacet) + .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_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_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_EVENT_FACET_SEARCH_JOB_RESULTS_RESP_1, + ) + + api = cbcsdk_mock.api + field = "process_name" + auth_events = ( + api.select(AuthEventFacet) + .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_event_facet_timeout(cbcsdk_mock): + """Testing AuthEventQuery.timeout().""" + api = cbcsdk_mock.api + query = ( + api.select(AuthEventFacet) + .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_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_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_EVENT_FACET_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING, + ) + + api = cbcsdk_mock.api + query = ( + api.select(AuthEventFacet) + .where("process_name:some_name") + .add_facet_field("process_name") + .timeout(1) + ) + with pytest.raises(TimeoutError): + query.results() + query = ( + api.select(AuthEventFacet) + .where("process_name:some_name") + .add_facet_field("process_name") + .timeout(1) + ) + with pytest.raises(TimeoutError): + query._count() + + +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(AuthEventFacet) + .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_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(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(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(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(AuthEventFacet).where(process_pid=1000).add_range( + range + ).add_facet_field("process_name") + + +def test_auth_event_facet_query_add_facet_field(cbcsdk_mock): + """Testing AuthEvent results sort.""" + api = cbcsdk_mock.api + auth_events = ( + api.select(AuthEventFacet) + .where(process_pid=1000) + .add_facet_field("process_name") + ) + assert auth_events._facet_fields[0] == "process_name" + + +def test_auth_event_facet_query_add_facet_fields(cbcsdk_mock): + """Testing AuthEvent results sort.""" + api = cbcsdk_mock.api + auth_events = ( + api.select(AuthEventFacet) + .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_event_facet_query_add_facet_invalid_fields(cbcsdk_mock): + """Testing AuthEvent results sort.""" + api = cbcsdk_mock.api + with pytest.raises(TypeError): + api.select(AuthEventFacet).where(process_pid=1000).add_facet_field(1337) + + +def test_auth_event_facet_limit(cbcsdk_mock): + """Testing AuthEvent results limit.""" + api = cbcsdk_mock.api + auth_events = ( + api.select(AuthEventFacet) + .where(process_pid=1000) + .limit(123) + .add_facet_field("process_name") + ) + assert auth_events._limit == 123 + + +def test_auth_event_facet_time_range(cbcsdk_mock): + """Testing AuthEvent results range.""" + api = cbcsdk_mock.api + auth_events = ( + api.select(AuthEventFacet) + .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_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_EVENT_FACET_SEARCH_JOB_RESP, + ) + api = cbcsdk_mock.api + auth_events = ( + api.select(AuthEventFacet) + .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_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_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_EVENT_FACET_SEARCH_JOB_RESULTS_RESP_1, + ) + + api = cbcsdk_mock.api + auth_events = ( + api.select(AuthEventFacet) + .where(process_pid=1000) + .add_facet_field("process_name") + ) + auth_events._count() + assert auth_events._count() == 116 + + +def test_auth_event_search_facet(cbcsdk_mock): + """Test _search method of AuthEventQuery class""" + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/auth_events/facet_jobs", + 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_EVENT_FACET_SEARCH_JOB_RESULTS_RESP_2, + ) + + api = cbcsdk_mock.api + auth_events = ( + api.select(AuthEventFacet) + .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_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_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_EVENT_FACET_SEARCH_JOB_RESULTS_RESP_2, + ) + + api = cbcsdk_mock.api + auth_events = ( + api.select(AuthEventFacet) + .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_event_aggregation_wrong_field(cbcsdk_mock): + """Testing passing wrong aggregation_field""" + api = cbcsdk_mock.api + with pytest.raises(ApiError): + for i in ( + api.select(AuthEvent) + .where(process_pid=2000) + .group_results("wrong_field") + ): + print(i) + with pytest.raises(ApiError): + for i in api.select(AuthEvent).where(process_pid=2000).group_results(1): + print(i) + + +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", + 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_EVENT_GROUPED_RESULTS_RESP, + ) + cbcsdk_mock.mock_request( + "POST", + "/api/investigate/v2/orgs/test/auth_events/detail_jobs", + 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_EVENT_DETAIL_JOB_RESULTS_RESP, + ) + + api = cbcsdk_mock.api + event_groups = list( + api.select(AuthEvent) + .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 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_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_EVENT_SEARCH_SUGGESTIONS_RESP, + ) + 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) 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..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 # ******************************************************* # * @@ -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/enterprise_edr/mock_auth_events.py b/src/tests/unit/fixtures/enterprise_edr/mock_auth_events.py new file mode 100644 index 000000000..4c21b9cbe --- /dev/null +++ b/src/tests/unit/fixtures/enterprise_edr/mock_auth_events.py @@ -0,0 +1,511 @@ +"""Mock responses for Auth Events queries.""" + +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, +} + + +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, +} + + +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, +} + + +GET_AUTH_EVENT_SEARCH_JOB_RESULTS_ZERO = { + "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, +} + + +GET_AUTH_EVENT_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING = { + "num_found": 808, + "num_available": 1, + "contacted": 6, + "completed": 0, + "results": [], +} + + +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, +} + + +"""Mocks for auth events facet query testing.""" + + +POST_AUTH_EVENT_FACET_SEARCH_JOB_RESP = { + "job_id": "62be5c2c-d080-4ce6-b4f3-7c519cc2b41c-sqs" +} + + +GET_AUTH_EVENT_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_EVENT_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_EVENT_FACET_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING = { + "ranges": [], + "terms": [], + "num_found": 0, + "contacted": 34, + "completed": 0, +} + + +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": [ + { + "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": [], + }, + ] +} + +AUTH_EVENT_SEARCH_VALIDATIONS_RESP = {"valid": True, "value_search_query": True} 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 24340fafe..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 # ******************************************************* # * @@ -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,55 @@ "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"} 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 new file mode 100644 index 000000000..8dfa525c2 --- /dev/null +++ b/src/tests/unit/fixtures/platform/mock_observations.py @@ -0,0 +1,522 @@ +"""Mock responses for observations queries.""" + +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, + "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"], + "rule_id": "8a4b43c5-5e0a-4f7d-aa46-bd729f1989a7", + } + ], +} + + +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", + "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_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", + "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", + "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", + ], + } + ], +} + + +"""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, +} + + +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", + "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, +} + + +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_policies.py b/src/tests/unit/fixtures/platform/mock_policies.py index e728f6203..7f88f6346 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" } ], - "rapid_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 = { @@ -1396,7 +1437,26 @@ "policy_modification": False, "quarantine": True }, - "rapid_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 = { @@ -1509,5 +1569,328 @@ "policy_modification": False, "quarantine": True }, - "rapid_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" + } + } +} + +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 [...]", + "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 [...]", + "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 [...]", + "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 [...]", + "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" + ] + } + ] + } + ] + } + ] +} + +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" + } +} + +BUILD_RULECONFIG_1 = { + "id": "88b19232-7ebb-48ef-a198-2a75a282de5d", + "name": "Privilege Escalation", + "inherited_from": "", + "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/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/fixtures/platform/mock_vulnerabilities.py b/src/tests/unit/fixtures/platform/mock_vulnerabilities.py index 1d0101f11..2739f3cc6 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" } ] } @@ -568,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_alertsv6_api.py b/src/tests/unit/platform/test_alertsv6_api.py index c50018724..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 # ******************************************************* # * @@ -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 @@ -53,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") @@ -681,26 +680,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=0"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" - "2c292717-80ed-4f0d-845f-779e09470920/results"), + "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 +717,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=0"), 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 +746,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=0"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" - "2c292717-80ed-4f0d-845f-779e09470920/results"), + "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 +775,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=0"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" - "2c292717-80ed-4f0d-845f-779e09470920/results"), + "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"), @@ -1079,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_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_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 new file mode 100644 index 000000000..1a05f678c --- /dev/null +++ b/src/tests/unit/platform/test_observations.py @@ -0,0 +1,1266 @@ +"""Testing Observation objects of cbc_sdk.platform""" + +import pytest +import logging + +from cbc_sdk.base import FacetQuery +from cbc_sdk.platform import Observation +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, + 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, + 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, + OBSERVATIONS_SEARCH_VALIDATIONS_RESP, + OBSERVATIONS_SEARCH_SUGGESTIONS_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", + 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( + "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", + 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( + "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", + 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( + "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", + 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( + "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", + 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( + "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", + 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_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", + 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( + "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", + 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( + "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", + POST_OBSERVATIONS_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "GET", + "/api/investigate/v2/orgs/test/observations/search_jobs/08ffa932-b633-4107-ba56-8741e929e48b/results", + 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( + "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", + 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 == observation_id + + +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 + 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( + "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", + 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( + "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", + 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( + "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", + 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( + "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", + 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( + "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", + 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( + "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", + 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 + + +# --------------------- 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( + "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", + 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" + + +def test_observation_aggregation_wrong_field(cbcsdk_mock): + """Testing passing wrong aggregation_field""" + api = cbcsdk_mock.api + with pytest.raises(ApiError): + 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_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", + POST_OBSERVATIONS_SEARCH_JOB_RESP, + ) + cbcsdk_mock.mock_request( + "POST", + "/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( + api.select(Observation) + .where(process_pid=2000) + .get_group_results( + "device_name", + max_events_per_group=10, + rows=5, + start=0, + range_field="backend_timestamp", + range_duration="-2y", + range_method="interval" + ) + ) + # 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].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_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", + 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_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_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", + "/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( + observation_id="8fbccc2da75f11ed937ae3cb089984c6:be6ff259-88e3-6286-789f-74defa192d2e" + ) + obs = obs_list[0] + with pytest.raises(ApiError): + obs.get_network_threat_metadata() + + +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", + 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_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/platform/test_platform_dynamic_reference.py b/src/tests/unit/platform/test_platform_dynamic_reference.py index 646514da7..d4a2d796d 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=0" ), 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=0" ), 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..6901aa9e9 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=0"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" - "2c292717-80ed-4f0d-845f-779e09470920/results"), + "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_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_platform_process.py b/src/tests/unit/platform/test_platform_process.py index 1344f017a..4f4cde0ed 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=0"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" - "2c292717-80ed-4f0d-845f-779e09470920/results"), + "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=0"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" - "2c292717-80ed-4f0d-845f-779e09470920/results"), + "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=0"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" - "2c292717-80ed-4f0d-845f-779e09470920/results"), + "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=0"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" - "2c292717-80ed-4f0d-845f-779e09470920/results"), + "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=0"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" - "2c292717-80ed-4f0d-845f-779e09470920/results"), + "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=0"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" - "2c292717-80ed-4f0d-845f-779e09470920/results"), + "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=0"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" - "2c292717-80ed-4f0d-845f-779e09470920/results"), + "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=0"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" - "2c292717-80ed-4f0d-845f-779e09470920/results"), + "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=0"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" - "2c292717-80ed-4f0d-845f-779e09470920/results"), + "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=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/" - "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=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/" - "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=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/" - "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=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/" - "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=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/" - "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=0"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" - "2c292717-80ed-4f0d-845f-779e09470920/results"), + "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,11 +1082,15 @@ 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/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=0"), GET_PROCESS_SEARCH_JOB_RESULTS_RESP_ZERO) api = cbcsdk_mock.api guid = 'WNEXFKQ7-0002b226-000015bd-00000000-1d6225bbba74c00' @@ -1074,11 +1105,15 @@ 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/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=0"), GET_PROCESS_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING) api = cbcsdk_mock.api guid = 'WNEXFKQ7-0002b226-000015bd-00000000-1d6225bbba74c00' @@ -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=0"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" - "2c292717-80ed-4f0d-845f-779e09470920/results"), + "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=0"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" - "2c292717-80ed-4f0d-845f-779e09470920/results"), + "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..93a5d0cf6 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=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/" @@ -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=0", + GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results - cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" - "2c292717-80ed-4f0d-845f-779e09470920/results"), 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=0", GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results - cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" - "2c292717-80ed-4f0d-845f-779e09470920/results"), + 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..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 # ******************************************************* # * @@ -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, + BUILD_RULECONFIG_1) 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,11 +110,13 @@ 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): """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 +147,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]) @@ -479,6 +489,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) @@ -497,6 +513,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() 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..548ede8a7 --- /dev/null +++ b/src/tests/unit/platform/test_policy_ruleconfigs.py @@ -0,0 +1,282 @@ +# ******************************************************* +# 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. + +"""Tests of the policy rule configurations support in the Platform API.""" + +import copy +import pytest +import logging +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, CORE_PREVENTION_RETURNS, + CORE_PREVENTION_UPDATE_1) + + +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) + + +@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", [ + ({"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, 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/65536/configs/presentation', + POLICY_CONFIG_PRESENTATION) + 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, policy): + """Tests the rule config refresh() operation.""" + cbcsdk_mock.mock_request('GET', '/policyservice/v1/orgs/test/policies/65536', FULL_POLICY_1) + # Replace all rule configs with the base class for purposes of this test + cfgs = policy._info.get("rule_configs", []) + 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 + for rule_config in policy.object_rule_configs_list: + 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): + """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) + 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) # 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() + with pytest.raises(NotImplementedError): + new_rule_config.save() + + +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) + # Replace all rule configs with the base class for purposes of this test + cfgs = policy._info.get("rule_configs", []) + 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 + with pytest.raises(NotImplementedError): + policy.delete_rule_config('88b19232-7ebb-48ef-a198-2a75a282de5d') + + +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.""" + with pytest.raises(ApiError): + policy.replace_rule_config('88b19266-7ebb-48ef-a198-2a75a282de5d', REPLACE_RULECONFIG) + + +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.""" + with pytest.raises(ApiError): + policy.delete_rule_config('88b19266-7ebb-48ef-a198-2a75a282de5d') + + +def test_rule_config_initialization_matches_categories(policy): + """Tests that rule configurations are initialized with the correct classes.""" + 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, 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_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_list: + 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, policy): + """Tests updating the core prevention data and saving it.""" + 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) + 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') + rule_config.save() + assert put_called + + +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 + + 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) + 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) + 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, policy): + """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('DELETE', '/policyservice/v1/orgs/test/policies/65536/rule_configs/core_prevention' + '/c4ed61b3-d5aa-41a9-814f-0f277451532b', on_delete) + 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 diff --git a/src/tests/unit/platform/test_reputation_overrides.py b/src/tests/unit/platform/test_reputation_overrides.py index 33d1925f1..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 # ******************************************************* # * @@ -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=0"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" - "2c292717-80ed-4f0d-845f-779e09470920/results"), + "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=0"), GET_PROCESS_SEARCH_JOB_RESP) # mock the GET to get search results cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" - "2c292717-80ed-4f0d-845f-779e09470920/results"), + "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=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", # 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=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", # 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_users.py b/src/tests/unit/platform/test_users.py index 4dc282f65..e55567be7 100644 --- a/src/tests/unit/platform/test_users.py +++ b/src/tests/unit/platform/test_users.py @@ -226,6 +226,12 @@ def check_post(uri, body, **kwargs): 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.""" api = cbcsdk_mock.api diff --git a/src/tests/unit/platform/test_vulnerability_assessment.py b/src/tests/unit/platform/test_vulnerability_assessment.py index 9d3c7465d..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 # ******************************************************* # * @@ -12,23 +12,26 @@ # * NON-INFRINGEMENT AND FITNESS FOR A PARTICULAR PURPOSE. """Unit test code for Vulnerability Assessment""" +import copy import pytest import logging 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, GET_VULNERABILITY_RESP, + GET_DISMISSED_VULNERABILITY_RESP, GET_AFFECTED_ASSETS_SPECIFIC_VULNERABILITY, GET_VULNERABILITY_RESP_MULTIPLE, 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') @@ -52,10 +55,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) @@ -69,7 +74,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 +100,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 +121,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) @@ -122,10 +133,26 @@ 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", - "/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 +165,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) @@ -147,10 +176,31 @@ 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", "/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", + copy.deepcopy(GET_VULNERABILITY_RESP)) + cbcsdk_mock.mock_request("POST", + "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search" + "?dataForExport=true", + copy.deepcopy(GET_VULNERABILITY_RESP)) api = cbcsdk_mock.api vulnerability = Vulnerability(api, "CVE-2014-4650") assert vulnerability._model_unique_id == "CVE-2014-4650" @@ -161,7 +211,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 +230,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 +245,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 +276,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 +306,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 +339,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 @@ -334,7 +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", + copy.deepcopy(GET_VULNERABILITY_RESP)) cbcsdk_mock.mock_request("POST", "/vulnerability/assessment/api/v1/orgs/test/vulnerabilities/CVE-2014-4650/devices", post_validate) @@ -356,7 +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", + 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) @@ -386,7 +469,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 +498,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() @@ -442,3 +531,185 @@ 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", + copy.deepcopy(GET_VULNERABILITY_RESP)) + cbcsdk_mock.mock_request("POST", + "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search" + "?dataForExport=true", + 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"] + 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", + copy.deepcopy(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): + 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", + copy.deepcopy(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): + 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", + copy.deepcopy(GET_VULNERABILITY_RESP)) + cbcsdk_mock.mock_request("POST", + "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search" + "?dataForExport=true", + copy.deepcopy(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", + copy.deepcopy(GET_VULNERABILITY_RESP)) + cbcsdk_mock.mock_request("POST", + "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search" + "?dataForExport=true", + copy.deepcopy(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", + copy.deepcopy(GET_VULNERABILITY_RESP)) + cbcsdk_mock.mock_request("POST", + "/vulnerability/assessment/api/v1/orgs/test/devices/vulnerabilities/_search" + "?dataForExport=true", + copy.deepcopy(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", + copy.deepcopy(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"] diff --git a/src/tests/unit/test_base_api.py b/src/tests/unit/test_base_api.py index b7a0b37c2..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 # ******************************************************* # * @@ -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,16 @@ 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 +244,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_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 143a11a4d..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 # ******************************************************* # * @@ -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 615634176..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 # ******************************************************* # * @@ -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 @@ -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: @@ -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()) diff --git a/src/tests/unit/test_rest_api.py b/src/tests/unit/test_rest_api.py index 2a0e70251..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 # ******************************************************* # * @@ -14,18 +14,24 @@ 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, +) @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 +42,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 +51,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 +70,70 @@ 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", - 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", - 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 diff --git a/src/tests/unit/test_utils.py b/src/tests/unit/test_utils.py index 6e92f6d7f..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 # ******************************************************* # * @@ -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") 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 # ******************************************************* # *