├── .github └── workflows │ └── python-app.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── access_control ├── __init__.py ├── create_subject.py ├── create_subject_test.py ├── delete_subject.py ├── delete_subject_test.py ├── get_subject.py ├── get_subject_test.py ├── list_roles.py ├── list_roles_test.py ├── list_subjects.py ├── list_subjects_test.py ├── update_role.py ├── update_role_test.py ├── update_subject.py └── update_subject_test.py ├── common ├── __init__.py ├── chronicle_auth.py ├── chronicle_auth_test.py ├── datetime_converter.py ├── datetime_converter_test.py ├── project_id.py ├── project_instance.py ├── regions.py └── regions_test.py ├── datatap ├── __init__.py ├── create_datatap.py ├── create_datatap_test.py ├── delete_datatap.py ├── delete_datatap_test.py ├── get_datatap.py ├── get_datatap_test.py ├── list_datatap.py ├── list_datatap_test.py ├── update_datatap.py └── update_datatap_test.py ├── detect ├── v1alpha │ ├── __init__.py │ ├── batch_update_curated_rule_set_deployments.py │ ├── bulk_update_alerts.py │ ├── create_retrohunt.py │ ├── create_rule.py │ ├── delete_rule.py │ ├── enable_rule.py │ ├── get_alert.py │ ├── get_retrohunt.py │ ├── get_rule.py │ ├── list_detections.py │ ├── list_errors.py │ ├── list_rules.py │ ├── search_rules_alerts.py │ ├── update_alert.py │ └── update_rule.py └── v2 │ ├── __init__.py │ ├── archive_rule.py │ ├── archive_rule_test.py │ ├── cancel_retrohunt.py │ ├── cancel_retrohunt_test.py │ ├── create_rule.py │ ├── create_rule_test.py │ ├── create_rule_version.py │ ├── create_rule_version_test.py │ ├── delete_rule.py │ ├── delete_rule_test.py │ ├── disable_alerting.py │ ├── disable_alerting_test.py │ ├── disable_live_rule.py │ ├── disable_live_rule_test.py │ ├── enable_alerting.py │ ├── enable_alerting_test.py │ ├── enable_live_rule.py │ ├── enable_live_rule_test.py │ ├── get_detection.py │ ├── get_detection_test.py │ ├── get_error.py │ ├── get_error_test.py │ ├── get_retrohunt.py │ ├── get_retrohunt_test.py │ ├── get_rule.py │ ├── get_rule_test.py │ ├── list_curated_rule_detections.py │ ├── list_curated_rule_detections_test.py │ ├── list_curated_rules.py │ ├── list_curated_rules_and_detections.py │ ├── list_curated_rules_and_detections_test.py │ ├── list_curated_rules_test.py │ ├── list_detections.py │ ├── list_detections_test.py │ ├── list_errors.py │ ├── list_errors_test.py │ ├── list_retrohunts.py │ ├── list_retrohunts_test.py │ ├── list_rule_versions.py │ ├── list_rule_versions_test.py │ ├── list_rules.py │ ├── list_rules_test.py │ ├── run_retrohunt.py │ ├── run_retrohunt_and_wait.py │ ├── run_retrohunt_and_wait_test.py │ ├── run_retrohunt_test.py │ ├── stream_detection_alerts.py │ ├── stream_detection_alerts_test.py │ ├── stream_test_rule.py │ ├── stream_test_rule_test.py │ ├── unarchive_rule.py │ ├── unarchive_rule_test.py │ ├── verify_rule.py │ └── verify_rule_test.py ├── feeds ├── __init__.py ├── create_azure_ad_context_feed.py ├── create_azure_ad_context_feed_test.py ├── create_azure_ad_feed.py ├── create_azure_ad_feed_test.py ├── create_okta_feed.py ├── create_okta_feed_test.py ├── create_okta_user_context_feed.py ├── create_okta_user_context_feed_test.py ├── create_workspace_activity_feed.py ├── create_workspace_activity_feed_test.py ├── create_workspace_alerts_feed.py ├── create_workspace_alerts_feed_test.py ├── delete_feed.py ├── delete_feed_test.py ├── disable_feed.py ├── disable_feed_test.py ├── enable_feed.py ├── enable_feed_test.py ├── get_feed.py ├── get_feed_test.py ├── list_feeds.py └── list_feeds_test.py ├── forwarders ├── README.md ├── __init__.py ├── create_collector.py ├── create_collector_test.py ├── create_forwarder.py ├── create_forwarder_test.py ├── delete_collector.py ├── delete_collector_test.py ├── delete_forwarder.py ├── delete_forwarder_test.py ├── generate_files.py ├── generate_files_test.py ├── get_collector.py ├── get_collector_test.py ├── get_forwarder.py ├── get_forwarder_test.py ├── list_collectors.py ├── list_collectors_test.py ├── list_forwarders.py ├── list_forwarders_test.py ├── update_collector.py ├── update_collector_test.py ├── update_forwarder.py └── update_forwarder_test.py ├── ingestion ├── __init__.py ├── create_entities.py ├── create_entities_test.py ├── create_udm_events.py ├── create_udm_events_test.py ├── create_unstructured_log_entries.py ├── create_unstructured_log_entries_test.py ├── example_input │ ├── sample_entities.json │ ├── sample_udm_events.json │ └── sample_unstructured_log_entries.txt ├── list_log_types.py ├── list_log_types_test.py └── v1alpha │ ├── create_udm_events.py │ └── get_udm_event.py ├── lists ├── __init__.py ├── append_to_list.py ├── create_list.py ├── create_list_test.py ├── example_input │ ├── coldriver_sha256.txt │ └── foo.txt ├── get_list.py ├── get_list_test.py ├── list_lists.py ├── list_lists_test.py ├── remove_from_list.py ├── update_list.py ├── update_list_test.py ├── v1alpha │ ├── create_list.py │ ├── get_list.py │ ├── patch_list.py │ └── patch_list_test.py ├── verify_list.py └── verify_list_test.py ├── requirements.txt ├── search ├── __init__.py ├── list_alerts.py ├── list_alerts_test.py ├── list_asset_events.py ├── list_asset_events_test.py ├── list_iocs.py ├── list_iocs_test.py ├── udm_search.py └── udm_search_test.py ├── service_management ├── README.md ├── __init__.py ├── create_gcp_association.py ├── create_gcp_association_test.py ├── delete_gcp_association.py ├── delete_gcp_association_test.py ├── get_gcp_association.py ├── get_gcp_association_test.py ├── get_gcp_log_flow_filter.py ├── get_gcp_log_flow_filter_test.py ├── get_gcp_settings.py ├── get_gcp_settings_test.py ├── update_gcp_log_flow_filter.py ├── update_gcp_log_flow_filter_test.py ├── update_gcp_settings.py └── update_gcp_settings_test.py └── uppercase ├── __init__.py ├── get_alert.py ├── get_alert_test.py ├── list_alerts.py └── list_alerts_test.py /.github/workflows/python-app.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Python application 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build-and-test: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Set up Python 3.8 20 | uses: actions/setup-python@v2 21 | with: 22 | python-version: 3.8 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install --upgrade pip 26 | pip install flake8 pytest 27 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 28 | - name: Lint with flake8 29 | run: | 30 | # stop the build if there are Python syntax errors or undefined names 31 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 32 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 33 | # flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 34 | - name: Test with pytest 35 | run: | 36 | python -m pytest 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store/ 2 | 3 | __pycache__/ 4 | venv/ 5 | 6 | node_modules/ 7 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution; 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code Reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | 25 | ## Community Guidelines 26 | 27 | This project follows [Google's Open Source Community 28 | Guidelines](https://opensource.google/conduct/). 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chronicle API Samples in Python 2 | 3 | Python samples and guidelines for using Chronicle APIs. 4 | 5 | ## Setup 6 | 7 | Follow these instructions: https://cloud.google.com/python/setup 8 | 9 | You may skip installing the Cloud Client Libraries and the Cloud SDK, they are 10 | unnecessary for interacting with Chronicle. 11 | 12 | After creating and activating a virtual environment, install Python 13 | library dependencies by running this command: 14 | 15 | ```shell 16 | pip install -r requirements.txt 17 | ``` 18 | 19 | It is assumed that you're using Python 3.7 or above. If you're using an older 20 | Python 3 version, you need to install this backported library as well: 21 | 22 | ```shell 23 | pip install dataclasses 24 | ``` 25 | 26 | ## Credentials 27 | 28 | Running the samples requires a JSON credentials file. By default, all the 29 | samples try to use the file `.chronicle_credentials.json` in the user's home 30 | directory. If this file is not found, you need to specify it explicitly by 31 | adding the following argument to the sample's command-line: 32 | 33 | ```shell 34 | -c 35 | ``` 36 | 37 | or 38 | 39 | ```shell 40 | --credentials_file 41 | ``` 42 | 43 | ## Usage 44 | 45 | You can run samples on the command-line, assuming the current working directory 46 | is the root directory of this repository (i.e. the directory which contains 47 | this `README.md` file): 48 | 49 | ### Detect API 50 | 51 | ```shell 52 | python3 -m detect.v2. -h 53 | ``` 54 | 55 | ### Lists API 56 | 57 | ```shell 58 | python3 -m lists. -h 59 | ``` 60 | 61 | ### Lists API v1alpha 62 | 63 | ``` 64 | python -m lists.v1alpha.create_list -h 65 | python -m lists.v1alpha.get_list -h 66 | python -m lists.v1alpha.patch_list -h 67 | ``` 68 | -------------------------------------------------------------------------------- /access_control/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | -------------------------------------------------------------------------------- /access_control/delete_subject_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Unit tests for the "delete_subject" module.""" 16 | 17 | import unittest 18 | import argparse 19 | 20 | from unittest import mock 21 | 22 | from google.auth.transport import requests 23 | 24 | from . import delete_subject 25 | 26 | 27 | class DeleteSubjectTest(unittest.TestCase): 28 | 29 | def test_initialize_command_line_args(self): 30 | actual = delete_subject.initialize_command_line_args( 31 | ["--name=test@test.com"]) 32 | self.assertEqual( 33 | actual, 34 | argparse.Namespace( 35 | credentials_file=None, name="test@test.com", region="us")) 36 | 37 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 38 | @mock.patch.object(requests.requests, "Response", autospec=True) 39 | def test_delete_subject_error(self, mock_response, mock_session): 40 | mock_session.request.return_value = mock_response 41 | type(mock_response).status_code = mock.PropertyMock(return_value=400) 42 | mock_response.raise_for_status.side_effect = ( 43 | requests.requests.exceptions.HTTPError()) 44 | 45 | with self.assertRaises(requests.requests.exceptions.HTTPError): 46 | delete_subject.delete_subject(mock_session, "") 47 | 48 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 49 | @mock.patch.object(requests.requests, "Response", autospec=True) 50 | def test_delete_subject(self, mock_response, mock_session): 51 | mock_session.request.return_value = mock_response 52 | type(mock_response).status_code = mock.PropertyMock(return_value=200) 53 | subject_id = "test@test.com" 54 | expected = None 55 | mock_response.json.return_value = expected 56 | actual = delete_subject.delete_subject(mock_session, subject_id) 57 | self.assertEqual(actual, expected) 58 | 59 | 60 | if __name__ == "__main__": 61 | unittest.main() 62 | -------------------------------------------------------------------------------- /access_control/list_roles_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Unit tests for the "list_roles" module.""" 16 | 17 | import unittest 18 | import argparse 19 | 20 | from unittest import mock 21 | 22 | from google.auth.transport import requests 23 | 24 | from . import list_roles 25 | 26 | 27 | class ListRolesTest(unittest.TestCase): 28 | 29 | def test_initialize_command_line_args(self): 30 | actual = list_roles.initialize_command_line_args([]) 31 | self.assertEqual(actual, 32 | argparse.Namespace(credentials_file=None, region="us")) 33 | 34 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 35 | @mock.patch.object(requests.requests, "Response", autospec=True) 36 | def test_list_roles_error(self, mock_response, mock_session): 37 | mock_session.request.return_value = mock_response 38 | type(mock_response).status_code = mock.PropertyMock(return_value=400) 39 | mock_response.raise_for_status.side_effect = ( 40 | requests.requests.exceptions.HTTPError()) 41 | 42 | with self.assertRaises(requests.requests.exceptions.HTTPError): 43 | list_roles.list_roles(mock_session) 44 | 45 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 46 | @mock.patch.object(requests.requests, "Response", autospec=True) 47 | def test_list_roles(self, mock_response, mock_session): 48 | mock_session.request.return_value = mock_response 49 | type(mock_response).status_code = mock.PropertyMock(return_value=200) 50 | expected = { 51 | "name": 52 | "Test", 53 | "title": 54 | "Test role", 55 | "description": 56 | "The Test role", 57 | "createTime": 58 | "2020-11-05T00:00:00Z", 59 | "isDefault": 60 | False, 61 | "permissions": [{ 62 | "name": "Test", 63 | "title": "Test permission", 64 | "description": "The Test permission", 65 | "createTime": "2020-11-05T00:00:00Z", 66 | },] 67 | }, 68 | mock_response.json.return_value = {"roles": [expected]} 69 | actual = list_roles.list_roles(mock_session) 70 | self.assertEqual(actual, [expected]) 71 | 72 | 73 | if __name__ == "__main__": 74 | unittest.main() 75 | -------------------------------------------------------------------------------- /access_control/update_role_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Unit tests for the "update_role" module.""" 16 | 17 | import unittest 18 | import argparse 19 | 20 | from unittest import mock 21 | 22 | from google.auth.transport import requests 23 | 24 | from . import update_role 25 | 26 | 27 | class UpdateRoleTest(unittest.TestCase): 28 | 29 | def test_initialize_command_line_args(self): 30 | actual = update_role.initialize_command_line_args( 31 | ["--name=Test", "--is-default=true"]) 32 | self.assertEqual( 33 | actual, 34 | argparse.Namespace( 35 | credentials_file=None, name="Test", is_default=True, region="us")) 36 | 37 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 38 | @mock.patch.object(requests.requests, "Response", autospec=True) 39 | def test_update_role_error(self, mock_response, mock_session): 40 | mock_session.request.return_value = mock_response 41 | type(mock_response).status_code = mock.PropertyMock(return_value=400) 42 | mock_response.raise_for_status.side_effect = ( 43 | requests.requests.exceptions.HTTPError()) 44 | 45 | with self.assertRaises(requests.requests.exceptions.HTTPError): 46 | update_role.update_role(mock_session, "", False) 47 | 48 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 49 | @mock.patch.object(requests.requests, "Response", autospec=True) 50 | def test_update_role(self, mock_response, mock_session): 51 | mock_session.request.return_value = mock_response 52 | type(mock_response).status_code = mock.PropertyMock(return_value=200) 53 | role_id = "Test" 54 | is_default = True 55 | expected = { 56 | "name": 57 | "Test", 58 | "title": 59 | "Test role", 60 | "description": 61 | "The Test role", 62 | "createTime": 63 | "2020-11-05T00:00:00Z", 64 | "isDefault": 65 | True, 66 | "permissions": [{ 67 | "name": "Test", 68 | "title": "Test permission", 69 | "description": "The Test permission", 70 | "createTime": "2020-11-05T00:00:00Z", 71 | },] 72 | } 73 | mock_response.json.return_value = expected 74 | actual = update_role.update_role(mock_session, role_id, is_default) 75 | self.assertEqual(actual, expected) 76 | 77 | 78 | if __name__ == "__main__": 79 | unittest.main() 80 | -------------------------------------------------------------------------------- /common/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | -------------------------------------------------------------------------------- /common/datetime_converter.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Helper functions to convert ISO 8601 strings into datetime objects. 16 | """ 17 | 18 | import datetime 19 | import re 20 | 21 | 22 | def iso8601_datetime_utc(utc_date_time: str) -> datetime.datetime: 23 | """Converts an ISO 8601 string ("yyyy-mm-ddThh:mm:ssZ") to a datetime object. 24 | 25 | More details: https://en.wikipedia.org/wiki/ISO_8601 26 | 27 | Args: 28 | utc_date_time: Date and time in the extended ("T") ISO 8601 format, where 29 | the time is in UTC ("Z"). 30 | 31 | Returns: 32 | Builtin datetime object with a UTC timezone. 33 | 34 | Raises: 35 | ValueError: Invalid input value. 36 | """ 37 | # Work-around fixable issues in user-specified timestamps. 38 | utc_date_time = re.sub(r"(\d{2}-\d{2}-\d{2})\s+(\d)", r"\1T\2", 39 | utc_date_time).upper() 40 | if utc_date_time[-1] != "Z": 41 | utc_date_time += "Z" 42 | 43 | # Append the suffix "+0000" in order to produce a timezone-aware UTC datetime, 44 | # because strptime's "%z" does not recognize the meaning of the "Z" suffix. 45 | try: 46 | # Support (but don't require) sub-second parsing, but ignore anything 47 | # smaller than microseconds. 48 | utc_date_time = re.sub(r"(\d{6})\d+Z", r"\1Z", utc_date_time) 49 | return datetime.datetime.strptime(f"{utc_date_time}+0000", 50 | "%Y-%m-%dT%H:%M:%S.%fZ%z") 51 | except ValueError: 52 | # No microseconds? No problem, try to parse without them. 53 | # If there's a different parsing problem, it will surface below too. 54 | pass 55 | 56 | return datetime.datetime.strptime(f"{utc_date_time}+0000", 57 | "%Y-%m-%dT%H:%M:%SZ%z") 58 | 59 | 60 | def strftime(utc_date_time: datetime.datetime) -> str: 61 | """Converts a datetime object to a string with the format "%Y-%m-%dT%H:%M:%SZ". 62 | 63 | Args: 64 | utc_date_time: Builtin datetime object with a UTC timezone. 65 | 66 | Returns: 67 | Date and time in the format "%Y-%m-%dT%H:%M:%SZ". 68 | 69 | Raises: 70 | ValueError: Invalid input value. 71 | """ 72 | if utc_date_time is None: 73 | return "" 74 | return utc_date_time.astimezone( 75 | datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") 76 | -------------------------------------------------------------------------------- /common/datetime_converter_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Tests for the "datetime_converter" module.""" 16 | 17 | import datetime 18 | import unittest 19 | 20 | from . import datetime_converter 21 | 22 | 23 | class DatetimeConverterTest(unittest.TestCase): 24 | 25 | def setUp(self): 26 | super().setUp() 27 | self.date_time = datetime.datetime( 28 | 2020, 11, 5, 0, 0, 0, 0, tzinfo=datetime.timezone.utc) 29 | 30 | def test_iso8601_datetime_utc(self): 31 | expected_date_time = self.date_time 32 | date_time = datetime_converter.iso8601_datetime_utc("2020-11-05T00:00:00Z") 33 | self.assertEqual(date_time, expected_date_time) 34 | 35 | def test_strftime(self): 36 | expected_date_time_str = "2020-11-05T00:00:00Z" 37 | date_time_str = datetime_converter.strftime(self.date_time) 38 | self.assertEqual(date_time_str, expected_date_time_str) 39 | 40 | 41 | if __name__ == "__main__": 42 | unittest.main() 43 | -------------------------------------------------------------------------------- /common/project_id.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Support for Project ID for v1alpha Chronicle API calls.""" 16 | import argparse 17 | 18 | 19 | def add_argument_project_id(parser: argparse.ArgumentParser): 20 | """Adds a shared command-line argument to all the sample modules.""" 21 | parser.add_argument( 22 | "-p", "--project_id", type=str, required=True, 23 | help="Your BYOP, project id", 24 | ) 25 | -------------------------------------------------------------------------------- /common/project_instance.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Support for Project INSTANCE for v1alpha Chronicle API calls.""" 16 | 17 | import argparse 18 | 19 | 20 | def add_argument_project_instance(parser: argparse.ArgumentParser): 21 | """Adds a shared command-line argument to all the sample modules.""" 22 | parser.add_argument( 23 | "-i", 24 | "--project_instance", 25 | type=str, 26 | required=True, 27 | help="Customer ID for Chronicle instance", 28 | ) 29 | -------------------------------------------------------------------------------- /common/regions.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Support for regional URLs in all Chronicle API calls. 16 | 17 | For backward compatibility, the US region is considered as the default. 18 | """ 19 | 20 | import argparse 21 | 22 | REGION_LIST = ( 23 | "asia-northeast1", 24 | "asia-south1", 25 | "asia-southeast1", 26 | "australia-southeast1", 27 | "eu", 28 | "europe", 29 | "europe-west12", 30 | "europe-west2", 31 | "europe-west3", 32 | "europe-west6", 33 | "europe-west9", 34 | "me-central1", 35 | "me-central2", 36 | "me-west1", 37 | "northamerica-northeast2", 38 | "southamerica-east1", 39 | "us", 40 | ) 41 | 42 | 43 | def add_argument_region(parser: argparse.ArgumentParser): 44 | """Adds a shared command-line argument to all the sample modules.""" 45 | parser.add_argument( 46 | "-r", 47 | "--region", 48 | type=str, 49 | required=False, 50 | default="us", 51 | choices=REGION_LIST, 52 | help="the region where the customer is located (default: us)", 53 | ) 54 | 55 | 56 | def url(base_url: str, region: str) -> str: 57 | """Returns a regionalized URL based on the default and the given region.""" 58 | if region != "us": 59 | base_url = base_url.replace("https://", f"https://{region}-") 60 | return base_url 61 | 62 | 63 | def url_always_prepend_region(base_url: str, region: str) -> str: 64 | """Returns a regionalized URL. 65 | 66 | Args: 67 | base_url: URL pointing to Chronicle API 68 | region: region in which the target project is located 69 | 70 | Returns: 71 | A string containing a regionalized URL. Unlike the url() function, 72 | this function always prepends region; this function also checks whether 73 | the URL already has the region prefix, and if so, returns the URL unchanged. 74 | v1alpha samples should use this function. 75 | """ 76 | if not base_url.startswith(f"https://{region}-"): 77 | base_url = base_url.replace("https://", f"https://{region}-") 78 | return base_url 79 | -------------------------------------------------------------------------------- /common/regions_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Tests for the "regions" module.""" 16 | 17 | import unittest 18 | 19 | from . import regions 20 | 21 | 22 | class RegionsTest(unittest.TestCase): 23 | 24 | def test_url_asia_southeast1(self): 25 | self.assertEqual( 26 | regions.url("https://test", "asia-southeast1"), 27 | "https://asia-southeast1-test") 28 | 29 | def test_url_eu(self): 30 | self.assertEqual( 31 | regions.url("https://test", "eu"), "https://eu-test") 32 | 33 | def test_url_us(self): 34 | self.assertEqual(regions.url("https://test", "us"), "https://test") 35 | 36 | def test_url_always_prepend_region_us(self): 37 | self.assertEqual( 38 | regions.url_always_prepend_region("https://test", "us"), 39 | "https://us-test", 40 | ) 41 | 42 | def test_url_always_prepend_region_e(self): 43 | self.assertEqual( 44 | regions.url_always_prepend_region("https://test", "eu"), 45 | "https://eu-test", 46 | ) 47 | 48 | def test_url_always_prepend_region_twice(self): 49 | url_once = regions.url_always_prepend_region("https://test", "eu") 50 | url_twice = regions.url_always_prepend_region(url_once, "eu") 51 | self.assertEqual( 52 | "https://eu-test", 53 | url_twice, 54 | ) 55 | 56 | 57 | if __name__ == "__main__": 58 | unittest.main() 59 | -------------------------------------------------------------------------------- /datatap/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | -------------------------------------------------------------------------------- /datatap/delete_datatap.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2022 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """Executable and reusable sample for deleting a Datatap. 18 | 19 | API reference: 20 | https://cloud.google.com/chronicle/docs/preview/datatap-config/datatapconfig-api?hl=en#delete 21 | """ 22 | 23 | import argparse 24 | import json 25 | import sys 26 | from typing import Any, Mapping 27 | from typing import Optional 28 | from typing import Sequence 29 | 30 | from google.auth.transport import requests 31 | 32 | from common import chronicle_auth 33 | from common import regions 34 | 35 | CHRONICLE_API_BASE_URL = "https://backstory.googleapis.com" 36 | 37 | 38 | def initialize_command_line_args( 39 | args: Optional[Sequence[str]] = None) -> Optional[argparse.Namespace]: 40 | """Initializes and checks all the command-line arguments.""" 41 | parser = argparse.ArgumentParser() 42 | chronicle_auth.add_argument_credentials_file(parser) 43 | regions.add_argument_region(parser) 44 | parser.add_argument( 45 | "-id", "--tapId", type=str, required=True, help="tap Id") 46 | 47 | return parser.parse_args(args) 48 | 49 | 50 | def delete_datatap(http_session: requests.AuthorizedSession, 51 | tap_id: str) -> Mapping[str, Sequence[Any]]: 52 | """Deletes the given datatap. 53 | 54 | Args: 55 | http_session: Authorized session for HTTP requests. 56 | tap_id: unique datatap Id returned on Datatap creation. 57 | 58 | Returns: 59 | Empty with 200 if success. 60 | 61 | Raises: 62 | requests.exceptions.HTTPError: HTTP request resulted in an error 63 | (response.status_code >= 400). 64 | """ 65 | url = f"{CHRONICLE_API_BASE_URL}/v1/dataTaps/{tap_id}" 66 | 67 | response = http_session.request("DELETE", url) 68 | 69 | if response.status_code >= 400: 70 | print(response.text) 71 | response.raise_for_status() 72 | return response.json() 73 | 74 | 75 | if __name__ == "__main__": 76 | cli = initialize_command_line_args() 77 | if not cli: 78 | sys.exit(1) # A sanity check failed. 79 | 80 | CHRONICLE_API_BASE_URL = regions.url(CHRONICLE_API_BASE_URL, cli.region) 81 | session = chronicle_auth.initialize_http_session(cli.credentials_file) 82 | print( 83 | json.dumps( 84 | delete_datatap(session, cli.tapId), 85 | indent=2)) 86 | -------------------------------------------------------------------------------- /datatap/delete_datatap_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Unit tests for the "delete_datatap" module.""" 16 | 17 | import unittest 18 | import argparse 19 | 20 | from unittest import mock 21 | 22 | from google.auth.transport import requests 23 | 24 | from . import delete_datatap 25 | 26 | 27 | class DeleteDatatapTest(unittest.TestCase): 28 | 29 | def test_initialize_command_line_args(self): 30 | actual = delete_datatap.initialize_command_line_args( 31 | ["--tapId=aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"]) 32 | self.assertEqual( 33 | actual, 34 | argparse.Namespace( 35 | credentials_file=None, 36 | tapId="aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", 37 | region="us")) 38 | 39 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 40 | @mock.patch.object(requests.requests, "Response", autospec=True) 41 | def test_delete_datatap_error(self, mock_response, mock_session): 42 | mock_session.request.return_value = mock_response 43 | type(mock_response).status_code = mock.PropertyMock(return_value=400) 44 | mock_response.raise_for_status.side_effect = ( 45 | requests.requests.exceptions.HTTPError()) 46 | 47 | with self.assertRaises(requests.requests.exceptions.HTTPError): 48 | delete_datatap.delete_datatap(mock_session, "") 49 | 50 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 51 | @mock.patch.object(requests.requests, "Response", autospec=True) 52 | def test_delete_datatap(self, mock_response, mock_session): 53 | mock_session.request.return_value = mock_response 54 | type(mock_response).status_code = mock.PropertyMock(return_value=200) 55 | tap_id = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" 56 | expected = {} 57 | 58 | mock_response.json.return_value = expected 59 | actual = delete_datatap.delete_datatap(mock_session, tap_id) 60 | self.assertEqual(actual, expected) 61 | 62 | 63 | if __name__ == "__main__": 64 | unittest.main() 65 | -------------------------------------------------------------------------------- /datatap/get_datatap_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Unit tests for the "get_datatap" module.""" 16 | 17 | import unittest 18 | import argparse 19 | 20 | from unittest import mock 21 | 22 | from google.auth.transport import requests 23 | 24 | from . import get_datatap 25 | 26 | 27 | class GetDatatapTest(unittest.TestCase): 28 | 29 | def test_initialize_command_line_args(self): 30 | actual = get_datatap.initialize_command_line_args( 31 | ["--tapId=aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"]) 32 | self.assertEqual( 33 | actual, 34 | argparse.Namespace( 35 | credentials_file=None, 36 | tapId="aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", 37 | region="us")) 38 | 39 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 40 | @mock.patch.object(requests.requests, "Response", autospec=True) 41 | def test_get_datatap_error(self, mock_response, mock_session): 42 | mock_session.request.return_value = mock_response 43 | type(mock_response).status_code = mock.PropertyMock(return_value=400) 44 | mock_response.raise_for_status.side_effect = ( 45 | requests.requests.exceptions.HTTPError()) 46 | 47 | with self.assertRaises(requests.requests.exceptions.HTTPError): 48 | get_datatap.get_datatap(mock_session, "") 49 | 50 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 51 | @mock.patch.object(requests.requests, "Response", autospec=True) 52 | def test_get_datatap(self, mock_response, mock_session): 53 | mock_session.request.return_value = mock_response 54 | type(mock_response).status_code = mock.PropertyMock(return_value=200) 55 | tap_id = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" 56 | expected = { 57 | "customerId": "cccccccc-cccc-cccc-cccc-cccccccccccc", 58 | "tapId": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", 59 | "displayName": "tap1", 60 | "cloudPubsubSink": { 61 | "topic": "projects/sample-project/topics/sample-topic", 62 | "filter": "ALL_UDM_EVENTS" 63 | } 64 | } 65 | 66 | mock_response.json.return_value = expected 67 | actual = get_datatap.get_datatap(mock_session, tap_id) 68 | self.assertEqual(actual, expected) 69 | 70 | 71 | if __name__ == "__main__": 72 | unittest.main() 73 | -------------------------------------------------------------------------------- /datatap/list_datatap_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Unit tests for the "list_datatap" module.""" 16 | 17 | import unittest 18 | import argparse 19 | 20 | from unittest import mock 21 | 22 | from google.auth.transport import requests 23 | 24 | from . import list_datatap 25 | 26 | 27 | class ListDatatapTest(unittest.TestCase): 28 | 29 | def test_initialize_command_line_args(self): 30 | actual = list_datatap.initialize_command_line_args([]) 31 | self.assertEqual( 32 | actual, 33 | argparse.Namespace( 34 | credentials_file=None, 35 | region="us")) 36 | 37 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 38 | @mock.patch.object(requests.requests, "Response", autospec=True) 39 | def test_list_datatap_error(self, mock_response, mock_session): 40 | mock_session.request.return_value = mock_response 41 | type(mock_response).status_code = mock.PropertyMock(return_value=400) 42 | mock_response.raise_for_status.side_effect = ( 43 | requests.requests.exceptions.HTTPError()) 44 | 45 | with self.assertRaises(requests.requests.exceptions.HTTPError): 46 | list_datatap.list_datatap(mock_session) 47 | 48 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 49 | @mock.patch.object(requests.requests, "Response", autospec=True) 50 | def test_list_datatap(self, mock_response, mock_session): 51 | mock_session.request.return_value = mock_response 52 | type(mock_response).status_code = mock.PropertyMock(return_value=200) 53 | expected = { 54 | "dataTaps": [{ 55 | "customerId": "cccccccc-cccc-cccc-cccc-cccccccccccc", 56 | "tapId": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", 57 | "displayName": "tap1", 58 | "filter": "ALL_UDM_EVENTS", 59 | "cloudPubsubSink": { 60 | "topic": "projects/sample-project/topics/sample-topic", 61 | } 62 | }] 63 | } 64 | 65 | mock_response.json.return_value = expected 66 | actual = list_datatap.list_datatap(mock_session) 67 | self.assertEqual(actual, expected) 68 | 69 | 70 | if __name__ == "__main__": 71 | unittest.main() 72 | -------------------------------------------------------------------------------- /detect/v1alpha/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | -------------------------------------------------------------------------------- /detect/v2/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | -------------------------------------------------------------------------------- /detect/v2/archive_rule.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2021 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """Executable and reusable sample for archiving a detection rule.""" 18 | 19 | import argparse 20 | 21 | from google.auth.transport import requests 22 | 23 | from common import chronicle_auth 24 | from common import regions 25 | 26 | CHRONICLE_API_BASE_URL = "https://backstory.googleapis.com" 27 | 28 | 29 | def archive_rule(http_session: requests.AuthorizedSession, version_id: str): 30 | """Archives a detection rule. 31 | 32 | Archiving a rule will fail if: 33 | - The provided version is not the latest rule version 34 | - The rule is enabled as live 35 | - The rule has retrohunts in progress 36 | 37 | If alerting is enabled for a rule, archiving the rule will automatically 38 | disable alerting for the rule. 39 | 40 | Args: 41 | http_session: Authorized session for HTTP requests. 42 | version_id: Unique ID of the detection rule to archive ("ru_" or 43 | "ru_@v__"). If a version suffix isn't 44 | specified we use the rule's latest version. 45 | 46 | Raises: 47 | requests.exceptions.HTTPError: HTTP request resulted in an error 48 | (response.status_code >= 400). 49 | """ 50 | url = f"{CHRONICLE_API_BASE_URL}/v2/detect/rules/{version_id}:archive" 51 | 52 | response = http_session.request("POST", url) 53 | # Expected server response: 54 | # {} 55 | 56 | if response.status_code >= 400: 57 | print(response.text) 58 | response.raise_for_status() 59 | 60 | 61 | if __name__ == "__main__": 62 | parser = argparse.ArgumentParser() 63 | chronicle_auth.add_argument_credentials_file(parser) 64 | regions.add_argument_region(parser) 65 | parser.add_argument( 66 | "-vi", 67 | "--version_id", 68 | type=str, 69 | required=True, 70 | help="version ID ('ru_[@v__]')") 71 | 72 | args = parser.parse_args() 73 | CHRONICLE_API_BASE_URL = regions.url(CHRONICLE_API_BASE_URL, args.region) 74 | session = chronicle_auth.initialize_http_session(args.credentials_file) 75 | archive_rule(session, args.version_id) 76 | -------------------------------------------------------------------------------- /detect/v2/archive_rule_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Unit tests for the "archive_rule" module.""" 16 | 17 | import unittest 18 | from unittest import mock 19 | 20 | from google.auth.transport import requests 21 | 22 | from . import archive_rule 23 | 24 | 25 | class ArchiveRuleTest(unittest.TestCase): 26 | 27 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 28 | @mock.patch.object(requests.requests, "Response", autospec=True) 29 | def test_http_error(self, mock_response, mock_session): 30 | mock_session.request.return_value = mock_response 31 | type(mock_response).status_code = mock.PropertyMock(return_value=400) 32 | mock_response.raise_for_status.side_effect = ( 33 | requests.requests.exceptions.HTTPError()) 34 | 35 | with self.assertRaises(requests.requests.exceptions.HTTPError): 36 | archive_rule.archive_rule(mock_session, 37 | "ru_12345678-1234-1234-1234-1234567890ab") 38 | 39 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 40 | @mock.patch.object(requests.requests, "Response", autospec=True) 41 | def test_happy_path(self, mock_response, mock_session): 42 | mock_session.request.return_value = mock_response 43 | type(mock_response).status_code = mock.PropertyMock(return_value=200) 44 | 45 | archive_rule.archive_rule(mock_session, 46 | "ru_12345678-1234-1234-1234-1234567890ab") 47 | 48 | 49 | if __name__ == "__main__": 50 | unittest.main() 51 | -------------------------------------------------------------------------------- /detect/v2/cancel_retrohunt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2021 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """Executable and reusable sample for cancelling a retrohunt. 18 | 19 | API reference: 20 | https://cloud.google.com/chronicle/docs/reference/detection-engine-api#cancelretrohunt 21 | """ 22 | 23 | import argparse 24 | 25 | from google.auth.transport import requests 26 | 27 | from common import chronicle_auth 28 | from common import regions 29 | 30 | CHRONICLE_API_BASE_URL = "https://backstory.googleapis.com" 31 | 32 | 33 | def cancel_retrohunt(http_session: requests.AuthorizedSession, version_id: str, 34 | retrohunt_id: str): 35 | """Cancel a specific retrohunt. 36 | 37 | Args: 38 | http_session: Authorized session for HTTP requests. 39 | version_id: Unique ID of the detection rule to cancel a retrohunt for 40 | ("ru_" or "ru_@v__"). If a version 41 | suffix isn't specified we use the rule's latest version. 42 | retrohunt_id: Id of the retrohunt to cancel. 43 | 44 | Raises: 45 | requests.exceptions.HTTPError: HTTP request resulted in an error 46 | (response.status_code >= 400). 47 | """ 48 | url = f"{CHRONICLE_API_BASE_URL}/v2/detect/rules/{version_id}/retrohunts/{retrohunt_id}:cancelRetrohunt" 49 | 50 | response = http_session.request("POST", url) 51 | # Expected server response: 52 | # {} 53 | 54 | if response.status_code >= 400: 55 | print(response.text) 56 | response.raise_for_status() 57 | 58 | 59 | if __name__ == "__main__": 60 | parser = argparse.ArgumentParser() 61 | chronicle_auth.add_argument_credentials_file(parser) 62 | regions.add_argument_region(parser) 63 | parser.add_argument( 64 | "-vi", 65 | "--version_id", 66 | type=str, 67 | required=True, 68 | help="version ID ('ru_[@v__]')") 69 | parser.add_argument( 70 | "-ri", 71 | "--retrohunt_id", 72 | type=str, 73 | required=True, 74 | help="retrohunt ID (for Retrohunts: 'oh_')") 75 | 76 | args = parser.parse_args() 77 | CHRONICLE_API_BASE_URL = regions.url(CHRONICLE_API_BASE_URL, args.region) 78 | session = chronicle_auth.initialize_http_session(args.credentials_file) 79 | cancel_retrohunt(session, args.version_id, args.retrohunt_id) 80 | -------------------------------------------------------------------------------- /detect/v2/cancel_retrohunt_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Unit tests for the "cancel_retrohunt" module.""" 16 | 17 | import unittest 18 | from unittest import mock 19 | 20 | from google.auth.transport import requests 21 | 22 | from . import cancel_retrohunt 23 | 24 | 25 | class CancelRetrohuntTest(unittest.TestCase): 26 | 27 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 28 | @mock.patch.object(requests.requests, "Response", autospec=True) 29 | def test_http_error(self, mock_response, mock_session): 30 | mock_session.request.return_value = mock_response 31 | type(mock_response).status_code = mock.PropertyMock(return_value=400) 32 | mock_response.raise_for_status.side_effect = ( 33 | requests.requests.exceptions.HTTPError()) 34 | 35 | with self.assertRaises(requests.requests.exceptions.HTTPError): 36 | cancel_retrohunt.cancel_retrohunt( 37 | mock_session, "ru_12345678-1234-1234-1234-1234567890ab", 38 | "oh_12345678-1234-1234-1234-1234567890ab") 39 | 40 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 41 | @mock.patch.object(requests.requests, "Response", autospec=True) 42 | def test_happy_path(self, mock_response, mock_session): 43 | mock_session.request.return_value = mock_response 44 | type(mock_response).status_code = mock.PropertyMock(return_value=200) 45 | 46 | cancel_retrohunt.cancel_retrohunt( 47 | mock_session, "ru_12345678-1234-1234-1234-1234567890ab", 48 | "oh_12345678-1234-1234-1234-1234567890ab") 49 | 50 | 51 | if __name__ == "__main__": 52 | unittest.main() 53 | -------------------------------------------------------------------------------- /detect/v2/create_rule_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Unit tests for the "create_rule" module.""" 16 | 17 | import unittest 18 | from unittest import mock 19 | 20 | from google.auth.transport import requests 21 | 22 | from . import create_rule 23 | 24 | 25 | class CreateRuleTest(unittest.TestCase): 26 | 27 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 28 | @mock.patch.object(requests.requests, "Response", autospec=True) 29 | def test_http_error(self, mock_response, mock_session): 30 | mock_session.request.return_value = mock_response 31 | type(mock_response).status_code = mock.PropertyMock(return_value=400) 32 | mock_response.raise_for_status.side_effect = ( 33 | requests.requests.exceptions.HTTPError()) 34 | 35 | with self.assertRaises(requests.requests.exceptions.HTTPError): 36 | create_rule.create_rule(mock_session, "new rule content") 37 | 38 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 39 | @mock.patch.object(requests.requests, "Response", autospec=True) 40 | def test_happy_path(self, mock_response, mock_session): 41 | mock_session.request.return_value = mock_response 42 | type(mock_response).status_code = mock.PropertyMock(return_value=200) 43 | expected_rule = { 44 | "ruleId": "ru_12345678-1234-1234-1234-1234567890ab", 45 | "rule": "rule content", 46 | "metadata": { 47 | "author": "first_author", 48 | "contributor": "first_contributor", 49 | }, 50 | } 51 | mock_response.json.return_value = expected_rule 52 | 53 | actual_rule = create_rule.create_rule(mock_session, "new rule content") 54 | self.assertEqual(actual_rule, expected_rule) 55 | 56 | 57 | if __name__ == "__main__": 58 | unittest.main() 59 | -------------------------------------------------------------------------------- /detect/v2/create_rule_version_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Unit tests for the "create_rule_version" module.""" 16 | 17 | import unittest 18 | from unittest import mock 19 | 20 | from google.auth.transport import requests 21 | 22 | from . import create_rule_version 23 | 24 | 25 | class CreateRuleVersionTest(unittest.TestCase): 26 | 27 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 28 | @mock.patch.object(requests.requests, "Response", autospec=True) 29 | def test_http_error(self, mock_response, mock_session): 30 | mock_session.request.return_value = mock_response 31 | type(mock_response).status_code = mock.PropertyMock(return_value=400) 32 | mock_response.raise_for_status.side_effect = ( 33 | requests.requests.exceptions.HTTPError()) 34 | 35 | with self.assertRaises(requests.requests.exceptions.HTTPError): 36 | create_rule_version.create_rule_version( 37 | mock_session, "ru_12345678-1234-1234-1234-1234567890ab", 38 | "new rule content") 39 | 40 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 41 | @mock.patch.object(requests.requests, "Response", autospec=True) 42 | def test_happy_path(self, mock_response, mock_session): 43 | mock_session.request.return_value = mock_response 44 | type(mock_response).status_code = mock.PropertyMock(return_value=200) 45 | expected_version_id = "ru_12345678-1234-1234-1234-1234567890ab@v_100000_000000" 46 | expected_rule = { 47 | "ruleId": "ru_12345678-1234-1234-1234-1234567890ab", 48 | "versionId": expected_version_id, 49 | "rule": "rule content", 50 | "metadata": { 51 | "author": "first_author", 52 | "contributor": "first_contributor", 53 | }, 54 | } 55 | mock_response.json.return_value = expected_rule 56 | 57 | actual_version_id = create_rule_version.create_rule_version( 58 | mock_session, "ru_12345678-1234-1234-1234-1234567890ab", 59 | "new rule content") 60 | self.assertEqual(actual_version_id, expected_version_id) 61 | 62 | 63 | if __name__ == "__main__": 64 | unittest.main() 65 | -------------------------------------------------------------------------------- /detect/v2/delete_rule.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2021 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """Executable and reusable sample for deleting a detection rule. 18 | 19 | API reference: 20 | https://cloud.google.com/chronicle/docs/reference/detection-engine-api#deleterule 21 | """ 22 | 23 | import argparse 24 | 25 | from google.auth.transport import requests 26 | 27 | from common import chronicle_auth 28 | from common import regions 29 | 30 | CHRONICLE_API_BASE_URL = "https://backstory.googleapis.com" 31 | 32 | 33 | def delete_rule(http_session: requests.AuthorizedSession, rule_id: str): 34 | """Delete a specific detection rule. 35 | 36 | Args: 37 | http_session: Authorized session for HTTP requests. 38 | rule_id: Unique ID of the detection rule to delete ("ru_"). It does 39 | not accept version id format ("ru_@v__"). 40 | 41 | Raises: 42 | requests.exceptions.HTTPError: HTTP request resulted in an error 43 | (response.status_code >= 400). 44 | """ 45 | url = f"{CHRONICLE_API_BASE_URL}/v2/detect/rules/{rule_id}" 46 | 47 | response = http_session.request("DELETE", url) 48 | # Expected server response: 49 | # {} 50 | 51 | if response.status_code >= 400: 52 | print(response.text) 53 | response.raise_for_status() 54 | 55 | 56 | if __name__ == "__main__": 57 | parser = argparse.ArgumentParser() 58 | chronicle_auth.add_argument_credentials_file(parser) 59 | regions.add_argument_region(parser) 60 | parser.add_argument( 61 | "-ri", "--rule_id", type=str, required=True, help="rule ID ('ru_')") 62 | 63 | args = parser.parse_args() 64 | CHRONICLE_API_BASE_URL = regions.url(CHRONICLE_API_BASE_URL, args.region) 65 | session = chronicle_auth.initialize_http_session(args.credentials_file) 66 | delete_rule(session, args.rule_id) 67 | -------------------------------------------------------------------------------- /detect/v2/delete_rule_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Unit tests for the "delete_rule" module.""" 16 | 17 | import unittest 18 | from unittest import mock 19 | 20 | from google.auth.transport import requests 21 | 22 | from . import delete_rule 23 | 24 | 25 | class DeleteRuleTest(unittest.TestCase): 26 | 27 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 28 | @mock.patch.object(requests.requests, "Response", autospec=True) 29 | def test_http_error(self, mock_response, mock_session): 30 | mock_session.request.return_value = mock_response 31 | type(mock_response).status_code = mock.PropertyMock(return_value=400) 32 | mock_response.raise_for_status.side_effect = ( 33 | requests.requests.exceptions.HTTPError()) 34 | 35 | with self.assertRaises(requests.requests.exceptions.HTTPError): 36 | delete_rule.delete_rule(mock_session, 37 | "ru_12345678-1234-1234-1234-1234567890ab") 38 | 39 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 40 | @mock.patch.object(requests.requests, "Response", autospec=True) 41 | def test_happy_path(self, mock_response, mock_session): 42 | mock_session.request.return_value = mock_response 43 | type(mock_response).status_code = mock.PropertyMock(return_value=200) 44 | 45 | delete_rule.delete_rule(mock_session, 46 | "ru_12345678-1234-1234-1234-1234567890ab") 47 | 48 | 49 | if __name__ == "__main__": 50 | unittest.main() 51 | -------------------------------------------------------------------------------- /detect/v2/disable_alerting.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2021 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """Executable and reusable sample for disabling alerting for a rule. 18 | 19 | API reference: 20 | https://cloud.google.com/chronicle/docs/reference/detection-engine-api#disablealerting 21 | """ 22 | 23 | import argparse 24 | 25 | from google.auth.transport import requests 26 | 27 | from common import chronicle_auth 28 | from common import regions 29 | 30 | CHRONICLE_API_BASE_URL = "https://backstory.googleapis.com" 31 | 32 | 33 | def disable_alerting(http_session: requests.AuthorizedSession, rule_id: str): 34 | """Disables alerting for a detection rule. 35 | 36 | Args: 37 | http_session: Authorized session for HTTP requests. 38 | rule_id: Unique ID of the detection rule to disable alerting for 39 | ("ru_"). A version suffix should not be provided, because alerting 40 | is set for a detection rule, not specific version of the rule. 41 | 42 | Raises: 43 | requests.exceptions.HTTPError: HTTP request resulted in an error 44 | (response.status_code >= 400). 45 | """ 46 | url = f"{CHRONICLE_API_BASE_URL}/v2/detect/rules/{rule_id}:disableAlerting" 47 | 48 | response = http_session.request("POST", url) 49 | # Expected server response: 50 | # {} 51 | 52 | if response.status_code >= 400: 53 | print(response.text) 54 | response.raise_for_status() 55 | 56 | 57 | if __name__ == "__main__": 58 | parser = argparse.ArgumentParser() 59 | chronicle_auth.add_argument_credentials_file(parser) 60 | regions.add_argument_region(parser) 61 | parser.add_argument( 62 | "-ri", "--rule_id", type=str, required=True, help="rule ID ('ru_')") 63 | 64 | args = parser.parse_args() 65 | CHRONICLE_API_BASE_URL = regions.url(CHRONICLE_API_BASE_URL, args.region) 66 | session = chronicle_auth.initialize_http_session(args.credentials_file) 67 | disable_alerting(session, args.rule_id) 68 | -------------------------------------------------------------------------------- /detect/v2/disable_alerting_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Unit tests for the "disable_alerting" module.""" 16 | 17 | import unittest 18 | from unittest import mock 19 | 20 | from google.auth.transport import requests 21 | 22 | from . import disable_alerting 23 | 24 | 25 | class DisableAlertingTest(unittest.TestCase): 26 | 27 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 28 | @mock.patch.object(requests.requests, "Response", autospec=True) 29 | def test_http_error(self, mock_response, mock_session): 30 | mock_session.request.return_value = mock_response 31 | type(mock_response).status_code = mock.PropertyMock(return_value=400) 32 | mock_response.raise_for_status.side_effect = ( 33 | requests.requests.exceptions.HTTPError()) 34 | 35 | with self.assertRaises(requests.requests.exceptions.HTTPError): 36 | disable_alerting.disable_alerting( 37 | mock_session, "ru_12345678-1234-1234-1234-1234567890ab") 38 | 39 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 40 | @mock.patch.object(requests.requests, "Response", autospec=True) 41 | def test_happy_path(self, mock_response, mock_session): 42 | mock_session.request.return_value = mock_response 43 | type(mock_response).status_code = mock.PropertyMock(return_value=200) 44 | 45 | disable_alerting.disable_alerting( 46 | mock_session, "ru_12345678-1234-1234-1234-1234567890ab") 47 | 48 | 49 | if __name__ == "__main__": 50 | unittest.main() 51 | -------------------------------------------------------------------------------- /detect/v2/disable_live_rule.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2021 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """Executable and reusable sample for disabling a live rule. 18 | 19 | API reference: 20 | https://cloud.google.com/chronicle/docs/reference/detection-engine-api#disableliverule 21 | """ 22 | 23 | import argparse 24 | 25 | from google.auth.transport import requests 26 | 27 | from common import chronicle_auth 28 | from common import regions 29 | 30 | CHRONICLE_API_BASE_URL = "https://backstory.googleapis.com" 31 | 32 | 33 | def disable_live_rule(http_session: requests.AuthorizedSession, rule_id: str): 34 | """Disables a detection rule that is currently enabled as live. 35 | 36 | If a version of a detection rule is enabled as live, then is updated with a 37 | new version, the following happens automatically: 38 | - The old version is disabled. 39 | - The new version is enabled as live. 40 | 41 | Args: 42 | http_session: Authorized session for HTTP requests. 43 | rule_id: Unique ID of the detection rule to disable ("ru_"). A version 44 | suffix should not be provided, because at most one version of a detection 45 | rule (by default the latest version of a rule) can be enabled at a time. 46 | 47 | Raises: 48 | requests.exceptions.HTTPError: HTTP request resulted in an error 49 | (response.status_code >= 400). 50 | """ 51 | url = f"{CHRONICLE_API_BASE_URL}/v2/detect/rules/{rule_id}:disableLiveRule" 52 | 53 | response = http_session.request("POST", url) 54 | # Expected server response: 55 | # {} 56 | 57 | if response.status_code >= 400: 58 | print(response.text) 59 | response.raise_for_status() 60 | 61 | 62 | if __name__ == "__main__": 63 | parser = argparse.ArgumentParser() 64 | chronicle_auth.add_argument_credentials_file(parser) 65 | regions.add_argument_region(parser) 66 | parser.add_argument( 67 | "-ri", "--rule_id", type=str, required=True, help="rule ID ('ru_')") 68 | 69 | args = parser.parse_args() 70 | CHRONICLE_API_BASE_URL = regions.url(CHRONICLE_API_BASE_URL, args.region) 71 | session = chronicle_auth.initialize_http_session(args.credentials_file) 72 | disable_live_rule(session, args.rule_id) 73 | -------------------------------------------------------------------------------- /detect/v2/disable_live_rule_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Unit tests for the "disable_live_rule" module.""" 16 | 17 | import unittest 18 | from unittest import mock 19 | 20 | from google.auth.transport import requests 21 | 22 | from . import disable_live_rule 23 | 24 | 25 | class DisableLiveRuleTest(unittest.TestCase): 26 | 27 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 28 | @mock.patch.object(requests.requests, "Response", autospec=True) 29 | def test_http_error(self, mock_response, mock_session): 30 | mock_session.request.return_value = mock_response 31 | type(mock_response).status_code = mock.PropertyMock(return_value=400) 32 | mock_response.raise_for_status.side_effect = ( 33 | requests.requests.exceptions.HTTPError()) 34 | 35 | with self.assertRaises(requests.requests.exceptions.HTTPError): 36 | disable_live_rule.disable_live_rule( 37 | mock_session, "ru_12345678-1234-1234-1234-1234567890ab") 38 | 39 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 40 | @mock.patch.object(requests.requests, "Response", autospec=True) 41 | def test_happy_path(self, mock_response, mock_session): 42 | mock_session.request.return_value = mock_response 43 | type(mock_response).status_code = mock.PropertyMock(return_value=200) 44 | 45 | disable_live_rule.disable_live_rule( 46 | mock_session, "ru_12345678-1234-1234-1234-1234567890ab") 47 | 48 | 49 | if __name__ == "__main__": 50 | unittest.main() 51 | -------------------------------------------------------------------------------- /detect/v2/enable_alerting.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2021 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """Executable and reusable sample for enabling alerting for a detection rule. 18 | 19 | API reference: 20 | https://cloud.google.com/chronicle/docs/reference/detection-engine-api#enablealerting 21 | """ 22 | 23 | import argparse 24 | 25 | from google.auth.transport import requests 26 | 27 | from common import chronicle_auth 28 | from common import regions 29 | 30 | CHRONICLE_API_BASE_URL = "https://backstory.googleapis.com" 31 | 32 | 33 | def enable_alerting(http_session: requests.AuthorizedSession, rule_id: str): 34 | """Enable alerting for a specific detection rule. 35 | 36 | Args: 37 | http_session: Authorized session for HTTP requests. 38 | rule_id: Unique ID of the detection rule to enable alerting for 39 | ("ru_"). It does not accept version id format 40 | ("ru_@v__"). 41 | 42 | Raises: 43 | requests.exceptions.HTTPError: HTTP request resulted in an error 44 | (response.status_code >= 400). 45 | """ 46 | url = f"{CHRONICLE_API_BASE_URL}/v2/detect/rules/{rule_id}:enableAlerting" 47 | 48 | response = http_session.request("POST", url) 49 | # Expected server response: 50 | # {} 51 | 52 | if response.status_code >= 400: 53 | print(response.text) 54 | response.raise_for_status() 55 | 56 | 57 | if __name__ == "__main__": 58 | parser = argparse.ArgumentParser() 59 | chronicle_auth.add_argument_credentials_file(parser) 60 | regions.add_argument_region(parser) 61 | parser.add_argument( 62 | "-ri", "--rule_id", type=str, required=True, help="rule ID ('ru_')") 63 | 64 | args = parser.parse_args() 65 | CHRONICLE_API_BASE_URL = regions.url(CHRONICLE_API_BASE_URL, args.region) 66 | session = chronicle_auth.initialize_http_session(args.credentials_file) 67 | enable_alerting(session, args.rule_id) 68 | -------------------------------------------------------------------------------- /detect/v2/enable_alerting_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Unit tests for the "enable_alerting" module.""" 16 | 17 | import unittest 18 | from unittest import mock 19 | 20 | from google.auth.transport import requests 21 | 22 | from . import enable_alerting 23 | 24 | 25 | class EnableAlertingTest(unittest.TestCase): 26 | 27 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 28 | @mock.patch.object(requests.requests, "Response", autospec=True) 29 | def test_http_error(self, mock_response, mock_session): 30 | mock_session.request.return_value = mock_response 31 | type(mock_response).status_code = mock.PropertyMock(return_value=400) 32 | mock_response.raise_for_status.side_effect = ( 33 | requests.requests.exceptions.HTTPError()) 34 | 35 | with self.assertRaises(requests.requests.exceptions.HTTPError): 36 | enable_alerting.enable_alerting(mock_session, 37 | "ru_12345678-1234-1234-1234-1234567890ab") 38 | 39 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 40 | @mock.patch.object(requests.requests, "Response", autospec=True) 41 | def test_happy_path(self, mock_response, mock_session): 42 | mock_session.request.return_value = mock_response 43 | type(mock_response).status_code = mock.PropertyMock(return_value=200) 44 | 45 | enable_alerting.enable_alerting(mock_session, 46 | "ru_12345678-1234-1234-1234-1234567890ab") 47 | 48 | 49 | if __name__ == "__main__": 50 | unittest.main() 51 | -------------------------------------------------------------------------------- /detect/v2/enable_live_rule.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2021 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """Executable and reusable sample for enabling a live detection rule. 18 | 19 | API reference: 20 | https://cloud.google.com/chronicle/docs/reference/detection-engine-api#enableliverule 21 | """ 22 | 23 | import argparse 24 | 25 | from google.auth.transport import requests 26 | 27 | from common import chronicle_auth 28 | from common import regions 29 | 30 | CHRONICLE_API_BASE_URL = "https://backstory.googleapis.com" 31 | 32 | 33 | def enable_live_rule(http_session: requests.AuthorizedSession, rule_id: str): 34 | """Enables a detection rule as live. 35 | 36 | The rule will run continuously against all *new* logs that your organization 37 | has that fall after the time the rule was enabled as live. 38 | 39 | To stop the rule from running, you can call the corresponding 40 | "disable_live_rule" action. 41 | 42 | If a version of a detection rule is enabled as live, then is updated with a 43 | new version, the following happens automatically: 44 | - The old version is disabled. 45 | - The new version is enabled as live. 46 | 47 | Args: 48 | http_session: Authorized session for HTTP requests. 49 | rule_id: Unique ID of the detection rule to enable ("ru_"). A version 50 | suffix should not be provided, because at most one version of a detection 51 | rule (by default the latest version of a rule) can be enabled at a time. 52 | 53 | Raises: 54 | requests.exceptions.HTTPError: HTTP request resulted in an error 55 | (response.status_code >= 400). 56 | """ 57 | url = f"{CHRONICLE_API_BASE_URL}/v2/detect/rules/{rule_id}:enableLiveRule" 58 | response = http_session.request("POST", url) 59 | # Expected server response: 60 | # {} 61 | 62 | if response.status_code >= 400: 63 | print(response.text) 64 | response.raise_for_status() 65 | 66 | 67 | if __name__ == "__main__": 68 | parser = argparse.ArgumentParser() 69 | chronicle_auth.add_argument_credentials_file(parser) 70 | regions.add_argument_region(parser) 71 | parser.add_argument( 72 | "-ri", "--rule_id", type=str, required=True, help="rule ID ('ru_')") 73 | 74 | args = parser.parse_args() 75 | CHRONICLE_API_BASE_URL = regions.url(CHRONICLE_API_BASE_URL, args.region) 76 | session = chronicle_auth.initialize_http_session(args.credentials_file) 77 | enable_live_rule(session, args.rule_id) 78 | -------------------------------------------------------------------------------- /detect/v2/enable_live_rule_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Unit tests for the "enable_live_rule" module.""" 16 | 17 | import unittest 18 | from unittest import mock 19 | 20 | from google.auth.transport import requests 21 | 22 | from . import enable_live_rule 23 | 24 | 25 | class EnableLiveRuleTest(unittest.TestCase): 26 | 27 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 28 | @mock.patch.object(requests.requests, "Response", autospec=True) 29 | def test_http_error(self, mock_response, mock_session): 30 | mock_session.request.return_value = mock_response 31 | type(mock_response).status_code = mock.PropertyMock(return_value=400) 32 | mock_response.raise_for_status.side_effect = ( 33 | requests.requests.exceptions.HTTPError()) 34 | 35 | with self.assertRaises(requests.requests.exceptions.HTTPError): 36 | enable_live_rule.enable_live_rule( 37 | mock_session, "ru_12345678-1234-1234-1234-1234567890ab") 38 | 39 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 40 | @mock.patch.object(requests.requests, "Response", autospec=True) 41 | def test_happy_path(self, mock_response, mock_session): 42 | mock_session.request.return_value = mock_response 43 | type(mock_response).status_code = mock.PropertyMock(return_value=200) 44 | 45 | enable_live_rule.enable_live_rule( 46 | mock_session, "ru_12345678-1234-1234-1234-1234567890ab") 47 | 48 | 49 | if __name__ == "__main__": 50 | unittest.main() 51 | -------------------------------------------------------------------------------- /detect/v2/get_error.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2021 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """Executable and reusable sample for retrieving an error. 18 | 19 | API reference: 20 | https://cloud.google.com/chronicle/docs/reference/detection-engine-api#geterror 21 | """ 22 | 23 | import argparse 24 | import json 25 | from typing import Any, Mapping 26 | 27 | from google.auth.transport import requests 28 | 29 | from common import chronicle_auth 30 | from common import regions 31 | 32 | CHRONICLE_API_BASE_URL = "https://backstory.googleapis.com" 33 | 34 | 35 | def get_error(http_session: requests.AuthorizedSession, 36 | error_id: str) -> Mapping[str, Any]: 37 | """Get error. 38 | 39 | Args: 40 | http_session: Authorized session for HTTP requests. 41 | error_id: Id of the error to get information for. 42 | 43 | Returns: 44 | Detection information. 45 | 46 | Raises: 47 | requests.exceptions.HTTPError: HTTP request resulted in an error 48 | (response.status_code >= 400). 49 | """ 50 | url = f"{CHRONICLE_API_BASE_URL}/v2/health/errors/{error_id}" 51 | 52 | response = http_session.request("GET", url) 53 | # Expected server response: 54 | # { 55 | # "category": "RULES_EXECUTION_ERROR", 56 | # "errorId": "ed_", 57 | # "errorTime": "yyyy-mm-ddThh:mm:ssZ", 58 | # "ruleExecution": { 59 | # "ruleId": "ru_", 60 | # "versionId": "ru_@v__", 61 | # "windowEndTime": "yyyy-mm-ddThh:mm:ssZ", 62 | # "windowStartTime": "yyyy-mm-ddThh:mm:ssZ" 63 | # }, 64 | # "text": "" 65 | # } 66 | 67 | if response.status_code >= 400: 68 | print(response.text) 69 | response.raise_for_status() 70 | return response.json() 71 | 72 | 73 | if __name__ == "__main__": 74 | parser = argparse.ArgumentParser() 75 | chronicle_auth.add_argument_credentials_file(parser) 76 | regions.add_argument_region(parser) 77 | parser.add_argument( 78 | "-ei", 79 | "--error_id", 80 | type=str, 81 | required=True, 82 | help="error ID (for Detect errors: 'ed_')") 83 | 84 | args = parser.parse_args() 85 | CHRONICLE_API_BASE_URL = regions.url(CHRONICLE_API_BASE_URL, args.region) 86 | session = chronicle_auth.initialize_http_session(args.credentials_file) 87 | error = get_error(session, args.error_id) 88 | print(json.dumps(error, indent=2)) 89 | -------------------------------------------------------------------------------- /detect/v2/get_error_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Unit tests for the "get_error" module.""" 16 | 17 | import unittest 18 | from unittest import mock 19 | 20 | from google.auth.transport import requests 21 | 22 | from . import get_error 23 | 24 | 25 | class GetErrorTest(unittest.TestCase): 26 | 27 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 28 | @mock.patch.object(requests.requests, "Response", autospec=True) 29 | def test_http_error(self, mock_response, mock_session): 30 | mock_session.request.return_value = mock_response 31 | type(mock_response).status_code = mock.PropertyMock(return_value=400) 32 | mock_response.raise_for_status.side_effect = ( 33 | requests.requests.exceptions.HTTPError()) 34 | 35 | with self.assertRaises(requests.requests.exceptions.HTTPError): 36 | get_error.get_error(mock_session, "") 37 | 38 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 39 | @mock.patch.object(requests.requests, "Response", autospec=True) 40 | def test_happy_path(self, mock_response, mock_session): 41 | mock_session.request.return_value = mock_response 42 | type(mock_response).status_code = mock.PropertyMock(return_value=200) 43 | error_id = "ed_12345678-1234-1234-1234-1234567890ab" 44 | version_id = "ru_12345678-1234-1234-1234-1234567890ab@v_100000_000000" 45 | expected_error = { 46 | "errorId": error_id, 47 | "text": "something went wrong", 48 | "category": "RULES_EXECUTION_ERROR", 49 | "errorTime": "2020-11-05T00:00:00Z", 50 | "metadata": { 51 | "ruleExecution": { 52 | "windowStartTime": "2020-11-05T00:00:00Z", 53 | "windowEndTime": "2020-11-05T01:00:00Z", 54 | "ruleId": "ru_12345678-1234-1234-1234-1234567890ab", 55 | "versionId": version_id, 56 | }, 57 | }, 58 | } 59 | mock_response.json.return_value = expected_error 60 | 61 | error = get_error.get_error(mock_session, error_id) 62 | self.assertEqual(error, expected_error) 63 | 64 | 65 | if __name__ == "__main__": 66 | unittest.main() 67 | -------------------------------------------------------------------------------- /detect/v2/get_retrohunt_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Unit tests for the "get_retrohunt" module.""" 16 | 17 | import unittest 18 | from unittest import mock 19 | 20 | from google.auth.transport import requests 21 | 22 | from . import get_retrohunt 23 | 24 | 25 | class GetRetrohuntTest(unittest.TestCase): 26 | 27 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 28 | @mock.patch.object(requests.requests, "Response", autospec=True) 29 | def test_http_error(self, mock_response, mock_session): 30 | mock_session.request.return_value = mock_response 31 | type(mock_response).status_code = mock.PropertyMock(return_value=400) 32 | mock_response.raise_for_status.side_effect = ( 33 | requests.requests.exceptions.HTTPError()) 34 | 35 | with self.assertRaises(requests.requests.exceptions.HTTPError): 36 | get_retrohunt.get_retrohunt(mock_session, 37 | "ru_12345678-1234-1234-1234-1234567890ab", 38 | "oh_87654321-4321-4321-4321-ba0987654321") 39 | 40 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 41 | @mock.patch.object(requests.requests, "Response", autospec=True) 42 | def test_happy_path(self, mock_response, mock_session): 43 | mock_session.request.return_value = mock_response 44 | type(mock_response).status_code = mock.PropertyMock(return_value=200) 45 | expected_response = { 46 | "retrohuntId": 47 | "oh_87654321-4321-4321-4321-ba0987654321", 48 | "ruleId": 49 | "ru_12345678-1234-1234-1234-1234567890ab", 50 | "versionId": 51 | "ru_12345678-1234-1234-1234-1234567890ab@v_1234567890_123456789", 52 | "eventStartTime": 53 | "2021-01-01T00:00:00Z", 54 | "eventEndTime": 55 | "2021-01-06T00:00:00Z", 56 | "retrohuntStartTime": 57 | "2021-04-28T00:00:00Z", 58 | "state": 59 | "RUNNING", 60 | "progressPercentage": 61 | 19.63, 62 | } 63 | mock_response.json.return_value = expected_response 64 | 65 | got_response = get_retrohunt.get_retrohunt( 66 | mock_session, 67 | "ru_12345678-1234-1234-1234-1234567890ab", 68 | "oh_87654321-4321-4321-4321-ba0987654321" 69 | ) 70 | self.assertEqual(got_response, expected_response) 71 | 72 | 73 | if __name__ == "__main__": 74 | unittest.main() 75 | -------------------------------------------------------------------------------- /detect/v2/get_rule_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Unit tests for the "get_rule" module.""" 16 | 17 | import unittest 18 | from unittest import mock 19 | 20 | from google.auth.transport import requests 21 | 22 | from . import get_rule 23 | 24 | 25 | class GetRuleTest(unittest.TestCase): 26 | 27 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 28 | @mock.patch.object(requests.requests, "Response", autospec=True) 29 | def test_http_error(self, mock_response, mock_session): 30 | mock_session.request.return_value = mock_response 31 | type(mock_response).status_code = mock.PropertyMock(return_value=400) 32 | mock_response.raise_for_status.side_effect = ( 33 | requests.requests.exceptions.HTTPError()) 34 | 35 | with self.assertRaises(requests.requests.exceptions.HTTPError): 36 | get_rule.get_rule(mock_session, "ru_12345678-1234-1234-1234-1234567890ab") 37 | 38 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 39 | @mock.patch.object(requests.requests, "Response", autospec=True) 40 | def test_happy_path(self, mock_response, mock_session): 41 | mock_session.request.return_value = mock_response 42 | type(mock_response).status_code = mock.PropertyMock(return_value=200) 43 | expected_rule_content = "rule content" 44 | expected_response = { 45 | "ruleId": "ru_12345678-1234-1234-1234-1234567890ab", 46 | "rule": expected_rule_content, 47 | } 48 | mock_response.json.return_value = expected_response 49 | 50 | got_response = get_rule.get_rule(mock_session, 51 | "ru_12345678-1234-1234-1234-1234567890ab") 52 | self.assertEqual(got_response, expected_response) 53 | 54 | 55 | if __name__ == "__main__": 56 | unittest.main() 57 | -------------------------------------------------------------------------------- /detect/v2/list_curated_rules_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Unit tests for the "list_curated_rules" module.""" 16 | 17 | import unittest 18 | from unittest import mock 19 | 20 | from google.auth.transport import requests 21 | 22 | from . import list_curated_rules 23 | 24 | 25 | class ListCuratedRulesTest(unittest.TestCase): 26 | 27 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 28 | @mock.patch.object(requests.requests, "Response", autospec=True) 29 | def test_http_error(self, mock_response, mock_session): 30 | mock_session.request.return_value = mock_response 31 | type(mock_response).status_code = mock.PropertyMock(return_value=400) 32 | mock_response.raise_for_status.side_effect = ( 33 | requests.requests.exceptions.HTTPError()) 34 | 35 | with self.assertRaises(requests.requests.exceptions.HTTPError): 36 | list_curated_rules.list_curated_rules(mock_session) 37 | 38 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 39 | @mock.patch.object(requests.requests, "Response", autospec=True) 40 | def test_happy_path_without_page_size(self, mock_response, mock_session): 41 | mock_session.request.return_value = mock_response 42 | type(mock_response).status_code = mock.PropertyMock(return_value=200) 43 | expected_rule = { 44 | "ruleId": "ur_sample_rule", 45 | "ruleName": "Sample Rule", 46 | "severity": "Info", 47 | "ruleType": "SINGLE_EVENT", 48 | "precision": "PRECISE", 49 | "tactics": ["TA0042",], 50 | "techniques": ["T1595.001",], 51 | "updateTime": "2023-01-01T00:00:00Z", 52 | "ruleSet": "87654321-4321-4321-4321-ba0987654321", 53 | "description": "Sample Rule Description", 54 | } 55 | expected_page_token = "page token here" 56 | mock_response.json.return_value = { 57 | "curatedRules": [expected_rule], 58 | "nextPageToken": expected_page_token, 59 | } 60 | 61 | rules, next_page_token = list_curated_rules.list_curated_rules(mock_session) 62 | self.assertEqual(len(rules), 1) 63 | self.assertEqual(rules[0], expected_rule) 64 | self.assertEqual(next_page_token, expected_page_token) 65 | 66 | 67 | if __name__ == "__main__": 68 | unittest.main() 69 | -------------------------------------------------------------------------------- /detect/v2/list_errors_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Unit tests for the "list_errors" module.""" 16 | 17 | import unittest 18 | from unittest import mock 19 | 20 | from google.auth.transport import requests 21 | 22 | from . import list_errors 23 | 24 | 25 | class ListErrorsTest(unittest.TestCase): 26 | 27 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 28 | @mock.patch.object(requests.requests, "Response", autospec=True) 29 | def test_http_error(self, mock_response, mock_session): 30 | mock_session.request.return_value = mock_response 31 | type(mock_response).status_code = mock.PropertyMock(return_value=400) 32 | mock_response.raise_for_status.side_effect = ( 33 | requests.requests.exceptions.HTTPError()) 34 | 35 | with self.assertRaises(requests.requests.exceptions.HTTPError): 36 | list_errors.list_errors(mock_session) 37 | 38 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 39 | @mock.patch.object(requests.requests, "Response", autospec=True) 40 | def test_happy_path_without_page_size(self, mock_response, mock_session): 41 | mock_session.request.return_value = mock_response 42 | type(mock_response).status_code = mock.PropertyMock(return_value=200) 43 | expected_error = { 44 | "category": "RULES_EXECUTION_ERROR", 45 | "errorId": "ed_12345678-1234-1234-1234-1234567890ab", 46 | "errorTime": "2020-12-09T16:00:00Z", 47 | "ruleExecution": { 48 | "ruleId": 49 | "ru_12345678-1234-1234-1234-1234567890ab", 50 | "versionId": 51 | "ru_12345678-1234-1234-1234-1234567890ab@v_100000_000000", 52 | "windowEndTime": 53 | "2020-12-09T16:00:00Z", 54 | "windowStartTime": 55 | "2020-12-09T15:00:00Z" 56 | }, 57 | "text": "rule error text" 58 | "line: 15 \n" 59 | "column: 37-74 " 60 | } 61 | expected_page_token = "page token here" 62 | mock_response.json.return_value = { 63 | "errors": [expected_error], 64 | "nextPageToken": expected_page_token, 65 | } 66 | 67 | errors, next_page_token = list_errors.list_errors(mock_session) 68 | self.assertEqual(len(errors), 1) 69 | self.assertEqual(errors[0], expected_error) 70 | self.assertEqual(next_page_token, expected_page_token) 71 | 72 | 73 | if __name__ == "__main__": 74 | unittest.main() 75 | -------------------------------------------------------------------------------- /detect/v2/list_rule_versions_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Unit tests for the "list_rule_versions" module.""" 16 | 17 | import unittest 18 | from unittest import mock 19 | 20 | from google.auth.transport import requests 21 | 22 | from . import list_rule_versions 23 | 24 | 25 | class ListRuleVersionsTest(unittest.TestCase): 26 | 27 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 28 | @mock.patch.object(requests.requests, "Response", autospec=True) 29 | def test_http_error(self, mock_response, mock_session): 30 | mock_session.request.return_value = mock_response 31 | type(mock_response).status_code = mock.PropertyMock(return_value=400) 32 | mock_response.raise_for_status.side_effect = ( 33 | requests.requests.exceptions.HTTPError()) 34 | 35 | with self.assertRaises(requests.requests.exceptions.HTTPError): 36 | list_rule_versions.list_rule_versions( 37 | mock_session, "ru_12345678-1234-1234-1234-1234567890ab") 38 | 39 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 40 | @mock.patch.object(requests.requests, "Response", autospec=True) 41 | def test_happy_path_without_page_size(self, mock_response, mock_session): 42 | mock_session.request.return_value = mock_response 43 | type(mock_response).status_code = mock.PropertyMock(return_value=200) 44 | expected_rule = { 45 | "ruleId": "ru_12345678-1234-1234-1234-1234567890ab", 46 | "versionId": "ru_12345678-1234-1234-1234-1234567890ab@v_100000_000000", 47 | "rule": "rule content", 48 | "metadata": { 49 | "author": "first_author", 50 | "contributor": "first_contributor", 51 | }, 52 | } 53 | expected_page_token = "page token here" 54 | mock_response.json.return_value = { 55 | "rules": [expected_rule], 56 | "nextPageToken": expected_page_token, 57 | } 58 | 59 | rules, next_page_token = list_rule_versions.list_rule_versions( 60 | mock_session, "ru_12345678-1234-1234-1234-1234567890ab") 61 | self.assertEqual(len(rules), 1) 62 | self.assertEqual(rules[0], expected_rule) 63 | self.assertEqual(next_page_token, expected_page_token) 64 | 65 | 66 | if __name__ == "__main__": 67 | unittest.main() 68 | -------------------------------------------------------------------------------- /detect/v2/list_rules_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Unit tests for the "list_rules" module.""" 16 | 17 | import unittest 18 | from unittest import mock 19 | 20 | from google.auth.transport import requests 21 | 22 | from . import list_rules 23 | 24 | 25 | class ListRulesTest(unittest.TestCase): 26 | 27 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 28 | @mock.patch.object(requests.requests, "Response", autospec=True) 29 | def test_http_error(self, mock_response, mock_session): 30 | mock_session.request.return_value = mock_response 31 | type(mock_response).status_code = mock.PropertyMock(return_value=400) 32 | mock_response.raise_for_status.side_effect = ( 33 | requests.requests.exceptions.HTTPError()) 34 | 35 | with self.assertRaises(requests.requests.exceptions.HTTPError): 36 | list_rules.list_rules(mock_session) 37 | 38 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 39 | @mock.patch.object(requests.requests, "Response", autospec=True) 40 | def test_happy_path_without_page_size(self, mock_response, mock_session): 41 | mock_session.request.return_value = mock_response 42 | type(mock_response).status_code = mock.PropertyMock(return_value=200) 43 | expected_rule = { 44 | "ruleId": "ru_12345678-1234-1234-1234-1234567890ab", 45 | "rule": "rule content", 46 | "metadata": { 47 | "author": "first_author", 48 | "contributor": "first_contributor", 49 | }, 50 | } 51 | expected_page_token = "page token here" 52 | mock_response.json.return_value = { 53 | "rules": [expected_rule], 54 | "nextPageToken": expected_page_token, 55 | } 56 | 57 | rules, next_page_token = list_rules.list_rules(mock_session) 58 | self.assertEqual(len(rules), 1) 59 | self.assertEqual(rules[0], expected_rule) 60 | self.assertEqual(next_page_token, expected_page_token) 61 | 62 | 63 | if __name__ == "__main__": 64 | unittest.main() 65 | -------------------------------------------------------------------------------- /detect/v2/unarchive_rule.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2021 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """Executable and reusable sample for unarchiving a detection rule.""" 18 | 19 | import argparse 20 | 21 | from google.auth.transport import requests 22 | 23 | from common import chronicle_auth 24 | from common import regions 25 | 26 | CHRONICLE_API_BASE_URL = "https://backstory.googleapis.com" 27 | 28 | 29 | def unarchive_rule(http_session: requests.AuthorizedSession, version_id: str): 30 | """Unarchives a detection rule. 31 | 32 | Unarchiving a rule will fail if the provided version is not the latest rule 33 | version. 34 | 35 | Args: 36 | http_session: Authorized session for HTTP requests. 37 | version_id: Unique ID of the detection rule to unarchive ("ru_" or 38 | "ru_@v__"). If a version suffix isn't 39 | specified we use the rule's latest version. 40 | 41 | Raises: 42 | requests.exceptions.HTTPError: HTTP request resulted in an error 43 | (response.status_code >= 400). 44 | """ 45 | url = f"{CHRONICLE_API_BASE_URL}/v2/detect/rules/{version_id}:unarchive" 46 | 47 | response = http_session.request("POST", url) 48 | # Expected server response: 49 | # {} 50 | 51 | if response.status_code >= 400: 52 | print(response.text) 53 | response.raise_for_status() 54 | 55 | 56 | if __name__ == "__main__": 57 | parser = argparse.ArgumentParser() 58 | chronicle_auth.add_argument_credentials_file(parser) 59 | regions.add_argument_region(parser) 60 | parser.add_argument( 61 | "-vi", 62 | "--version_id", 63 | type=str, 64 | required=True, 65 | help="version ID ('ru_[@v__]')") 66 | 67 | args = parser.parse_args() 68 | CHRONICLE_API_BASE_URL = regions.url(CHRONICLE_API_BASE_URL, args.region) 69 | session = chronicle_auth.initialize_http_session(args.credentials_file) 70 | unarchive_rule(session, args.version_id) 71 | -------------------------------------------------------------------------------- /detect/v2/unarchive_rule_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Unit tests for the "unarchive_rule" module.""" 16 | 17 | import unittest 18 | from unittest import mock 19 | 20 | from google.auth.transport import requests 21 | 22 | from . import unarchive_rule 23 | 24 | 25 | class UnrchiveRuleTest(unittest.TestCase): 26 | 27 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 28 | @mock.patch.object(requests.requests, "Response", autospec=True) 29 | def test_http_error(self, mock_response, mock_session): 30 | mock_session.request.return_value = mock_response 31 | type(mock_response).status_code = mock.PropertyMock(return_value=400) 32 | mock_response.raise_for_status.side_effect = ( 33 | requests.requests.exceptions.HTTPError()) 34 | 35 | with self.assertRaises(requests.requests.exceptions.HTTPError): 36 | unarchive_rule.unarchive_rule(mock_session, 37 | "ru_12345678-1234-1234-1234-1234567890ab") 38 | 39 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 40 | @mock.patch.object(requests.requests, "Response", autospec=True) 41 | def test_happy_path(self, mock_response, mock_session): 42 | mock_session.request.return_value = mock_response 43 | type(mock_response).status_code = mock.PropertyMock(return_value=200) 44 | 45 | unarchive_rule.unarchive_rule(mock_session, 46 | "ru_12345678-1234-1234-1234-1234567890ab") 47 | 48 | 49 | if __name__ == "__main__": 50 | unittest.main() 51 | -------------------------------------------------------------------------------- /detect/v2/verify_rule.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2021 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """Executable and reusable sample for verifying a detection rule. 18 | 19 | API reference: 20 | https://cloud.google.com/chronicle/docs/reference/detection-engine-api#verifyrule 21 | """ 22 | 23 | import argparse 24 | import json 25 | from typing import Any, Mapping 26 | 27 | from google.auth.transport import requests 28 | 29 | from common import chronicle_auth 30 | from common import regions 31 | 32 | CHRONICLE_API_BASE_URL = "https://backstory.googleapis.com" 33 | 34 | 35 | def verify_rule(http_session: requests.AuthorizedSession, 36 | rule_content: str) -> Mapping[str, Any]: 37 | """Validates that a detection rule is a valid YL2 rule. 38 | 39 | Args: 40 | http_session: Authorized session for HTTP requests. 41 | rule_content: Content of the detection rule. 42 | 43 | Returns: 44 | A compilation error if there is one. 45 | 46 | Raises: 47 | requests.exceptions.HTTPError: HTTP request resulted in an error 48 | (response.status_code >= 400). 49 | """ 50 | url = f"{CHRONICLE_API_BASE_URL}/v2/detect/rules:verifyRule" 51 | body = {"rule_text": rule_content} 52 | 53 | response = http_session.request("POST", url, json=body) 54 | # Expected server response: 55 | # { 56 | # "compilationError": "", <-- IFF compilation failed. 57 | # } 58 | 59 | if response.status_code >= 400: 60 | print(response.text) 61 | response.raise_for_status() 62 | return response.json() 63 | 64 | 65 | if __name__ == "__main__": 66 | parser = argparse.ArgumentParser() 67 | chronicle_auth.add_argument_credentials_file(parser) 68 | regions.add_argument_region(parser) 69 | parser.add_argument( 70 | "-f", 71 | "--rule_file", 72 | type=argparse.FileType("r"), 73 | required=True, 74 | # File example: python3 verify_rule.py -f 75 | # STDIN example: cat rule.txt | python3 verify_rule.py -f - 76 | help="path of a file with the rule's content, or - for STDIN") 77 | 78 | args = parser.parse_args() 79 | CHRONICLE_API_BASE_URL = regions.url(CHRONICLE_API_BASE_URL, args.region) 80 | session = chronicle_auth.initialize_http_session(args.credentials_file) 81 | resp = verify_rule(session, args.rule_file.read()) 82 | print(json.dumps(resp, indent=2)) 83 | -------------------------------------------------------------------------------- /detect/v2/verify_rule_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Unit tests for the "verify_rule" module.""" 16 | 17 | import unittest 18 | from unittest import mock 19 | 20 | from google.auth.transport import requests 21 | 22 | from . import verify_rule 23 | 24 | 25 | class VerifyRuleTest(unittest.TestCase): 26 | 27 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 28 | @mock.patch.object(requests.requests, "Response", autospec=True) 29 | def test_http_error(self, mock_response, mock_session): 30 | mock_session.request.return_value = mock_response 31 | type(mock_response).status_code = mock.PropertyMock(return_value=400) 32 | mock_response.raise_for_status.side_effect = ( 33 | requests.requests.exceptions.HTTPError()) 34 | 35 | with self.assertRaises(requests.requests.exceptions.HTTPError): 36 | verify_rule.verify_rule(mock_session, "new rule content") 37 | 38 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 39 | @mock.patch.object(requests.requests, "Response", autospec=True) 40 | def test_happy_path(self, mock_response, mock_session): 41 | mock_session.request.return_value = mock_response 42 | type(mock_response).status_code = mock.PropertyMock(return_value=200) 43 | expected_resp = { 44 | "compilationError": "generic::invalid_argument compilation error", 45 | } 46 | mock_response.json.return_value = expected_resp 47 | 48 | actual_rule = verify_rule.verify_rule(mock_session, "new rule content") 49 | self.assertEqual(actual_rule, expected_resp) 50 | 51 | 52 | if __name__ == "__main__": 53 | unittest.main() 54 | -------------------------------------------------------------------------------- /feeds/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | -------------------------------------------------------------------------------- /feeds/create_okta_feed_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Unit tests for the "create_okta_feed" module.""" 16 | 17 | import unittest 18 | from unittest import mock 19 | 20 | from google.auth.transport import requests 21 | 22 | from . import create_okta_feed 23 | 24 | 25 | class CreateFeedTest(unittest.TestCase): 26 | 27 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 28 | @mock.patch.object(requests.requests, "Response", autospec=True) 29 | def test_http_error(self, mock_response, mock_session): 30 | mock_session.request.return_value = mock_response 31 | type(mock_response).status_code = mock.PropertyMock(return_value=400) 32 | mock_response.raise_for_status.side_effect = ( 33 | requests.requests.exceptions.HTTPError()) 34 | 35 | with self.assertRaises(requests.requests.exceptions.HTTPError): 36 | create_okta_feed.create_okta_feed(mock_session, "secret_example", 37 | "hostname.example.com", "my feed name") 38 | 39 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 40 | @mock.patch.object(requests.requests, "Response", autospec=True) 41 | def test_happy_path(self, mock_response, mock_session): 42 | mock_session.request.return_value = mock_response 43 | type(mock_response).status_code = mock.PropertyMock(return_value=200) 44 | expected_feed = { 45 | "name": "feeds/cf49ebc5-e7bf-4562-8061-cab43cecba35", 46 | "display_name": "my feed name", 47 | "details": { 48 | "logType": "OKTA", 49 | "feedSourceType": "API", 50 | "oktaSettings": { 51 | "authentication": { 52 | "headerKeyValues": [{ 53 | "key": "key_example", 54 | "value": "value_example" 55 | }] 56 | } 57 | }, 58 | }, 59 | "feedState": "PENDING_ENABLEMENT" 60 | } 61 | 62 | mock_response.json.return_value = expected_feed 63 | 64 | actual_feed = create_okta_feed.create_okta_feed( 65 | mock_session, "secret_example", "hostname.example.com", "my feed name") 66 | self.assertEqual(actual_feed, expected_feed) 67 | 68 | 69 | if __name__ == "__main__": 70 | unittest.main() 71 | -------------------------------------------------------------------------------- /feeds/create_okta_user_context_feed_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Unit tests for the "create_okta_user_context_feed" module.""" 16 | 17 | import unittest 18 | from unittest import mock 19 | 20 | from google.auth.transport import requests 21 | 22 | from . import create_okta_user_context_feed 23 | 24 | 25 | class CreateFeedTest(unittest.TestCase): 26 | 27 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 28 | @mock.patch.object(requests.requests, "Response", autospec=True) 29 | def test_http_error(self, mock_response, mock_session): 30 | mock_session.request.return_value = mock_response 31 | type(mock_response).status_code = mock.PropertyMock(return_value=400) 32 | mock_response.raise_for_status.side_effect = ( 33 | requests.requests.exceptions.HTTPError()) 34 | 35 | with self.assertRaises(requests.requests.exceptions.HTTPError): 36 | create_okta_user_context_feed.create_okta_user_context_feed( 37 | mock_session, "secret_example", "hostname.example.com", 38 | "my feed name") 39 | 40 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 41 | @mock.patch.object(requests.requests, "Response", autospec=True) 42 | def test_happy_path(self, mock_response, mock_session): 43 | mock_session.request.return_value = mock_response 44 | type(mock_response).status_code = mock.PropertyMock(return_value=200) 45 | expected_feed = { 46 | "name": "feeds/cf49ebc5-e7bf-4562-8061-cab43cecba35", 47 | "display_name": "my feed name", 48 | "details": { 49 | "logType": "OKTA_USER_CONTEXT", 50 | "feedSourceType": "API", 51 | "oktaSettings": { 52 | "authentication": { 53 | "headerKeyValues": [{ 54 | "key": "key_example", 55 | "value": "value_example" 56 | }] 57 | } 58 | }, 59 | }, 60 | "feedState": "PENDING_ENABLEMENT" 61 | } 62 | 63 | mock_response.json.return_value = expected_feed 64 | 65 | actual_feed = create_okta_user_context_feed.create_okta_user_context_feed( 66 | mock_session, "secret_example", "hostname.example.com", "my feed name") 67 | self.assertEqual(actual_feed, expected_feed) 68 | 69 | 70 | if __name__ == "__main__": 71 | unittest.main() 72 | -------------------------------------------------------------------------------- /feeds/delete_feed.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2022 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """Executable and reusable sample for deleting a feed.""" 18 | 19 | import argparse 20 | 21 | from google.auth.transport import requests 22 | 23 | from common import chronicle_auth 24 | from common import regions 25 | 26 | CHRONICLE_API_BASE_URL = "https://backstory.googleapis.com" 27 | 28 | 29 | def delete_feed(http_session: requests.AuthorizedSession, name: str): 30 | """Delete a specific feed. 31 | 32 | Args: 33 | http_session: Authorized session for HTTP requests. 34 | name: Unique name for the feed. 35 | 36 | Raises: 37 | requests.exceptions.HTTPError: HTTP request resulted in an error 38 | (response.status_code >= 400). 39 | """ 40 | url = f"{CHRONICLE_API_BASE_URL}/v1/feeds/{name}" 41 | 42 | response = http_session.request("DELETE", url) 43 | # Expected server response: 44 | # {} 45 | 46 | if response.status_code >= 400: 47 | print(response.text) 48 | response.raise_for_status() 49 | 50 | 51 | if __name__ == "__main__": 52 | parser = argparse.ArgumentParser() 53 | chronicle_auth.add_argument_credentials_file(parser) 54 | regions.add_argument_region(parser) 55 | parser.add_argument( 56 | "-n", "--name", type=str, required=True, help="unique name for the feed") 57 | 58 | args = parser.parse_args() 59 | CHRONICLE_API_BASE_URL = regions.url(CHRONICLE_API_BASE_URL, args.region) 60 | session = chronicle_auth.initialize_http_session(args.credentials_file) 61 | delete_feed(session, args.name) 62 | -------------------------------------------------------------------------------- /feeds/delete_feed_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Unit tests for the "delete_feed" module.""" 16 | 17 | import unittest 18 | from unittest import mock 19 | 20 | from google.auth.transport import requests 21 | 22 | from . import delete_feed 23 | 24 | 25 | class DeleteFeedTest(unittest.TestCase): 26 | 27 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 28 | @mock.patch.object(requests.requests, "Response", autospec=True) 29 | def test_http_error(self, mock_response, mock_session): 30 | mock_session.request.return_value = mock_response 31 | type(mock_response).status_code = mock.PropertyMock(return_value=400) 32 | mock_response.raise_for_status.side_effect = ( 33 | requests.requests.exceptions.HTTPError()) 34 | 35 | with self.assertRaises(requests.requests.exceptions.HTTPError): 36 | delete_feed.delete_feed(mock_session, "feed name") 37 | 38 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 39 | @mock.patch.object(requests.requests, "Response", autospec=True) 40 | def test_happy_path(self, mock_response, mock_session): 41 | mock_session.request.return_value = mock_response 42 | type(mock_response).status_code = mock.PropertyMock(return_value=200) 43 | 44 | delete_feed.delete_feed(mock_session, "feed name") 45 | 46 | 47 | if __name__ == "__main__": 48 | unittest.main() 49 | -------------------------------------------------------------------------------- /feeds/disable_feed.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2022 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """Executable and reusable sample for disabling a feed.""" 18 | 19 | import argparse 20 | 21 | from google.auth.transport import requests 22 | 23 | from common import chronicle_auth 24 | from common import regions 25 | 26 | CHRONICLE_API_BASE_URL = "https://backstory.googleapis.com" 27 | 28 | 29 | def disable_feed(http_session: requests.AuthorizedSession, name: str): 30 | """Disable a specific feed. 31 | 32 | Args: 33 | http_session: Authorized session for HTTP requests. 34 | name: Unique name for the feed. 35 | 36 | Raises: 37 | requests.exceptions.HTTPError: HTTP request resulted in an error 38 | (response.status_code >= 400). 39 | """ 40 | url = f"{CHRONICLE_API_BASE_URL}/v1/feeds/{name}:disable" 41 | 42 | response = http_session.request("POST", url) 43 | # Expected server response: 44 | # {} 45 | 46 | if response.status_code >= 400: 47 | print(response.text) 48 | response.raise_for_status() 49 | 50 | 51 | if __name__ == "__main__": 52 | parser = argparse.ArgumentParser() 53 | chronicle_auth.add_argument_credentials_file(parser) 54 | regions.add_argument_region(parser) 55 | parser.add_argument( 56 | "-n", "--name", type=str, required=True, help="unique name for the feed") 57 | 58 | args = parser.parse_args() 59 | CHRONICLE_API_BASE_URL = regions.url(CHRONICLE_API_BASE_URL, args.region) 60 | session = chronicle_auth.initialize_http_session(args.credentials_file) 61 | disable_feed(session, args.name) 62 | -------------------------------------------------------------------------------- /feeds/disable_feed_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Unit tests for the "disable_feed" module.""" 16 | 17 | import unittest 18 | from unittest import mock 19 | 20 | from google.auth.transport import requests 21 | 22 | from . import disable_feed 23 | 24 | 25 | class DeleteFeedTest(unittest.TestCase): 26 | 27 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 28 | @mock.patch.object(requests.requests, "Response", autospec=True) 29 | def test_http_error(self, mock_response, mock_session): 30 | mock_session.request.return_value = mock_response 31 | type(mock_response).status_code = mock.PropertyMock(return_value=400) 32 | mock_response.raise_for_status.side_effect = ( 33 | requests.requests.exceptions.HTTPError()) 34 | 35 | with self.assertRaises(requests.requests.exceptions.HTTPError): 36 | disable_feed.disable_feed(mock_session, "feed name") 37 | 38 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 39 | @mock.patch.object(requests.requests, "Response", autospec=True) 40 | def test_happy_path(self, mock_response, mock_session): 41 | mock_session.request.return_value = mock_response 42 | type(mock_response).status_code = mock.PropertyMock(return_value=200) 43 | 44 | disable_feed.disable_feed(mock_session, "feed name") 45 | 46 | 47 | if __name__ == "__main__": 48 | unittest.main() 49 | -------------------------------------------------------------------------------- /feeds/enable_feed.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2022 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """Executable and reusable sample for enabling a feed.""" 18 | 19 | import argparse 20 | 21 | from google.auth.transport import requests 22 | 23 | from common import chronicle_auth 24 | from common import regions 25 | 26 | CHRONICLE_API_BASE_URL = "https://backstory.googleapis.com" 27 | 28 | 29 | def enable_feed(http_session: requests.AuthorizedSession, name: str): 30 | """Enable a specific feed. 31 | 32 | Args: 33 | http_session: Authorized session for HTTP requests. 34 | name: Unique name for the feed. 35 | 36 | Raises: 37 | requests.exceptions.HTTPError: HTTP request resulted in an error 38 | (response.status_code >= 400). 39 | """ 40 | url = f"{CHRONICLE_API_BASE_URL}/v1/feeds/{name}:enable" 41 | 42 | response = http_session.request("POST", url) 43 | # Expected server response: 44 | # {} 45 | 46 | if response.status_code >= 400: 47 | print(response.text) 48 | response.raise_for_status() 49 | 50 | 51 | if __name__ == "__main__": 52 | parser = argparse.ArgumentParser() 53 | chronicle_auth.add_argument_credentials_file(parser) 54 | regions.add_argument_region(parser) 55 | parser.add_argument( 56 | "-n", "--name", type=str, required=True, help="unique name for the feed") 57 | 58 | args = parser.parse_args() 59 | CHRONICLE_API_BASE_URL = regions.url(CHRONICLE_API_BASE_URL, args.region) 60 | session = chronicle_auth.initialize_http_session(args.credentials_file) 61 | enable_feed(session, args.name) 62 | -------------------------------------------------------------------------------- /feeds/enable_feed_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Unit tests for the "enable_feed" module.""" 16 | 17 | import unittest 18 | from unittest import mock 19 | 20 | from google.auth.transport import requests 21 | 22 | from . import enable_feed 23 | 24 | 25 | class DeleteFeedTest(unittest.TestCase): 26 | 27 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 28 | @mock.patch.object(requests.requests, "Response", autospec=True) 29 | def test_http_error(self, mock_response, mock_session): 30 | mock_session.request.return_value = mock_response 31 | type(mock_response).status_code = mock.PropertyMock(return_value=400) 32 | mock_response.raise_for_status.side_effect = ( 33 | requests.requests.exceptions.HTTPError()) 34 | 35 | with self.assertRaises(requests.requests.exceptions.HTTPError): 36 | enable_feed.enable_feed(mock_session, "feed name") 37 | 38 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 39 | @mock.patch.object(requests.requests, "Response", autospec=True) 40 | def test_happy_path(self, mock_response, mock_session): 41 | mock_session.request.return_value = mock_response 42 | type(mock_response).status_code = mock.PropertyMock(return_value=200) 43 | 44 | enable_feed.enable_feed(mock_session, "feed name") 45 | 46 | 47 | if __name__ == "__main__": 48 | unittest.main() 49 | -------------------------------------------------------------------------------- /feeds/get_feed.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2022 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """Executable and reusable sample for retrieving a feed.""" 18 | 19 | import argparse 20 | import json 21 | from typing import Mapping, Any 22 | 23 | from google.auth.transport import requests 24 | 25 | from common import chronicle_auth 26 | from common import regions 27 | 28 | CHRONICLE_API_BASE_URL = "https://backstory.googleapis.com" 29 | 30 | 31 | def get_feed(http_session: requests.AuthorizedSession, 32 | name: str) -> Mapping[str, Any]: 33 | """Retrieves a feed. 34 | 35 | Args: 36 | http_session: Authorized session for HTTP requests. 37 | name: Unique name for the feed. 38 | 39 | Returns: 40 | Array containing each line of the feed's content. 41 | 42 | Raises: 43 | requests.exceptions.HTTPError: HTTP request resulted in an error 44 | (response.status_code >= 400). 45 | """ 46 | url = f"{CHRONICLE_API_BASE_URL}/v1/feeds/{name}" 47 | 48 | response = http_session.request("GET", url) 49 | # Expected server response: 50 | # { 51 | # "name": "", 52 | # "feedDetails": { 53 | # feedType: "...", 54 | # ...settings... 55 | # }, 56 | # "feedState": "ACTIVE / INACTIVE / PENDING_ENABLEMENT", 57 | # } 58 | 59 | if response.status_code >= 400: 60 | print(response.text) 61 | response.raise_for_status() 62 | return response.json() 63 | 64 | 65 | if __name__ == "__main__": 66 | parser = argparse.ArgumentParser() 67 | chronicle_auth.add_argument_credentials_file(parser) 68 | regions.add_argument_region(parser) 69 | parser.add_argument( 70 | "-n", 71 | "--name", 72 | type=str, 73 | required=True, 74 | help="unique name for the feed") 75 | 76 | args = parser.parse_args() 77 | CHRONICLE_API_BASE_URL = regions.url(CHRONICLE_API_BASE_URL, args.region) 78 | session = chronicle_auth.initialize_http_session(args.credentials_file) 79 | print(json.dumps(get_feed(session, args.name), indent=2)) 80 | -------------------------------------------------------------------------------- /feeds/get_feed_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Unit tests for the "get_feed" module.""" 16 | 17 | import unittest 18 | from unittest import mock 19 | 20 | from google.auth.transport import requests 21 | 22 | from . import get_feed 23 | 24 | 25 | class GetFeedTest(unittest.TestCase): 26 | 27 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 28 | @mock.patch.object(requests.requests, "Response", autospec=True) 29 | def test_http_error(self, mock_response, mock_session): 30 | mock_session.request.return_value = mock_response 31 | type(mock_response).status_code = mock.PropertyMock(return_value=400) 32 | mock_response.raise_for_status.side_effect = ( 33 | requests.requests.exceptions.HTTPError()) 34 | 35 | with self.assertRaises(requests.requests.exceptions.HTTPError): 36 | get_feed.get_feed(mock_session, "feed name") 37 | 38 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 39 | @mock.patch.object(requests.requests, "Response", autospec=True) 40 | def test_happy_path(self, mock_response, mock_session): 41 | mock_session.request.return_value = mock_response 42 | type(mock_response).status_code = mock.PropertyMock(return_value=200) 43 | expected_feed = { 44 | "feedDetails": { 45 | "cortexXdrSettings": { 46 | "hostname": "api-XXXX.xdr.XX.paloaltonetworks.com", 47 | }, 48 | "feedType": "CORTEX_XDR", 49 | }, 50 | "name": "feed name", 51 | } 52 | mock_response.json.return_value = expected_feed 53 | 54 | actual_feed = get_feed.get_feed(mock_session, "feed name") 55 | self.assertEqual(actual_feed, expected_feed) 56 | 57 | 58 | if __name__ == "__main__": 59 | unittest.main() 60 | -------------------------------------------------------------------------------- /feeds/list_feeds.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2022 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """Executable and reusable sample for retrieving a list of feeds.""" 18 | 19 | import argparse 20 | import json 21 | from typing import Mapping, Any 22 | 23 | from google.auth.transport import requests 24 | 25 | from common import chronicle_auth 26 | from common import regions 27 | 28 | CHRONICLE_API_BASE_URL = "https://backstory.googleapis.com" 29 | 30 | 31 | def list_feeds(http_session: requests.AuthorizedSession) -> Mapping[str, Any]: 32 | """Retrieves all feeds for the tenant. 33 | 34 | Args: 35 | http_session: Authorized session for HTTP requests. 36 | 37 | Returns: 38 | Array containing each line of the feed's content. 39 | 40 | Raises: 41 | requests.exceptions.HTTPError: HTTP request resulted in an error 42 | (response.status_code >= 400). 43 | """ 44 | url = f"{CHRONICLE_API_BASE_URL}/v1/feeds" 45 | 46 | response = http_session.request("GET", url) 47 | # Expected server response: 48 | # { 49 | # "feeds": [ 50 | # { 51 | # "name": "feeds/19e82867-ab6d-4955-b9c8-bd4aee189439", 52 | # "details": { 53 | # "logType": "AZURE_AD_CONTEXT", 54 | # "feedSourceType": "API", 55 | # "azureAdContextSettings": {} 56 | # }, 57 | # "feedState": "INACTIVE" 58 | # }, 59 | # { 60 | # "name": "feeds/cdc096a5-93a8-4854-94d9-c05cf0c14d47", 61 | # "details": { 62 | # "logType": "PAN_PRISMA_CLOUD", 63 | # "feedSourceType": "API", 64 | # "panPrismaCloudSettings": { 65 | # "hostname": "api2.prismacloud.io" 66 | # } 67 | # }, 68 | # "feedState": "ACTIVE" 69 | # } 70 | # ] 71 | # } 72 | 73 | if response.status_code >= 400: 74 | print(response.text) 75 | response.raise_for_status() 76 | return response.json() 77 | 78 | 79 | if __name__ == "__main__": 80 | parser = argparse.ArgumentParser() 81 | chronicle_auth.add_argument_credentials_file(parser) 82 | regions.add_argument_region(parser) 83 | 84 | args = parser.parse_args() 85 | CHRONICLE_API_BASE_URL = regions.url(CHRONICLE_API_BASE_URL, args.region) 86 | session = chronicle_auth.initialize_http_session(args.credentials_file) 87 | print(json.dumps(list_feeds(session), indent=2)) 88 | -------------------------------------------------------------------------------- /feeds/list_feeds_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Unit tests for the "list_feeds" function.""" 16 | 17 | import unittest 18 | from unittest import mock 19 | 20 | from google.auth.transport import requests 21 | 22 | from . import list_feeds 23 | 24 | 25 | class ListFeedsTest(unittest.TestCase): 26 | 27 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 28 | @mock.patch.object(requests.requests, "Response", autospec=True) 29 | def test_http_error(self, mock_response, mock_session): 30 | mock_session.request.return_value = mock_response 31 | type(mock_response).status_code = mock.PropertyMock(return_value=400) 32 | mock_response.raise_for_status.side_effect = ( 33 | requests.requests.exceptions.HTTPError()) 34 | 35 | with self.assertRaises(requests.requests.exceptions.HTTPError): 36 | list_feeds.list_feeds(mock_session) 37 | 38 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 39 | @mock.patch.object(requests.requests, "Response", autospec=True) 40 | def test_happy_path(self, mock_response, mock_session): 41 | mock_session.request.return_value = mock_response 42 | type(mock_response).status_code = mock.PropertyMock(return_value=200) 43 | expected_feeds = { 44 | "feeds": [ 45 | { 46 | "name": "feeds/19e82867-ab6d-4955-b9c8-bd4aee189439", 47 | "details": { 48 | "logType": "AZURE_AD_CONTEXT", 49 | "feedSourceType": "API", 50 | "azureAdContextSettings": {} 51 | }, 52 | "feedState": "INACTIVE" 53 | }, 54 | { 55 | "name": "feeds/cdc096a5-93a8-4854-94d9-c05cf0c14d47", 56 | "details": { 57 | "logType": "PAN_PRISMA_CLOUD", 58 | "feedSourceType": "API", 59 | "panPrismaCloudSettings": { 60 | "hostname": "api2.prismacloud.io" 61 | } 62 | }, 63 | "feedState": "ACTIVE" 64 | }, 65 | ], 66 | } 67 | mock_response.json.return_value = expected_feeds 68 | 69 | actual_feeds = list_feeds.list_feeds(mock_session) 70 | self.assertEqual(actual_feeds, expected_feeds) 71 | 72 | 73 | if __name__ == "__main__": 74 | unittest.main() 75 | -------------------------------------------------------------------------------- /forwarders/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | -------------------------------------------------------------------------------- /forwarders/create_collector_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Unit tests for the "create_collector" module.""" 16 | 17 | import unittest 18 | from unittest import mock 19 | 20 | from google.auth.transport import requests 21 | 22 | from . import create_collector 23 | 24 | 25 | class CreateCollectorTest(unittest.TestCase): 26 | 27 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 28 | @mock.patch.object(requests.requests, "Response", autospec=True) 29 | def test_http_error(self, mock_response, mock_session): 30 | mock_session.request.return_value = mock_response 31 | type(mock_response).status_code = mock.PropertyMock(return_value=400) 32 | mock_response.raise_for_status.side_effect = ( 33 | requests.requests.exceptions.HTTPError()) 34 | 35 | with self.assertRaises(requests.requests.exceptions.HTTPError): 36 | create_collector.create_collector(mock_session, "forwarder name") 37 | 38 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 39 | @mock.patch.object(requests.requests, "Response", autospec=True) 40 | def test_happy_path(self, mock_response, mock_session): 41 | mock_session.request.return_value = mock_response 42 | type(mock_response).status_code = mock.PropertyMock(return_value=200) 43 | expected_collector = { 44 | "name": "forwarders/uuid/collectors/uuid", 45 | "displayName": "TestCollector1", 46 | "config": { 47 | "logType": "PAN_FIREWALL", 48 | "maxSecondsPerBatch": 10, 49 | "maxBytesPerBatch": "1048576", 50 | "syslogSettings": { 51 | "protocol": "TCP", 52 | "address": "0.0.0.0", 53 | "port": 10514, 54 | "bufferSize": "65536", 55 | "connectionTimeout": 60 56 | } 57 | }, 58 | "state": "ACTIVE" 59 | } 60 | 61 | mock_response.json.return_value = expected_collector 62 | 63 | actual_collector = create_collector.create_collector( 64 | mock_session, "forwarder name") 65 | self.assertEqual(actual_collector, expected_collector) 66 | 67 | 68 | if __name__ == "__main__": 69 | unittest.main() 70 | -------------------------------------------------------------------------------- /forwarders/create_forwarder_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Unit tests for the "create_forwarder" module.""" 16 | 17 | import unittest 18 | from unittest import mock 19 | 20 | from google.auth.transport import requests 21 | 22 | from . import create_forwarder 23 | 24 | 25 | class CreateForwarderTest(unittest.TestCase): 26 | 27 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 28 | @mock.patch.object(requests.requests, "Response", autospec=True) 29 | def test_http_error(self, mock_response, mock_session): 30 | mock_session.request.return_value = mock_response 31 | type(mock_response).status_code = mock.PropertyMock(return_value=400) 32 | mock_response.raise_for_status.side_effect = ( 33 | requests.requests.exceptions.HTTPError()) 34 | 35 | with self.assertRaises(requests.requests.exceptions.HTTPError): 36 | create_forwarder.create_forwarder(mock_session) 37 | 38 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 39 | @mock.patch.object(requests.requests, "Response", autospec=True) 40 | def test_happy_path(self, mock_response, mock_session): 41 | mock_session.request.return_value = mock_response 42 | type(mock_response).status_code = mock.PropertyMock(return_value=200) 43 | expected_forwarder = { 44 | "name": "forwarders/c37e1d99-f6ba-43da-b591-788261d32cae", 45 | "displayName": "TestForwarder", 46 | "config": { 47 | "uploadCompression": True 48 | }, 49 | "state": "ACTIVE" 50 | } 51 | 52 | mock_response.json.return_value = expected_forwarder 53 | 54 | actual_forwarder = create_forwarder.create_forwarder(mock_session) 55 | self.assertEqual(actual_forwarder, expected_forwarder) 56 | 57 | 58 | if __name__ == "__main__": 59 | unittest.main() 60 | -------------------------------------------------------------------------------- /forwarders/delete_collector.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2022 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """Executable and reusable sample for deleting a collector.""" 18 | 19 | import argparse 20 | 21 | from google.auth.transport import requests 22 | 23 | from common import chronicle_auth 24 | from common import regions 25 | 26 | CHRONICLE_API_BASE_URL = "https://backstory.googleapis.com" 27 | 28 | 29 | def delete_collector(http_session: requests.AuthorizedSession, name: str): 30 | """Delete a specific collector. 31 | 32 | Args: 33 | http_session: Authorized session for HTTP requests. 34 | name: Resource name for the Collector (collectors/{UUID}). 35 | 36 | Raises: 37 | requests.exceptions.HTTPError: HTTP request resulted in an error 38 | (response.status_code >= 400). 39 | """ 40 | url = f"{CHRONICLE_API_BASE_URL}/v2/{name}" 41 | 42 | response = http_session.request("DELETE", url) 43 | # Expected server response: 44 | # {} 45 | 46 | if response.status_code >= 400: 47 | print(response.text) 48 | response.raise_for_status() 49 | 50 | 51 | if __name__ == "__main__": 52 | parser = argparse.ArgumentParser() 53 | chronicle_auth.add_argument_credentials_file(parser) 54 | regions.add_argument_region(parser) 55 | parser.add_argument( 56 | "-n", 57 | "--name", 58 | type=str, 59 | required=True, 60 | help="resource name for the Collector (collectors/{UUID})") 61 | 62 | args = parser.parse_args() 63 | CHRONICLE_API_BASE_URL = regions.url(CHRONICLE_API_BASE_URL, args.region) 64 | session = chronicle_auth.initialize_http_session(args.credentials_file) 65 | delete_collector(session, args.name) 66 | -------------------------------------------------------------------------------- /forwarders/delete_collector_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Unit tests for the "delete_collector" module.""" 16 | 17 | import unittest 18 | from unittest import mock 19 | 20 | from google.auth.transport import requests 21 | 22 | from . import delete_collector 23 | 24 | 25 | class DeleteCollectorTest(unittest.TestCase): 26 | 27 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 28 | @mock.patch.object(requests.requests, "Response", autospec=True) 29 | def test_http_error(self, mock_response, mock_session): 30 | mock_session.request.return_value = mock_response 31 | type(mock_response).status_code = mock.PropertyMock(return_value=400) 32 | mock_response.raise_for_status.side_effect = ( 33 | requests.requests.exceptions.HTTPError()) 34 | 35 | with self.assertRaises(requests.requests.exceptions.HTTPError): 36 | delete_collector.delete_collector(mock_session, "collector name") 37 | 38 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 39 | @mock.patch.object(requests.requests, "Response", autospec=True) 40 | def test_happy_path(self, mock_response, mock_session): 41 | mock_session.request.return_value = mock_response 42 | type(mock_response).status_code = mock.PropertyMock(return_value=200) 43 | 44 | delete_collector.delete_collector(mock_session, "collector name") 45 | 46 | 47 | if __name__ == "__main__": 48 | unittest.main() 49 | -------------------------------------------------------------------------------- /forwarders/delete_forwarder.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2022 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """Executable and reusable sample for deleting a forwarder.""" 18 | 19 | import argparse 20 | 21 | from google.auth.transport import requests 22 | 23 | from common import chronicle_auth 24 | from common import regions 25 | 26 | CHRONICLE_API_BASE_URL = "https://backstory.googleapis.com" 27 | 28 | 29 | def delete_forwarder(http_session: requests.AuthorizedSession, name: str): 30 | """Delete a specific forwarder. 31 | 32 | Args: 33 | http_session: Authorized session for HTTP requests. 34 | name: Resource name for the Forwarder (forwarders/{UUID}). 35 | 36 | Raises: 37 | requests.exceptions.HTTPError: HTTP request resulted in an error 38 | (response.status_code >= 400). 39 | """ 40 | url = f"{CHRONICLE_API_BASE_URL}/v2/{name}" 41 | 42 | response = http_session.request("DELETE", url) 43 | # Expected server response: 44 | # {} 45 | 46 | if response.status_code >= 400: 47 | print(response.text) 48 | response.raise_for_status() 49 | 50 | 51 | if __name__ == "__main__": 52 | parser = argparse.ArgumentParser() 53 | chronicle_auth.add_argument_credentials_file(parser) 54 | regions.add_argument_region(parser) 55 | parser.add_argument( 56 | "-n", 57 | "--name", 58 | type=str, 59 | required=True, 60 | help="resource name for the Forwarder (forwarders/{UUID})") 61 | 62 | args = parser.parse_args() 63 | CHRONICLE_API_BASE_URL = regions.url(CHRONICLE_API_BASE_URL, args.region) 64 | session = chronicle_auth.initialize_http_session(args.credentials_file) 65 | delete_forwarder(session, args.name) 66 | -------------------------------------------------------------------------------- /forwarders/delete_forwarder_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Unit tests for the "delete_forwarder" module.""" 16 | 17 | import unittest 18 | from unittest import mock 19 | 20 | from google.auth.transport import requests 21 | 22 | from . import delete_forwarder 23 | 24 | 25 | class DeleteForwarderTest(unittest.TestCase): 26 | 27 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 28 | @mock.patch.object(requests.requests, "Response", autospec=True) 29 | def test_http_error(self, mock_response, mock_session): 30 | mock_session.request.return_value = mock_response 31 | type(mock_response).status_code = mock.PropertyMock(return_value=400) 32 | mock_response.raise_for_status.side_effect = ( 33 | requests.requests.exceptions.HTTPError()) 34 | 35 | with self.assertRaises(requests.requests.exceptions.HTTPError): 36 | delete_forwarder.delete_forwarder(mock_session, "forwarder name") 37 | 38 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 39 | @mock.patch.object(requests.requests, "Response", autospec=True) 40 | def test_happy_path(self, mock_response, mock_session): 41 | mock_session.request.return_value = mock_response 42 | type(mock_response).status_code = mock.PropertyMock(return_value=200) 43 | 44 | delete_forwarder.delete_forwarder(mock_session, "forwarder name") 45 | 46 | 47 | if __name__ == "__main__": 48 | unittest.main() 49 | -------------------------------------------------------------------------------- /forwarders/generate_files_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Unit tests for the "generate_files" module.""" 16 | 17 | import unittest 18 | from unittest import mock 19 | 20 | from google.auth.transport import requests 21 | 22 | from . import generate_files 23 | 24 | 25 | class GenerateFilesTest(unittest.TestCase): 26 | 27 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 28 | @mock.patch.object(requests.requests, "Response", autospec=True) 29 | def test_http_error(self, mock_response, mock_session): 30 | mock_session.request.return_value = mock_response 31 | type(mock_response).status_code = mock.PropertyMock(return_value=400) 32 | mock_response.raise_for_status.side_effect = ( 33 | requests.requests.exceptions.HTTPError()) 34 | 35 | with self.assertRaises(requests.requests.exceptions.HTTPError): 36 | generate_files.generate_files(mock_session, "forwarder name") 37 | 38 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 39 | @mock.patch.object(requests.requests, "Response", autospec=True) 40 | def test_happy_path(self, mock_response, mock_session): 41 | mock_session.request.return_value = mock_response 42 | type(mock_response).status_code = mock.PropertyMock(return_value=200) 43 | expected_files = { 44 | "config": "CONFIG FILE CONTENTS", 45 | "auth": "AUTH FILE CONTENTS", 46 | } 47 | 48 | mock_response.json.return_value = expected_files 49 | 50 | actual_files = generate_files.generate_files(mock_session, "forwarder name") 51 | self.assertEqual(actual_files, expected_files) 52 | 53 | 54 | if __name__ == "__main__": 55 | unittest.main() 56 | -------------------------------------------------------------------------------- /forwarders/get_collector.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2022 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """Executable and reusable sample for retrieving a collector.""" 18 | 19 | import argparse 20 | import json 21 | from typing import Mapping, Any 22 | 23 | from google.auth.transport import requests 24 | 25 | from common import chronicle_auth 26 | from common import regions 27 | 28 | CHRONICLE_API_BASE_URL = "https://backstory.googleapis.com" 29 | 30 | 31 | def get_collector(http_session: requests.AuthorizedSession, 32 | name: str) -> Mapping[str, Any]: 33 | """Retrieves a collector. 34 | 35 | Args: 36 | http_session: Authorized session for HTTP requests. 37 | name: Resource name for the Collector (collectors/{UUID}). 38 | 39 | Returns: 40 | Array containing each line of the collector's content. 41 | 42 | Raises: 43 | requests.exceptions.HTTPError: HTTP request resulted in an error 44 | (response.status_code >= 400). 45 | """ 46 | url = f"{CHRONICLE_API_BASE_URL}/v2/{name}" 47 | 48 | response = http_session.request("GET", url) 49 | # Example server response: 50 | # { 51 | # "name": "forwarders/{UUID}/collectors/{UUID}", 52 | # "displayName": "SyslogCollector1", 53 | # "config": { 54 | # "logType": "PAN_FIREWALL", 55 | # "maxSecondsPerBatch": 10, 56 | # "maxBytesPerBatch": "1048576", 57 | # "syslogSettings": { 58 | # "protocol": "TCP", 59 | # "address": "0.0.0.0", 60 | # "port": 10514, 61 | # "bufferSize": "65536", 62 | # "connectionTimeout": 60 63 | # } 64 | # }, 65 | # "state": "ACTIVE" 66 | # } 67 | 68 | if response.status_code >= 400: 69 | print(response.text) 70 | response.raise_for_status() 71 | return response.json() 72 | 73 | 74 | if __name__ == "__main__": 75 | parser = argparse.ArgumentParser() 76 | chronicle_auth.add_argument_credentials_file(parser) 77 | regions.add_argument_region(parser) 78 | parser.add_argument( 79 | "-n", 80 | "--name", 81 | type=str, 82 | required=True, 83 | help="resource name for the Collector (collectors/{UUID})") 84 | 85 | args = parser.parse_args() 86 | CHRONICLE_API_BASE_URL = regions.url(CHRONICLE_API_BASE_URL, args.region) 87 | session = chronicle_auth.initialize_http_session(args.credentials_file) 88 | print(json.dumps(get_collector(session, args.name), indent=2)) 89 | -------------------------------------------------------------------------------- /forwarders/get_collector_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Unit tests for the "get_collector" module.""" 16 | 17 | import unittest 18 | from unittest import mock 19 | 20 | from google.auth.transport import requests 21 | 22 | from . import get_collector 23 | 24 | 25 | class GetCollectorTest(unittest.TestCase): 26 | 27 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 28 | @mock.patch.object(requests.requests, "Response", autospec=True) 29 | def test_http_error(self, mock_response, mock_session): 30 | mock_session.request.return_value = mock_response 31 | type(mock_response).status_code = mock.PropertyMock(return_value=400) 32 | mock_response.raise_for_status.side_effect = ( 33 | requests.requests.exceptions.HTTPError()) 34 | 35 | with self.assertRaises(requests.requests.exceptions.HTTPError): 36 | get_collector.get_collector(mock_session, "collector name") 37 | 38 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 39 | @mock.patch.object(requests.requests, "Response", autospec=True) 40 | def test_happy_path(self, mock_response, mock_session): 41 | mock_session.request.return_value = mock_response 42 | type(mock_response).status_code = mock.PropertyMock(return_value=200) 43 | expected_collector = { 44 | "name": "forwarders/{UUID}/collectors/{UUID}", 45 | "displayName": "SyslogCollector1", 46 | "config": { 47 | "logType": "PAN_FIREWALL", 48 | "maxSecondsPerBatch": 10, 49 | "maxBytesPerBatch": "1048576", 50 | "syslogSettings": { 51 | "protocol": "TCP", 52 | "address": "0.0.0.0", 53 | "port": 10514, 54 | "bufferSize": "65536", 55 | "connectionTimeout": 60 56 | } 57 | }, 58 | "state": "ACTIVE" 59 | } 60 | 61 | mock_response.json.return_value = expected_collector 62 | 63 | actual_collector = get_collector.get_collector(mock_session, 64 | "collector name") 65 | self.assertEqual(actual_collector, expected_collector) 66 | 67 | 68 | if __name__ == "__main__": 69 | unittest.main() 70 | -------------------------------------------------------------------------------- /forwarders/get_forwarder.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2022 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """Executable and reusable sample for retrieving a forwarder.""" 18 | 19 | import argparse 20 | import json 21 | from typing import Mapping, Any 22 | 23 | from google.auth.transport import requests 24 | 25 | from common import chronicle_auth 26 | from common import regions 27 | 28 | CHRONICLE_API_BASE_URL = "https://backstory.googleapis.com" 29 | 30 | 31 | def get_forwarder(http_session: requests.AuthorizedSession, 32 | name: str) -> Mapping[str, Any]: 33 | """Retrieves a forwarder. 34 | 35 | Args: 36 | http_session: Authorized session for HTTP requests. 37 | name: Resource name for the Forwarder (forwarders/{UUID}). 38 | 39 | Returns: 40 | Array containing each line of the forwarder's content. 41 | 42 | Raises: 43 | requests.exceptions.HTTPError: HTTP request resulted in an error 44 | (response.status_code >= 400). 45 | """ 46 | url = f"{CHRONICLE_API_BASE_URL}/v2/{name}" 47 | 48 | response = http_session.request("GET", url) 49 | # Expected server response: 50 | # { 51 | # "name": "forwarders/{forwarderUUID}", 52 | # "displayName": "TestForwarder", 53 | # "config": { 54 | # "uploadCompression": true 55 | # }, 56 | # "state": "ACTIVE" 57 | # } 58 | 59 | if response.status_code >= 400: 60 | print(response.text) 61 | response.raise_for_status() 62 | return response.json() 63 | 64 | 65 | if __name__ == "__main__": 66 | parser = argparse.ArgumentParser() 67 | chronicle_auth.add_argument_credentials_file(parser) 68 | regions.add_argument_region(parser) 69 | parser.add_argument( 70 | "-n", 71 | "--name", 72 | type=str, 73 | required=True, 74 | help="resource name for the Forwarder (forwarders/{UUID})") 75 | 76 | args = parser.parse_args() 77 | CHRONICLE_API_BASE_URL = regions.url(CHRONICLE_API_BASE_URL, args.region) 78 | session = chronicle_auth.initialize_http_session(args.credentials_file) 79 | print(json.dumps(get_forwarder(session, args.name), indent=2)) 80 | -------------------------------------------------------------------------------- /forwarders/get_forwarder_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Unit tests for the "get_forwarder" module.""" 16 | 17 | import unittest 18 | from unittest import mock 19 | 20 | from google.auth.transport import requests 21 | 22 | from . import get_forwarder 23 | 24 | 25 | class GetForwarderTest(unittest.TestCase): 26 | 27 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 28 | @mock.patch.object(requests.requests, "Response", autospec=True) 29 | def test_http_error(self, mock_response, mock_session): 30 | mock_session.request.return_value = mock_response 31 | type(mock_response).status_code = mock.PropertyMock(return_value=400) 32 | mock_response.raise_for_status.side_effect = ( 33 | requests.requests.exceptions.HTTPError()) 34 | 35 | with self.assertRaises(requests.requests.exceptions.HTTPError): 36 | get_forwarder.get_forwarder(mock_session, "forwarder name") 37 | 38 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 39 | @mock.patch.object(requests.requests, "Response", autospec=True) 40 | def test_happy_path(self, mock_response, mock_session): 41 | mock_session.request.return_value = mock_response 42 | type(mock_response).status_code = mock.PropertyMock(return_value=200) 43 | expected_forwarder = { 44 | "name": "forwarders/928b3c1e-1430-4511-892d-2202206b4d8c", 45 | "displayName": "TestForwarder", 46 | "config": { 47 | "uploadCompression": True 48 | }, 49 | "state": "ACTIVE" 50 | } 51 | 52 | mock_response.json.return_value = expected_forwarder 53 | 54 | actual_forwarder = get_forwarder.get_forwarder(mock_session, 55 | "forwarder name") 56 | self.assertEqual(actual_forwarder, expected_forwarder) 57 | 58 | 59 | if __name__ == "__main__": 60 | unittest.main() 61 | -------------------------------------------------------------------------------- /forwarders/list_forwarders.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2022 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """Executable and reusable sample for retrieving a list of forwarders.""" 18 | 19 | import argparse 20 | import json 21 | from typing import Mapping, Any 22 | 23 | from google.auth.transport import requests 24 | 25 | from common import chronicle_auth 26 | from common import regions 27 | 28 | CHRONICLE_API_BASE_URL = "https://backstory.googleapis.com" 29 | 30 | 31 | def list_forwarders( 32 | http_session: requests.AuthorizedSession) -> Mapping[str, Any]: 33 | """Retrieves all forwarders for the tenant. 34 | 35 | Args: 36 | http_session: Authorized session for HTTP requests. 37 | 38 | Returns: 39 | Array containing each forwarder associated with the instance. 40 | 41 | Raises: 42 | requests.exceptions.HTTPError: HTTP request resulted in an error 43 | (response.status_code >= 400). 44 | """ 45 | url = f"{CHRONICLE_API_BASE_URL}/v2/forwarders" 46 | 47 | response = http_session.request("GET", url) 48 | # Expected server response: 49 | # { 50 | # "forwarders": [ 51 | # { 52 | # "name": "forwarders/{forwarderUUID}", 53 | # "displayName": "TestForwarder1", 54 | # "config": { 55 | # "uploadCompression": true, 56 | # }, 57 | # "state": "ACTIVE" 58 | # } 59 | # ] 60 | # } 61 | 62 | if response.status_code >= 400: 63 | print(response.text) 64 | response.raise_for_status() 65 | return response.json() 66 | 67 | 68 | if __name__ == "__main__": 69 | parser = argparse.ArgumentParser() 70 | chronicle_auth.add_argument_credentials_file(parser) 71 | regions.add_argument_region(parser) 72 | 73 | args = parser.parse_args() 74 | CHRONICLE_API_BASE_URL = regions.url(CHRONICLE_API_BASE_URL, args.region) 75 | session = chronicle_auth.initialize_http_session(args.credentials_file) 76 | print(json.dumps(list_forwarders(session), indent=2)) 77 | -------------------------------------------------------------------------------- /forwarders/list_forwarders_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Unit tests for the "list_forwarders" function.""" 16 | 17 | import unittest 18 | from unittest import mock 19 | 20 | from google.auth.transport import requests 21 | 22 | from . import list_forwarders 23 | 24 | 25 | class ListForwardersTest(unittest.TestCase): 26 | 27 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 28 | @mock.patch.object(requests.requests, "Response", autospec=True) 29 | def test_http_error(self, mock_response, mock_session): 30 | mock_session.request.return_value = mock_response 31 | type(mock_response).status_code = mock.PropertyMock(return_value=400) 32 | mock_response.raise_for_status.side_effect = ( 33 | requests.requests.exceptions.HTTPError()) 34 | 35 | with self.assertRaises(requests.requests.exceptions.HTTPError): 36 | list_forwarders.list_forwarders(mock_session) 37 | 38 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 39 | @mock.patch.object(requests.requests, "Response", autospec=True) 40 | def test_happy_path(self, mock_response, mock_session): 41 | mock_session.request.return_value = mock_response 42 | type(mock_response).status_code = mock.PropertyMock(return_value=200) 43 | expected_forwarders = { 44 | "forwarders": [{ 45 | "name": "forwarders/af2235e5-08d0-45e6-aaf8-98dbfa83a178", 46 | "displayName": "TestForwarder1", 47 | "config": { 48 | "uploadCompression": True, 49 | }, 50 | "state": "ACTIVE" 51 | }] 52 | } 53 | 54 | mock_response.json.return_value = expected_forwarders 55 | 56 | actual_forwarders = list_forwarders.list_forwarders(mock_session) 57 | self.assertEqual(actual_forwarders, expected_forwarders) 58 | 59 | 60 | if __name__ == "__main__": 61 | unittest.main() 62 | -------------------------------------------------------------------------------- /forwarders/update_collector_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Unit tests for the "update_collector" module.""" 16 | 17 | import unittest 18 | from unittest import mock 19 | 20 | from google.auth.transport import requests 21 | 22 | from . import update_collector 23 | 24 | 25 | class UpdateCollectorTest(unittest.TestCase): 26 | 27 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 28 | @mock.patch.object(requests.requests, "Response", autospec=True) 29 | def test_http_error(self, mock_response, mock_session): 30 | mock_session.request.return_value = mock_response 31 | type(mock_response).status_code = mock.PropertyMock(return_value=400) 32 | mock_response.raise_for_status.side_effect = ( 33 | requests.requests.exceptions.HTTPError()) 34 | 35 | with self.assertRaises(requests.requests.exceptions.HTTPError): 36 | update_collector.update_collector(mock_session, "collector name") 37 | 38 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 39 | @mock.patch.object(requests.requests, "Response", autospec=True) 40 | def test_happy_path(self, mock_response, mock_session): 41 | mock_session.request.return_value = mock_response 42 | type(mock_response).status_code = mock.PropertyMock(return_value=200) 43 | expected_collector = { 44 | "name": "forwarders/{forwarderUUID}/collectors/{collectorUUID}", 45 | "displayName": "UpdatedCollector", 46 | "config": { 47 | "logType": "WINDOWS_DHCP", 48 | "maxSecondsPerBatch": 10, 49 | "maxBytesPerBatch": "1048576", 50 | "fileSettings": { 51 | "filePath": "/new/path/to/file.txt" 52 | } 53 | }, 54 | "state": "ACTIVE" 55 | } 56 | 57 | mock_response.json.return_value = expected_collector 58 | 59 | actual_collector = update_collector.update_collector( 60 | mock_session, "collector name") 61 | self.assertEqual(actual_collector, expected_collector) 62 | 63 | 64 | if __name__ == "__main__": 65 | unittest.main() 66 | -------------------------------------------------------------------------------- /forwarders/update_forwarder_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Unit tests for the "update_forwarder" module.""" 16 | 17 | import unittest 18 | from unittest import mock 19 | 20 | from google.auth.transport import requests 21 | 22 | from . import update_forwarder 23 | 24 | 25 | class UpdateForwarderTest(unittest.TestCase): 26 | 27 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 28 | @mock.patch.object(requests.requests, "Response", autospec=True) 29 | def test_http_error(self, mock_response, mock_session): 30 | mock_session.request.return_value = mock_response 31 | type(mock_response).status_code = mock.PropertyMock(return_value=400) 32 | mock_response.raise_for_status.side_effect = ( 33 | requests.requests.exceptions.HTTPError()) 34 | 35 | with self.assertRaises(requests.requests.exceptions.HTTPError): 36 | update_forwarder.update_forwarder(mock_session, "forwarder name") 37 | 38 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 39 | @mock.patch.object(requests.requests, "Response", autospec=True) 40 | def test_happy_path(self, mock_response, mock_session): 41 | mock_session.request.return_value = mock_response 42 | type(mock_response).status_code = mock.PropertyMock(return_value=200) 43 | expected_forwarder = { 44 | "name": "forwarders/c37e1d99-f6ba-43da-b591-788261d32cae", 45 | "displayName": "UpdatedForwarder", 46 | "config": { 47 | "uploadCompression": True 48 | }, 49 | "state": "ACTIVE" 50 | } 51 | 52 | mock_response.json.return_value = expected_forwarder 53 | 54 | actual_forwarder = update_forwarder.update_forwarder( 55 | mock_session, "forwarder name") 56 | self.assertEqual(actual_forwarder, expected_forwarder) 57 | 58 | 59 | if __name__ == "__main__": 60 | unittest.main() 61 | -------------------------------------------------------------------------------- /ingestion/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | -------------------------------------------------------------------------------- /ingestion/create_unstructured_log_entries_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Unit tests for the create_unstructured_log_entries binary.""" 16 | 17 | import unittest 18 | from unittest import mock 19 | 20 | from google.auth.transport import requests 21 | 22 | from . import create_unstructured_log_entries 23 | 24 | 25 | class CreateUnstructuredLogEntryTest(unittest.TestCase): 26 | 27 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 28 | @mock.patch.object(requests.requests, "Response", autospec=True) 29 | def test_http_error(self, mock_response, mock_session): 30 | mock_session.request.return_value = mock_response 31 | type(mock_response).status_code = mock.PropertyMock(return_value=400) 32 | mock_response.raise_for_status.side_effect = ( 33 | requests.requests.exceptions.HTTPError()) 34 | 35 | with self.assertRaises(requests.requests.exceptions.HTTPError): 36 | create_unstructured_log_entries.create_logs( 37 | mock_session, "LOG_TYPE", "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", 38 | "log1\tlog1\r\nlog2\tlog2") 39 | 40 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 41 | @mock.patch.object(requests.requests, "Response", autospec=True) 42 | def test_happy_path(self, mock_response, mock_session): 43 | mock_session.request.return_value = mock_response 44 | type(mock_response).status_code = mock.PropertyMock(return_value=200) 45 | 46 | create_unstructured_log_entries.create_logs( 47 | mock_session, "LOG_TYPE", "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", 48 | "log1\tlog1\r\nlog2\tlog2") 49 | 50 | 51 | if __name__ == "__main__": 52 | unittest.main() 53 | -------------------------------------------------------------------------------- /ingestion/example_input/sample_entities.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "metadata": 4 | { 5 | "collected_timestamp": "2021-07-14T15:30:18.142265Z", 6 | "entity_type": "USER", 7 | "vendor_name": "ABC", 8 | "product_name": "product1" 9 | }, 10 | "entity": 11 | { 12 | "user": { 13 | "userid": "employee", 14 | "product_object_id": "doejohn" 15 | } 16 | } 17 | }, 18 | { 19 | "metadata": 20 | { 21 | "collected_timestamp": "2021-07-15T15:30:18.142265Z", 22 | "entity_type": "USER", 23 | "vendor_name": "ABC", 24 | "product_name": "product1" 25 | }, 26 | "entity": 27 | { 28 | "user": { 29 | "userid": "employee1", 30 | "product_object_id": "doejohn1" 31 | } 32 | } 33 | } 34 | ] 35 | -------------------------------------------------------------------------------- /ingestion/example_input/sample_udm_events.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "metadata": 4 | { 5 | "eventTimestamp": "2021-07-14T15:30:18.142265Z", 6 | "eventType": "USER_LOGIN" 7 | }, 8 | "additional": 9 | { 10 | "id": "9876-54321" 11 | }, 12 | "principal": 13 | { 14 | "hostname": "userhost", 15 | "ip": ["10.0.0.75"], 16 | "mac": ["28:80:23:0b:c9:b2"] 17 | }, 18 | "target": 19 | { 20 | "hostname": "systemhost", 21 | "user": 22 | { 23 | "userid": "employee" 24 | }, 25 | "ip": ["10.0.0.77"], 26 | "mac": ["28:80:23:0b:c9:b3"] 27 | }, 28 | "securityResult": 29 | [ 30 | { 31 | "action": ["ALLOW"] 32 | } 33 | ], 34 | "extensions": 35 | { 36 | "auth": 37 | { 38 | "type": "MACHINE", 39 | "mechanism": ["USERNAME_PASSWORD"] 40 | } 41 | } 42 | } 43 | ] 44 | -------------------------------------------------------------------------------- /ingestion/example_input/sample_unstructured_log_entries.txt: -------------------------------------------------------------------------------- 1 | 2020-07-13 09:03:06.000000001 client 192.168.109.254#12345: query: google.com IN A + (1.2.3.4) 2 | 2020-07-13 09:03:07.000000001 client 192.168.109.252#12345: query: bbc.com IN A + (5.6.7.8) 3 | -------------------------------------------------------------------------------- /ingestion/list_log_types.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2021 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """Executable and reusable sample for listing supported log types. 18 | 19 | WARNING: This script makes use of the Ingestion API V2. V2 is currently only in 20 | preview for certain Chronicle customers. Please reach out to your Chronicle 21 | representative if you wish to use this API. 22 | 23 | API reference: 24 | https://cloud.google.com/chronicle/docs/reference/ingestion-api#logtypes 25 | """ 26 | 27 | import argparse 28 | import json 29 | from typing import Any, Mapping 30 | 31 | from google.auth.transport import requests 32 | 33 | from common import chronicle_auth 34 | from common import regions 35 | 36 | INGESTION_API_BASE_URL = "https://malachiteingestion-pa.googleapis.com" 37 | AUTHORIZATION_SCOPES = ["https://www.googleapis.com/auth/malachite-ingestion"] 38 | 39 | 40 | def list_log_types( 41 | http_session: requests.AuthorizedSession) -> Mapping[str, Any]: 42 | """Retrieves a list of log types suitable for Chronicle ingestion. 43 | 44 | Args: 45 | http_session: Authorized session for HTTP requests. 46 | 47 | Returns: 48 | The log types suitable for Chronicle ingestion. 49 | 50 | Raises: 51 | requests.exceptions.HTTPError: HTTP request resulted in an error 52 | (response.status_code >= 400). 53 | """ 54 | url = f"{INGESTION_API_BASE_URL}/v2/logtypes" 55 | 56 | response = http_session.request("GET", url) 57 | response.raise_for_status() 58 | return response.json() 59 | 60 | 61 | if __name__ == "__main__": 62 | parser = argparse.ArgumentParser() 63 | chronicle_auth.add_argument_credentials_file(parser) 64 | regions.add_argument_region(parser) 65 | 66 | args = parser.parse_args() 67 | INGESTION_API_BASE_URL = regions.url(INGESTION_API_BASE_URL, args.region) 68 | session = chronicle_auth.initialize_http_session(args.credentials_file, 69 | scopes=AUTHORIZATION_SCOPES) 70 | print(json.dumps(list_log_types(session), indent=2)) 71 | -------------------------------------------------------------------------------- /ingestion/list_log_types_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Unit tests for the "list_log_types" function.""" 16 | 17 | import unittest 18 | from unittest import mock 19 | 20 | from google.auth.transport import requests 21 | 22 | from . import list_log_types 23 | 24 | 25 | class GetFeedTest(unittest.TestCase): 26 | 27 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 28 | @mock.patch.object(requests.requests, "Response", autospec=True) 29 | def test_http_error(self, mock_response, mock_session): 30 | mock_session.request.return_value = mock_response 31 | type(mock_response).status_code = mock.PropertyMock(return_value=400) 32 | mock_response.raise_for_status.side_effect = ( 33 | requests.requests.exceptions.HTTPError()) 34 | 35 | with self.assertRaises(requests.requests.exceptions.HTTPError): 36 | list_log_types.list_log_types(mock_session) 37 | 38 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 39 | @mock.patch.object(requests.requests, "Response", autospec=True) 40 | def test_happy_path(self, mock_response, mock_session): 41 | mock_session.request.return_value = mock_response 42 | type(mock_response).status_code = mock.PropertyMock(return_value=200) 43 | expected_log_types = { 44 | "logTypes": [ 45 | { 46 | "logType": "FOO_LOGS", 47 | "description": "Foo Logs", 48 | }, 49 | { 50 | "logType": "BAR_LOGS", 51 | "description": "Bar Logs", 52 | }, 53 | ], 54 | } 55 | mock_response.json.return_value = expected_log_types 56 | 57 | actual_log_types = list_log_types.list_log_types(mock_session) 58 | self.assertEqual(actual_log_types, expected_log_types) 59 | 60 | 61 | if __name__ == "__main__": 62 | unittest.main() 63 | -------------------------------------------------------------------------------- /lists/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | -------------------------------------------------------------------------------- /lists/create_list_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Unit tests for the "create_list" module.""" 16 | 17 | import unittest 18 | from unittest import mock 19 | 20 | from google.auth.transport import requests 21 | 22 | from . import create_list 23 | 24 | 25 | class CreateListTest(unittest.TestCase): 26 | 27 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 28 | @mock.patch.object(requests.requests, "Response", autospec=True) 29 | def test_http_error(self, mock_response, mock_session): 30 | mock_session.request.return_value = mock_response 31 | type(mock_response).status_code = mock.PropertyMock(return_value=400) 32 | mock_response.raise_for_status.side_effect = ( 33 | requests.requests.exceptions.HTTPError()) 34 | 35 | with self.assertRaises(requests.requests.exceptions.HTTPError): 36 | create_list.create_list(mock_session, "name", "description", 37 | ["content line"], "CONTENT_TYPE_DEFAULT_STRING") 38 | 39 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 40 | @mock.patch.object(requests.requests, "Response", autospec=True) 41 | def test_happy_path(self, mock_response, mock_session): 42 | mock_session.request.return_value = mock_response 43 | type(mock_response).status_code = mock.PropertyMock(return_value=200) 44 | expected_create_time = "2021-01-01T00:00:00Z" 45 | expected_list = { 46 | "name": "name", 47 | "description": "description", 48 | "lines": ["content line 1", "content line 2"], 49 | "contentType": "CONTENT_TYPE_DEFAULT_STRING", 50 | "createTime": expected_create_time 51 | } 52 | mock_response.json.return_value = expected_list 53 | 54 | actual_create_time = create_list.create_list( 55 | mock_session, "name", "description", 56 | ["content line 1", "content line 2"], 57 | "CONTENT_TYPE_DEFAULT_STRING",) 58 | self.assertEqual(actual_create_time, expected_create_time) 59 | 60 | 61 | if __name__ == "__main__": 62 | unittest.main() 63 | -------------------------------------------------------------------------------- /lists/example_input/coldriver_sha256.txt: -------------------------------------------------------------------------------- 1 | 0f6b9d2ada67cebc8c0f03786c442c61c05cef5b92641ec4c1bdd8f5baeb2ee1 2 | A949ec428116489f5e77cefc67fea475017e0f50d2289e17c3eb053072adcf24 3 | C97acea1a6ef59d58a498f1e1f0e0648d6979c4325de3ee726038df1fc2e831d 4 | Ac270310b5410e7430fe7e36a079525cd8724b002b38e13a6ee6e09b326f4847 5 | 84523ddad722e205e2d52eedfb682026928b63f919a7bf1ce6f1ad4180d0f507 6 | 37c52481711631a5c73a6341bd8bea302ad57f02199db7624b580058547fb5a9 -------------------------------------------------------------------------------- /lists/example_input/foo.txt: -------------------------------------------------------------------------------- 1 | foo 2 | bar 3 | baz 4 | foo 5 | bar 6 | foo -------------------------------------------------------------------------------- /lists/get_list_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Unit tests for the "get_list" module.""" 16 | 17 | import unittest 18 | from unittest import mock 19 | 20 | from google.auth.transport import requests 21 | 22 | from . import get_list 23 | 24 | 25 | class GetListTest(unittest.TestCase): 26 | 27 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 28 | @mock.patch.object(requests.requests, "Response", autospec=True) 29 | def test_http_error(self, mock_response, mock_session): 30 | mock_session.request.return_value = mock_response 31 | type(mock_response).status_code = mock.PropertyMock(return_value=400) 32 | mock_response.raise_for_status.side_effect = ( 33 | requests.requests.exceptions.HTTPError()) 34 | 35 | with self.assertRaises(requests.requests.exceptions.HTTPError): 36 | get_list.get_list(mock_session, "name") 37 | 38 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 39 | @mock.patch.object(requests.requests, "Response", autospec=True) 40 | def test_happy_path(self, mock_response, mock_session): 41 | mock_session.request.return_value = mock_response 42 | type(mock_response).status_code = mock.PropertyMock(return_value=200) 43 | expected_list_lines = ["content line 1", "content line 2"] 44 | expected_list = { 45 | "name": "name", 46 | "description": "description", 47 | "lines": expected_list_lines, 48 | "createTime": "2021-01-01T00:00:00Z" 49 | } 50 | mock_response.json.return_value = expected_list 51 | 52 | actual_list_lines = get_list.get_list(mock_session, "name") 53 | self.assertEqual(actual_list_lines, expected_list_lines) 54 | 55 | 56 | if __name__ == "__main__": 57 | unittest.main() 58 | -------------------------------------------------------------------------------- /lists/list_lists_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Unit tests for the "list_lists" module.""" 16 | 17 | import unittest 18 | from unittest import mock 19 | 20 | from google.auth.transport import requests 21 | 22 | from . import list_lists 23 | 24 | 25 | class ListListsTest(unittest.TestCase): 26 | 27 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 28 | @mock.patch.object(requests.requests, "Response", autospec=True) 29 | def test_http_error(self, mock_response, mock_session): 30 | mock_session.request.return_value = mock_response 31 | type(mock_response).status_code = mock.PropertyMock(return_value=400) 32 | mock_response.raise_for_status.side_effect = ( 33 | requests.requests.exceptions.HTTPError()) 34 | 35 | with self.assertRaises(requests.requests.exceptions.HTTPError): 36 | list_lists.list_lists(mock_session) 37 | 38 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 39 | @mock.patch.object(requests.requests, "Response", autospec=True) 40 | def test_happy_path(self, mock_response, mock_session): 41 | mock_session.request.return_value = mock_response 42 | type(mock_response).status_code = mock.PropertyMock(return_value=200) 43 | expected_list = { 44 | "name": "name", 45 | "description": "description", 46 | "createTime": "2021-01-01T00:00:00Z" 47 | } 48 | expected_page_token = "page token here" 49 | mock_response.json.return_value = { 50 | "lists": [expected_list], 51 | "nextPageToken": expected_page_token, 52 | } 53 | 54 | lists, next_page_token = list_lists.list_lists(mock_session) 55 | self.assertEqual(len(lists), 1) 56 | self.assertEqual(lists[0], expected_list) 57 | self.assertEqual(next_page_token, expected_page_token) 58 | 59 | 60 | if __name__ == "__main__": 61 | unittest.main() 62 | -------------------------------------------------------------------------------- /lists/update_list_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Unit tests for the "update_list" module.""" 16 | 17 | import unittest 18 | from unittest import mock 19 | 20 | from google.auth.transport import requests 21 | 22 | from . import update_list 23 | 24 | 25 | class UpdateListTest(unittest.TestCase): 26 | 27 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 28 | @mock.patch.object(requests.requests, "Response", autospec=True) 29 | def test_http_error(self, mock_response, mock_session): 30 | mock_session.request.return_value = mock_response 31 | type(mock_response).status_code = mock.PropertyMock(return_value=400) 32 | mock_response.raise_for_status.side_effect = ( 33 | requests.requests.exceptions.HTTPError()) 34 | 35 | with self.assertRaises(requests.requests.exceptions.HTTPError): 36 | update_list.update_list(mock_session, "name", "description", 37 | ["content line"], "CONTENT_TYPE_DEFAULT_STRING") 38 | 39 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 40 | @mock.patch.object(requests.requests, "Response", autospec=True) 41 | def test_happy_path(self, mock_response, mock_session): 42 | mock_session.request.return_value = mock_response 43 | type(mock_response).status_code = mock.PropertyMock(return_value=200) 44 | expected_time = "2021-01-01T00:00:00Z" 45 | expected_list = { 46 | "name": "name", 47 | "description": "description", 48 | "lines": ["content line 1", "content line 2"], 49 | "createTime": expected_time, 50 | "contentType": "CONTENT_TYPE_DEFAULT_STRING", 51 | } 52 | mock_response.json.return_value = expected_list 53 | 54 | actual_time = update_list.update_list( 55 | mock_session, "name", "description", 56 | ["content line 1", "content line 2"], 57 | "CONTENT_TYPE_DEFAULT_STRING", 58 | ) 59 | self.assertEqual(actual_time, expected_time) 60 | 61 | 62 | if __name__ == "__main__": 63 | unittest.main() 64 | -------------------------------------------------------------------------------- /lists/verify_list_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Unit tests for the "verify_list" module.""" 16 | 17 | import unittest 18 | from unittest import mock 19 | 20 | from google.auth.transport import requests 21 | 22 | from . import verify_list 23 | 24 | 25 | class verifyListTest(unittest.TestCase): 26 | 27 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 28 | @mock.patch.object(requests.requests, "Response", autospec=True) 29 | def test_http_error(self, mock_response, mock_session): 30 | mock_session.request.return_value = mock_response 31 | type(mock_response).status_code = mock.PropertyMock(return_value=400) 32 | mock_response.raise_for_status.side_effect = ( 33 | requests.requests.exceptions.HTTPError()) 34 | 35 | with self.assertRaises(requests.requests.exceptions.HTTPError): 36 | verify_list.verify_list(mock_session, ["content line"], 37 | "CONTENT_TYPE_DEFAULT_STRING") 38 | 39 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 40 | @mock.patch.object(requests.requests, "Response", autospec=True) 41 | def test_happy_path(self, mock_response, mock_session): 42 | mock_session.request.return_value = mock_response 43 | type(mock_response).status_code = mock.PropertyMock(return_value=200) 44 | expected_resp = { 45 | "success": True, 46 | } 47 | 48 | mock_response.json.return_value = expected_resp 49 | 50 | 51 | if __name__ == "__main__": 52 | unittest.main() 53 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | google-auth 2 | requests < 2.32 3 | -------------------------------------------------------------------------------- /search/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | -------------------------------------------------------------------------------- /search/list_iocs_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Tests for the "list_iocs" module.""" 16 | 17 | import datetime 18 | import unittest 19 | from unittest import mock 20 | 21 | from google.auth.transport import requests 22 | 23 | from . import list_iocs 24 | 25 | 26 | class ListIocsTest(unittest.TestCase): 27 | 28 | def test_initialize_command_line_args_local_time(self): 29 | actual = list_iocs.initialize_command_line_args( 30 | ["--start_time=2021-05-07T11:22:33", "--local_time"]) 31 | self.assertIsNotNone(actual) 32 | 33 | def test_initialize_command_line_args_utc(self): 34 | actual = list_iocs.initialize_command_line_args( 35 | ["-ts=2021-05-07T11:22:33Z"]) 36 | self.assertIsNotNone(actual) 37 | 38 | def test_initialize_command_line_args_page_size(self): 39 | actual = list_iocs.initialize_command_line_args( 40 | ["-ts=2021-05-07T11:22:33Z", "--page_size=1000"]) 41 | self.assertIsNotNone(actual) 42 | 43 | def test_initialize_command_line_args_zero_page_size(self): 44 | actual = list_iocs.initialize_command_line_args( 45 | ["-ts=2021-05-07T11:22:33Z", "--page_size=0"]) 46 | self.assertIsNone(actual) 47 | 48 | def test_initialize_command_line_args_large_page_size(self): 49 | actual = list_iocs.initialize_command_line_args( 50 | ["-ts=2021-05-07T11:22:33Z", "--page_size=10001"]) 51 | self.assertIsNone(actual) 52 | 53 | def test_initialize_command_line_args_future(self): 54 | start_time = datetime.datetime.utcnow().astimezone(datetime.timezone.utc) 55 | start_time += datetime.timedelta(days=2) 56 | actual = list_iocs.initialize_command_line_args( 57 | [start_time.strftime("-ts=%Y-%m-%dT%H:%M:%SZ")]) 58 | self.assertIsNone(actual) 59 | 60 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 61 | @mock.patch.object(requests.requests, "Response", autospec=True) 62 | def test_list_iocs(self, mock_response, mock_session): 63 | mock_session.request.return_value = mock_response 64 | type(mock_response).status_code = mock.PropertyMock(return_value=200) 65 | mock_response.json.return_value = {"mock": "json"} 66 | actual = list_iocs.list_iocs(mock_session, 67 | datetime.datetime(2021, 5, 7, 11, 22, 33)) 68 | self.assertEqual(actual, {"mock": "json"}) 69 | 70 | 71 | if __name__ == "__main__": 72 | unittest.main() 73 | -------------------------------------------------------------------------------- /service_management/README.md: -------------------------------------------------------------------------------- 1 | # Service Management APIs in Python 2 | 3 | Python samples and guidelines for using Chronicle Service Management APIs. 4 | 5 | ## Authentication Setup 6 | 7 | To access the Chronicle Service Management API programmatically, use Cloud Shell 8 | to authenticate a service account under your GCP organization. 9 | 10 | ### Environment Variables Setup 11 | 12 | 1. Go to the Google Cloud Console. 13 | 2. Activate Cloud Chell. 14 | 3. Set environment variables by running: 15 | a. Set your organization name: 16 | ``` 17 | export ORG_ID=[YOUR_ORGANIZATION_ID] 18 | ``` 19 | b. Set the project ID: 20 | ``` 21 | export PROJECT_ID=[SERVICE_MANAGEMENT_ENABLED_PROJECT_ID] 22 | ``` 23 | c. Set the service account name: 24 | ``` 25 | export SERVICE_ACCOUNT=[SERVICE_ACCOUNT_NAME] 26 | ``` 27 | d. Set the path in which the service account key should be stored: 28 | ``` 29 | export KEY_LOCATION=[FULL_PATH] 30 | # This is used by client libraries to find the key. 31 | export GOOGLE_APPLICATION_CREDENTIALS=$KEY_LOCATION 32 | ``` 33 | 34 | ### Service Account Setup 35 | 36 | 1. Create a service account that's associated with your project ID. 37 | ``` 38 | gcloud iam service-accounts create $SERVICE_ACCOUNT --display-name \ 39 | "Service Account for Chronicle Service Management APIs" --project $PROJECT_ID 40 | ``` 41 | 2. Create a key to associate with the service account. 42 | ``` 43 | gcloud iam service-accounts keys create $KEY_LOCATION --iam-account \ 44 | $SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com 45 | ``` 46 | 3. Grant the service account the `chroniclesm.admin` role for the organization. 47 | ``` 48 | gcloud organizations add-iam-policy-binding $ORG_ID \ 49 | --member="serviceAccount:$SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com" \ 50 | --role='roles/chroniclesm.admin' 51 | ``` 52 | 53 | -------------------------------------------------------------------------------- /service_management/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | -------------------------------------------------------------------------------- /service_management/delete_gcp_association_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Tests for the "delete_gcp_association" module.""" 16 | 17 | import unittest 18 | from unittest import mock 19 | 20 | from google.auth.transport import requests 21 | 22 | from . import delete_gcp_association 23 | 24 | 25 | class DeleteGCPAssociationTest(unittest.TestCase): 26 | 27 | def test_initialize_command_line_args(self): 28 | actual = delete_gcp_association.initialize_command_line_args( 29 | ["--credentials_file=./foo.json", "--organization_id=123"]) 30 | self.assertIsNotNone(actual) 31 | 32 | def test_initialize_command_line_args_organization_id_too_big(self): 33 | invalid_organization_id = 2**64 34 | actual = delete_gcp_association.initialize_command_line_args( 35 | [f"--organization_id={invalid_organization_id}"]) 36 | self.assertIsNone(actual) 37 | 38 | def test_initialize_command_line_args_negative_organization_id(self): 39 | actual = delete_gcp_association.initialize_command_line_args( 40 | ["--organization_id=-1"]) 41 | self.assertIsNone(actual) 42 | 43 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 44 | @mock.patch.object(requests.requests, "Response", autospec=True) 45 | def test_http_error(self, mock_response, mock_session): 46 | mock_session.request.return_value = mock_response 47 | type(mock_response).status_code = mock.PropertyMock(return_value=400) 48 | mock_response.raise_for_status.side_effect = ( 49 | requests.requests.exceptions.HTTPError()) 50 | 51 | with self.assertRaises(requests.requests.exceptions.HTTPError): 52 | delete_gcp_association.delete_gcp_association(mock_session, 123) 53 | 54 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 55 | @mock.patch.object(requests.requests, "Response", autospec=True) 56 | def test_happy_path(self, mock_response, mock_session): 57 | mock_session.request.return_value = mock_response 58 | type(mock_response).status_code = mock.PropertyMock(return_value=200) 59 | 60 | delete_gcp_association.delete_gcp_association(mock_session, 123) 61 | 62 | 63 | if __name__ == "__main__": 64 | unittest.main() 65 | -------------------------------------------------------------------------------- /service_management/get_gcp_association_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Tests for the "get_gcp_association" module.""" 16 | 17 | import unittest 18 | from unittest import mock 19 | 20 | from google.auth.transport import requests 21 | 22 | from . import get_gcp_association 23 | 24 | 25 | class GetGCPAssociationTest(unittest.TestCase): 26 | 27 | def test_initialize_command_line_args(self): 28 | actual = get_gcp_association.initialize_command_line_args( 29 | ["--credentials_file=./foo.json", "--organization_id=123"]) 30 | self.assertIsNotNone(actual) 31 | 32 | def test_initialize_command_line_args_organization_id_too_big(self): 33 | invalid_organization_id = 2**64 34 | actual = get_gcp_association.initialize_command_line_args( 35 | [f"--organization_id={invalid_organization_id}"]) 36 | self.assertIsNone(actual) 37 | 38 | def test_initialize_command_line_args_negative_organization_id(self): 39 | actual = get_gcp_association.initialize_command_line_args( 40 | ["--organization_id=-1"]) 41 | self.assertIsNone(actual) 42 | 43 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 44 | @mock.patch.object(requests.requests, "Response", autospec=True) 45 | def test_http_error(self, mock_response, mock_session): 46 | mock_session.request.return_value = mock_response 47 | type(mock_response).status_code = mock.PropertyMock(return_value=400) 48 | mock_response.raise_for_status.side_effect = ( 49 | requests.requests.exceptions.HTTPError()) 50 | 51 | with self.assertRaises(requests.requests.exceptions.HTTPError): 52 | get_gcp_association.get_gcp_association(mock_session, 123) 53 | 54 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 55 | @mock.patch.object(requests.requests, "Response", autospec=True) 56 | def test_happy_path(self, mock_response, mock_session): 57 | mock_session.request.return_value = mock_response 58 | type(mock_response).status_code = mock.PropertyMock(return_value=200) 59 | 60 | get_gcp_association.get_gcp_association(mock_session, 123) 61 | 62 | 63 | if __name__ == "__main__": 64 | unittest.main() 65 | -------------------------------------------------------------------------------- /service_management/get_gcp_settings_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Tests for the "get_gcp_settings" module.""" 16 | 17 | import unittest 18 | from unittest import mock 19 | 20 | from google.auth.transport import requests 21 | 22 | from . import get_gcp_settings 23 | 24 | 25 | class GetGCPSettingsTest(unittest.TestCase): 26 | 27 | def test_initialize_command_line_args(self): 28 | actual = get_gcp_settings.initialize_command_line_args( 29 | ["--credentials_file=./foo.json", "--organization_id=123"]) 30 | self.assertIsNotNone(actual) 31 | 32 | def test_initialize_command_line_args_organization_id_too_big(self): 33 | invalid_organization_id = 2**64 34 | actual = get_gcp_settings.initialize_command_line_args( 35 | [f"--organization_id={invalid_organization_id}"]) 36 | self.assertIsNone(actual) 37 | 38 | def test_initialize_command_line_args_negative_organization_id(self): 39 | actual = get_gcp_settings.initialize_command_line_args( 40 | ["--organization_id=-1"]) 41 | self.assertIsNone(actual) 42 | 43 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 44 | @mock.patch.object(requests.requests, "Response", autospec=True) 45 | def test_http_error(self, mock_response, mock_session): 46 | mock_session.request.return_value = mock_response 47 | type(mock_response).status_code = mock.PropertyMock(return_value=400) 48 | mock_response.raise_for_status.side_effect = ( 49 | requests.requests.exceptions.HTTPError()) 50 | 51 | with self.assertRaises(requests.requests.exceptions.HTTPError): 52 | get_gcp_settings.get_gcp_settings(mock_session, 123) 53 | 54 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 55 | @mock.patch.object(requests.requests, "Response", autospec=True) 56 | def test_happy_path(self, mock_response, mock_session): 57 | mock_session.request.return_value = mock_response 58 | type(mock_response).status_code = mock.PropertyMock(return_value=200) 59 | 60 | get_gcp_settings.get_gcp_settings(mock_session, 123) 61 | 62 | 63 | if __name__ == "__main__": 64 | unittest.main() 65 | -------------------------------------------------------------------------------- /service_management/update_gcp_settings_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Tests for the "update_gcp_settings" module.""" 16 | 17 | import unittest 18 | from unittest import mock 19 | 20 | from google.auth.transport import requests 21 | 22 | from . import update_gcp_settings 23 | 24 | 25 | class UpdateGCPSettingsTest(unittest.TestCase): 26 | 27 | def test_initialize_command_line_args_enable_ingestion(self): 28 | actual = update_gcp_settings.initialize_command_line_args( 29 | ["--credentials_file=./foo.json", "--organization_id=123", "--enable"]) 30 | self.assertIsNotNone(actual) 31 | 32 | def test_initialize_command_line_args_disable_ingestion(self): 33 | actual = update_gcp_settings.initialize_command_line_args( 34 | ["--credentials_file=./foo.json", "--organization_id=123", "--disable"]) 35 | self.assertIsNotNone(actual) 36 | 37 | def test_initialize_command_line_args_organization_id_too_big(self): 38 | invalid_organization_id = 2**64 39 | actual = update_gcp_settings.initialize_command_line_args( 40 | [f"--organization_id={invalid_organization_id}"]) 41 | self.assertIsNone(actual) 42 | 43 | def test_initialize_command_line_args_negative_organization_id(self): 44 | actual = update_gcp_settings.initialize_command_line_args( 45 | ["--organization_id=-1"]) 46 | self.assertIsNone(actual) 47 | 48 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 49 | @mock.patch.object(requests.requests, "Response", autospec=True) 50 | def test_http_error(self, mock_response, mock_session): 51 | mock_session.request.return_value = mock_response 52 | type(mock_response).status_code = mock.PropertyMock(return_value=400) 53 | mock_response.raise_for_status.side_effect = ( 54 | requests.requests.exceptions.HTTPError()) 55 | 56 | with self.assertRaises(requests.requests.exceptions.HTTPError): 57 | update_gcp_settings.update_gcp_settings(mock_session, 123, True) 58 | 59 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 60 | @mock.patch.object(requests.requests, "Response", autospec=True) 61 | def test_happy_path(self, mock_response, mock_session): 62 | mock_session.request.return_value = mock_response 63 | type(mock_response).status_code = mock.PropertyMock(return_value=200) 64 | 65 | update_gcp_settings.update_gcp_settings(mock_session, 123, True) 66 | 67 | 68 | if __name__ == "__main__": 69 | unittest.main() 70 | -------------------------------------------------------------------------------- /uppercase/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | -------------------------------------------------------------------------------- /uppercase/get_alert_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Tests for the "get_alert" module.""" 16 | 17 | import unittest 18 | from unittest import mock 19 | 20 | from google.auth.transport import requests 21 | 22 | from . import get_alert 23 | 24 | 25 | class GetAlertTest(unittest.TestCase): 26 | 27 | def test_initialize_command_line_args(self): 28 | actual = get_alert.initialize_command_line_args(["--id=1"]) 29 | self.assertIsNotNone(actual) 30 | 31 | def test_initialize_command_line_args_short(self): 32 | actual = get_alert.initialize_command_line_args(["-i=1"]) 33 | self.assertIsNotNone(actual) 34 | 35 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 36 | @mock.patch.object(requests.requests, "Response", autospec=True) 37 | def test_http_error(self, mock_response, mock_session): 38 | mock_session.request.return_value = mock_response 39 | type(mock_response).status_code = mock.PropertyMock(return_value=400) 40 | mock_response.raise_for_status.side_effect = ( 41 | requests.requests.exceptions.HTTPError()) 42 | 43 | with self.assertRaises(requests.requests.exceptions.HTTPError): 44 | get_alert.get_alert(mock_session, "1") 45 | 46 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 47 | @mock.patch.object(requests.requests, "Response", autospec=True) 48 | def test_get_alert(self, mock_response, mock_session): 49 | mock_session.request.return_value = mock_response 50 | type(mock_response).status_code = mock.PropertyMock(return_value=200) 51 | mock_response.json.return_value = {"mock": "json"} 52 | actual = get_alert.get_alert(mock_session, "1") 53 | self.assertEqual(actual, {"mock": "json"}) 54 | 55 | 56 | if __name__ == "__main__": 57 | unittest.main() 58 | -------------------------------------------------------------------------------- /uppercase/list_alerts_test.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """Tests for the "list_alerts" module.""" 16 | 17 | import unittest 18 | from unittest import mock 19 | 20 | from google.auth.transport import requests 21 | 22 | from . import list_alerts 23 | 24 | 25 | class ListAlertsTest(unittest.TestCase): 26 | 27 | def test_initialize_command_line_args(self): 28 | actual = list_alerts.initialize_command_line_args(["--page_size=5"]) 29 | self.assertIsNotNone(actual) 30 | 31 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 32 | @mock.patch.object(requests.requests, "Response", autospec=True) 33 | def test_http_error(self, mock_response, mock_session): 34 | mock_session.request.return_value = mock_response 35 | type(mock_response).status_code = mock.PropertyMock(return_value=400) 36 | mock_response.raise_for_status.side_effect = ( 37 | requests.requests.exceptions.HTTPError()) 38 | 39 | with self.assertRaises(requests.requests.exceptions.HTTPError): 40 | list_alerts.list_alerts(mock_session) 41 | 42 | @mock.patch.object(requests, "AuthorizedSession", autospec=True) 43 | @mock.patch.object(requests.requests, "Response", autospec=True) 44 | def test_list_alerts(self, mock_response, mock_session): 45 | mock_session.request.return_value = mock_response 46 | type(mock_response).status_code = mock.PropertyMock(return_value=200) 47 | 48 | expected_alert = {"alertId": "1"} 49 | expected_page_token = "page token here" 50 | mock_response.json.return_value = { 51 | "uppercaseAlerts": [expected_alert], 52 | "nextPageToken": expected_page_token, 53 | } 54 | 55 | alerts, next_page_token = list_alerts.list_alerts(mock_session) 56 | self.assertCountEqual(alerts, [expected_alert]) 57 | self.assertEqual(next_page_token, expected_page_token) 58 | 59 | 60 | if __name__ == "__main__": 61 | unittest.main() 62 | --------------------------------------------------------------------------------