426 lines
13 KiB
Python
426 lines
13 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.
|
|
"""Common flag parsing for management gcloud."""
|
|
import json
|
|
import re
|
|
|
|
from apitools.base.py import encoding
|
|
from googlecloudsdk.api_lib.resource_manager import folders
|
|
from googlecloudsdk.command_lib.scc.manage import constants
|
|
from googlecloudsdk.command_lib.scc.manage import errors
|
|
from googlecloudsdk.core import properties
|
|
from googlecloudsdk.core import resources
|
|
from googlecloudsdk.core import yaml
|
|
from googlecloudsdk.generated_clients.apis.securitycentermanagement.v1 import securitycentermanagement_v1_messages as messages
|
|
|
|
_CUSTOM_MODULE_ID_REGEX = re.compile('[0-9]{1,20}')
|
|
|
|
|
|
def GetParentResourceNameFromArgs(args) -> str:
|
|
"""Returns the relative path to the parent from args.
|
|
|
|
Args:
|
|
args: command line args.
|
|
|
|
Returns:
|
|
The relative path. e.g. 'projects/foo/locations/global',
|
|
'folders/1234/locations/global'.
|
|
"""
|
|
if args.parent:
|
|
return f'{_ParseParent(args.parent).RelativeName()}/locations/global'
|
|
|
|
return f'{_GetParentResourceFromArgs(args).RelativeName()}/locations/global'
|
|
|
|
|
|
def _GetParentResourceFromArgs(args):
|
|
if args.organization:
|
|
return resources.REGISTRY.Parse(
|
|
args.organization, collection='cloudresourcemanager.organizations'
|
|
)
|
|
elif args.folder:
|
|
return folders.FoldersRegistry().Parse(
|
|
args.folder, collection='cloudresourcemanager.folders'
|
|
)
|
|
else:
|
|
return resources.REGISTRY.Parse(
|
|
args.project or properties.VALUES.core.project.Get(required=True),
|
|
collection='cloudresourcemanager.projects',
|
|
)
|
|
|
|
|
|
def GetServiceNameFromArgs(args) -> str:
|
|
"""Returns the specified service name from args if it exists.
|
|
|
|
Otherwise, an exception is raised detailing the parsing error along with the
|
|
expectation.
|
|
|
|
Args:
|
|
args: The argument input as the gcloud command.
|
|
|
|
Raises:
|
|
InvalidServiceNameError: the specified service name was invalid.
|
|
"""
|
|
|
|
parent = GetParentResourceNameFromArgs(args)
|
|
|
|
maybe_service_name_or_abbr = args.service_name.lower()
|
|
service = constants.SERVICE_INVENTORY.get(maybe_service_name_or_abbr)
|
|
|
|
if service:
|
|
return f'{parent}/{constants.SERVICE_RESOURCE_PLURAL_NAME}/{service.name}'
|
|
else:
|
|
raise errors.InvalidServiceNameError(args.service_name)
|
|
|
|
|
|
def GetModuleIdFromArgs(args) -> str:
|
|
"""Returns the module id from args."""
|
|
if not args.module_id_or_name:
|
|
raise errors.InvalidCustomModuleIdError(None)
|
|
|
|
match = _CUSTOM_MODULE_ID_REGEX.fullmatch(args.module_id_or_name)
|
|
|
|
if match:
|
|
return match[0]
|
|
else:
|
|
raise errors.InvalidCustomModuleIdError(args.module_id_or_name)
|
|
|
|
|
|
def GetModuleNameFromArgs(args, module_type: constants.CustomModuleType) -> str:
|
|
"""Returns the specified module name from args if it exists.
|
|
|
|
Otherwise, an exception is raised detailing the parsing error along with the
|
|
expectation.
|
|
|
|
Args:
|
|
args: the args
|
|
module_type: the module type (see
|
|
googlecloudsdk.command_lib.scc.manage.constants)
|
|
|
|
Raises:
|
|
MissingCustomModuleNameOrIdError: no module name or id was specified.
|
|
InvalidCustomModuleNameError: the specified module name was invalid.
|
|
InvalidCustomModuleIdError: the specified module id was invalid.
|
|
"""
|
|
|
|
if not args.module_id_or_name:
|
|
raise errors.MissingCustomModuleNameOrIdError()
|
|
|
|
# First try to see if we can parse a resource name
|
|
collections = [
|
|
f'securitycentermanagement.organizations.locations.{module_type}',
|
|
f'securitycentermanagement.projects.locations.{module_type}',
|
|
f'securitycentermanagement.folders.locations.{module_type}',
|
|
]
|
|
|
|
is_possible_resource_name = (
|
|
_IsPossibleResourceName(args.module_id_or_name)
|
|
or len(args.GetSpecifiedArgNames()) == 1
|
|
)
|
|
|
|
for collection in collections:
|
|
try:
|
|
return resources.REGISTRY.Parse(
|
|
args.module_id_or_name, collection=collection
|
|
).RelativeName()
|
|
except resources.RequiredFieldOmittedException:
|
|
pass
|
|
|
|
if is_possible_resource_name:
|
|
# The error messages provided by the default gcloud parsing are awful so we
|
|
# detect a resource name misformatting here and print a better error
|
|
raise errors.InvalidCustomModuleNameError(
|
|
args.module_id_or_name, module_type
|
|
)
|
|
|
|
parent = GetParentResourceNameFromArgs(args)
|
|
module_id = GetModuleIdFromArgs(args)
|
|
|
|
return f'{parent}/{module_type}/{module_id}'
|
|
|
|
|
|
def _ParseParent(parent: str) -> str:
|
|
"""Extracts parent name from a string of the form {organizations|projects|folders}/<id>."""
|
|
|
|
if parent.startswith('organizations/'):
|
|
return resources.REGISTRY.Parse(
|
|
parent, collection='cloudresourcemanager.organizations'
|
|
)
|
|
elif parent.startswith('folders/'):
|
|
return folders.FoldersRegistry().Parse(
|
|
parent, collection='cloudresourcemanager.folders'
|
|
)
|
|
elif parent.startswith('projects/'):
|
|
return resources.REGISTRY.Parse(
|
|
parent,
|
|
collection='cloudresourcemanager.projects',
|
|
)
|
|
else:
|
|
raise errors.InvalidParentError(parent)
|
|
|
|
|
|
def _IsPossibleResourceName(name: str) -> bool:
|
|
return (
|
|
name.startswith('organizations')
|
|
or name.startswith('projects')
|
|
or name.startswith('folders')
|
|
)
|
|
|
|
|
|
def GetCustomConfigFromArgs(file):
|
|
"""Process the custom config file for the custom module."""
|
|
if file is not None:
|
|
try:
|
|
config_dict = yaml.load(file)
|
|
return encoding.DictToMessage(config_dict, messages.CustomConfig)
|
|
except yaml.YAMLParseError as ype:
|
|
raise errors.InvalidCustomConfigFileError(
|
|
'Error parsing custom config file [{}]'.format(ype)
|
|
)
|
|
|
|
|
|
def GetTestResourceFromArgs(file):
|
|
"""Process the test resource data file for the custom module to test against."""
|
|
try:
|
|
resource_dict = yaml.load(file)
|
|
|
|
return encoding.DictToMessage(resource_dict, messages.SimulatedResource)
|
|
except yaml.YAMLParseError as ype:
|
|
raise errors.InvalidResourceFileError(
|
|
'Error parsing resource file [{}]'.format(ype)
|
|
)
|
|
|
|
|
|
def GetConfigValueFromArgs(file):
|
|
"""Process the config custom file for the custom module."""
|
|
if file is not None:
|
|
try:
|
|
config = json.loads(file)
|
|
return encoding.DictToMessage(
|
|
config, messages.EventThreatDetectionCustomModule.ConfigValue
|
|
)
|
|
except json.JSONDecodeError as e:
|
|
raise errors.InvalidConfigValueFileError(
|
|
'Error parsing config value file [{}]'.format(e)
|
|
)
|
|
else:
|
|
return None
|
|
|
|
|
|
def ParseJSONFile(file):
|
|
"""Converts the contents of a JSON file into a string."""
|
|
if file is not None:
|
|
try:
|
|
config = json.loads(file)
|
|
return json.dumps(config)
|
|
except json.JSONDecodeError as e:
|
|
raise errors.InvalidConfigValueFileError(
|
|
'Error parsing config value file [{}]'.format(e)
|
|
)
|
|
else:
|
|
return None
|
|
|
|
|
|
def GetEnablementStateFromArgs(
|
|
enablement_state: str,
|
|
module_type: constants.CustomModuleType
|
|
):
|
|
"""Parse the enablement state."""
|
|
if module_type == constants.CustomModuleType.SHA:
|
|
state_enum = (
|
|
messages.SecurityHealthAnalyticsCustomModule.EnablementStateValueValuesEnum
|
|
)
|
|
elif module_type == constants.CustomModuleType.ETD:
|
|
state_enum = (
|
|
messages.EventThreatDetectionCustomModule.EnablementStateValueValuesEnum
|
|
)
|
|
else:
|
|
raise errors.InvalidModuleTypeError(
|
|
f'Module type "{module_type}" is not a valid module type.'
|
|
)
|
|
|
|
if enablement_state is None:
|
|
raise errors.InvalidEnablementStateError(
|
|
'Error parsing enablement state. Enablement state cannot be empty.'
|
|
)
|
|
|
|
state = enablement_state.upper()
|
|
|
|
if state == 'ENABLED':
|
|
return state_enum.ENABLED
|
|
elif state == 'DISABLED':
|
|
return state_enum.DISABLED
|
|
elif state == 'INHERITED':
|
|
return state_enum.INHERITED
|
|
else:
|
|
raise errors.InvalidEnablementStateError(
|
|
f'Error parsing enablement state. "{state}" is not a valid enablement'
|
|
' state. Please provide one of ENABLED, DISABLED, or INHERITED.'
|
|
)
|
|
|
|
|
|
def CreateUpdateMaskFromArgs(args):
|
|
"""Create an update mask with the args given."""
|
|
if args.enablement_state is not None and args.custom_config_file is not None:
|
|
return 'enablement_state,custom_config'
|
|
elif args.enablement_state is not None:
|
|
return 'enablement_state'
|
|
elif args.custom_config_file is not None:
|
|
return 'custom_config'
|
|
else:
|
|
raise errors.InvalidUpdateMaskInputError(
|
|
'Error parsing Update Mask. Either a custom configuration or an'
|
|
' enablement state (or both) must be provided to update the custom'
|
|
' module.'
|
|
)
|
|
|
|
|
|
def GetModuleConfigValueFromArgs(file: str):
|
|
"""Process the module config file for the service."""
|
|
if file is not None:
|
|
try:
|
|
config = yaml.load(file)
|
|
return encoding.DictToMessage(
|
|
config, messages.SecurityCenterService.ModulesValue
|
|
)
|
|
except (yaml.YAMLParseError, AttributeError) as ype:
|
|
raise errors.InvalidConfigValueFileError(
|
|
f'Error parsing config value file [{ype}]'
|
|
)
|
|
else:
|
|
return None
|
|
|
|
|
|
def GetServiceEnablementStateFromArgs(enablement_state: str):
|
|
"""Parse the service enablement state."""
|
|
state_enum = (
|
|
messages.SecurityCenterService.IntendedEnablementStateValueValuesEnum
|
|
)
|
|
|
|
if enablement_state is None:
|
|
return None
|
|
|
|
state = enablement_state.upper()
|
|
if state == 'ENABLED':
|
|
return state_enum.ENABLED
|
|
elif state == 'DISABLED':
|
|
return state_enum.DISABLED
|
|
elif state == 'INHERITED':
|
|
return state_enum.INHERITED
|
|
else:
|
|
raise errors.InvalidEnablementStateError(
|
|
f'Error parsing enablement state. "{state}" is not a valid enablement'
|
|
' state. Please provide one of ENABLED, DISABLED, or INHERITED.'
|
|
)
|
|
|
|
|
|
def CreateUpdateMaskFromArgsForService(args):
|
|
"""Create an update mask with the args given for the given service."""
|
|
if args.enablement_state is not None and args.module_config_file is not None:
|
|
return 'intended_enablement_state,modules'
|
|
elif args.enablement_state is not None:
|
|
return 'intended_enablement_state'
|
|
elif args.module_config_file is not None:
|
|
return 'modules'
|
|
else:
|
|
raise errors.InvalidUpdateMaskInputError(
|
|
'Error parsing Update Mask. Either a module configuration or an'
|
|
' enablement state (or both) must be provided to update the service.'
|
|
)
|
|
|
|
|
|
def GetModuleListFromArgs(args) -> {str}:
|
|
"""Returns a list of module names from args."""
|
|
|
|
if not args.filter_modules:
|
|
return []
|
|
|
|
modules = args.filter_modules.strip('[]')
|
|
modules_list = modules.split(',')
|
|
modules_set = {module.strip() for module in modules_list}
|
|
|
|
return modules_set
|
|
|
|
|
|
def GetModuleNamePathFromArgs(
|
|
args, module_type: constants.CustomModuleType
|
|
) -> str:
|
|
"""Returns the specified module name path from args if it exists.
|
|
|
|
Args:
|
|
args: command line args.
|
|
module_type: the module type (see
|
|
googlecloudsdk.command_lib.scc.manage.constants)
|
|
|
|
Returns:
|
|
The relative path. e.g.
|
|
'organizations/1234/locations/global/{module_type}',
|
|
'projects/foo/locations/global/{module_type}'.
|
|
"""
|
|
if args.parent:
|
|
return (
|
|
f'{_ParseParentFlag(args.parent).RelativeName()}/locations/global/'
|
|
f'{module_type}'
|
|
)
|
|
|
|
return (
|
|
f'{_GetParentResourceFromArg(args).RelativeName()}/locations/global/'
|
|
f'{module_type}'
|
|
)
|
|
|
|
|
|
def _ParseParentFlag(parent: str) -> str:
|
|
"""Extracts parent name from {organizations|projects}/<id>.
|
|
|
|
Args:
|
|
parent: The parent string to parse.
|
|
|
|
Returns:
|
|
The relative path of the parent.
|
|
|
|
Raises:
|
|
InvalidParentFlagError: The provided parent string is invalid.
|
|
"""
|
|
|
|
if parent.startswith('organizations/'):
|
|
return resources.REGISTRY.Parse(
|
|
parent, collection='cloudresourcemanager.organizations'
|
|
)
|
|
if parent.startswith('projects/'):
|
|
return resources.REGISTRY.Parse(
|
|
parent,
|
|
collection='cloudresourcemanager.projects',
|
|
)
|
|
|
|
raise errors.InvalidParentFlagError(parent)
|
|
|
|
|
|
def _GetParentResourceFromArg(args):
|
|
"""Returns the parent resource from the given args.
|
|
|
|
Args:
|
|
args: command line args.
|
|
|
|
Returns:
|
|
The parent resource.
|
|
"""
|
|
if args.organization:
|
|
return resources.REGISTRY.Parse(
|
|
args.organization, collection='cloudresourcemanager.organizations'
|
|
)
|
|
return resources.REGISTRY.Parse(
|
|
args.project or properties.VALUES.core.project.Get(required=True),
|
|
collection='cloudresourcemanager.projects',
|
|
)
|