From e705a7bef41321442d7ebae741297d3fa705d131 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Fri, 19 Nov 2021 23:48:39 +0000 Subject: [PATCH] feat: Added a new API method UpdateExternalSystem (#256) .. which enables updating a finding w/ external system metadata. External systems are a child resource under finding, and are housed on the finding itself, and can also be filtered on in Notifications, the ListFindings and GroupFindings API. - [ ] Regenerate this pull request now. PiperOrigin-RevId: 411093163 Source-Link: https://github.com/googleapis/googleapis/commit/be8f9889c7662043b89f63d475e72eea9a0621af Source-Link: https://github.com/googleapis/googleapis-gen/commit/186665a0ff6acaf9bf6d5ef0cef663dd828b48a7 Copy-Tag: eyJwIjoiLmdpdGh1Yi8uT3dsQm90LnlhbWwiLCJoIjoiMTg2NjY1YTBmZjZhY2FmOWJmNmQ1ZWYwY2VmNjYzZGQ4MjhiNDhhNyJ9 --- .../google/cloud/securitycenter/__init__.py | 6 + .../cloud/securitycenter_v1/__init__.py | 4 + .../securitycenter_v1/gapic_metadata.json | 10 + .../services/security_center/async_client.py | 90 +++++ .../services/security_center/client.py | 107 +++++ .../security_center/transports/base.py | 18 + .../security_center/transports/grpc.py | 30 ++ .../transports/grpc_asyncio.py | 30 ++ .../cloud/securitycenter_v1/types/__init__.py | 4 + .../types/external_system.py | 59 +++ .../cloud/securitycenter_v1/types/finding.py | 8 + .../types/securitycenter_service.py | 23 ++ .../fixup_securitycenter_v1_keywords.py | 1 + .../securitycenter_v1/test_security_center.py | 370 ++++++++++++++++-- 14 files changed, 722 insertions(+), 38 deletions(-) create mode 100644 packages/google-cloud-securitycenter/google/cloud/securitycenter_v1/types/external_system.py diff --git a/packages/google-cloud-securitycenter/google/cloud/securitycenter/__init__.py b/packages/google-cloud-securitycenter/google/cloud/securitycenter/__init__.py index 5f2e4ba81b11..b940ed8a4c2b 100644 --- a/packages/google-cloud-securitycenter/google/cloud/securitycenter/__init__.py +++ b/packages/google-cloud-securitycenter/google/cloud/securitycenter/__init__.py @@ -22,6 +22,7 @@ ) from google.cloud.securitycenter_v1.types.asset import Asset +from google.cloud.securitycenter_v1.types.external_system import ExternalSystem from google.cloud.securitycenter_v1.types.finding import Finding from google.cloud.securitycenter_v1.types.folder import Folder from google.cloud.securitycenter_v1.types.indicator import Indicator @@ -122,6 +123,9 @@ SetFindingStateRequest, ) from google.cloud.securitycenter_v1.types.securitycenter_service import SetMuteRequest +from google.cloud.securitycenter_v1.types.securitycenter_service import ( + UpdateExternalSystemRequest, +) from google.cloud.securitycenter_v1.types.securitycenter_service import ( UpdateFindingRequest, ) @@ -150,6 +154,7 @@ "SecurityCenterClient", "SecurityCenterAsyncClient", "Asset", + "ExternalSystem", "Finding", "Folder", "Indicator", @@ -190,6 +195,7 @@ "RunAssetDiscoveryRequest", "SetFindingStateRequest", "SetMuteRequest", + "UpdateExternalSystemRequest", "UpdateFindingRequest", "UpdateMuteConfigRequest", "UpdateNotificationConfigRequest", diff --git a/packages/google-cloud-securitycenter/google/cloud/securitycenter_v1/__init__.py b/packages/google-cloud-securitycenter/google/cloud/securitycenter_v1/__init__.py index 743b5fee579b..e1cecc33d07d 100644 --- a/packages/google-cloud-securitycenter/google/cloud/securitycenter_v1/__init__.py +++ b/packages/google-cloud-securitycenter/google/cloud/securitycenter_v1/__init__.py @@ -18,6 +18,7 @@ from .services.security_center import SecurityCenterAsyncClient from .types.asset import Asset +from .types.external_system import ExternalSystem from .types.finding import Finding from .types.folder import Folder from .types.indicator import Indicator @@ -58,6 +59,7 @@ from .types.securitycenter_service import RunAssetDiscoveryRequest from .types.securitycenter_service import SetFindingStateRequest from .types.securitycenter_service import SetMuteRequest +from .types.securitycenter_service import UpdateExternalSystemRequest from .types.securitycenter_service import UpdateFindingRequest from .types.securitycenter_service import UpdateMuteConfigRequest from .types.securitycenter_service import UpdateNotificationConfigRequest @@ -83,6 +85,7 @@ "Cvssv3", "DeleteMuteConfigRequest", "DeleteNotificationConfigRequest", + "ExternalSystem", "Finding", "Folder", "GetMuteConfigRequest", @@ -118,6 +121,7 @@ "SetFindingStateRequest", "SetMuteRequest", "Source", + "UpdateExternalSystemRequest", "UpdateFindingRequest", "UpdateMuteConfigRequest", "UpdateNotificationConfigRequest", diff --git a/packages/google-cloud-securitycenter/google/cloud/securitycenter_v1/gapic_metadata.json b/packages/google-cloud-securitycenter/google/cloud/securitycenter_v1/gapic_metadata.json index 50c3bcb4fab6..0d42f887a994 100644 --- a/packages/google-cloud-securitycenter/google/cloud/securitycenter_v1/gapic_metadata.json +++ b/packages/google-cloud-securitycenter/google/cloud/securitycenter_v1/gapic_metadata.json @@ -130,6 +130,11 @@ "test_iam_permissions" ] }, + "UpdateExternalSystem": { + "methods": [ + "update_external_system" + ] + }, "UpdateFinding": { "methods": [ "update_finding" @@ -285,6 +290,11 @@ "test_iam_permissions" ] }, + "UpdateExternalSystem": { + "methods": [ + "update_external_system" + ] + }, "UpdateFinding": { "methods": [ "update_finding" diff --git a/packages/google-cloud-securitycenter/google/cloud/securitycenter_v1/services/security_center/async_client.py b/packages/google-cloud-securitycenter/google/cloud/securitycenter_v1/services/security_center/async_client.py index ff090b80d3b2..011a66dce41c 100644 --- a/packages/google-cloud-securitycenter/google/cloud/securitycenter_v1/services/security_center/async_client.py +++ b/packages/google-cloud-securitycenter/google/cloud/securitycenter_v1/services/security_center/async_client.py @@ -34,6 +34,7 @@ from google.api_core import operation # type: ignore from google.api_core import operation_async # type: ignore from google.cloud.securitycenter_v1.services.security_center import pagers +from google.cloud.securitycenter_v1.types import external_system as gcs_external_system from google.cloud.securitycenter_v1.types import finding from google.cloud.securitycenter_v1.types import finding as gcs_finding from google.cloud.securitycenter_v1.types import indicator @@ -74,6 +75,10 @@ class SecurityCenterAsyncClient: asset_path = staticmethod(SecurityCenterClient.asset_path) parse_asset_path = staticmethod(SecurityCenterClient.parse_asset_path) + external_system_path = staticmethod(SecurityCenterClient.external_system_path) + parse_external_system_path = staticmethod( + SecurityCenterClient.parse_external_system_path + ) finding_path = staticmethod(SecurityCenterClient.finding_path) parse_finding_path = staticmethod(SecurityCenterClient.parse_finding_path) mute_config_path = staticmethod(SecurityCenterClient.mute_config_path) @@ -2406,6 +2411,91 @@ async def test_iam_permissions( # Done; return the response. return response + async def update_external_system( + self, + request: Union[securitycenter_service.UpdateExternalSystemRequest, dict] = None, + *, + external_system: gcs_external_system.ExternalSystem = None, + update_mask: field_mask_pb2.FieldMask = None, + retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: float = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> gcs_external_system.ExternalSystem: + r"""Updates external system. This is for a given finding. + + Args: + request (Union[google.cloud.securitycenter_v1.types.UpdateExternalSystemRequest, dict]): + The request object. Request message for updating a + ExternalSystem resource. + external_system (:class:`google.cloud.securitycenter_v1.types.ExternalSystem`): + Required. The external system + resource to update. + + This corresponds to the ``external_system`` field + on the ``request`` instance; if ``request`` is provided, this + should not be set. + update_mask (:class:`google.protobuf.field_mask_pb2.FieldMask`): + The FieldMask to use when updating + the external system resource. + If empty all mutable fields will be + updated. + + This corresponds to the ``update_mask`` field + on the ``request`` instance; if ``request`` is provided, this + should not be set. + retry (google.api_core.retry.Retry): Designation of what errors, if any, + should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + + Returns: + google.cloud.securitycenter_v1.types.ExternalSystem: + Representation of third party + SIEM/SOAR fields within SCC. + + """ + # Create or coerce a protobuf request object. + # Sanity check: If we got a request object, we should *not* have + # gotten any keyword arguments that map to the request. + has_flattened_params = any([external_system, update_mask]) + if request is not None and has_flattened_params: + raise ValueError( + "If the `request` argument is set, then none of " + "the individual field arguments should be set." + ) + + request = securitycenter_service.UpdateExternalSystemRequest(request) + + # If we have keyword arguments corresponding to fields on the + # request, apply these. + if external_system is not None: + request.external_system = external_system + if update_mask is not None: + request.update_mask = update_mask + + # Wrap the RPC method; this adds retry and timeout information, + # and friendly error handling. + rpc = gapic_v1.method_async.wrap_method( + self._client._transport.update_external_system, + default_timeout=None, + client_info=DEFAULT_CLIENT_INFO, + ) + + # Certain fields should be provided within the metadata header; + # add these here. + metadata = tuple(metadata) + ( + gapic_v1.routing_header.to_grpc_metadata( + (("external_system.name", request.external_system.name),) + ), + ) + + # Send the request. + response = await rpc(request, retry=retry, timeout=timeout, metadata=metadata,) + + # Done; return the response. + return response + async def update_finding( self, request: Union[securitycenter_service.UpdateFindingRequest, dict] = None, diff --git a/packages/google-cloud-securitycenter/google/cloud/securitycenter_v1/services/security_center/client.py b/packages/google-cloud-securitycenter/google/cloud/securitycenter_v1/services/security_center/client.py index 6a6918b21b03..f9f7097a7654 100644 --- a/packages/google-cloud-securitycenter/google/cloud/securitycenter_v1/services/security_center/client.py +++ b/packages/google-cloud-securitycenter/google/cloud/securitycenter_v1/services/security_center/client.py @@ -37,6 +37,7 @@ from google.api_core import operation # type: ignore from google.api_core import operation_async # type: ignore from google.cloud.securitycenter_v1.services.security_center import pagers +from google.cloud.securitycenter_v1.types import external_system as gcs_external_system from google.cloud.securitycenter_v1.types import finding from google.cloud.securitycenter_v1.types import finding as gcs_finding from google.cloud.securitycenter_v1.types import indicator @@ -200,6 +201,27 @@ def parse_asset_path(path: str) -> Dict[str, str]: ) return m.groupdict() if m else {} + @staticmethod + def external_system_path( + organization: str, source: str, finding: str, externalsystem: str, + ) -> str: + """Returns a fully-qualified external_system string.""" + return "organizations/{organization}/sources/{source}/findings/{finding}/externalSystems/{externalsystem}".format( + organization=organization, + source=source, + finding=finding, + externalsystem=externalsystem, + ) + + @staticmethod + def parse_external_system_path(path: str) -> Dict[str, str]: + """Parses a external_system path into its component segments.""" + m = re.match( + r"^organizations/(?P.+?)/sources/(?P.+?)/findings/(?P.+?)/externalSystems/(?P.+?)$", + path, + ) + return m.groupdict() if m else {} + @staticmethod def finding_path(organization: str, source: str, finding: str,) -> str: """Returns a fully-qualified finding string.""" @@ -2584,6 +2606,91 @@ def test_iam_permissions( # Done; return the response. return response + def update_external_system( + self, + request: Union[securitycenter_service.UpdateExternalSystemRequest, dict] = None, + *, + external_system: gcs_external_system.ExternalSystem = None, + update_mask: field_mask_pb2.FieldMask = None, + retry: OptionalRetry = gapic_v1.method.DEFAULT, + timeout: float = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> gcs_external_system.ExternalSystem: + r"""Updates external system. This is for a given finding. + + Args: + request (Union[google.cloud.securitycenter_v1.types.UpdateExternalSystemRequest, dict]): + The request object. Request message for updating a + ExternalSystem resource. + external_system (google.cloud.securitycenter_v1.types.ExternalSystem): + Required. The external system + resource to update. + + This corresponds to the ``external_system`` field + on the ``request`` instance; if ``request`` is provided, this + should not be set. + update_mask (google.protobuf.field_mask_pb2.FieldMask): + The FieldMask to use when updating + the external system resource. + If empty all mutable fields will be + updated. + + This corresponds to the ``update_mask`` field + on the ``request`` instance; if ``request`` is provided, this + should not be set. + retry (google.api_core.retry.Retry): Designation of what errors, if any, + should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + + Returns: + google.cloud.securitycenter_v1.types.ExternalSystem: + Representation of third party + SIEM/SOAR fields within SCC. + + """ + # Create or coerce a protobuf request object. + # Sanity check: If we got a request object, we should *not* have + # gotten any keyword arguments that map to the request. + has_flattened_params = any([external_system, update_mask]) + if request is not None and has_flattened_params: + raise ValueError( + "If the `request` argument is set, then none of " + "the individual field arguments should be set." + ) + + # Minor optimization to avoid making a copy if the user passes + # in a securitycenter_service.UpdateExternalSystemRequest. + # There's no risk of modifying the input as we've already verified + # there are no flattened fields. + if not isinstance(request, securitycenter_service.UpdateExternalSystemRequest): + request = securitycenter_service.UpdateExternalSystemRequest(request) + # If we have keyword arguments corresponding to fields on the + # request, apply these. + if external_system is not None: + request.external_system = external_system + if update_mask is not None: + request.update_mask = update_mask + + # Wrap the RPC method; this adds retry and timeout information, + # and friendly error handling. + rpc = self._transport._wrapped_methods[self._transport.update_external_system] + + # Certain fields should be provided within the metadata header; + # add these here. + metadata = tuple(metadata) + ( + gapic_v1.routing_header.to_grpc_metadata( + (("external_system.name", request.external_system.name),) + ), + ) + + # Send the request. + response = rpc(request, retry=retry, timeout=timeout, metadata=metadata,) + + # Done; return the response. + return response + def update_finding( self, request: Union[securitycenter_service.UpdateFindingRequest, dict] = None, diff --git a/packages/google-cloud-securitycenter/google/cloud/securitycenter_v1/services/security_center/transports/base.py b/packages/google-cloud-securitycenter/google/cloud/securitycenter_v1/services/security_center/transports/base.py index 6ee5d6cf3653..51fac89b0f0d 100644 --- a/packages/google-cloud-securitycenter/google/cloud/securitycenter_v1/services/security_center/transports/base.py +++ b/packages/google-cloud-securitycenter/google/cloud/securitycenter_v1/services/security_center/transports/base.py @@ -26,6 +26,7 @@ from google.auth import credentials as ga_credentials # type: ignore from google.oauth2 import service_account # type: ignore +from google.cloud.securitycenter_v1.types import external_system as gcs_external_system from google.cloud.securitycenter_v1.types import finding from google.cloud.securitycenter_v1.types import finding as gcs_finding from google.cloud.securitycenter_v1.types import mute_config @@ -349,6 +350,11 @@ def _prep_wrapped_messages(self, client_info): default_timeout=60.0, client_info=client_info, ), + self.update_external_system: gapic_v1.method.wrap_method( + self.update_external_system, + default_timeout=None, + client_info=client_info, + ), self.update_finding: gapic_v1.method.wrap_method( self.update_finding, default_timeout=60.0, client_info=client_info, ), @@ -638,6 +644,18 @@ def test_iam_permissions( ]: raise NotImplementedError() + @property + def update_external_system( + self, + ) -> Callable[ + [securitycenter_service.UpdateExternalSystemRequest], + Union[ + gcs_external_system.ExternalSystem, + Awaitable[gcs_external_system.ExternalSystem], + ], + ]: + raise NotImplementedError() + @property def update_finding( self, diff --git a/packages/google-cloud-securitycenter/google/cloud/securitycenter_v1/services/security_center/transports/grpc.py b/packages/google-cloud-securitycenter/google/cloud/securitycenter_v1/services/security_center/transports/grpc.py index 8b1bae77bea7..a67df2d74a68 100644 --- a/packages/google-cloud-securitycenter/google/cloud/securitycenter_v1/services/security_center/transports/grpc.py +++ b/packages/google-cloud-securitycenter/google/cloud/securitycenter_v1/services/security_center/transports/grpc.py @@ -25,6 +25,7 @@ import grpc # type: ignore +from google.cloud.securitycenter_v1.types import external_system as gcs_external_system from google.cloud.securitycenter_v1.types import finding from google.cloud.securitycenter_v1.types import finding as gcs_finding from google.cloud.securitycenter_v1.types import mute_config @@ -949,6 +950,35 @@ def test_iam_permissions( ) return self._stubs["test_iam_permissions"] + @property + def update_external_system( + self, + ) -> Callable[ + [securitycenter_service.UpdateExternalSystemRequest], + gcs_external_system.ExternalSystem, + ]: + r"""Return a callable for the update external system method over gRPC. + + Updates external system. This is for a given finding. + + Returns: + Callable[[~.UpdateExternalSystemRequest], + ~.ExternalSystem]: + A function that, when called, will call the underlying RPC + on the server. + """ + # Generate a "stub function" on-the-fly which will actually make + # the request. + # gRPC handles serialization and deserialization, so we just need + # to pass in the functions for each. + if "update_external_system" not in self._stubs: + self._stubs["update_external_system"] = self.grpc_channel.unary_unary( + "/google.cloud.securitycenter.v1.SecurityCenter/UpdateExternalSystem", + request_serializer=securitycenter_service.UpdateExternalSystemRequest.serialize, + response_deserializer=gcs_external_system.ExternalSystem.deserialize, + ) + return self._stubs["update_external_system"] + @property def update_finding( self, diff --git a/packages/google-cloud-securitycenter/google/cloud/securitycenter_v1/services/security_center/transports/grpc_asyncio.py b/packages/google-cloud-securitycenter/google/cloud/securitycenter_v1/services/security_center/transports/grpc_asyncio.py index a173cfa675b1..ece0ef4baea0 100644 --- a/packages/google-cloud-securitycenter/google/cloud/securitycenter_v1/services/security_center/transports/grpc_asyncio.py +++ b/packages/google-cloud-securitycenter/google/cloud/securitycenter_v1/services/security_center/transports/grpc_asyncio.py @@ -25,6 +25,7 @@ import grpc # type: ignore from grpc.experimental import aio # type: ignore +from google.cloud.securitycenter_v1.types import external_system as gcs_external_system from google.cloud.securitycenter_v1.types import finding from google.cloud.securitycenter_v1.types import finding as gcs_finding from google.cloud.securitycenter_v1.types import mute_config @@ -965,6 +966,35 @@ def test_iam_permissions( ) return self._stubs["test_iam_permissions"] + @property + def update_external_system( + self, + ) -> Callable[ + [securitycenter_service.UpdateExternalSystemRequest], + Awaitable[gcs_external_system.ExternalSystem], + ]: + r"""Return a callable for the update external system method over gRPC. + + Updates external system. This is for a given finding. + + Returns: + Callable[[~.UpdateExternalSystemRequest], + Awaitable[~.ExternalSystem]]: + A function that, when called, will call the underlying RPC + on the server. + """ + # Generate a "stub function" on-the-fly which will actually make + # the request. + # gRPC handles serialization and deserialization, so we just need + # to pass in the functions for each. + if "update_external_system" not in self._stubs: + self._stubs["update_external_system"] = self.grpc_channel.unary_unary( + "/google.cloud.securitycenter.v1.SecurityCenter/UpdateExternalSystem", + request_serializer=securitycenter_service.UpdateExternalSystemRequest.serialize, + response_deserializer=gcs_external_system.ExternalSystem.deserialize, + ) + return self._stubs["update_external_system"] + @property def update_finding( self, diff --git a/packages/google-cloud-securitycenter/google/cloud/securitycenter_v1/types/__init__.py b/packages/google-cloud-securitycenter/google/cloud/securitycenter_v1/types/__init__.py index aae0aa3d08b2..6bf0d25684dd 100644 --- a/packages/google-cloud-securitycenter/google/cloud/securitycenter_v1/types/__init__.py +++ b/packages/google-cloud-securitycenter/google/cloud/securitycenter_v1/types/__init__.py @@ -14,6 +14,7 @@ # limitations under the License. # from .asset import Asset +from .external_system import ExternalSystem from .finding import Finding from .folder import Folder from .indicator import Indicator @@ -55,6 +56,7 @@ RunAssetDiscoveryRequest, SetFindingStateRequest, SetMuteRequest, + UpdateExternalSystemRequest, UpdateFindingRequest, UpdateMuteConfigRequest, UpdateNotificationConfigRequest, @@ -72,6 +74,7 @@ __all__ = ( "Asset", + "ExternalSystem", "Finding", "Folder", "Indicator", @@ -112,6 +115,7 @@ "RunAssetDiscoveryRequest", "SetFindingStateRequest", "SetMuteRequest", + "UpdateExternalSystemRequest", "UpdateFindingRequest", "UpdateMuteConfigRequest", "UpdateNotificationConfigRequest", diff --git a/packages/google-cloud-securitycenter/google/cloud/securitycenter_v1/types/external_system.py b/packages/google-cloud-securitycenter/google/cloud/securitycenter_v1/types/external_system.py new file mode 100644 index 000000000000..d31544354138 --- /dev/null +++ b/packages/google-cloud-securitycenter/google/cloud/securitycenter_v1/types/external_system.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +import proto # type: ignore + +from google.protobuf import timestamp_pb2 # type: ignore + + +__protobuf__ = proto.module( + package="google.cloud.securitycenter.v1", manifest={"ExternalSystem",}, +) + + +class ExternalSystem(proto.Message): + r"""Representation of third party SIEM/SOAR fields within SCC. + + Attributes: + name (str): + External System Name e.g. jira, demisto, etc. e.g.: + ``organizations/1234/sources/5678/findings/123456/externalSystems/jira`` + ``folders/1234/sources/5678/findings/123456/externalSystems/jira`` + ``projects/1234/sources/5678/findings/123456/externalSystems/jira`` + assignees (Sequence[str]): + References primary/secondary etc assignees in + the external system. + external_uid (str): + Identifier that's used to track the given + finding in the external system. + status (str): + Most recent status of the corresponding + finding's ticket/tracker in the external system. + external_system_update_time (google.protobuf.timestamp_pb2.Timestamp): + The most recent time when the corresponding + finding's ticket/tracker was updated in the + external system. + """ + + name = proto.Field(proto.STRING, number=1,) + assignees = proto.RepeatedField(proto.STRING, number=2,) + external_uid = proto.Field(proto.STRING, number=3,) + status = proto.Field(proto.STRING, number=4,) + external_system_update_time = proto.Field( + proto.MESSAGE, number=5, message=timestamp_pb2.Timestamp, + ) + + +__all__ = tuple(sorted(__protobuf__.manifest)) diff --git a/packages/google-cloud-securitycenter/google/cloud/securitycenter_v1/types/finding.py b/packages/google-cloud-securitycenter/google/cloud/securitycenter_v1/types/finding.py index de6db7be42c9..e12600b59489 100644 --- a/packages/google-cloud-securitycenter/google/cloud/securitycenter_v1/types/finding.py +++ b/packages/google-cloud-securitycenter/google/cloud/securitycenter_v1/types/finding.py @@ -15,6 +15,7 @@ # import proto # type: ignore +from google.cloud.securitycenter_v1.types import external_system from google.cloud.securitycenter_v1.types import indicator as gcs_indicator from google.cloud.securitycenter_v1.types import security_marks as gcs_security_marks from google.cloud.securitycenter_v1.types import vulnerability as gcs_vulnerability @@ -121,6 +122,10 @@ class Finding(proto.Message): mute_update_time (google.protobuf.timestamp_pb2.Timestamp): Output only. The most recent time this finding was muted or unmuted. + external_systems (Sequence[google.cloud.securitycenter_v1.types.Finding.ExternalSystemsEntry]): + Output only. Third party SIEM/SOAR fields + within SCC, contains external system information + and external system finding fields. mute_initiator (str): First known as mute_annotation. Records additional information about the mute operation e.g. mute config that @@ -183,6 +188,9 @@ class FindingClass(proto.Enum): mute_update_time = proto.Field( proto.MESSAGE, number=21, message=timestamp_pb2.Timestamp, ) + external_systems = proto.MapField( + proto.STRING, proto.MESSAGE, number=22, message=external_system.ExternalSystem, + ) mute_initiator = proto.Field(proto.STRING, number=28,) diff --git a/packages/google-cloud-securitycenter/google/cloud/securitycenter_v1/types/securitycenter_service.py b/packages/google-cloud-securitycenter/google/cloud/securitycenter_v1/types/securitycenter_service.py index 6033a41dcb5a..b36295d4c14a 100644 --- a/packages/google-cloud-securitycenter/google/cloud/securitycenter_v1/types/securitycenter_service.py +++ b/packages/google-cloud-securitycenter/google/cloud/securitycenter_v1/types/securitycenter_service.py @@ -16,6 +16,7 @@ import proto # type: ignore from google.cloud.securitycenter_v1.types import asset as gcs_asset +from google.cloud.securitycenter_v1.types import external_system as gcs_external_system from google.cloud.securitycenter_v1.types import finding as gcs_finding from google.cloud.securitycenter_v1.types import folder from google.cloud.securitycenter_v1.types import mute_config as gcs_mute_config @@ -66,6 +67,7 @@ "SetFindingStateRequest", "SetMuteRequest", "RunAssetDiscoveryRequest", + "UpdateExternalSystemRequest", "UpdateFindingRequest", "UpdateMuteConfigRequest", "UpdateNotificationConfigRequest", @@ -1436,6 +1438,27 @@ class RunAssetDiscoveryRequest(proto.Message): parent = proto.Field(proto.STRING, number=1,) +class UpdateExternalSystemRequest(proto.Message): + r"""Request message for updating a ExternalSystem resource. + + Attributes: + external_system (google.cloud.securitycenter_v1.types.ExternalSystem): + Required. The external system resource to + update. + update_mask (google.protobuf.field_mask_pb2.FieldMask): + The FieldMask to use when updating the + external system resource. + If empty all mutable fields will be updated. + """ + + external_system = proto.Field( + proto.MESSAGE, number=1, message=gcs_external_system.ExternalSystem, + ) + update_mask = proto.Field( + proto.MESSAGE, number=2, message=field_mask_pb2.FieldMask, + ) + + class UpdateFindingRequest(proto.Message): r"""Request message for updating or creating a finding. diff --git a/packages/google-cloud-securitycenter/scripts/fixup_securitycenter_v1_keywords.py b/packages/google-cloud-securitycenter/scripts/fixup_securitycenter_v1_keywords.py index 5649718b7983..ee3fea5bb7ed 100644 --- a/packages/google-cloud-securitycenter/scripts/fixup_securitycenter_v1_keywords.py +++ b/packages/google-cloud-securitycenter/scripts/fixup_securitycenter_v1_keywords.py @@ -63,6 +63,7 @@ class securitycenterCallTransformer(cst.CSTTransformer): 'set_iam_policy': ('resource', 'policy', ), 'set_mute': ('name', 'mute', ), 'test_iam_permissions': ('resource', 'permissions', ), + 'update_external_system': ('external_system', 'update_mask', ), 'update_finding': ('finding', 'update_mask', ), 'update_mute_config': ('mute_config', 'update_mask', ), 'update_notification_config': ('notification_config', 'update_mask', ), diff --git a/packages/google-cloud-securitycenter/tests/unit/gapic/securitycenter_v1/test_security_center.py b/packages/google-cloud-securitycenter/tests/unit/gapic/securitycenter_v1/test_security_center.py index 447cd0bc11cd..210e6dbb658c 100644 --- a/packages/google-cloud-securitycenter/tests/unit/gapic/securitycenter_v1/test_security_center.py +++ b/packages/google-cloud-securitycenter/tests/unit/gapic/securitycenter_v1/test_security_center.py @@ -40,6 +40,8 @@ from google.cloud.securitycenter_v1.services.security_center import SecurityCenterClient from google.cloud.securitycenter_v1.services.security_center import pagers from google.cloud.securitycenter_v1.services.security_center import transports +from google.cloud.securitycenter_v1.types import external_system +from google.cloud.securitycenter_v1.types import external_system as gcs_external_system from google.cloud.securitycenter_v1.types import finding from google.cloud.securitycenter_v1.types import finding as gcs_finding from google.cloud.securitycenter_v1.types import indicator @@ -7032,6 +7034,266 @@ async def test_test_iam_permissions_flattened_error_async(): ) +def test_update_external_system( + transport: str = "grpc", + request_type=securitycenter_service.UpdateExternalSystemRequest, +): + client = SecurityCenterClient( + credentials=ga_credentials.AnonymousCredentials(), transport=transport, + ) + + # Everything is optional in proto3 as far as the runtime is concerned, + # and we are mocking out the actual API, so just send an empty request. + request = request_type() + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.update_external_system), "__call__" + ) as call: + # Designate an appropriate return value for the call. + call.return_value = gcs_external_system.ExternalSystem( + name="name_value", + assignees=["assignees_value"], + external_uid="external_uid_value", + status="status_value", + ) + response = client.update_external_system(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) == 1 + _, args, _ = call.mock_calls[0] + assert args[0] == securitycenter_service.UpdateExternalSystemRequest() + + # Establish that the response is the type that we expect. + assert isinstance(response, gcs_external_system.ExternalSystem) + assert response.name == "name_value" + assert response.assignees == ["assignees_value"] + assert response.external_uid == "external_uid_value" + assert response.status == "status_value" + + +def test_update_external_system_from_dict(): + test_update_external_system(request_type=dict) + + +def test_update_external_system_empty_call(): + # This test is a coverage failsafe to make sure that totally empty calls, + # i.e. request == None and no flattened fields passed, work. + client = SecurityCenterClient( + credentials=ga_credentials.AnonymousCredentials(), transport="grpc", + ) + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.update_external_system), "__call__" + ) as call: + client.update_external_system() + call.assert_called() + _, args, _ = call.mock_calls[0] + assert args[0] == securitycenter_service.UpdateExternalSystemRequest() + + +@pytest.mark.asyncio +async def test_update_external_system_async( + transport: str = "grpc_asyncio", + request_type=securitycenter_service.UpdateExternalSystemRequest, +): + client = SecurityCenterAsyncClient( + credentials=ga_credentials.AnonymousCredentials(), transport=transport, + ) + + # Everything is optional in proto3 as far as the runtime is concerned, + # and we are mocking out the actual API, so just send an empty request. + request = request_type() + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.update_external_system), "__call__" + ) as call: + # Designate an appropriate return value for the call. + call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( + gcs_external_system.ExternalSystem( + name="name_value", + assignees=["assignees_value"], + external_uid="external_uid_value", + status="status_value", + ) + ) + response = await client.update_external_system(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) + _, args, _ = call.mock_calls[0] + assert args[0] == securitycenter_service.UpdateExternalSystemRequest() + + # Establish that the response is the type that we expect. + assert isinstance(response, gcs_external_system.ExternalSystem) + assert response.name == "name_value" + assert response.assignees == ["assignees_value"] + assert response.external_uid == "external_uid_value" + assert response.status == "status_value" + + +@pytest.mark.asyncio +async def test_update_external_system_async_from_dict(): + await test_update_external_system_async(request_type=dict) + + +def test_update_external_system_field_headers(): + client = SecurityCenterClient(credentials=ga_credentials.AnonymousCredentials(),) + + # Any value that is part of the HTTP/1.1 URI should be sent as + # a field header. Set these to a non-empty value. + request = securitycenter_service.UpdateExternalSystemRequest() + + request.external_system.name = "external_system.name/value" + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.update_external_system), "__call__" + ) as call: + call.return_value = gcs_external_system.ExternalSystem() + client.update_external_system(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) == 1 + _, args, _ = call.mock_calls[0] + assert args[0] == request + + # Establish that the field header was sent. + _, _, kw = call.mock_calls[0] + assert ( + "x-goog-request-params", + "external_system.name=external_system.name/value", + ) in kw["metadata"] + + +@pytest.mark.asyncio +async def test_update_external_system_field_headers_async(): + client = SecurityCenterAsyncClient( + credentials=ga_credentials.AnonymousCredentials(), + ) + + # Any value that is part of the HTTP/1.1 URI should be sent as + # a field header. Set these to a non-empty value. + request = securitycenter_service.UpdateExternalSystemRequest() + + request.external_system.name = "external_system.name/value" + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.update_external_system), "__call__" + ) as call: + call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( + gcs_external_system.ExternalSystem() + ) + await client.update_external_system(request) + + # Establish that the underlying gRPC stub method was called. + assert len(call.mock_calls) + _, args, _ = call.mock_calls[0] + assert args[0] == request + + # Establish that the field header was sent. + _, _, kw = call.mock_calls[0] + assert ( + "x-goog-request-params", + "external_system.name=external_system.name/value", + ) in kw["metadata"] + + +def test_update_external_system_flattened(): + client = SecurityCenterClient(credentials=ga_credentials.AnonymousCredentials(),) + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.update_external_system), "__call__" + ) as call: + # Designate an appropriate return value for the call. + call.return_value = gcs_external_system.ExternalSystem() + # Call the method with a truthy value for each flattened field, + # using the keyword arguments to the method. + client.update_external_system( + external_system=gcs_external_system.ExternalSystem(name="name_value"), + update_mask=field_mask_pb2.FieldMask(paths=["paths_value"]), + ) + + # Establish that the underlying call was made with the expected + # request object values. + assert len(call.mock_calls) == 1 + _, args, _ = call.mock_calls[0] + arg = args[0].external_system + mock_val = gcs_external_system.ExternalSystem(name="name_value") + assert arg == mock_val + arg = args[0].update_mask + mock_val = field_mask_pb2.FieldMask(paths=["paths_value"]) + assert arg == mock_val + + +def test_update_external_system_flattened_error(): + client = SecurityCenterClient(credentials=ga_credentials.AnonymousCredentials(),) + + # Attempting to call a method with both a request object and flattened + # fields is an error. + with pytest.raises(ValueError): + client.update_external_system( + securitycenter_service.UpdateExternalSystemRequest(), + external_system=gcs_external_system.ExternalSystem(name="name_value"), + update_mask=field_mask_pb2.FieldMask(paths=["paths_value"]), + ) + + +@pytest.mark.asyncio +async def test_update_external_system_flattened_async(): + client = SecurityCenterAsyncClient( + credentials=ga_credentials.AnonymousCredentials(), + ) + + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.update_external_system), "__call__" + ) as call: + # Designate an appropriate return value for the call. + call.return_value = gcs_external_system.ExternalSystem() + + call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( + gcs_external_system.ExternalSystem() + ) + # Call the method with a truthy value for each flattened field, + # using the keyword arguments to the method. + response = await client.update_external_system( + external_system=gcs_external_system.ExternalSystem(name="name_value"), + update_mask=field_mask_pb2.FieldMask(paths=["paths_value"]), + ) + + # Establish that the underlying call was made with the expected + # request object values. + assert len(call.mock_calls) + _, args, _ = call.mock_calls[0] + arg = args[0].external_system + mock_val = gcs_external_system.ExternalSystem(name="name_value") + assert arg == mock_val + arg = args[0].update_mask + mock_val = field_mask_pb2.FieldMask(paths=["paths_value"]) + assert arg == mock_val + + +@pytest.mark.asyncio +async def test_update_external_system_flattened_error_async(): + client = SecurityCenterAsyncClient( + credentials=ga_credentials.AnonymousCredentials(), + ) + + # Attempting to call a method with both a request object and flattened + # fields is an error. + with pytest.raises(ValueError): + await client.update_external_system( + securitycenter_service.UpdateExternalSystemRequest(), + external_system=gcs_external_system.ExternalSystem(name="name_value"), + update_mask=field_mask_pb2.FieldMask(paths=["paths_value"]), + ) + + def test_update_finding( transport: str = "grpc", request_type=securitycenter_service.UpdateFindingRequest ): @@ -8651,6 +8913,7 @@ def test_security_center_base_transport(): "set_mute", "set_iam_policy", "test_iam_permissions", + "update_external_system", "update_finding", "update_mute_config", "update_notification_config", @@ -9006,10 +9269,41 @@ def test_parse_asset_path(): assert expected == actual -def test_finding_path(): +def test_external_system_path(): organization = "oyster" source = "nudibranch" finding = "cuttlefish" + externalsystem = "mussel" + expected = "organizations/{organization}/sources/{source}/findings/{finding}/externalSystems/{externalsystem}".format( + organization=organization, + source=source, + finding=finding, + externalsystem=externalsystem, + ) + actual = SecurityCenterClient.external_system_path( + organization, source, finding, externalsystem + ) + assert expected == actual + + +def test_parse_external_system_path(): + expected = { + "organization": "winkle", + "source": "nautilus", + "finding": "scallop", + "externalsystem": "abalone", + } + path = SecurityCenterClient.external_system_path(**expected) + + # Check that the path construction is reversible. + actual = SecurityCenterClient.parse_external_system_path(path) + assert expected == actual + + +def test_finding_path(): + organization = "squid" + source = "clam" + finding = "whelk" expected = "organizations/{organization}/sources/{source}/findings/{finding}".format( organization=organization, source=source, finding=finding, ) @@ -9019,9 +9313,9 @@ def test_finding_path(): def test_parse_finding_path(): expected = { - "organization": "mussel", - "source": "winkle", - "finding": "nautilus", + "organization": "octopus", + "source": "oyster", + "finding": "nudibranch", } path = SecurityCenterClient.finding_path(**expected) @@ -9031,8 +9325,8 @@ def test_parse_finding_path(): def test_mute_config_path(): - organization = "scallop" - mute_config = "abalone" + organization = "cuttlefish" + mute_config = "mussel" expected = "organizations/{organization}/muteConfigs/{mute_config}".format( organization=organization, mute_config=mute_config, ) @@ -9042,8 +9336,8 @@ def test_mute_config_path(): def test_parse_mute_config_path(): expected = { - "organization": "squid", - "mute_config": "clam", + "organization": "winkle", + "mute_config": "nautilus", } path = SecurityCenterClient.mute_config_path(**expected) @@ -9053,8 +9347,8 @@ def test_parse_mute_config_path(): def test_notification_config_path(): - organization = "whelk" - notification_config = "octopus" + organization = "scallop" + notification_config = "abalone" expected = "organizations/{organization}/notificationConfigs/{notification_config}".format( organization=organization, notification_config=notification_config, ) @@ -9066,8 +9360,8 @@ def test_notification_config_path(): def test_parse_notification_config_path(): expected = { - "organization": "oyster", - "notification_config": "nudibranch", + "organization": "squid", + "notification_config": "clam", } path = SecurityCenterClient.notification_config_path(**expected) @@ -9077,7 +9371,7 @@ def test_parse_notification_config_path(): def test_organization_settings_path(): - organization = "cuttlefish" + organization = "whelk" expected = "organizations/{organization}/organizationSettings".format( organization=organization, ) @@ -9087,7 +9381,7 @@ def test_organization_settings_path(): def test_parse_organization_settings_path(): expected = { - "organization": "mussel", + "organization": "octopus", } path = SecurityCenterClient.organization_settings_path(**expected) @@ -9097,8 +9391,8 @@ def test_parse_organization_settings_path(): def test_security_marks_path(): - organization = "winkle" - asset = "nautilus" + organization = "oyster" + asset = "nudibranch" expected = "organizations/{organization}/assets/{asset}/securityMarks".format( organization=organization, asset=asset, ) @@ -9108,8 +9402,8 @@ def test_security_marks_path(): def test_parse_security_marks_path(): expected = { - "organization": "scallop", - "asset": "abalone", + "organization": "cuttlefish", + "asset": "mussel", } path = SecurityCenterClient.security_marks_path(**expected) @@ -9119,8 +9413,8 @@ def test_parse_security_marks_path(): def test_source_path(): - organization = "squid" - source = "clam" + organization = "winkle" + source = "nautilus" expected = "organizations/{organization}/sources/{source}".format( organization=organization, source=source, ) @@ -9130,8 +9424,8 @@ def test_source_path(): def test_parse_source_path(): expected = { - "organization": "whelk", - "source": "octopus", + "organization": "scallop", + "source": "abalone", } path = SecurityCenterClient.source_path(**expected) @@ -9141,8 +9435,8 @@ def test_parse_source_path(): def test_topic_path(): - project = "oyster" - topic = "nudibranch" + project = "squid" + topic = "clam" expected = "projects/{project}/topics/{topic}".format(project=project, topic=topic,) actual = SecurityCenterClient.topic_path(project, topic) assert expected == actual @@ -9150,8 +9444,8 @@ def test_topic_path(): def test_parse_topic_path(): expected = { - "project": "cuttlefish", - "topic": "mussel", + "project": "whelk", + "topic": "octopus", } path = SecurityCenterClient.topic_path(**expected) @@ -9161,7 +9455,7 @@ def test_parse_topic_path(): def test_common_billing_account_path(): - billing_account = "winkle" + billing_account = "oyster" expected = "billingAccounts/{billing_account}".format( billing_account=billing_account, ) @@ -9171,7 +9465,7 @@ def test_common_billing_account_path(): def test_parse_common_billing_account_path(): expected = { - "billing_account": "nautilus", + "billing_account": "nudibranch", } path = SecurityCenterClient.common_billing_account_path(**expected) @@ -9181,7 +9475,7 @@ def test_parse_common_billing_account_path(): def test_common_folder_path(): - folder = "scallop" + folder = "cuttlefish" expected = "folders/{folder}".format(folder=folder,) actual = SecurityCenterClient.common_folder_path(folder) assert expected == actual @@ -9189,7 +9483,7 @@ def test_common_folder_path(): def test_parse_common_folder_path(): expected = { - "folder": "abalone", + "folder": "mussel", } path = SecurityCenterClient.common_folder_path(**expected) @@ -9199,7 +9493,7 @@ def test_parse_common_folder_path(): def test_common_organization_path(): - organization = "squid" + organization = "winkle" expected = "organizations/{organization}".format(organization=organization,) actual = SecurityCenterClient.common_organization_path(organization) assert expected == actual @@ -9207,7 +9501,7 @@ def test_common_organization_path(): def test_parse_common_organization_path(): expected = { - "organization": "clam", + "organization": "nautilus", } path = SecurityCenterClient.common_organization_path(**expected) @@ -9217,7 +9511,7 @@ def test_parse_common_organization_path(): def test_common_project_path(): - project = "whelk" + project = "scallop" expected = "projects/{project}".format(project=project,) actual = SecurityCenterClient.common_project_path(project) assert expected == actual @@ -9225,7 +9519,7 @@ def test_common_project_path(): def test_parse_common_project_path(): expected = { - "project": "octopus", + "project": "abalone", } path = SecurityCenterClient.common_project_path(**expected) @@ -9235,8 +9529,8 @@ def test_parse_common_project_path(): def test_common_location_path(): - project = "oyster" - location = "nudibranch" + project = "squid" + location = "clam" expected = "projects/{project}/locations/{location}".format( project=project, location=location, ) @@ -9246,8 +9540,8 @@ def test_common_location_path(): def test_parse_common_location_path(): expected = { - "project": "cuttlefish", - "location": "mussel", + "project": "whelk", + "location": "octopus", } path = SecurityCenterClient.common_location_path(**expected)