254 lines
9.2 KiB
Python
254 lines
9.2 KiB
Python
# -*- 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 Hooks for Cloud SCC surface arguments."""
|
|
|
|
from __future__ import absolute_import
|
|
from __future__ import division
|
|
from __future__ import print_function
|
|
from __future__ import unicode_literals
|
|
|
|
import json
|
|
import re
|
|
|
|
from apitools.base.py import encoding
|
|
from googlecloudsdk.api_lib.scc import securitycenter_client as sc_client
|
|
from googlecloudsdk.command_lib.scc.errors import InvalidSCCInputError
|
|
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
|
|
from googlecloudsdk.core import exceptions as core_exceptions
|
|
from googlecloudsdk.core import properties
|
|
from googlecloudsdk.core import yaml
|
|
|
|
|
|
class InvalidCustomConfigFileError(core_exceptions.Error):
|
|
"""Error if a custom config file is improperly formatted."""
|
|
|
|
|
|
class InvalidTestDataFileError(core_exceptions.Error):
|
|
"""Error if a test data file is improperly formatted."""
|
|
|
|
|
|
class InvalidResourceFileError(core_exceptions.Error):
|
|
"""Error if a resource file is improperly formatted."""
|
|
|
|
|
|
def AppendOrgArg():
|
|
"""Add Organization as a positional resource."""
|
|
org_spec_data = yaml_data.ResourceYAMLData.FromPath("scc.organization")
|
|
arg_specs = [
|
|
resource_args.GetResourcePresentationSpec(
|
|
verb="to be used for the SCC (Security Command Center) command",
|
|
name="organization",
|
|
required=True,
|
|
prefixes=False,
|
|
positional=True,
|
|
resource_data=org_spec_data.GetData()),
|
|
]
|
|
return [concept_parsers.ConceptParser(arg_specs, [])]
|
|
|
|
|
|
def AppendParentArg():
|
|
"""Add Parent as a positional resource."""
|
|
parent_spec_data = yaml_data.ResourceYAMLData.FromPath("scc.parent")
|
|
arg_specs = [
|
|
resource_args.GetResourcePresentationSpec(
|
|
verb="to be used for the `gcloud scc` command",
|
|
name="parent",
|
|
help_text="{name} organization, folder, or project in the Google Cloud resource hierarchy {verb}. Specify the argument as either [RESOURCE_TYPE/RESOURCE_ID] or [RESOURCE_ID], as shown in the preceding examples.",
|
|
required=True,
|
|
prefixes=False,
|
|
positional=True,
|
|
resource_data=parent_spec_data.GetData()),
|
|
]
|
|
return [concept_parsers.ConceptParser(arg_specs, [])]
|
|
|
|
|
|
def SourcePropertiesHook(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 SecurityMarksHook(parsed_dict):
|
|
"""Hook to capture "key1=val1,key2=val2" as SecurityMarks object."""
|
|
messages = sc_client.GetMessages()
|
|
security_marks = messages.SecurityMarks()
|
|
security_marks.marks = encoding.DictToMessage(
|
|
parsed_dict, messages.SecurityMarks.MarksValue)
|
|
return security_marks
|
|
|
|
|
|
def GetOrganization(args):
|
|
"""Prepend organizations/ to org if necessary."""
|
|
resource_pattern = re.compile("organizations/[0-9]+")
|
|
id_pattern = re.compile("[0-9]+")
|
|
if not args.organization:
|
|
organization = properties.VALUES.scc.organization.Get()
|
|
else:
|
|
organization = args.organization
|
|
if organization is None:
|
|
raise InvalidSCCInputError("Could not find Organization argument. Please "
|
|
"provide the organization argument.")
|
|
if (not resource_pattern.match(organization) and
|
|
not id_pattern.match(organization)):
|
|
raise InvalidSCCInputError(
|
|
"Organization must match either organizations/[0-9]+ or [0-9]+.")
|
|
if resource_pattern.match(organization):
|
|
return organization
|
|
return "organizations/" + organization
|
|
|
|
|
|
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 InvalidSCCInputError(
|
|
"Organization must match either organizations/[0-9]+ or [0-9]+.")
|
|
if resource_pattern.match(organization):
|
|
return organization
|
|
return "organizations/" + organization
|
|
|
|
|
|
def GetDefaultParent():
|
|
"""Converts user input to one of: organization, project, or folder."""
|
|
organization_resource_pattern = re.compile("organizations/[0-9]+$")
|
|
id_pattern = re.compile("[0-9]+")
|
|
parent = properties.VALUES.scc.parent.Get()
|
|
if id_pattern.match(parent):
|
|
# Prepend organizations/ if only number value is provided.
|
|
parent = "organizations/" + parent
|
|
if not (organization_resource_pattern.match(parent) or
|
|
parent.startswith("projects/") or parent.startswith("folders/")):
|
|
raise InvalidSCCInputError(
|
|
"""Parent must match either [0-9]+, organizations/[0-9]+, projects/.*
|
|
or folders/.*.""")
|
|
return parent
|
|
|
|
|
|
def CleanUpUserInput(mask):
|
|
"""Removes spaces from a field mask provided by user."""
|
|
return mask.replace(" ", "")
|
|
|
|
|
|
def GetParentFromResourceName(resource_name):
|
|
resource_pattern = re.compile("(organizations|projects|folders)/.*")
|
|
if not resource_pattern.match(resource_name):
|
|
raise InvalidSCCInputError(
|
|
"When providing a full resource path, it must also include the pattern "
|
|
"the organization, project, or folder prefix.")
|
|
list_organization_components = resource_name.split("/")
|
|
return list_organization_components[0] + "/" + list_organization_components[1]
|
|
|
|
|
|
def GetSourceParentFromResourceName(resource_name):
|
|
resource_pattern = re.compile(
|
|
"(organizations|projects|folders)/.*/sources/[0-9]+")
|
|
if not resource_pattern.match(resource_name):
|
|
raise InvalidSCCInputError(
|
|
"When providing a full resource path, it must also include "
|
|
"the organization, project, or folder prefix.")
|
|
list_source_components = resource_name.split("/")
|
|
return (GetParentFromResourceName(resource_name) + "/" +
|
|
list_source_components[2] + "/" + list_source_components[3])
|
|
|
|
|
|
def ProcessCustomEtdConfigFile(file_contents):
|
|
"""Processes the configuration file for the ETD custom module."""
|
|
messages = sc_client.GetMessages()
|
|
try:
|
|
config = json.loads(file_contents)
|
|
return encoding.DictToMessage(
|
|
config, messages.EventThreatDetectionCustomModule.ConfigValue
|
|
)
|
|
except json.JSONDecodeError as e:
|
|
raise InvalidCustomConfigFileError(
|
|
"Error parsing custom config file [{}]".format(e)
|
|
)
|
|
|
|
|
|
def ProcessCustomConfigFile(file_contents):
|
|
"""Process the custom configuration file for the custom module."""
|
|
messages = sc_client.GetMessages()
|
|
try:
|
|
config_dict = yaml.load(file_contents)
|
|
return encoding.DictToMessage(
|
|
config_dict, messages.GoogleCloudSecuritycenterV1CustomConfig
|
|
)
|
|
except yaml.YAMLParseError as ype:
|
|
raise InvalidCustomConfigFileError(
|
|
"Error parsing custom config file [{}]".format(ype))
|
|
|
|
|
|
def ExtractTestData(test_data_input):
|
|
"""Extract test data into list structure, accept both list and dict."""
|
|
if isinstance(test_data_input, list):
|
|
return test_data_input
|
|
elif isinstance(test_data_input, dict):
|
|
if "testData" in test_data_input:
|
|
return test_data_input["testData"]
|
|
else:
|
|
return None
|
|
else:
|
|
if not test_data_input:
|
|
raise InvalidTestDataFileError(
|
|
"Error parsing test data file: no data records defined in file"
|
|
)
|
|
|
|
|
|
def ProcessTestResourceDataFile(file_contents):
|
|
"""Process the test resource data file for the custom module to test against."""
|
|
messages = sc_client.GetMessages()
|
|
try:
|
|
test_data = ExtractTestData(yaml.load(file_contents))
|
|
test_data_messages = []
|
|
for field in test_data:
|
|
test_data_messages.append(
|
|
encoding.DictToMessage(field, messages.TestData)
|
|
)
|
|
|
|
return test_data_messages
|
|
except yaml.YAMLParseError as ype:
|
|
raise InvalidTestDataFileError(
|
|
"Error parsing test data file [{}]".format(ype)
|
|
)
|
|
|
|
|
|
def ProcessSimulatedResourceFile(file_contents):
|
|
"""Process the simulate resource data file for the custom module to validate against."""
|
|
messages = sc_client.GetMessages()
|
|
try:
|
|
resource_dict = yaml.load(file_contents)
|
|
|
|
return encoding.DictToMessage(resource_dict, messages.SimulatedResource)
|
|
except yaml.YAMLParseError as ype:
|
|
raise InvalidResourceFileError(
|
|
"Error parsing resource file [{}]".format(ype)
|
|
)
|
|
|
|
def ProcessTFPlanFile(file_contents):
|
|
"""Process the custom configuration file for the custom module."""
|
|
try:
|
|
config = json.loads(file_contents)
|
|
return json.dumps(config).encode("utf-8")
|
|
except json.JSONDecodeError as e:
|
|
raise InvalidCustomConfigFileError(
|
|
"Error parsing terraform plan file [{}]".format(e)
|
|
)
|