feat: Add new gcloud commands, API clients, and third-party libraries across various services.

This commit is contained in:
2026-01-01 20:26:35 +01:00
parent 5e23cbece0
commit a19e592eb7
25221 changed files with 8324611 additions and 0 deletions

View File

@@ -0,0 +1,224 @@
# -*- 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 flags definitions for finding commands."""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import re
import textwrap
from apitools.base.py import encoding
from googlecloudsdk.api_lib.scc import securitycenter_client as sc_client
from googlecloudsdk.calliope import actions
from googlecloudsdk.calliope import arg_parsers
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.scc import errors
from googlecloudsdk.command_lib.util.args import resource_args
from googlecloudsdk.command_lib.util.concepts import concept_parsers
from googlecloudsdk.core import properties
COMPARE_DURATION_FLAG = base.Argument(
'--compare-duration',
help="""
When compare_duration is set, the result's "state_change" attribute is
updated to indicate whether the finding had its state changed, the
finding's state remained unchanged, or if the finding was added during
the compare_duration period of time that precedes the read_time. This
is the time between (read_time - compare_duration) and read_time. The
state_change value is derived based on the presence and state of the
finding at the two points in time. Intermediate state changes between
the two times don't affect the result. For example, the results aren't
affected if the finding is made inactive and then active again.
Possible "state_change" values when compare_duration is specified:
* 'CHANGED': indicates that the finding was present at the start of
compare_duration, but changed its state at read_time.
* 'UNCHANGED': indicates that the finding was present at the start of
compare_duration and did not change state at read_time.
* 'ADDED': indicates that the finding was not present at the start of
compare_duration, but was present at read_time.
* 'REMOVED': indicates that the finding was present at the start of
compare_duration, but was not present at read_time.
If compare_duration is not specified, then the only possible
state_change is 'UNUSED', which will be the state_change set for all
findings present at read_time. If this field is set then 'state_change'
must be a specified field in 'group_by'. See $ gcloud topic datetimes
for information on supported duration formats.""",
action=actions.DeprecationAction(
'--compare-duration',
warn=textwrap.dedent("""\
The --compare-duration option is deprecated.
For more information, [see the deprecation notice](https://cloud.google.com/security-command-center/docs/release-notes#April_15_2024)
on the SCC release notes page."""),
removed=False,
),
)
EVENT_TIME_FLAG_NOT_REQUIRED = base.Argument(
'--event-time',
help="""Time at which the event took place. For example, if the finding
represents an open firewall it would capture the time the open firewall
was detected. If event-time is not provided, it will default to UTC
version of NOW. See `$ gcloud topic datetimes` for information on
supported time formats.""",
required=False,
)
EVENT_TIME_FLAG_REQUIRED = base.Argument(
'--event-time',
help="""Time at which the event took place. For example, if the finding
represents an open firewall it would capture the time the open firewall
was detected. If event-time is not provided, it will default to UTC
version of NOW. See `$ gcloud topic datetimes` for information on
supported time formats.""",
required=True,
)
EXTERNAL_URI_FLAG = base.Argument(
'--external-uri',
help="""URI that, if available, points to a web page outside of Cloud SCC (Security Command Center)
where additional information about the finding can be found. This field is guaranteed to be
either empty or a well formed URL.""",
)
SOURCE_FLAG = base.Argument(
'--source', help='Source id. Defaults to all sources.', default='-'
)
SOURCE_PROPERTIES_FLAG = base.Argument(
'--source-properties',
help="""Source specific properties. These properties are managed by the
source that writes the finding. The key names in the source_properties map
must be between 1 and 255 characters, and must start with a letter and
contain alphanumeric characters or underscores only. For example
"key1=val1,key2=val2" """,
metavar='KEY=VALUE',
type=arg_parsers.ArgDict(),
)
STATE_FLAG = base.ChoiceArgument(
'--state',
help_str='State is one of: [ACTIVE, INACTIVE].',
choices=['active', 'inactive', 'state-unspecified'],
)
FINDING_FLAG = base.Argument(
'finding',
help='ID of the finding or fully qualified identifier for the finding.'
)
def CreateFindingArg():
"""Create finding as positional resource."""
finding_spec_data = {
'name': 'finding',
'collection': 'securitycenter.organizations.sources.findings',
'attributes': [
{
'parameter_name': 'organizationsId',
'attribute_name': 'organization',
'help': """(Optional) If the full resource name isn't provided e.g. organizations/123, then provide the
organization id which is the suffix of the organization. Example: organizations/123, the id is
123.""",
'fallthroughs': [{
'hook': 'googlecloudsdk.command_lib.scc.findings.flags:GetDefaultOrganization',
'hint': """Set the organization property in configuration using `gcloud config set scc/organization`
if it is not specified in command line.""",
}],
},
{
'parameter_name': 'sourcesId',
'attribute_name': 'source',
'help': """(Optional) If the full resource name isn't provided e.g. organizations/123/sources/456, then
provide the source id which is the suffix of the source.
Example: organizations/123/sources/456, the id is 456.""",
},
{
'parameter_name': 'findingId',
'attribute_name': 'finding',
'help': """Optional) If the full resource name isn't provided e.g.
organizations/123/sources/456/findings/789, then provide the finding id which is the suffix of
the finding. Example: organizations/123/sources/456/findings/789, the id is 789.""",
},
],
'disable_auto_completers': 'false',
}
arg_specs = [
resource_args.GetResourcePresentationSpec(
verb='to be used for the SCC (Security Command Center) command',
name='finding',
required=True,
prefixes=False,
positional=True,
resource_data=finding_spec_data,
),
]
return concept_parsers.ConceptParser(arg_specs, [])
def GetDefaultOrganization():
"""Prepend organizations/ to org if necessary."""
resource_pattern = re.compile('organizations/[0-9]+')
id_pattern = re.compile('[0-9]+')
organization = properties.VALUES.scc.organization.Get()
if not resource_pattern.match(organization) and not id_pattern.match(
organization
):
raise errors.InvalidSCCInputError(
'Organization must match either organizations/[0-9]+ or [0-9]+.'
)
if resource_pattern.match(organization):
return organization
return 'organizations/' + organization
def ConvertSourceProperties(source_properties_dict):
"""Hook to capture "key1=val1,key2=val2" as SourceProperties object."""
messages = sc_client.GetMessages()
return encoding.DictToMessage(
source_properties_dict, messages.Finding.SourcePropertiesValue
)
def AddParentGroup(parser):
"""Adds a parent group to the parser."""
parent_group = parser.add_mutually_exclusive_group(required=False)
parent_group.add_argument(
'--organization',
help='The organization ID (e.g., 123) that contains the finding.',
)
parent_group.add_argument(
'--folder',
help='The folder ID (e.g., 456) that contains the finding.',
)
parent_group.add_argument(
'--project',
help='The project ID (e.g., example-project) that contains the finding.',
)
return parser

View File

@@ -0,0 +1,39 @@
# -*- coding: utf-8 -*- #
# Copyright 2019 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.
"""Declarative Resource Hooks for Cloud SCC's Findings."""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from googlecloudsdk.command_lib.util.apis import yaml_data
from googlecloudsdk.command_lib.util.args import resource_args
from googlecloudsdk.command_lib.util.concepts import concept_parsers
def AppendFindingArg():
"""Add finding as positional resource."""
finding_spec_data = yaml_data.ResourceYAMLData.FromPath("scc.finding")
arg_specs = [
resource_args.GetResourcePresentationSpec(
verb="to be used for the SCC (Security Command Center) command",
name="finding",
required=True,
prefixes=False,
positional=True,
resource_data=finding_spec_data.GetData()),
]
return [concept_parsers.ConceptParser(arg_specs, [])]

View File

@@ -0,0 +1,398 @@
# -*- 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 findings commands."""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import re
from apitools.base.py import encoding
from googlecloudsdk.api_lib.scc import securitycenter_client
from googlecloudsdk.command_lib.scc import errors
from googlecloudsdk.command_lib.scc import util as scc_util
from googlecloudsdk.core.util import times
def ValidateMutexOnFindingAndSourceAndOrganization(args):
"""Validates that only a full resource name or split arguments are provided."""
if "/" in args.finding and (
args.IsKnownAndSpecified("organization")
or args.IsKnownAndSpecified("source")
):
raise errors.InvalidSCCInputError(
"Only provide a full resource name "
"(organizations/123/sources/456/findings/789) or an --organization flag"
" and --sources flag, not both."
)
def GetFullFindingName(args, version):
"""Returns relative resource name for a finding name.
Args:
args: Argument namespace.
version: Api version.
Returns:
Relative resource name
if no location is specified the result will be one of the following forms
`organizations/{organization_id}/sources/{source_id}/finding/{finding_id}`
`folders/{folders_id}/sources/{source_id}/finding/{finding_id}`
`projects/{projects_id}/sources/{source_id}/finding/{finding_id}`
if a location is specified the result will be one of the following forms
`organizations/{organization_id}/sources/{source_id}/locations/{location_id}/finding/{finding_id}`
`folders/{folders_id}/sources/{source_id}/locations/{location_id}/finding/{finding_id}`
`projects/{projects_id}/sources/{source_id}/locations/{location_id}/finding/{finding_id}`
"""
resource_pattern = re.compile(
"(organizations|projects|folders)/.*/sources/[0-9-]+/findings/[a-zA-Z0-9]+$"
)
region_resource_pattern = re.compile(
"(organizations|projects|folders)/.*/sources/[0-9-]+/locations/.*/findings/[a-zA-Z0-9]+$"
)
id_pattern = re.compile("^[a-zA-Z0-9]+$")
if region_resource_pattern.match(args.finding):
# Handle finding id as full resource name.
return args.finding
if resource_pattern.match(args.finding):
if version == "v2":
return GetRegionalizedResourceName(args, version)
return args.finding
if id_pattern.match(args.finding):
return f"{GetFullSourceName(args, version)}/findings/{args.finding}"
raise errors.InvalidSCCInputError(
"Finding must match either the full resource name or only the finding id."
)
def GetFullSourceName(args, version):
"""Returns relative resource name for a source from --source argument.
Args:
args: Argument namespace.
version: Api version.
Returns:
Relative resource name
if args.source is not provided an exception will be raised
if no location is specified in argument: sources/{source_id}
if a location is specified: sources/{source_id}/locations/{location_id}
"""
resource_pattern = re.compile(
"(organizations|projects|folders)/.*/sources/[0-9-]+"
)
region_resource_pattern = re.compile(
"(organizations|projects|folders)/.*/sources/[0-9-]+/locations/[a-zA-Z0-9-]+$"
)
id_pattern = re.compile("[0-9-]+")
if not args.source:
raise errors.InvalidSCCInputError(
"Finding source must be provided in --source flag or full resource"
" name."
)
if region_resource_pattern.match(args.source):
return args.source
location = scc_util.ValidateAndGetLocation(args, version)
if resource_pattern.match(args.source):
source = args.source
if version == "v2":
return f"{source}/locations/{location}"
return source
if (id_pattern.match(args.source) and
(hasattr(args, "finding") or hasattr(args, "parent"))):
source = f"{scc_util.GetFindingsParentFromPositionalArguments(args)}/sources/{args.source}"
if version == "v2":
return f"{source}/locations/{location}"
return source
if id_pattern.match(args.source):
source = f"{scc_util.GetParentFromPositionalArguments(args)}/sources/{args.source}"
if version == "v2":
return f"{source}/locations/{location}"
return source
raise errors.InvalidSCCInputError(
"The source must either be the full resource "
"name or the numeric source ID."
)
def GetSourceParentFromFindingName(resource_name, version):
"""Get parent (with source) from Finding name i.e remove 'findings/{finding_name}'.
Args:
resource_name: finding name {parent with source}/findings/{findingID}
version: API version.
Returns:
The parent with source or parent with source and location
examples:
if no location is specified the result will be one of the following forms
`organizations/{organization_id}/sources/{source_id}`
`folders/{folders_id}/sources/{source_id}`
`projects/{projects_id}/sources/{source_id}`
if a location is specified the result will be one of the following forms
`organizations/{organization_id}/sources/{source_id}/locations/{location_id}`
`folders/{folders_id}/sources/{source_id}/locations/{location_id}`
`projects/{projects_id}/sources/{source_id}/locations/{location_id}`
"""
resource_pattern = re.compile(
"(organizations|projects|folders)/.*/sources/[0-9]+"
)
if not resource_pattern.match(resource_name):
raise errors.InvalidSCCInputError(
"When providing a full resource path, it must also include "
"the organization, project, or folder prefix."
)
list_source_components = resource_name.split("/")
if version == "v1":
return f"{GetParentFromResourceName(resource_name)}/{list_source_components[2]}/{list_source_components[3]}"
if version == "v2":
# Include location.
return f"{GetParentFromResourceName(resource_name)}/{list_source_components[2]}/{list_source_components[3]}/{list_source_components[4]}/{list_source_components[5]}"
def GetFindingIdFromName(finding_name):
"""Gets a finding id from the full resource name."""
resource_pattern = re.compile(
"(organizations|projects|folders)/.*/sources/[0-9-]+/findings/[a-zA-Z0-9]+$"
)
region_resource_pattern = re.compile(
"(organizations|projects|folders)/.*/sources/[0-9-]+/locations/.*/findings/[a-zA-Z0-9]+$"
)
if not resource_pattern.match(
finding_name
) and not region_resource_pattern.match(finding_name):
raise errors.InvalidSCCInputError(
"When providing a full resource path, it must include the pattern "
"organizations/[0-9]+/sources/[0-9-]+/findings/[a-zA-Z0-9]+."
)
list_finding_components = finding_name.split("/")
return list_finding_components[len(list_finding_components) - 1]
def GetParentFromResourceName(resource_name):
list_organization_components = resource_name.split("/")
return f"{list_organization_components[0]}/{list_organization_components[1]}"
def ConvertStateInput(state, version):
"""Convert state input to messages.Finding.StateValueValuesEnum object."""
messages = securitycenter_client.GetMessages(version)
if state:
state = state.upper()
state_dict = {}
if version == "v1":
unspecified_state = messages.Finding.StateValueValuesEnum.STATE_UNSPECIFIED
state_dict["v1"] = {
"ACTIVE": messages.Finding.StateValueValuesEnum.ACTIVE,
"INACTIVE": messages.Finding.StateValueValuesEnum.INACTIVE,
"STATE_UNSPECIFIED": unspecified_state,
}
else:
v2_unspecified_state = (
messages.GoogleCloudSecuritycenterV2Finding.StateValueValuesEnum.STATE_UNSPECIFIED
)
state_dict["v2"] = {
"ACTIVE": (
messages.GoogleCloudSecuritycenterV2Finding.StateValueValuesEnum.ACTIVE
),
"INACTIVE": (
messages.GoogleCloudSecuritycenterV2Finding.StateValueValuesEnum.INACTIVE
),
"STATE_UNSPECIFIED": v2_unspecified_state,
}
return state_dict[version].get(
state, state_dict[version]["STATE_UNSPECIFIED"]
)
def ConvertMuteStateInput(mute_state, messages):
"""Convert mute state input to messages.BulkMuteFindingsRequest.MuteStateValueValuesEnum object."""
if mute_state == "muted":
return messages.BulkMuteFindingsRequest.MuteStateValueValuesEnum.MUTED
elif mute_state == "undefined":
return messages.BulkMuteFindingsRequest.MuteStateValueValuesEnum.UNDEFINED
raise errors.InvalidSCCInputError(
"Mute state must be one of [muted, undefined]."
)
def ValidateAndGetParent(args):
"""Validates parent."""
if args.organization is not None: # Validates organization.
if "/" in args.organization:
pattern = re.compile("^organizations/[0-9]{1,19}$")
if not pattern.match(args.organization):
raise errors.InvalidSCCInputError(
"When providing a full resource path, it must include the pattern "
"'^organizations/[0-9]{1,19}$'."
)
else:
return args.organization
else:
pattern = re.compile("^[0-9]{1,19}$")
if not pattern.match(args.organization):
raise errors.InvalidSCCInputError(
"Organization does not match the pattern '^[0-9]{1,19}$'."
)
else:
return f"organizations/{args.organization}"
if args.folder is not None: # Validates folder.
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: # Validates project.
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 ValidateMutexOnSourceAndParent(args):
"""Validates that only a full resource name or split arguments are provided."""
if "/" in args.source and args.parent is not None:
raise errors.InvalidSCCInputError(
"Only provide a full resource name "
"(organizations/123/sources/456) or a --parent flag, not both."
)
def ExtractSecurityMarksFromResponse(response, args):
"""Returns security marks from finding response."""
del args
if isinstance(response, list):
list_finding_response = response
else:
list_finding_response = list(response)
if len(list_finding_response) > 1:
raise errors.InvalidSCCInputError(
"ListFindingResponse must only return one finding since it is "
"filtered by Finding Name."
)
for finding_result in list_finding_response:
return finding_result.finding.securityMarks
def ValidateSourceAndFindingIdIfParentProvided(args):
"""Validates that source and finding id are provided if parent is provided."""
if args.source is None:
raise errors.InvalidSCCInputError("--source flag must be provided.")
if "/" in args.finding:
raise errors.InvalidSCCInputError(
"Finding id must be provided, instead of the full resource name."
)
def ValidateLocationAndGetRegionalizedParent(args, parent):
"""Appends location to parent."""
if args.location:
if "/" in args.location:
pattern = re.compile("^locations/[A-Za-z0-9-]{0,61}$")
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 GetRegionalizedResourceName(args, version):
"""Returns regionalized resource name."""
location = scc_util.ValidateAndGetLocation(args, version)
name_components = args.finding.split("/")
return f"{name_components[0]}/{name_components[1]}/{name_components[2]}/{name_components[3]}/locations/{location}/{name_components[4]}/{name_components[5]}"
def ConvertSourceProperties(source_properties_dict, version):
"""Hook to capture "key1=val1,key2=val2" as SourceProperties object."""
messages = securitycenter_client.GetMessages(version)
if version == "v1":
return encoding.DictToMessage(
source_properties_dict, messages.Finding.SourcePropertiesValue
)
elif version == "v2":
return encoding.DictToMessage(
source_properties_dict,
messages.GoogleCloudSecuritycenterV2Finding.SourcePropertiesValue,
)
raise errors.InvalidAPIVersion("Invalid API version")
def GetApiVersionUsingDeprecatedArgs(args, deprecated_args):
"""Determine what version to call from --location and --api-version."""
if not args.parent:
# If the parent argument is not provided in the command, it can be derived
# from properties set by gcloud config.
parent = scc_util.GetParentFromPositionalArguments(args)
else:
parent = args.parent
return scc_util.GetVersionFromArguments(args, parent, deprecated_args)
def ValidateAndFormatExportTime(export_time):
"""Validates the export time."""
try:
read_time_dt = times.ParseDateTime(export_time)
return times.FormatDateTime(read_time_dt)
except (times.DateTimeSyntaxError, times.DateTimeValueError) as e:
raise errors.InvalidSCCInputError(
"Invalid export time format. Please provide a valid date/time. Example:"
" 2024-08-20T12:00:00Z"
) from e
def ValidateDataset(dataset):
"""Validates the dataset."""
dataset_pattern = re.compile(
"^projects/[a-zA-Z0-9-]+/datasets/[a-zA-Z0-9_]+$"
)
if not dataset_pattern.match(dataset):
raise errors.InvalidSCCInputError(
"Dataset must match the pattern"
" projects/[a-zA-Z0-9-]+/datasets/[a-zA-Z0-9_]+."
)
return dataset