# -*- coding: utf-8 -*- # # Copyright 2023 Google LLC. All Rights Reserved. # # 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. """Shared utility functions for Cloud SCC muteconfigs commands.""" from __future__ import absolute_import from __future__ import division from __future__ import print_function from __future__ import unicode_literals import argparse import re import typing from googlecloudsdk.command_lib.scc import errors from googlecloudsdk.command_lib.scc import util as scc_util from googlecloudsdk.core.util import times from googlecloudsdk.generated_clients.apis.securitycenter.v1 import securitycenter_v1_messages from googlecloudsdk.generated_clients.apis.securitycenter.v2 import securitycenter_v2_messages def ValidateAndGetParent(args): """Validates parent.""" if args.organization is not None: name_pattern = re.compile("^organizations/[0-9]{1,19}$") id_pattern = re.compile("^[0-9]{1,19}$") if name_pattern.match(args.organization): return args.organization if id_pattern.match(args.organization): return f"organizations/{args.organization}" if "/" in args.organization: raise errors.InvalidSCCInputError( "When providing a full resource path, it must include the pattern " "'^organizations/[0-9]{1,19}$'." ) raise errors.InvalidSCCInputError( "Organization does not match the pattern '^[0-9]{1,19}$'." ) if args.folder is not None: if "/" in args.folder: pattern = re.compile("^folders/.*$") if not pattern.match(args.folder): raise errors.InvalidSCCInputError( "When providing a full resource path, it must include the pattern " "'^folders/.*$'." ) else: return args.folder else: return f"folders/{args.folder}" if args.project is not None: if "/" in args.project: pattern = re.compile("^projects/.*$") if not pattern.match(args.project): raise errors.InvalidSCCInputError( "When providing a full resource path, it must include the pattern " "'^projects/.*$'." ) else: return args.project else: return f"projects/{args.project}" def ValidateAndGetMuteConfigId(args): """Validate muteConfigId.""" mute_config_id = args.mute_config pattern = re.compile("^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$") if not pattern.match(mute_config_id): raise errors.InvalidSCCInputError( "Mute config id does not match the pattern" " '^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$'." ) else: return mute_config_id def ValidateAndGetMuteConfigFullResourceName(args, version): """Validates muteConfig full resource name.""" mute_config = args.mute_config resource_pattern = re.compile( "(organizations|projects|folders)/.*/muteConfigs/[a-z]([a-z0-9-]{0,61}[a-z0-9])?$" ) regionalized_resource_pattern = re.compile( "(organizations|projects|folders)/.*/locations/.*/muteConfigs/[a-z]([a-z0-9-]{0,61}[a-z0-9])?$" ) if regionalized_resource_pattern.match(mute_config): return mute_config # Add location to parent if user didn't specify location in the mute_config # but wants to use v2. if resource_pattern.match(mute_config): if version == "v2": mute_config_components = mute_config.split("/") return f"{mute_config_components[0]}/{mute_config_components[1]}/locations/{args.location}/{mute_config_components[2]}/{mute_config_components[3]}" else: return mute_config # TODO: b/282774006 - Update message to include information about location. raise errors.InvalidSCCInputError( "Mute config must match the full resource name, or `--organization=`," " `--folder=` or `--project=` must be provided." ) def GetMuteConfigIdFromFullResourceName(mute_config): """Gets muteConfig id from the full resource name.""" mute_config_components = mute_config.split("/") return mute_config_components[len(mute_config_components) - 1] def GetParentFromFullResourceName(mute_config, version): """Gets parent from the full resource name.""" mute_config_components = mute_config.split("/") if version == "v1": # Return parent as "organizations/{organizationsID}" # or "folders/{foldersID}" # or "projects/{projectsID}" return f"{mute_config_components[0]}/{mute_config_components[1]}" if version == "v2": # Return parent as "organizations/{organizationsID}/locations/{locationsID}" # or "folders/{foldersID}/locations/{locationsID}" # or "projects/{projectsID}/locations/{locationsID}" return f"{mute_config_components[0]}/{mute_config_components[1]}/{mute_config_components[2]}/{mute_config_components[3]}" def GenerateMuteConfigName(args, req, version): """Generates the name of the mute config.""" parent = ValidateAndGetParent(args) if parent is not None: if version == "v2": parent = ValidateAndGetRegionalizedParent(args, parent) mute_config_id = ValidateAndGetMuteConfigId(args) req.name = f"{parent}/muteConfigs/{mute_config_id}" else: args.location = scc_util.ValidateAndGetLocation(args, version) mute_config = ValidateAndGetMuteConfigFullResourceName(args, version) req.name = mute_config return req def ValidateAndGetRegionalizedParent(args, parent): """Appends location to parent.""" if args.location is not None: if "/" in args.location: pattern = re.compile("^locations/.*$") if not pattern.match(args.location): raise errors.InvalidSCCInputError( "When providing a full resource path, it must include the pattern " "'^locations/.*$'." ) else: return f"{parent}/{args.location}" else: return f"{parent}/locations/{args.location}" def ValidateAndGetType(args: argparse.Namespace, version: str) -> typing.Union[ securitycenter_v1_messages.GoogleCloudSecuritycenterV1MuteConfig.TypeValueValuesEnum, securitycenter_v2_messages.GoogleCloudSecuritycenterV2MuteConfig.TypeValueValuesEnum, ]: """Parses and validates type.""" mute_config_class = ( securitycenter_v2_messages.GoogleCloudSecuritycenterV2MuteConfig if version == "v2" else securitycenter_v1_messages.GoogleCloudSecuritycenterV1MuteConfig ) if args.type == "static": return mute_config_class.TypeValueValuesEnum.STATIC elif args.type == "dynamic": return mute_config_class.TypeValueValuesEnum.DYNAMIC raise errors.InvalidSCCInputError( "Type must be either 'static' or 'dynamic'." ) def ValidateAndGetExpiryTime( args: argparse.Namespace, ) -> typing.Union[str, None]: """Parses and validates expiry time.""" if args.expiry_time is None: return None if hasattr(args, "type") and args.type != "dynamic": raise errors.InvalidSCCInputError( "Expiry time is only supported for 'dynamic' mute configs." ) try: expiry_time_dt = times.ParseDateTime(args.expiry_time) return times.FormatDateTime(expiry_time_dt) except (times.DateTimeSyntaxError, times.DateTimeValueError): raise errors.InvalidSCCInputError( "Invalid expiry time. See `$ gcloud topic datetimes` for information on" " supported time formats." )