From 7fd3de01a977d8a55bff08874545824eba3bb784 Mon Sep 17 00:00:00 2001 From: Fry Date: Thu, 5 Dec 2024 13:00:58 +1100 Subject: [PATCH 1/8] Bump version --- libcloudforensics/__init__.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libcloudforensics/__init__.py b/libcloudforensics/__init__.py index 57c6a981..aabacb77 100644 --- a/libcloudforensics/__init__.py +++ b/libcloudforensics/__init__.py @@ -16,4 +16,4 @@ # Since moving to poetry, ensure the version number tracked in pyproject.toml is # also updated -__version__ = '20241018' +__version__ = '20241205' diff --git a/pyproject.toml b/pyproject.toml index 7e1b1c86..3531d13d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "libcloudforensics" -version = "20241018" +version = "20241205" description = "libcloudforensics is a set of tools to help acquire forensic evidence from Cloud platforms." authors = ["cloud-forensics-utils development team "] license = "Apache-2.0" From 0b4e8d8758c27ce7a39cfa816f49c53f51671a99 Mon Sep 17 00:00:00 2001 From: Fry Date: Fri, 6 Dec 2024 14:58:13 +1100 Subject: [PATCH 2/8] OrgPolicy inside CRM --- .../gcp/internal/cloudresourcemanager.py | 145 +++++++++++++++++- tests/providers/gcp/gcp_mocks.py | 12 ++ .../gcp/internal/test_cloudresourcemanager.py | 73 +++++++++ 3 files changed, 229 insertions(+), 1 deletion(-) diff --git a/libcloudforensics/providers/gcp/internal/cloudresourcemanager.py b/libcloudforensics/providers/gcp/internal/cloudresourcemanager.py index d138df4d..5eb4bcb8 100644 --- a/libcloudforensics/providers/gcp/internal/cloudresourcemanager.py +++ b/libcloudforensics/providers/gcp/internal/cloudresourcemanager.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. """Google Cloud Resource Manager functionality.""" -from typing import TYPE_CHECKING, Dict, List, Any +from typing import TYPE_CHECKING, Dict, List, Any, Optional from googleapiclient import errors as google_api_errors from libcloudforensics import logging_utils @@ -164,3 +164,146 @@ def GetIamPolicy(self, name: str) -> Dict[str, Any]: resource_client, 'getIamPolicy', request)[0] return response + + def GetOrgPolicy(self, resource, constraint: str) -> Dict[str, Any]: + """Gets a particular Org Policy on a resource. + + Args: + resource (str): a resource identifier in the format + resource_type/resource_number e.g. projects/123456789012 where + project_type is one of projects, folders or organizations. + constraint (str): the name of the constraint to get. + + Returns: + Dict[str, Any]: The Org Policy details. + See https://cloud.google.com/resource-manager/reference/rest/v1/Policy + + Raises: + TypeError: if an invalid resource type is provided. + """ + resource_type = resource.split('/')[0] + if resource_type not in self.RESOURCE_TYPES: + raise TypeError('Invalid resource type "{0:s}", resource must be one of ' + '"projects", "folders" or "organizations" provided in the format ' + '"resource_type/resource_number".'.format(resource)) + + if not constraint.startswith('constraints/'): + constraint = 'constraints/' + constraint + + # Override API version, since this doesn't exist in v2 or v3 + self.RESOURCE_MANAGER_API_VERSION = 'v1' + service = self.GrmApi() + resource_client = getattr(service, resource_type)() + response = resource_client.getOrgPolicy( + resource=resource, body={'constraint': constraint} + ).execute() + return response + + def ListOrgPolicy(self, resource: str) -> Dict[str, Any]: + """Lists all Org Policies on a resource. + + Args: + resource (str): a resource identifier in the format + resource_type/resource_number e.g. projects/123456789012 where + project_type is one of projects, folders or organizations. + + Returns: + Dict[str, Any]: The Org Policy details. + See https://cloud.google.com/resource-manager/reference/rest/v1/Policy + + Raises: + TypeError: if an invalid resource type is provided. + """ + resource_type = resource.split('/')[0] + if resource_type not in self.RESOURCE_TYPES: + raise TypeError('Invalid resource type "{0:s}", resource must be one of ' + '"projects", "folders" or "organizations" provided in the format ' + '"resource_type/resource_number".'.format(resource)) + + # Override API version, since this doesn't exist in v2 or v3 + self.RESOURCE_MANAGER_API_VERSION = 'v1' + service = self.GrmApi() + resource_client = getattr(service, resource_type)() + response = resource_client.listOrgPolicies(resource=resource).execute() + return response + + def SetOrgPolicy( + self, resource: str, policy: Dict[str, Any], + etag: Optional[str] = None) -> Dict[str, Any]: + """Updates the specified Policy on the resource. + Creates a new Policy for that Constraint on the resource if one does not exist. + + + Args: + resource (str): a resource identifier in the format + resource_type/resource_number e.g. projects/123456789012 where + project_type is one of projects, folders or organizations. + policy (dict): The policy to create, as per + https://cloud.google.com/resource-manager/reference/rest/v1/Policy + etag (str): The current version, for concurrency control. + Not supplying an etag on the request Policy results in an unconditional + write of the Policy. + + Returns: + Dict[str, Any]: The Org Policy that was created. + https://cloud.google.com/resource-manager/reference/rest/v1/Policy + + Raises: + TypeError: if an invalid resource type is provided. + """ + resource_type = resource.split('/')[0] + if resource_type not in self.RESOURCE_TYPES: + raise TypeError('Invalid resource type "{0:s}", resource must be one of ' + '"projects", "folders" or "organizations" provided in the format ' + '"resource_type/resource_number".'.format(resource)) + + # Override API version, since this doesn't exist in v2 or v3 + self.RESOURCE_MANAGER_API_VERSION = 'v1' + service = self.GrmApi() + resource_client = getattr(service, resource_type)() + body = {'policy': policy} + if etag: + body['policy']['etag'] = etag + response = resource_client.setOrgPolicy(resource=resource, + body=body).execute() + return response + + def DeleteOrgPolicy( + self, resource, constraint: str, etag: Optional[str] = None) -> bool: + """Removes a particular Org Policy on a resource. + + Args: + resource (str): a resource identifier in the format + resource_type/resource_number e.g. projects/123456789012 where + project_type is one of projects, folders or organizations. + constraint (str): the name of the constraint to get. + etag (str): The current version, for concurrency control. + Not sending an etag will cause the Policy to be cleared blindly. + + Returns: + bool: True if successful, + + Raises: + TypeError: if an invalid resource type is provided. + """ + resource_type = resource.split('/')[0] + if resource_type not in self.RESOURCE_TYPES: + raise TypeError('Invalid resource type "{0:s}", resource must be one of ' + '"projects", "folders" or "organizations" provided in the format ' + '"resource_type/resource_number".'.format(resource)) + + if not constraint.startswith('constraints/'): + constraint = 'constraints/' + constraint + + # Override API version, since this doesn't exist in v2 or v3 + self.RESOURCE_MANAGER_API_VERSION = 'v1' + service = self.GrmApi() + resource_client = getattr(service, resource_type)() + body = {'constraint': constraint} + if etag: + body['etag'] = etag + response = resource_client.clearOrgPolicy(resource=resource, + body=body).execute() + if not response: + return True + return False diff --git a/tests/providers/gcp/gcp_mocks.py b/tests/providers/gcp/gcp_mocks.py index 1dc4e32c..06e868b6 100644 --- a/tests/providers/gcp/gcp_mocks.py +++ b/tests/providers/gcp/gcp_mocks.py @@ -1090,3 +1090,15 @@ } ] } + +MOCK_ORG_POLICY = { + 'constraint': 'constraints/testpolicy', + 'etag': 'abcdefghijk=' +} + +MOCK_ORG_POLICIES = { + 'policies': [ + {'constraint': 'constraints/compute.requireShieldedVm', 'etag': 'abcdefghijk', 'updateTime': '2024-12-02T03:38:34.276794Z', 'booleanPolicy': {}}, + {'constraint': 'constraints/compute.storageResourceUseRestrictions', 'etag': 'abcdefghijk', 'updateTime': '2024-12-06T02:01:04.737315Z', 'listPolicy': {'allValues': 'ALLOW'}}, + ] +} diff --git a/tests/providers/gcp/internal/test_cloudresourcemanager.py b/tests/providers/gcp/internal/test_cloudresourcemanager.py index 7c3fefe9..3387d968 100644 --- a/tests/providers/gcp/internal/test_cloudresourcemanager.py +++ b/tests/providers/gcp/internal/test_cloudresourcemanager.py @@ -130,3 +130,76 @@ def testGetIamPolicy(self, mock_grm_api, mock_execute_request): } ] }) + + @typing.no_type_check + @mock.patch('libcloudforensics.providers.gcp.internal.cloudresourcemanager.GoogleCloudResourceManager.GrmApi') + def testGetOrgPolicy(self, mock_grm_api): + """Validates the GetOrgPolicy function""" + api_get_org_policy = mock_grm_api.return_value.projects.return_value.getOrgPolicy + api_get_org_policy.return_value.execute.return_value = gcp_mocks.MOCK_ORG_POLICY + response = gcp_mocks.FAKE_CLOUD_RESOURCE_MANAGER.GetOrgPolicy( + 'projects/000000000000', 'fake-policy') + api_get_org_policy.assert_called_with( + resource='projects/000000000000', + body={'constraint': 'constraints/fake-policy'}) + self.assertEqual(response, { + 'constraint': 'constraints/testpolicy', + 'etag': 'abcdefghijk=' + }) + + + @typing.no_type_check + @mock.patch('libcloudforensics.providers.gcp.internal.cloudresourcemanager.GoogleCloudResourceManager.GrmApi') + def testListOrgPolicy(self, mock_grm_api): + """Validates the ListOrgPolicy function""" + api_list_org_policy = mock_grm_api.return_value.projects.return_value.listOrgPolicies + api_list_org_policy.return_value.execute.return_value = gcp_mocks.MOCK_ORG_POLICIES + response = gcp_mocks.FAKE_CLOUD_RESOURCE_MANAGER.ListOrgPolicy( + 'projects/000000000000' + ) + api_list_org_policy.assert_called_with( + resource='projects/000000000000') + self.assertEqual(len(response.get('policies', [])), 2) + + @typing.no_type_check + @mock.patch('libcloudforensics.providers.gcp.internal.cloudresourcemanager.GoogleCloudResourceManager.GrmApi') + def testSetOrgPolicy(self, mock_grm_api): + """Validates the SetOrgPolicy function""" + api_set_org_policy = mock_grm_api.return_value.projects.return_value.setOrgPolicy + api_set_org_policy.return_value.execute.return_value = gcp_mocks.MOCK_ORG_POLICY + response = gcp_mocks.FAKE_CLOUD_RESOURCE_MANAGER.SetOrgPolicy( + 'projects/000000000000', + { + 'constraint': 'constraints/compute.storageResourceUseRestrictions', + 'listPolicy': { + 'inheritFromParent': False, 'allValues': 'ALLOW' + } + }, + 'abc123') + api_set_org_policy.assert_called_with( + resource='projects/000000000000', + body={ + 'policy': { + 'constraint': 'constraints/compute.storageResourceUseRestrictions', + 'listPolicy': { + 'inheritFromParent': False, 'allValues': 'ALLOW' + }, + 'etag': 'abc123' + } + }) + + @typing.no_type_check + @mock.patch('libcloudforensics.providers.gcp.internal.cloudresourcemanager.GoogleCloudResourceManager.GrmApi') + def testDeleteOrgPolicy(self, mock_grm_api): + """Validates the DeleteOrgPolicy function""" + api_delete_org_policy = mock_grm_api.return_value.projects.return_value.clearOrgPolicy + api_delete_org_policy.return_value.execute.return_value = True + response = gcp_mocks.FAKE_CLOUD_RESOURCE_MANAGER.DeleteOrgPolicy( + 'projects/000000000000', + 'fake-policy', + 'abc123' + ) + api_delete_org_policy.assert_called_with( + resource='projects/000000000000', + body={'constraint': 'constraints/fake-policy', 'etag': 'abc123'} + ) From 37397b5c6e0f3e071ac39e35595df10f6ad6a8fb Mon Sep 17 00:00:00 2001 From: Fry Date: Fri, 6 Dec 2024 16:58:54 +1100 Subject: [PATCH 3/8] Bump version again --- libcloudforensics/__init__.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libcloudforensics/__init__.py b/libcloudforensics/__init__.py index aabacb77..6719442a 100644 --- a/libcloudforensics/__init__.py +++ b/libcloudforensics/__init__.py @@ -16,4 +16,4 @@ # Since moving to poetry, ensure the version number tracked in pyproject.toml is # also updated -__version__ = '20241205' +__version__ = '20241207' diff --git a/pyproject.toml b/pyproject.toml index 3531d13d..9e9fbd3f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "libcloudforensics" -version = "20241205" +version = "20241207" description = "libcloudforensics is a set of tools to help acquire forensic evidence from Cloud platforms." authors = ["cloud-forensics-utils development team "] license = "Apache-2.0" From 890d95346ebcd1be4a04e8eb645c73ccee730400 Mon Sep 17 00:00:00 2001 From: Fry Date: Mon, 9 Dec 2024 16:47:33 +1100 Subject: [PATCH 4/8] Show warning --- libcloudforensics/providers/gcp/internal/cloudresourcemanager.py | 1 + 1 file changed, 1 insertion(+) diff --git a/libcloudforensics/providers/gcp/internal/cloudresourcemanager.py b/libcloudforensics/providers/gcp/internal/cloudresourcemanager.py index 5eb4bcb8..f2191d72 100644 --- a/libcloudforensics/providers/gcp/internal/cloudresourcemanager.py +++ b/libcloudforensics/providers/gcp/internal/cloudresourcemanager.py @@ -306,4 +306,5 @@ def DeleteOrgPolicy( body=body).execute() if not response: return True + logger.warning("Unable to delete Org Policy: {0:s}".format(response)) return False From b7bd9cda257e13cde1d763d1265879b742893c0e Mon Sep 17 00:00:00 2001 From: Fry Date: Tue, 10 Dec 2024 09:42:26 +1100 Subject: [PATCH 5/8] Satisfy type checking --- .../providers/gcp/internal/cloudresourcemanager.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/libcloudforensics/providers/gcp/internal/cloudresourcemanager.py b/libcloudforensics/providers/gcp/internal/cloudresourcemanager.py index f2191d72..0648ec64 100644 --- a/libcloudforensics/providers/gcp/internal/cloudresourcemanager.py +++ b/libcloudforensics/providers/gcp/internal/cloudresourcemanager.py @@ -231,7 +231,8 @@ def SetOrgPolicy( self, resource: str, policy: Dict[str, Any], etag: Optional[str] = None) -> Dict[str, Any]: """Updates the specified Policy on the resource. - Creates a new Policy for that Constraint on the resource if one does not exist. + Creates a new Policy for that Constraint on the resource if one does + not exist. Args: @@ -302,9 +303,9 @@ def DeleteOrgPolicy( body = {'constraint': constraint} if etag: body['etag'] = etag - response = resource_client.clearOrgPolicy(resource=resource, - body=body).execute() + response: Dict = resource_client.clearOrgPolicy( + resource=resource, body=body).execute() if not response: return True - logger.warning("Unable to delete Org Policy: {0:s}".format(response)) + logger.warning("Unable to delete Org Policy: {0}".format(response)) return False From 115097fa413cc4fa8b439216f9441f7017a12fe1 Mon Sep 17 00:00:00 2001 From: Fry Date: Tue, 10 Dec 2024 09:56:17 +1100 Subject: [PATCH 6/8] Satisfy linter --- .../providers/gcp/internal/cloudresourcemanager.py | 2 +- tests/providers/gcp/internal/test_cloudresourcemanager.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/libcloudforensics/providers/gcp/internal/cloudresourcemanager.py b/libcloudforensics/providers/gcp/internal/cloudresourcemanager.py index 0648ec64..66b8d66d 100644 --- a/libcloudforensics/providers/gcp/internal/cloudresourcemanager.py +++ b/libcloudforensics/providers/gcp/internal/cloudresourcemanager.py @@ -191,7 +191,7 @@ def GetOrgPolicy(self, resource, constraint: str) -> Dict[str, Any]: constraint = 'constraints/' + constraint # Override API version, since this doesn't exist in v2 or v3 - self.RESOURCE_MANAGER_API_VERSION = 'v1' + self.RESOURCE_MANAGER_API_VERSION = 'v1' # pylint: disable=invalid-name service = self.GrmApi() resource_client = getattr(service, resource_type)() response = resource_client.getOrgPolicy( diff --git a/tests/providers/gcp/internal/test_cloudresourcemanager.py b/tests/providers/gcp/internal/test_cloudresourcemanager.py index 3387d968..eab2f17e 100644 --- a/tests/providers/gcp/internal/test_cloudresourcemanager.py +++ b/tests/providers/gcp/internal/test_cloudresourcemanager.py @@ -167,7 +167,7 @@ def testSetOrgPolicy(self, mock_grm_api): """Validates the SetOrgPolicy function""" api_set_org_policy = mock_grm_api.return_value.projects.return_value.setOrgPolicy api_set_org_policy.return_value.execute.return_value = gcp_mocks.MOCK_ORG_POLICY - response = gcp_mocks.FAKE_CLOUD_RESOURCE_MANAGER.SetOrgPolicy( + gcp_mocks.FAKE_CLOUD_RESOURCE_MANAGER.SetOrgPolicy( 'projects/000000000000', { 'constraint': 'constraints/compute.storageResourceUseRestrictions', @@ -194,7 +194,7 @@ def testDeleteOrgPolicy(self, mock_grm_api): """Validates the DeleteOrgPolicy function""" api_delete_org_policy = mock_grm_api.return_value.projects.return_value.clearOrgPolicy api_delete_org_policy.return_value.execute.return_value = True - response = gcp_mocks.FAKE_CLOUD_RESOURCE_MANAGER.DeleteOrgPolicy( + gcp_mocks.FAKE_CLOUD_RESOURCE_MANAGER.DeleteOrgPolicy( 'projects/000000000000', 'fake-policy', 'abc123' From ac2b0c001178c5ab4b230442a38923b9cf7320f2 Mon Sep 17 00:00:00 2001 From: Fry Date: Tue, 10 Dec 2024 10:23:00 +1100 Subject: [PATCH 7/8] More linty typy --- .../providers/gcp/internal/cloudresourcemanager.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/libcloudforensics/providers/gcp/internal/cloudresourcemanager.py b/libcloudforensics/providers/gcp/internal/cloudresourcemanager.py index 66b8d66d..e8ec103f 100644 --- a/libcloudforensics/providers/gcp/internal/cloudresourcemanager.py +++ b/libcloudforensics/providers/gcp/internal/cloudresourcemanager.py @@ -165,7 +165,7 @@ def GetIamPolicy(self, name: str) -> Dict[str, Any]: return response - def GetOrgPolicy(self, resource, constraint: str) -> Dict[str, Any]: + def GetOrgPolicy(self, resource: str, constraint: str) -> Dict[str, Any]: """Gets a particular Org Policy on a resource. Args: @@ -194,7 +194,7 @@ def GetOrgPolicy(self, resource, constraint: str) -> Dict[str, Any]: self.RESOURCE_MANAGER_API_VERSION = 'v1' # pylint: disable=invalid-name service = self.GrmApi() resource_client = getattr(service, resource_type)() - response = resource_client.getOrgPolicy( + response: Dict[str, Any] = resource_client.getOrgPolicy( resource=resource, body={'constraint': constraint} ).execute() return response @@ -224,7 +224,8 @@ def ListOrgPolicy(self, resource: str) -> Dict[str, Any]: self.RESOURCE_MANAGER_API_VERSION = 'v1' service = self.GrmApi() resource_client = getattr(service, resource_type)() - response = resource_client.listOrgPolicies(resource=resource).execute() + response: Dict[str, Any] = resource_client.listOrgPolicies( + resource=resource).execute() return response def SetOrgPolicy( @@ -265,12 +266,12 @@ def SetOrgPolicy( body = {'policy': policy} if etag: body['policy']['etag'] = etag - response = resource_client.setOrgPolicy(resource=resource, + response: Dict[str, Any] = resource_client.setOrgPolicy(resource=resource, body=body).execute() return response def DeleteOrgPolicy( - self, resource, constraint: str, etag: Optional[str] = None) -> bool: + self, resource: str, constraint: str, etag: Optional[str] = None) -> bool: """Removes a particular Org Policy on a resource. Args: @@ -303,7 +304,7 @@ def DeleteOrgPolicy( body = {'constraint': constraint} if etag: body['etag'] = etag - response: Dict = resource_client.clearOrgPolicy( + response: Dict[str, Any] = resource_client.clearOrgPolicy( resource=resource, body=body).execute() if not response: return True From 035add864ecdc7217842d69059662596572174ec Mon Sep 17 00:00:00 2001 From: Fryyyyy Date: Tue, 10 Dec 2024 11:03:39 +1100 Subject: [PATCH 8/8] Update libcloudforensics/providers/gcp/internal/cloudresourcemanager.py Co-authored-by: Ramo --- .../providers/gcp/internal/cloudresourcemanager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libcloudforensics/providers/gcp/internal/cloudresourcemanager.py b/libcloudforensics/providers/gcp/internal/cloudresourcemanager.py index e8ec103f..79135031 100644 --- a/libcloudforensics/providers/gcp/internal/cloudresourcemanager.py +++ b/libcloudforensics/providers/gcp/internal/cloudresourcemanager.py @@ -283,7 +283,7 @@ def DeleteOrgPolicy( Not sending an etag will cause the Policy to be cleared blindly. Returns: - bool: True if successful, + bool: True if successful, False otherwise. Raises: TypeError: if an invalid resource type is provided.