214 lines
7.5 KiB
Python
214 lines
7.5 KiB
Python
# -*- 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."
|
|
)
|