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,40 @@
# -*- coding: utf-8 -*- #
# Copyright 2024 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.
"""Constants used in the IAC remediation commands."""
FINDINGS_API_NAME = 'securitycenter'
FINDINGS_API_VERSION = 'v1'
LLM_API_NAME = 'aiplatform'
LLM_API_VERSION = 'v1'
# LLM model parameters
LLM_DEFAULT_MODEL_NAME = 'gemini-1.5-pro-002'
TEMP = 0.1
TOPK = 40
TOPP = 0.95
MAX_OUTPUT_TOKENS = 8192
SUPPORTED_FINDING_CATEGORIES = (
# go/keep-sorted start
'IAM_ROLE_HAS_EXCESSIVE_PERMISSIONS',
'IAM_ROLE_REPLACEMENT',
'SERVICE_AGENT_GRANTED_BASIC_ROLE',
'SERVICE_AGENT_ROLE_REPLACED_WITH_BASIC_ROLE',
'UNUSED_IAM_ROLE',
# go/keep-sorted end
)
SUPPORTED_IAM_MEMBER_COUNT_LIMIT = 4

View File

@@ -0,0 +1,144 @@
# -*- coding: utf-8 -*- #
# Copyright 2024 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.
"""Library for interacting with the Security Command Center Findings API."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import json
import re
from typing import Dict, List
from apitools.base.py import encoding
from googlecloudsdk.api_lib.scc.iac_remediation import const
from googlecloudsdk.api_lib.util import apis
from googlecloudsdk.command_lib.scc.iac_remediation import errors
def GetClient():
"""Returns the Security Command Center Findings API client."""
return apis.GetClientInstance(const.FINDINGS_API_NAME,
const.FINDINGS_API_VERSION)
def GetMessages():
"""Returns the Security Command Center Findings API messages."""
return apis.GetMessagesModule(const.FINDINGS_API_NAME,
const.FINDINGS_API_VERSION)
def ParseName(finding_name) -> str:
"""Parses the finding name to get the finding id.
Args:
finding_name: Canonical name of the finding.
Returns:
Finding id, if found else throws an error
"""
pattern = r"projects/(\d+)/sources/(\d+)/locations/(\w+)/findings/(\w+)"
match = re.search(pattern, finding_name)
if match:
# Finding id is the 4th group captured from the regex pattern
return match.group(4)
else:
raise errors.InvalidFindingNameError(finding_name)
def MakeApiCall(finding_org_id, finding_name) -> str:
"""Makes an API call to the Security Command Center Findings API.
Args:
finding_org_id: Organization ID of the finding
finding_name: Canonical name of the finding.
Returns:
JSON response from the API call.
"""
client = GetClient()
messages = GetMessages()
finding_id = ParseName(finding_name)
request = messages.SecuritycenterOrganizationsSourcesFindingsListRequest()
request.filter = f"name : \"{finding_id}\" "
request.parent = f"organizations/{finding_org_id}/sources/-"
resp = client.organizations_sources_findings.List(request)
json_resp = encoding.MessageToJson(resp)
ValidateFinding(json_resp)
return json_resp
def FetchIAMBinding(
finding_json: json
)-> Dict[str, Dict[str, List[str]]]:
"""Fetches the IAMBindings from the finding data.
Args:
finding_json: JSON response from the API call to fetch the finding.
Returns:
IAM binding details per member.
"""
iam_bindings = {}
for binding in finding_json["listFindingsResults"][0]["finding"][
"iamBindings"
]:
if binding["member"] not in iam_bindings:
iam_bindings[binding["member"]] = dict()
if binding["action"] == "ADD":
if "ADD" not in iam_bindings[binding["member"]]:
iam_bindings[binding["member"]]["ADD"] = []
if binding["role"] not in iam_bindings[binding["member"]]["ADD"]:
iam_bindings[binding["member"]]["ADD"].append(binding["role"])
elif binding["action"] == "REMOVE":
if "REMOVE" not in iam_bindings[binding["member"]]:
iam_bindings[binding["member"]]["REMOVE"] = []
if binding["role"] not in iam_bindings[binding["member"]]["REMOVE"]:
iam_bindings[binding["member"]]["REMOVE"].append(binding["role"])
return iam_bindings
def FetchResourceName(finding_json: json) -> str:
"""Fetches the resource name from the finding data.
Args:
finding_json: JSON response from the API call to fetch the finding.
Returns:
Resource name for which the finding was generated.
"""
return finding_json["listFindingsResults"][0]["resource"]["displayName"]
def ValidateFinding(finding_data):
"""Checks if the finding is supported or not.
Args:
finding_data: JSON response from the API call.
"""
try:
finding_data = json.loads(finding_data)
finding = finding_data["listFindingsResults"][0]["finding"]
except:
raise errors.FindingNotFoundError()
finding_category = finding["category"]
if finding_category not in const.SUPPORTED_FINDING_CATEGORIES:
raise errors.UnsupportedFindingCategoryError(finding_category)
iam_bindings = FetchIAMBinding(finding_data)
if (
len(iam_bindings) > const.SUPPORTED_IAM_MEMBER_COUNT_LIMIT
or len(iam_bindings) < 1
):
raise errors.ExcessiveMembersError(len(iam_bindings))

View File

@@ -0,0 +1,180 @@
# -*- coding: utf-8 -*- #
# Copyright 2024 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.
"""Helper functions to interact with git and github for iac remediation."""
import os
import subprocess
import tempfile
from googlecloudsdk.command_lib.scc.iac_remediation import errors
from googlecloudsdk.core.util import files
def validate_git_config(git_config_file):
"""Validates the git config file."""
if git_config_file['remote'] is None:
raise errors.InvalidGitConfigError('remote')
if git_config_file['main-branch-name'] is None:
raise errors.InvalidGitConfigError('main-branch-name')
if git_config_file['branch-prefix'] is None:
raise errors.InvalidGitConfigError('branch-prefix')
def is_git_repo():
"""Check whether the current directory is a git repo or not.
Returns:
True, repo_root_path if the current directory is a git repo
False, None otherwise.
"""
try:
git_check_cmd = ('git rev-parse --show-toplevel')
result = subprocess.run(
git_check_cmd,
shell=True, check=True, cwd=os.getcwd(),
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
text=True,
)
return True, result.stdout.strip()
except subprocess.CalledProcessError:
return False, None
def branch_remote_exists(remote_name, branch_name):
"""Helper function to check if a branch exists in the remote.
Args:
remote_name: Name of the remote of the repo at which to check.
branch_name: Name of the branch to check.
Returns:
Boolean indicating whether the branch exists in the remote.
"""
result = subprocess.run(
['git', 'ls-remote', '--heads', remote_name, branch_name],
check=False, cwd=os.getcwd(),
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
text=True
)
return bool(result.stdout.strip())
def get_working_tree_dir(remote_name, branch_name):
"""Returns the working tree directory for the branch.
Args:
remote_name: Name of the remote of the repo at which to check.
branch_name: Name of the branch for which the working tree directory is
required.
Returns:
Working tree directory path for the branch.
"""
worktree_dir = None
# check if there is a worktree already for the branch
existing_worktrees = subprocess.check_output(
['git', 'worktree', 'list'] # output format is: <path> <branch>
).decode('utf-8')
for line in existing_worktrees.splitlines():
if branch_name in line:
# if worktree found for the branch, set it,
worktree_dir = line.split()[0]
break
if worktree_dir is None: # else create a new worktree
worktree_dir = tempfile.mkdtemp()
subprocess.run(
['git', 'worktree', 'add', worktree_dir, '-B', branch_name],
check=True, cwd=os.getcwd(),
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
)
# check if the branch exists in the remote and push the branch if not
if not branch_remote_exists(remote_name, branch_name):
subprocess.run(
['git', 'push', '--set-upstream', remote_name, branch_name],
check=False, cwd=worktree_dir,
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
)
# pull the latest changes from the remote for the branch
subprocess.run(
['git', 'pull'],
check=False, cwd=worktree_dir,
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
)
return worktree_dir
def push_commit(files_data, commit_message, remote_name, branch_name):
"""Pushes the commit to the given branch with the given files data and commit message.
Args:
files_data: Dictionary of file path (absolute path of the files in original
repo) and file data to be written
commit_message: Message to be added to the commit
remote_name: Name of the remote of the repo at which to check.
branch_name: Name of the branch where commit needs to be pushed
"""
is_repo_flag, repo_root_dir = is_git_repo()
del is_repo_flag
worktree_dir = get_working_tree_dir(remote_name, branch_name)
# overwrite the files in the worktree dir's for the branch
for file_path, file_data in files_data.items():
rel_path = os.path.relpath(file_path, repo_root_dir)
abs_file_path = os.path.join(worktree_dir, rel_path)
files.WriteFileContents(abs_file_path, file_data)
subprocess.run( # add them to the git index
['git', 'add', abs_file_path],
check=True, cwd=worktree_dir,
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
)
subprocess.run(
['git', 'commit', '-m', commit_message],
check=False, cwd=worktree_dir,
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
)
# push the commit
subprocess.run(
['git', 'push'],
check=False, cwd=worktree_dir,
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
)
def raise_pr(pr_title, pr_desc, remote_name, branch_name, base_branch):
"""Creates a PR for the given branch to the main base branch.
Args:
pr_title: PR title
pr_desc: PR description
remote_name: Name of the remote of the repo at which to check.
branch_name: The branch from which PR needs to be created.
base_branch: The main branch name to be which PR needs to be merged.
"""
worktree_dir = get_working_tree_dir(remote_name, branch_name)
pr_command = (
f'gh pr create --base {base_branch} --head {branch_name} --title'
f' "{pr_title}" --body "{pr_desc}"'
)
subprocess.run( # create a PR
pr_command, shell=True, check=False, cwd=worktree_dir,
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
)
subprocess.run( # cleanup the worktree
['git', 'worktree', 'remove', '--force', worktree_dir],
check=False, cwd=worktree_dir,
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
)

View File

@@ -0,0 +1,86 @@
# -*- coding: utf-8 -*- #
# Copyright 2024 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.
"""Library for interacting with the LLM model APIs."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.scc.iac_remediation import const
from googlecloudsdk.api_lib.util import apis
from googlecloudsdk.command_lib.scc.iac_remediation import errors
def GetClient():
return apis.GetClientInstance(const.LLM_API_NAME, const.LLM_API_VERSION)
def GetMessages():
return apis.GetMessagesModule(const.LLM_API_NAME, const.LLM_API_VERSION)
def MakeLLMCall(
input_text,
proj_id,
model_name=const.LLM_DEFAULT_MODEL_NAME,
) -> str:
"""Makes a call to the LLM.
Args:
input_text: string of the prompt to be sent to the LLM
proj_id: project_id of the LLM enabled project
model_name: name of the LLM model to be used
Returns:
LLM response in string
"""
client = GetClient()
messages = GetMessages()
request = messages.AiplatformProjectsLocationsEndpointsGenerateContentRequest(
googleCloudAiplatformV1GenerateContentRequest=messages.GoogleCloudAiplatformV1GenerateContentRequest(
contents=[
messages.GoogleCloudAiplatformV1Content(
parts=[
messages.GoogleCloudAiplatformV1Part(text=input_text)
],
role='user')
],
generationConfig=messages.GoogleCloudAiplatformV1GenerationConfig(
temperature=const.TEMP,
topK=const.TOPK,
topP=const.TOPP,
maxOutputTokens=const.MAX_OUTPUT_TOKENS,
),
),
# model API endpoint to be used for the LLM call
model=f'projects/{proj_id}/locations/us-central1/publishers/google/models/{model_name}',
)
resp = client.projects_locations_endpoints.GenerateContent(request)
ValidateLLMResponse(resp)
return resp.candidates[0].content.parts[0].text
def ValidateLLMResponse(response):
"""Validates the LLM response.
Args:
response: LLM response.
"""
if (
not response.candidates
or not response.candidates[0].content
or not response.candidates[0].content.parts
or not response.candidates[0].content.parts[0].text
):
raise errors.EmptyLLMResponseError()

View File

@@ -0,0 +1,144 @@
# -*- coding: utf-8 -*- #
# Copyright 2024 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.
"""Library for fetching TF Files."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import json
from typing import Dict, List
from googlecloudsdk.api_lib.scc.iac_remediation import prompt_format
from googlecloudsdk.core.util import files
def read_file(file_path: str) -> str:
"""Reads the TF file.
Args:
file_path: The path of the file to read.
Returns:
The contents of the file.
"""
file = files.ReadFileContents(file_path)
return file
def fetch_input_prompt(
tfstate_information: str,
iam_bindings: Dict[str, Dict[str, List[str]]],
resource_name: str,
tf_files: List[str],
member: str,
) -> str:
"""Generates the prompt for iam policy.
Args:
tfstate_information: TFState information for the given IAM bindings.
iam_bindings: IAM bindings for the resource.
resource_name: Resource name for which the finding was generated.
tf_files: List of TF files.
member: Member for which the prompt is generated.
Returns:
Prompt string.
"""
prompt_format_data = prompt_format.PromptFormatLookup()
if "google_project_iam_policy" in tfstate_information:
prompt_str = prompt_format_data.get_policy_prompt_template()
else:
prompt_str = prompt_format_data.get_binding_prompt_template()
return _fetch_prompt(
iam_bindings,
tfstate_information,
resource_name,
tf_files,
prompt_str,
member,
)
def _fetch_prompt(
iam_bindings: Dict[str, Dict[str, List[str]]],
tfstate_information: str,
resource_name: str,
tf_files: List[str],
prompt_str: str,
member: str,
) -> str:
"""Generates the prompt string.
Args:
iam_bindings: IAM bindings for the resource.
tfstate_information: TFState information for the given IAM bindings.
resource_name: Resource name for which the finding was generated.
tf_files: List of TF files.
prompt_str: Prompt file name.
member: Member for which the prompt is generated.
Returns:
Prompt for iam policy.
"""
iam_bindings_str = "member: " + member + "\n"
for action, roles in iam_bindings.items():
iam_bindings_str += action + " : \n" + json.dumps(roles) + "\n"
prompt_str = prompt_str.replace(
"{{iam_bindings}}", iam_bindings_str
)
prompt_str = prompt_str.replace(
"{{tfstate_information}}", json.dumps(tfstate_information)
)
prompt_str = prompt_str.replace("{{resource_name}}", resource_name)
files_str = ""
for tf_file in tf_files:
files_str += "FilePath= " + tf_file + "\n" + "```\n"
files_str += read_file(tf_file) + "\n```\n"
prompt_str = prompt_str.replace("{{input_tf_files}}", files_str)
return prompt_str
def llm_response_parser(
response: str
)-> Dict[str, str]:
"""Parses the LLM response.
Args:
response: LLM response.
Returns:
Dict of file path and file content.
"""
response_dict = {}
file_path = ""
file_content = ""
for line in response.splitlines():
if line.startswith("FilePath"):
if file_path:
file_content = file_content.replace("```\n", "")
file_content = file_content.replace("\n```", "")
file_content = file_content.replace("```", "")
response_dict[file_path] = file_content
file_path = line.split("=")[1].strip()
file_content = ""
else:
file_content += line + "\n"
if file_path:
file_content = file_content.replace("```\n", "")
file_content = file_content.replace("\n```", "")
file_content = file_content.replace("```", "")
response_dict[file_path] = file_content
return response_dict

View File

@@ -0,0 +1,835 @@
# -*- coding: utf-8 -*- #
# Copyright 2024 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.
"""Prompt Format Lookup."""
from __future__ import annotations
class PromptFormatLookup:
"""Prompt lookup."""
def get_policy_prompt_template(self) -> str:
"""Get policy prompt template."""
self._policy_prompt_format = """
# SYSTEM INSTRUCTIONS:
- You are an expert in creating terraform resources for GCP.
- You have access to a complete and up-to-date knowledge base of all GCP resource types terraform documentation.
- Identify the most relevant GCP terraform resource used to grant the permission from the given IAM BINDINGS .
- You have to update the relevant terraform files for the given IAM binding as per the roles and permissions required.
#TASK
- Analyze the INPUT_TF_FILES and identify the resource types that are most likely to contain the information needed.
- ** Identify the resource types in INPUT_TF_FILES that are most likely to contain the IAM_BINDINGS information.**
- ** Prioritize resource types which are most likely to create or remove the permissions given in IAM binding for the given RESOURCE_NAME.**
- ** DO NOT update the permission for any member which is not present in the IAM_BINDINGS.**
- ** Verify the output to check no other permissions are updated for the member which is not present in the IAM_BINDINGS.**
- The output should contain the complete INPUT_TF_FILES which were updated as per given IAM bindings .
- The resource "google_project_iam_member_remove" should be created only if the role needs to be removed.
- Refer to the summary of TERRAFORM_DOCUMENTATION given below for updating the files.
- Refer to TFSTATE_INFORMATION to identify the terraform files from INPUT_TF_FILES which needs to be updated.
- The output must be a valid json file with the following OUTPUT_FORMAT.
- The output must contains only those files which were updated.
- The output should be in the OUTPUT_FORMAT given below.
# OUTPUT_FORMAT is as follows
FilePath= "file_path1"
```
The complete terraform file(s) which were updated as per given IAM bindings .
The output must be valid .tf files.
```
FilePath= "file_path2"
```
The complete terraform file(s) which were updated as per given IAM bindings .
The output must be valid .tf files.
```
# IAM_BINDINGS
{{iam_bindings}}
# RESOURCE_NAME
{{resource_name}}
# INPUT_TF_FILES
{{input_tf_files}}
# TFSTATE_INFORMATION
{{tfstate_information}}
# EXAMPLE1 is as follows
## INPUT_TF_FILES
FilePath= "file_path1"
```
resource "google_project_iam_policy" "project" {
project = "project_name_test"
policy_data = "${data.google_iam_policy.admin.policy_data}"
}
data "google_iam_policy" "admin" {
binding {
role = "roles/compute.admin"
members = [
"user:jane@example.com",
"user:user1@google.com",
"user:user2@google.com",
]
condition {
title = "expires_after_2019_12_31"
description = "Expiring at midnight of 2019-12-31"
expression = "request.time < timestamp(\"2020-01-01T00:00:00Z\")"
}
}
binding {
role = "roles/compute.viewer"
members = [
"user:jane@example.com",
]
condition {
title = "expires_after_2019_12_31"
description = "Expiring at midnight of 2019-12-31"
expression = "request.time < timestamp(\"2020-01-01T00:00:00Z\")"
}
}
}
```
FilePath= "file_path2"
```
resource "google_project_iam_policy" "project" {
project = "your-project-id"
policy_data = "${data.google_iam_policy.admin.policy_data}"
}
data "google_iam_policy" "admin" {
binding {
role = "roles/compute.admin"
members = [
"user:jane@example.com",
]
condition {
title = "expires_after_2019_12_31"
description = "Expiring at midnight of 2019-12-31"
expression = "request.time < timestamp(\"2020-01-01T00:00:00Z\")"
}
}
}
```
## IAM_BINDINGS
Member : "user:user1@google.com"
Action: "ADD"
Role : "roles/compute.viewer"
Role: "roles/storage.objectViewer"
Action: "REMOVE"
Role: "roles/compute.admin"
Role: "roles/storage.objectAdmin"
## RESOURCE_NAME
project_name_test
## TFSTATE_INFORMATION
** ANSWER **
FilePath= "file_path1"
```
resource "google_project_iam_policy" "project" {
project = "project_name_test"
policy_data = "${data.google_iam_policy.admin.policy_data}"
}
data "google_iam_policy" "admin" {
binding {
role = "roles/compute.admin"
members = [
"user:jane@example.com",
"user:user2@google.com",
]
condition {
title = "expires_after_2019_12_31"
description = "Expiring at midnight of 2019-12-31"
expression = "request.time < timestamp(\"2020-01-01T00:00:00Z\")"
}
}
binding {
role = "roles/compute.viewer"
members = [
"user:jane@example.com",
"user:user1@google.com",
]
condition {
title = "expires_after_2019_12_31"
description = "Expiring at midnight of 2019-12-31"
expression = "request.time < timestamp(\"2020-01-01T00:00:00Z\")"
}
}
binding {
role = "roles/storage.objectViewer"
members = [
"user:user1@google.com",
]
}
}
# Remove the permission for user1 for storage.objectAdmin
resource "google_project_iam_member_remove" "remove_permission_for_user1" {
role = "roles/storage.objectAdmin"
project = "project_name_test"
member = "user:user1@google.com"
}
```
** Steps **
1. **Identify the file**: Identify the file(s) that contains the resources that need to be updated as per information in IAM Binding.
2. **Identify the resource**: Identify the resource(s) that need to be updated as per information in IAM Binding.
3. ** If the binding for a role is not present in the policy data**, then add the binding to the policy data.
4. **If the binding for a role is present in the policy data for removing the permission**, Create a resource to remove the permission for the user.
5. **If the binding for a role is present in the policy data for adding the permission**, Add the permission for the user.
6. **If the binding for a role is not present in the policy data for adding the permission**, Add the binding for the role and assign permission to the user.
7. **If the binding for a role is not present in the policy data for removing the permission**, Create a resource to remove the permission for the user.
# TERRAFORM_DOCUMENTATION
## Documentation for google_project_iam_member_remove
data "google_project" "target_project" {}
resource "google_project_iam_member_remove" "foo" {
# (Required) The target role that should be removed.
role = "roles/editor"
# (Required) The project id of the target project.
project = google_project.target_project.project_id
# (Required) The IAM principal that should not have the target role. Each entry can have one of the following values:
# user:{emailid}: An email address that represents a specific Google account. For example, alice@gmail.com or joe@example.com.
# serviceAccount:{emailid}: An email address that represents a service account. For example, my-other-app@appspot.gserviceaccount.com.
# group:{emailid}: An email address that represents a Google group. For example, admins@example.com.
# domain:{domain}: A G Suite domain (primary, instead of alias) name that represents all the users of that domain. For example, google.com or example.com.
member = "serviceAccount:${google_project.target_project.number}-compute@developer.gserviceaccount.com"
}
## Documentation for google_project_iam_policy
resource "google_project_iam_policy" "project" {
# (Required) The project id of the target project. This is not inferred from the provider.
project = "your-project-id"
# (Required) The google_iam_policy data source that represents the IAM policy that will be applied to the project. The policy will be merged with any existing policy applied to the project.
# Changing this updates the policy.
# Deleting this removes all policies from the project, locking out users without organization-level access.
policy_data = "${data.google_iam_policy.admin.policy_data}"
}
data "google_iam_policy" "admin" {
binding {
# (Required) The role that should be applied. Only one google_project_iam_binding can be used per role. Note that custom roles must be of the format [projects|organizations]/{parent-name}/roles/{role-name}
role = "roles/compute.admin"
# (Required) Identities that will be granted the privilege in role. google_project_iam_binding expects members field while google_project_iam_member expects member field. Each entry can have one of the following values:
# user:{emailid}: An email address that represents a specific Google account. For example, alice@gmail.com or joe@example.com.
# serviceAccount:{emailid}: An email address that represents a service account. For example, my-other-app@appspot.gserviceaccount.com.
# group:{emailid}: An email address that represents a Google group. For example, admins@example.com.
# domain:{domain}: A G Suite domain (primary, instead of alias) name that represents all the users of that domain. For example, google.com or example.com.
members = [
"user:jane@example.com",
]
# (Optional) An IAM Condition for a given binding.
condition {
# (Required) A title for the expression, i.e. a short string describing its purpose.
title = "expires_after_2019_12_31"
# (Optional) An optional description of the expression. This is a longer text which describes the expression, e.g. when hovered over it in a UI.
description = "Expiring at midnight of 2019-12-31"
# (Required) Textual representation of an expression in Common Expression Language syntax.
expression = "request.time < timestamp(\"2020-01-01T00:00:00Z\")"
}
}
}
NOTE: google_project_iam_policy cannot be used in conjunction with google_project_iam_binding, google_project_iam_member, or google_project_iam_audit_config or they will fight over what your policy should be.
"""
return self._policy_prompt_format
def get_binding_prompt_template(self) -> str:
"""Get binding prompt template."""
self._binding_prompt_format = """
# SYSTEM INSTRUCTIONS:
- You are an expert in creating terraform resources for GCP.
- You have access to a complete and up-to-date knowledge base of all GCP resource types terraform documentation.
- Identify the most relevant GCP terraform resource used to grant the permission from the given IAM BINDING .
- You have to update the relevant terraform file for the given IAM binding as per the roles and permissions required.
- When a resource that grants an IAM role is deleted, do not create a google_project_iam_member_remove resource for that role and member. The deletion of the resource implicitly removes the permission.
- Ensure that the generated Terraform code is idempotent. Avoid creating unnecessary resources, especially if the desired state is already achieved by deleting an existing resource.
#TASK
- Analyze the INPUT_TF_FILES and identify the files that needs to be updated by referring to TFSTATE_INFORMATION.
- Assume that the complete system information is provided in INPUT_TF_FILES.
- **Identify the resource types in INPUT_TF_FILES that are most likely to contain the IAM_BINDING information.**
- ** Give meaningful names to the resources which are being created. A name must start with a letter or underscore and may contain only letters, digits, underscores, and dashes.**
- **Prioritize resource types which are most likely to create or remove the permissions given in IAM binding for the given RESOURCE_NAME.**
- The output should contain the complete INPUT_TF_FILES which were updated as per given IAM bindings .
- **Pay close attention to resources that use `for_each` loops or dynamic blocks and handle changes within those constructs to ensure idempotency.**
- google_project_iam_binding resources can be used in conjunction with google_project_iam_member resources only if they do not grant privilege to the same role.
- Refer to the summary of TERRAFORM_DOCUMENTATION given below for updating the files.
- **Validate that the generated Terraform code does not create unnecessary google_project_iam_member_remove resources when the corresponding resource granting the role is already deleted.**
- When faced with complex scenarios involving multiple projects or resources within a module, consider if decomposing the module into smaller, more specialized modules would improve clarity, maintainability, and reduce the risk of unintended changes.
- The output must be list of valid .tf files.
- The output must contains only those files which were updated.
- The output should start with: ```
- The output must end with: ```
- The output should be in the FORMAT given below.
# TERRAFORM DOCUMENTATION
Purpose: The google_project_iam resource empowers you to define who (users, groups, service accounts) has what permissions (roles) on your Google Cloud projects. This is crucial for controlling access to your cloud resources and ensuring security.
Functionality:
Granting Permissions: You can use this resource to assign specific roles to different entities (users, groups, service accounts) on your Google Cloud project. This allows you to give them the exact permissions they need to perform their tasks.
Revoking Permissions: Similarly, you can use this resource to remove existing roles from entities, effectively revoking their permissions on the project.
Managing Complex Policies: The resource supports intricate scenarios, allowing you to define conditional bindings based on attributes like resource tags or request conditions.
Key Attributes:
project: The ID of the Google Cloud project where you want to manage IAM.
role: The specific role you want to grant or revoke (e.g., "roles/editor", "roles/viewer").
members: A list of entities (in the format "user:example@example.com", "group:group-name@example.com", "serviceAccount:serviceaccount-id@project.iam.gserviceaccount.com") that should be granted or revoked the specified role.
condition: (Optional) Allows you to define conditions under which the role binding should be active.
Usage Examples: The documentation provides comprehensive examples of how to use the google_project_iam resource in various scenarios, including:
Granting a user the "Editor" role on a project.
Allowing a service account to access resources in a project.
Setting up conditional access based on specific attributes.
In essence, the google_project_iam resource is a fundamental tool for managing access control and security for your Google Cloud projects within your Terraform infrastructure code.
## Documentation for google_project_iam_binding
resource "google_project_iam_binding" "project" {
# (Required) The project id of the target project. This is not inferred from the provider.
project = "your-project-id"
# (Required) The role that should be applied. Only one google_project_iam_binding can be used per role. Note that custom roles must be of the format [projects|organizations]/{parent-name}/roles/{role-name}
role = "roles/container.admin"
# (Required) Identities that will be granted the privilege in role. google_project_iam_binding expects members field while google_project_iam_member expects member field. Each entry can have one of the following values:
# user:{emailid}: An email address that represents a specific Google account. For example, alice@gmail.com or joe@example.com.
# serviceAccount:{emailid}: An email address that represents a service account. For example, my-other-app@appspot.gserviceaccount.com.
# group:{emailid}: An email address that represents a Google group. For example, admins@example.com.
# domain:{domain}: A G Suite domain (primary, instead of alias) name that represents all the users of that domain. For example, google.com or example.com.
members = [
"user:jane@example.com",
]
# (Optional) An IAM Condition for a given binding.
condition {
# (Required) A title for the expression, i.e. a short string describing its purpose.
title = "expires_after_2019_12_31"
# (Optional) An optional description of the expression. This is a longer text which describes the expression, e.g. when hovered over it in a UI.
description = "Expiring at midnight of 2019-12-31"
# (Required) Textual representation of an expression in Common Expression Language syntax.
expression = "request.time < timestamp("2020-01-01T00:00:00Z")"
}
}
#DOCUMENTATION for "google_project_iam_member_remove"
data "google_project" "target_project" {}
resource "google_project_iam_member_remove" "foo" {
# (Required) The target role that should be removed.
role = "roles/editor"
# (Required) The project id of the target project.
project = google_project.target_project.project_id
# (Required) The IAM principal that should not have the target role. Each entry can have one of the following values:
# user:{emailid}: An email address that represents a specific Google account. For example, alice@gmail.com or joe@example.com.
# serviceAccount:{emailid}: An email address that represents a service account. For example, my-other-app@appspot.gserviceaccount.com.
# group:{emailid}: An email address that represents a Google group. For example, admins@example.com.
# domain:{domain}: A G Suite domain (primary, instead of alias) name that represents all the users of that domain. For example, google.com or example.com.
member = "serviceAccount:${google_project.target_project.number}-compute@developer.gserviceaccount.com"
}
## Documentation for google_project_iam_member
resource "google_project_iam_member" "project" {
# (Required) The project id of the target project. This is not inferred from the provider.
project = "your-project-id"
# (Required) The role that should be applied. Only one google_project_iam_binding can be used per role. Note that custom roles must be of the format [projects|organizations]/{parent-name}/roles/{role-name}
role = "roles/firebase.admin"
# (Required) Identities that will be granted the privilege in role. google_project_iam_binding expects members field while google_project_iam_member expects member field. Each entry can have one of the following values:
# user:{emailid}: An email address that represents a specific Google account. For example, alice@gmail.com or joe@example.com.
# serviceAccount:{emailid}: An email address that represents a service account. For example, my-other-app@appspot.gserviceaccount.com.
# group:{emailid}: An email address that represents a Google group. For example, admins@example.com.
# domain:{domain}: A G Suite domain (primary, instead of alias) name that represents all the users of that domain. For example, google.com or example.com.
member = "user:jane@example.com"
# (Optional) An IAM Condition for a given binding.
condition {
# (Required) A title for the expression, i.e. a short string describing its purpose.
title = "expires_after_2019_12_31"
# (Optional) An optional description of the expression. This is a longer text which describes the expression, e.g. when hovered over it in a UI.
description = "Expiring at midnight of 2019-12-31"
# (Required) Textual representation of an expression in Common Expression Language syntax.
expression = "request.time < timestamp("2020-01-01T00:00:00Z")"
}
}
## Documentation for terraform modules
Terraform modules are self-contained packages of Terraform configurations that manage a specific collection of resources. Think of them as reusable building blocks for your infrastructure.
Types:
1. Public Modules: Published online (e.g., Terraform Registry), offering pre-built solutions for common scenarios.
2. Private Modules: Custom modules created for internal use within an organization.
Basic Usage:
1. Define a Module: Create a directory containing .tf files with resource definitions.
2. Call the Module: Use the module block in your main configuration to invoke and configure the module.
Example:
Imagine a module for deploying an AWS EC2 instance:
# module/ec2-instance/main.tf
resource "aws_instance" "example" {
# ... instance configuration ...
}
You can then use this module in your main configuration:
# main.tf
module "my_instance" {
source = "./modules/ec2-instance"
# ... module input variables ...
}
Advanced Usage:
1. Define a Module: Create a directory containing .tf files with resource definitions.
2. Call the Module: Use the module block in your main configuration to invoke and configure the module.
3. Use Variables: Define variables in the main.tf file to customize the module's behavior.
4. Use Outputs: Specify the values that the module returns as outputs.
5. Use Providers: Specify the provider block to use the module in a specific environment.
#Handling Modules in code.
1. Module Integrity:
"Do not modify the internal configuration of modules directly, especially when those modules manage multiple resources."
"Treat modules as black boxes when possible: modify their behavior through inputs and outputs, rather than altering their internal code."
2. Project-Specific Actions:
"When making changes to IAM bindings for a specific project, ensure that those changes are isolated to that project and do not affect other projects managed by the same module."
"If a module manages multiple projects and you need to make project-specific IAM changes, consider using separate google_project_iam_* resources outside the module to target those specific projects."
3. Modularity as a Solution:
"When faced with complex scenarios involving multiple projects or resources within a module, consider if decomposing the module into smaller, more specialized modules would improve clarity, maintainability, and reduce the risk of unintended changes."
#NOTE:
1. google_project_iam_binding resources can be used in conjunction with google_project_iam_member resources only if they do not grant privilege to the same role.
2. google_project_iam_member-remove resource will conflict with google_project_iam_policy and google_project_iam_binding resources that share a role, as well as google_project_iam_member resources that target the same membership. When multiple resources conflict the final state is not guaranteed to include or omit the membership.
# FORMAT is as follows
FilePath= "file_path1"
```
The complete terraform file(s) which were updated as per given IAM bindings .
The output must be valid .tf files.
```
FilePath= "file_path2"
```
The complete terraform file(s) which were updated as per given IAM bindings .
The output must be valid .tf files.
```
## IAM_BINDINGS
{{iam_bindings}}
## RESOURCE_NAME
{{resource_name}}
## INPUT_TF_FILES
{{input_tf_files}}
## TFSTATE_INFORMATION
{{tfstate_information}}
# EXAMPLE1 is as follows
## INPUT_TF_FILES
FilePath1= "file_path1"
```
resource "google_project_iam_binding" "iam_binding_for_admin_project" {
project = project_name_test
role = "roles/compute.admin"
members = [
"user:user1@google.com",
"user:user2@google.com",
"user:user3@google.com",
]
}
resource "google_project_iam_binding" "iam_binding_for_bigquery" {
project = project_name_test
role = "roles/bigquery.dataViewer"
members = [
"user:user2@google.com",
"user:user3@google.com",
]
}
```
FilePath2= "file_path2"
```
resource "google_project_iam_binding" "iam_binding_for_admin_project2" {
project = var.project_id2
role = "roles/compute.admin"
members = [
"user:user1@google.com",
"user:user2@google.com",
]
}
```
## IAM_BINDINGS
Member: "user:user1@google.com"
Action: "ADD"
Role: "roles/compute.viewer"
Role: "roles/bigquery.dataViewer"
Action: "REMOVE"
Role: "roles/compute.admin"
Role: "roles/bigquery.dataEditor"
## RESOURCE_NAME
project_name_test
** ANSWER **
FilePath= "file_path1"
```
resource "google_project_iam_binding" "iam_binding_for_admin_project" {
project = project_name_test
role = "roles/compute.admin"
members = [
"user:user2@google.com",
"user:user3@google.com",
]
}
resource "google_project_iam_member" "iam_member_for_bigquery" {
project = project_name_test
role = "roles/compute.viewer"
member = "user:user1@google.com"
}
resource "google_project_iam_binding" "iam_binding_for_bigquery" {
project = project_name_test
role = "roles/bigquery.dataViewer"
members = [
"user:user2@google.com",
"user:user3@google.com",
"user:user1@google.com",
]
}
resource "google_project_iam_member_remove" "iam_member_remove_for_bigquery" {
project = project_name_test
role = "roles/bigquery.dataEditor"
member = "user:user1@google.com"
}
```
# EXAMPLE2 is as follows
## INPUT_TF_FILES
FilePath= "file_path1"
```
resource "google_project_iam_binding" "iam_binding_for_admin_project" {
for_each = toset([
"roles/billing.projectManager", # assign billing account to project
"roles/billing.user", # view and associate billing accounts
"roles/compute.admin",
"roles/iam.organizationRoleAdmin", # create custom org roles
"roles/iam.serviceAccountAdmin", # create service accounts and setIamPolicy
"roles/iam.workloadIdentityPoolAdmin", # create workload identity pool and providers
"roles/orgpolicy.policyAdmin", # create org policies
"roles/resourcemanager.organizationAdmin", # setIamPolicy on organization
"roles/resourcemanager.folderAdmin", # create folders
"roles/resourcemanager.projectCreator", # create projects
"roles/resourcemanager.tagAdmin", # create tag keys and values
"roles/resourcemanager.tagUser", # create tag bindings on resources
"roles/serviceusage.serviceUsageAdmin", # enable services
])
project = project_name_test
role = each.value
members = [
"user:user1@google.com",
"user:user2@google.com",
"user:user3@google.com",
]
}
```
FilePath= "file_path2"
```
resource "google_project_iam_binding" "iam_binding" {
project = var.project_id2
role = "roles/compute.admin"
members = [
"user:user1@google.com",
"user:user2@google.com",
]
}
```
## IAM_BINDINGS
Member: "user:user1@google.com"
Action: "ADD"
Role: "roles/compute.viewer"
Role: "roles/bigquery.dataViewer"
Action: "REMOVE"
Role: "roles/compute.admin"
Role: "roles/bigquery.dataEditor"
## RESOURCE_NAME
project_name_test
** ANSWER **
FilePath= "file_path1"
```
resource "google_project_iam_binding" "iam_binding_for_admin_project" {
for_each = toset([
"roles/billing.projectManager", # assign billing account to project
"roles/billing.user", # view and associate billing accounts
"roles/iam.organizationRoleAdmin", # create custom org roles
"roles/iam.serviceAccountAdmin", # create service accounts and setIamPolicy
"roles/iam.workloadIdentityPoolAdmin", # create workload identity pool and providers
"roles/orgpolicy.policyAdmin", # create org policies
"roles/resourcemanager.organizationAdmin", # setIamPolicy on organization
"roles/resourcemanager.folderAdmin", # create folders
"roles/resourcemanager.projectCreator", # create projects
"roles/resourcemanager.tagAdmin", # create tag keys and values
"roles/resourcemanager.tagUser", # create tag bindings on resources
"roles/serviceusage.serviceUsageAdmin", # enable services
])
project = project_name_test
role = each.value
members = [
"user:user1@google.com",
"user:user2@google.com",
"user:user3@google.com",
]
}
resource "google_project_iam_binding" "iam_binding_for_admin_project" {
project = project_name_test
role = "roles/compute.admin"
members = [
"user:user2@google.com",
"user:user3@google.com",
]
}
resource "google_project_iam_member" "iam_member_for_compute_viewer" {
project = project_name_test
role = "roles/compute.viewer"
member = "user:user1@google.com"
}
resource "google_project_iam_member_remove" "iam_member_remove_for_bigquery" {
project = project_name_test
role = "roles/bigquery.dataEditor"
member = "user:user1@google.com"
}
resource "google_project_iam_member" "iam_member_for_bigquery" {
project = project_name_test
role = "roles/bigquery.dataViewer"
member = "user:user1@google.com"
}
```
# EXAMPLE3 is as follows
## INPUT_TF_FILES
FilePath= "file_path1"
```
module "project_iam_binding" {
source = "terraform-google-modules/iam/google//modules/projects_iam"
version = "~> 7.0"
projects = [var.project_one, var.project_two]
mode = "additive"
bindings = {
"roles/compute.networkAdmin" = [
"serviceAccount:${var.sa_email}",
"group:${var.group_email}",
"user:${var.user_email}",
]
"roles/appengine.appAdmin" = [
"serviceAccount:${var.sa_email}",
"group:${var.group_email}",
"user:${var.user_email}",
]
}
}
```
FilePath= "file_path2"
```
variable "group_email" {
type = string
description = "Email for group to receive roles (ex. group@example.com)"
default = "scc-ai-eng@google.com"
}
variable "sa_email" {
type = string
description = "Email for Service Account to receive roles (Ex. default-sa@example-project-id.iam.gserviceaccount.com)"
default = "bqdw-uat-1@drs-issue-2.iam.gserviceaccount.com"
}
variable "user_email" {
type = string
description = "Email for group to receive roles (Ex. user@example.com)"
default = "varunbhardwaj@google.com"
}
/******************************************
project_iam_binding variables
*****************************************/
variable "project_one" {
type = string
description = "First project id to add the IAM policies/bindings"
default = "project-name-test"
}
variable "project_two" {
type = string
description = "Second project id to add the IAM policies/bindings"
default = "test-blueprint-deployment"
}
```
## IAM_BINDINGS
Member: "user:varunbhardwaj@google.com"
Action: "ADD"
Role: "roles/compute.viewer"
Action: "REMOVE"
Role: "roles/compute.admin"
## RESOURCE_NAME
project-name-test
** ANSWER **
FilePath= "file_path1"
```
module "project_iam_binding_for_project_one" {
source = "terraform-google-modules/iam/google//modules/projects_iam"
version = "~> 7.0"
projects = [var.project_one]
mode = "additive"
bindings = {
"roles/compute.networkAdmin" = [
"serviceAccount:${var.sa_email}",
"group:${var.group_email}",
"user:${var.user_email}",
]
"roles/compute.admin" = [
"serviceAccount:${var.sa_email}",
"group:${var.group_email}",
]
}
}
module "project_iam_binding_for_project_two" {
source = "terraform-google-modules/iam/google//modules/projects_iam"
version = "~> 7.0"
projects = [var.project_two]
mode = "additive"
bindings = {
"roles/compute.networkAdmin" = [
"serviceAccount:${var.sa_email}",
"group:${var.group_email}",
"user:${var.user_email}",
]
"roles/compute.admin" = [
"serviceAccount:${var.sa_email}",
"group:${var.group_email}",
"user:${var.user_email}",
]
}
}
resource "google_project_iam_member" "project_iam_member" {
project = var.project_one
role = "roles/compute.viewer"
member = "user:varunbhardwaj@google.com"
}
```
** Steps **
1. **Identify the file**: Identify the file(s) that contains the resources that need to be updated as per information in IAM Binding and TFSTATE_INFORMATION.
2. **Identify the actions**: Identify the ADD and REMOVE actions for the MEMBER in IAM Bindings.
3. **Identify the resource**: Identify the resource(s) that need to be updated as per information in IAM Binding.
4. **Identify the role**: Identify the role that needs to be assigned to the user(s) as per information in IAM Binding.
5. **Identify the permissions**: Identify the permissions that need to be granted to the user(s) as per information in IAM Binding.
6. **Identify the action**: Identify the action that needs to be taken as per information in IAM Binding.
7. **For action "ADD"**:
7a. If the role is present in google_project_iam_binding, Add the user(s) to the role(s) with the permissions.
7b. If the role is not present in google_project_iam_binding, use "google_project_iam_member" to add the user(s) to the role(s) with the permissions.
7c. If the role is present in google_project_iam_binding but have multiple roles in a for construct, use "google_project_iam_member" to add the user(s) to the role(s) with the permissions.
8. **For action "REMOVE"**:
8a.Identify the Resources: Determine which resources in your Terraform code (e.g., google_project_iam_binding, google_project_iam_member) might be granting the roles you want to remove to member specified.
8b. Remove the Member:
1. If the roles are managed within a google_project_iam_binding resource using the members attribute, remove member from that list for each role.
2. **If you find individual google_project_iam_member resources granting those specific roles to member, you can either delete those resources entirely or comment them out**.
3. If the role is not present in the INPUT_TF_FILES, use "google_project_iam_member_remove" to remove the user(s) from the role(s).
4. If a "google_project_iam_binding" exist for the role and member list in IAM_BINDING is not part of it, Nothing needs to be done for that role.
9. If the role is present in for loop in a "google_project_iam_binding" and any action has to be performed, create a new binding for the role and perform action while preserving the existing permissions.
10. If the role is present in for loop in a "google_project_iam_member" and any action has to be performed, create a new binding for the role and perform action while preserving the existing permissions.
11. Ensure that no new permissions are being assigned or removed to any user other than asked in IAM_BINDING field.
12. **Update the file**: Update the file(s) as per information in IAM Binding.
13. **Test the file**: Test the file(s) to ensure that the changes made are correct. Always verify that both the ADD and REMOVE actions are working as expected.
"""
return self._binding_prompt_format

View File

@@ -0,0 +1,68 @@
# -*- coding: utf-8 -*- #
# Copyright 2024 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.
"""Library for creating pull request related messages."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import json
def CreateCommitMessage(finding_data, member_name):
"""Creates a commit message.
Args:
finding_data: Finding data in JSON format.
member_name: The name of the member to be added to the commit message.
Returns:
A string containing the commit message.
"""
finding_data = json.loads(finding_data)
finding_result = finding_data["listFindingsResults"][0]
finding_category = finding_result["finding"][
"category"
]
crm_node = finding_result["resource"]["displayName"]
finding_name = finding_result["finding"]["name"]
return (
f"Fixing {finding_name} of category {finding_category} for"
f" {member_name} in {crm_node}"
)
def CreatePRMessage(finding_data):
"""Creates a commit message for a pull request.
Args:
finding_data: Finding data in JSON format.
Returns:
A string containing the Pull Request(PR) message.
"""
finding_data = json.loads(finding_data)
finding_result = finding_data["listFindingsResults"][0]
finding_category = finding_result["finding"][
"category"
]
crm_node = finding_result["resource"]["displayName"]
finding_name = finding_result["finding"]["name"]
return (
f"Fixing Finding:{finding_name} of Category:{finding_category}"
f" in {crm_node}"
)

View File

@@ -0,0 +1,275 @@
# -*- coding: utf-8 -*- #
# Copyright 2024 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.
"""Library for fetching TF Files."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import collections
import os
import re
from typing import Dict, List
from googlecloudsdk.command_lib.code import run_subprocess
from googlecloudsdk.core.util import files
import hcl2
def get_tfstate_information_per_member(
iam_bindings: Dict[str, Dict[str, List[str]]],
tfstate_json_list: List[Dict[str, str]],
resource_name: str,
) -> Dict[str, List[Dict[str, str]]]:
"""Gets the TFState information for the given IAM bindings.
Args:
iam_bindings: IAM bindings for the resource.
tfstate_json_list: List of TFState files.
resource_name: Resource name for which the finding was generated.
Returns:
List of TFState information for the given IAM bindings.
"""
tfstate_information: Dict[str, List[Dict[str, str]]] = {}
for member, binding in iam_bindings.items():
for tfstate_json in tfstate_json_list:
if "ADD" in binding:
for role in binding["ADD"]:
resource_data = fetch_relevant_modules(
tfstate_json, resource_name, role, member
)
if resource_data:
if member not in tfstate_information:
tfstate_information[member] = []
tfstate_information[member].append(resource_data)
if "REMOVE" in binding:
for role in binding["REMOVE"]:
resource_data = fetch_relevant_modules(
tfstate_json, resource_name, role, member
)
if resource_data:
if member not in tfstate_information:
tfstate_information[member] = []
tfstate_information[member].append(resource_data)
return tfstate_information
def read_original_files_content(
tf_files_paths: List[str],
)-> Dict[str, str]:
"""Reads the original files content.
Args:
tf_files_paths: List of TF files paths.
Returns:
Dict of file path and file content.
"""
original_tf_files = dict[str, str]()
for file_path in tf_files_paths:
original_file_content = files.ReadFileContents(file_path)
original_tf_files[file_path] = original_file_content
return original_tf_files
def update_tf_files(
response_dict: Dict[str, str],
):
"""Updates the TF files with the response dict.
Args:
response_dict: Response dict containing the updated TF files.
"""
for file_path, file_data in response_dict.items():
_ = files.WriteFileContents(file_path, file_data)
def validate_tf_files(
response_dict: Dict[str, str]
) -> (bool, Dict[str, str]):
"""Validates the TFState information for the given IAM bindings.
Args:
response_dict: response dict containing the updated TF files.
Returns:
True if the response is valid, False otherwise.
updated_response_dict: Updated response dict containing the original TF
files.
"""
original_tf_files = dict[str, str]()
for file_path, file_data in response_dict.items():
original_file_content = files.ReadFileContents(file_path)
original_tf_files[file_path] = original_file_content
try:
_ = files.WriteFileContents(file_path, file_data)
cmd = ["terraform", "fmt", "-write=true", file_path]
run_subprocess.GetOutputLines(cmd, timeout_sec=25, show_stderr=False)
response_dict[file_path] = files.ReadFileContents(file_path)
except Exception as e: # pylint: disable=broad-exception-caught
update_tf_files(original_tf_files)
return False, e
cmd = ["terraform", "validate"]
try:
validate_output = run_subprocess.GetOutputLines(
cmd, timeout_sec=25, show_stderr=False
)
except Exception as e: # pylint: disable=broad-exception-caught
update_tf_files(original_tf_files)
return False, e
update_tf_files(original_tf_files)
if re.search("Success", validate_output[0], re.IGNORECASE):
return True, response_dict
return False, None
def fetch_tfstate_json_from_dir(dir_path: str) -> str:
"""Fetches the TFState json for the given directory.
Args:
dir_path: The path of the directory to fetch the TFState files from.
Returns:
The json of the TFState file or None if there is an error.
"""
try:
os.chdir(dir_path)
cmd = ["terraform", "init"]
run_subprocess.GetOutputLines(cmd, timeout_sec=10)
except Exception as _: # pylint: disable=broad-exception-caught
return ""
try:
cmd = ["terraform", "show", "-json"]
tfstate_json = run_subprocess.GetOutputLines(cmd, timeout_sec=10)
except Exception as _: # pylint: disable=broad-exception-caught
return ""
return tfstate_json
def fetch_tfstate_json_from_file(file_path: str) -> str:
"""Fetches the TFState json for the given tfstate file path.
Args:
file_path: The path of the file to fetch the TFState json from.
Returns:
The json of the TFState files.
"""
file = files.ReadFileContents(file_path)
tfstate_json = hcl2.loads(file)
return tfstate_json
def fetch_relevant_modules(
tfstate_json: Dict[str, str],
resource_name: str, role_name: str, member_name: str,
) -> str:
"""Fetches the relevant modules from the given TFState files."""
resource_data = ""
if (
"values" not in tfstate_json
or "root_module" not in tfstate_json["values"]
or "resources" not in tfstate_json["values"]["root_module"]
):
return resource_data
for resource in tfstate_json["values"]["root_module"]["resources"]:
if (
"values" in resource
and "member" in resource["values"]
and "role" in resource["values"]
and "project_id" in resource["values"]
and resource["values"]["member"] == member_name
and resource["values"]["role"] == role_name
and resource["values"]["project_id"] == resource_name
):
resource_data = resource
break
return resource_data
def find_tf_files(root_dir: str) -> List[str]:
"""Finds all the TF files in the given directory.
Args:
root_dir: The path of the directory to find the TF files in.
Returns:
A list of the TF files paths in the given directory.
"""
tf_files = []
queue = collections.deque([root_dir])
while queue:
current_dir = queue.popleft()
for item in os.listdir(current_dir):
item_path = os.path.join(current_dir, item)
if os.path.isdir(item_path):
if not item.startswith("."):
queue.append(item_path)
elif os.path.isfile(item_path) and (
item_path.endswith(".tf") or item_path.endswith(".tfvars")
and not item_path.startswith(".")
):
tf_files.append(item_path)
return tf_files
def fetch_tfstate_list(
tfstate_file_paths: List[str],
root_dir: str,
) -> List[Dict[str, str]]:
"""Fetches the TFState list for the given TFState file paths.
Args:
tfstate_file_paths: List of TFState file paths.
root_dir: The path of the root directory.
Returns:
List of TFState json.
"""
tfstate_json_list = []
if tfstate_file_paths:
for tfstate_file_path in tfstate_file_paths:
tfstate_json_list.append(
fetch_tfstate_json_from_file(tfstate_file_path)
)
else:
return find_tfstate_jsons(root_dir)
return tfstate_json_list
def find_tfstate_jsons(
dir_path: str
) -> List[Dict[str, str]]:
"""Finds the TFState jsons in the given directory.
Args:
dir_path: The path of the directory to find the TFState jsons in.
Returns:
List of TFState jsons.
"""
tfstate_jsons = []
queue = collections.deque([dir_path])
while queue:
current_dir = queue.popleft()
tfstate_jsons.append(fetch_tfstate_json_from_dir(current_dir))
for item in os.listdir(current_dir):
if not item.startswith("."):
item_path = os.path.join(current_dir, item)
if os.path.isdir(item_path):
queue.append(item_path)
return tfstate_jsons

View File

@@ -0,0 +1,36 @@
# -*- coding: utf-8 -*- #
# Copyright 2025 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.
"""Useful commands for interacting with the Cloud SCC API."""
from googlecloudsdk.api_lib.util import apis
from googlecloudsdk.generated_clients.apis.securitycentermanagement.v1 import securitycentermanagement_v1_messages as messages
class BillingMetadataClient(object):
"""Client for Billing Metadata interaction with the Security Center Management API."""
def __init__(self):
# Although this client looks specific to projects, this is a codegen
# artifact. It can be used for any parent types.
self._client = apis.GetClientInstance(
'securitycentermanagement', 'v1'
).projects_locations
def Get(self, name: str) -> messages.BillingMetadata:
"""Get a Billing Metadata."""
req = messages.SecuritycentermanagementProjectsLocationsGetBillingMetadataRequest(
name=name
)
return self._client.GetBillingMetadata(req)

View File

@@ -0,0 +1,223 @@
# -*- 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.
"""Useful commands for interacting with the Cloud SCC API."""
from typing import Generator
from apitools.base.py import list_pager
from googlecloudsdk.api_lib.util import apis
from googlecloudsdk.command_lib.scc import util as scc_util
from googlecloudsdk.core import log
from googlecloudsdk.generated_clients.apis.securitycentermanagement.v1 import securitycentermanagement_v1_messages as messages
class ETDCustomModuleClient(object):
"""Client for ETD custom module interaction with the Security Center Management API."""
def __init__(self):
# Although this client looks specific to projects, this is a codegen
# artifact. It can be used for any parent types.
self._client = apis.GetClientInstance(
'securitycentermanagement', 'v1'
).projects_locations_eventThreatDetectionCustomModules
def Get(self, name: str) -> messages.EventThreatDetectionCustomModule:
"""Get a ETD custom module."""
req = messages.SecuritycentermanagementProjectsLocationsEventThreatDetectionCustomModulesGetRequest(
name=name
)
return self._client.Get(req)
def Validate(
self, parent: str, custom_config_json: str, module_type: str
) -> messages.ValidateEventThreatDetectionCustomModuleResponse:
"""Validate a ETD module."""
validate_request = messages.ValidateEventThreatDetectionCustomModuleRequest(
rawText=custom_config_json,
type=module_type,
)
req = messages.SecuritycentermanagementProjectsLocationsEventThreatDetectionCustomModulesValidateRequest(
parent=parent,
validateEventThreatDetectionCustomModuleRequest=validate_request,
)
response = self._client.Validate(req)
if not response.errors:
log.status.Print('Module is valid.')
return None
else:
log.status.Print(response)
return response
def Delete(self, name: str, validate_only: bool):
"""Delete a ETD custom module."""
req = messages.SecuritycentermanagementProjectsLocationsEventThreatDetectionCustomModulesDeleteRequest(
name=name, validateOnly=validate_only
)
response = self._client.Delete(req)
if validate_only:
log.status.Print('Request is valid.')
return response
log.DeletedResource(name)
return response
def Update(
self,
name: str,
validate_only: bool,
custom_config,
enablement_state,
update_mask: str,
) -> messages.EventThreatDetectionCustomModule:
"""Update an ETD custom module."""
event_threat_detection_custom_module = (
messages.EventThreatDetectionCustomModule(
config=custom_config,
enablementState=enablement_state,
name=name,
)
)
req = messages.SecuritycentermanagementProjectsLocationsEventThreatDetectionCustomModulesPatchRequest(
eventThreatDetectionCustomModule=event_threat_detection_custom_module,
name=name,
updateMask=scc_util.CleanUpUserMaskInput(update_mask),
validateOnly=validate_only,
)
response = self._client.Patch(req)
if validate_only:
log.status.Print('Request is valid.')
return response
log.UpdatedResource(name)
return response
def Create(
self,
parent: str,
validate_only: bool,
custom_config: messages.EventThreatDetectionCustomModule.ConfigValue,
enablement_state: messages.EventThreatDetectionCustomModule.EnablementStateValueValuesEnum,
module_type: str,
display_name: str,
) -> messages.EventThreatDetectionCustomModule:
"""Create an ETD custom module."""
event_threat_detection_custom_module = (
messages.EventThreatDetectionCustomModule(
config=custom_config,
enablementState=enablement_state,
displayName=display_name,
type=module_type,
)
)
req = messages.SecuritycentermanagementProjectsLocationsEventThreatDetectionCustomModulesCreateRequest(
eventThreatDetectionCustomModule=event_threat_detection_custom_module,
parent=parent,
validateOnly=validate_only,
)
response = self._client.Create(req)
if validate_only:
log.status.Print('Request is valid.')
return response
log.CreatedResource(display_name)
return response
def List(
self, page_size: int, parent: str, limit: int
) -> Generator[
messages.EventThreatDetectionCustomModule,
None,
messages.ListEventThreatDetectionCustomModulesResponse,
]:
"""List details of resident and inherited Event Threat Detection Custom Modules."""
req = messages.SecuritycentermanagementProjectsLocationsEventThreatDetectionCustomModulesListRequest(
pageSize=page_size, parent=parent
)
return list_pager.YieldFromList(
self._client,
request=req,
limit=limit,
field='eventThreatDetectionCustomModules',
batch_size=page_size,
batch_size_attribute='pageSize',
)
def ListDescendant(
self, page_size: int, parent: str, limit: int
) -> Generator[
messages.EventThreatDetectionCustomModule,
None,
messages.ListEventThreatDetectionCustomModulesResponse,
]:
"""List the details of the resident and descendant ETD custom modules."""
req = messages.SecuritycentermanagementProjectsLocationsEventThreatDetectionCustomModulesListDescendantRequest(
pageSize=page_size, parent=parent
)
return list_pager.YieldFromList(
self._client,
method='ListDescendant',
request=req,
limit=limit,
field='eventThreatDetectionCustomModules',
batch_size=page_size,
batch_size_attribute='pageSize',
)
class EffectiveETDCustomModuleClient(object):
"""Client for effective ETD custom module interaction with the Security Center Management API."""
def __init__(self):
self._client = apis.GetClientInstance(
'securitycentermanagement', 'v1'
).projects_locations_effectiveEventThreatDetectionCustomModules
def Get(
self, name: str) -> messages.EffectiveEventThreatDetectionCustomModule:
"""Get a ETD effective custom module."""
req = messages.SecuritycentermanagementProjectsLocationsEffectiveEventThreatDetectionCustomModulesGetRequest(
name=name
)
return self._client.Get(req)
def List(
self, page_size: int, parent: str, limit: int
) -> Generator[
messages.EffectiveEventThreatDetectionCustomModule,
None,
messages.ListEffectiveEventThreatDetectionCustomModulesResponse,
]:
"""List the details of the resident and descendant ETD effective custom modules."""
req = messages.SecuritycentermanagementProjectsLocationsEffectiveEventThreatDetectionCustomModulesListRequest(
pageSize=page_size, parent=parent
)
return list_pager.YieldFromList(
self._client,
request=req,
limit=limit,
field='effectiveEventThreatDetectionCustomModules',
batch_size=page_size,
batch_size_attribute='pageSize',
)

View File

@@ -0,0 +1,90 @@
# -*- coding: utf-8 -*- #
# Copyright 2024 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.
"""Useful commands for interacting with the Cloud SCC API."""
from typing import Generator
from apitools.base.py import list_pager
from googlecloudsdk.api_lib.util import apis
from googlecloudsdk.command_lib.scc import util as scc_util
from googlecloudsdk.core import log
from googlecloudsdk.generated_clients.apis.securitycentermanagement.v1 import securitycentermanagement_v1_messages as messages
class SecurityCenterServicesClient(object):
"""Client for Security Center Services interaction with the Security Center Management API."""
def __init__(self):
# Although this client looks specific to projects, this is a codegen
# artifact. It can be used for any parent types.
self._client = apis.GetClientInstance(
'securitycentermanagement', 'v1'
).projects_locations_securityCenterServices
def Get(self, name: str) -> messages.SecurityCenterService:
"""Get a Security Center Service."""
req = messages.SecuritycentermanagementProjectsLocationsSecurityCenterServicesGetRequest(
name=name
)
return self._client.Get(req)
def List(self, page_size: int, parent: str, limit: int) -> Generator[
messages.SecurityCenterService,
None,
messages.ListSecurityCenterServicesResponse,
]:
"""List the details of a Security Center Services."""
req = messages.SecuritycentermanagementProjectsLocationsSecurityCenterServicesListRequest(
pageSize=page_size, parent=parent
)
return list_pager.YieldFromList(
self._client,
request=req,
limit=limit,
field='securityCenterServices',
batch_size=page_size,
batch_size_attribute='pageSize',
)
def Update(
self,
name: str,
validate_only: bool,
module_config: messages.SecurityCenterService.ModulesValue,
enablement_state: messages.SecurityCenterService.IntendedEnablementStateValueValuesEnum,
update_mask: str,
) -> messages.SecurityCenterService:
"""Update a Security Center Service."""
security_center_service = messages.SecurityCenterService(
modules=module_config,
intendedEnablementState=enablement_state,
name=name,
)
req = messages.SecuritycentermanagementProjectsLocationsSecurityCenterServicesPatchRequest(
securityCenterService=security_center_service,
name=name,
updateMask=scc_util.CleanUpUserMaskInput(update_mask),
validateOnly=validate_only,
)
response = self._client.Patch(req)
if validate_only:
log.status.Print('Request is valid.')
return response
log.UpdatedResource(name)
return response

View File

@@ -0,0 +1,211 @@
# -*- 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.
"""Useful commands for interacting with the Cloud SCC API."""
from typing import Generator
from apitools.base.py import list_pager
from googlecloudsdk.api_lib.util import apis
from googlecloudsdk.command_lib.scc import util as scc_util
from googlecloudsdk.core import log
from googlecloudsdk.generated_clients.apis.securitycentermanagement.v1 import securitycentermanagement_v1_messages as messages
class SHACustomModuleClient(object):
"""Client for SHA custom module interaction with the Security Center Management API."""
def __init__(self):
# Although this client looks specific to projects, this is a codegen
# artifact. It can be used for any parent types.
self._client = apis.GetClientInstance(
'securitycentermanagement', 'v1'
).projects_locations_securityHealthAnalyticsCustomModules
def Get(self, name: str) -> messages.SecurityHealthAnalyticsCustomModule:
"""Get a SHA custom module."""
req = messages.SecuritycentermanagementProjectsLocationsSecurityHealthAnalyticsCustomModulesGetRequest(
name=name
)
return self._client.Get(req)
def Simulate(
self, parent, custom_config, resource
) -> messages.SimulateSecurityHealthAnalyticsCustomModuleResponse:
"""Simulate a SHA custom module."""
sim_req = messages.SimulateSecurityHealthAnalyticsCustomModuleRequest(
customConfig=custom_config, resource=resource
)
req = messages.SecuritycentermanagementProjectsLocationsSecurityHealthAnalyticsCustomModulesSimulateRequest(
parent=parent,
simulateSecurityHealthAnalyticsCustomModuleRequest=sim_req,
)
return self._client.Simulate(req)
def Update(
self,
name: str,
validate_only: bool,
custom_config: messages.CustomConfig,
enablement_state: messages.SecurityHealthAnalyticsCustomModule.EnablementStateValueValuesEnum,
update_mask: str,
) -> messages.SecurityHealthAnalyticsCustomModule:
"""Update a SHA custom module."""
security_health_analytics_custom_module = (
messages.SecurityHealthAnalyticsCustomModule(
customConfig=custom_config,
enablementState=enablement_state,
name=name,
)
)
req = messages.SecuritycentermanagementProjectsLocationsSecurityHealthAnalyticsCustomModulesPatchRequest(
securityHealthAnalyticsCustomModule=security_health_analytics_custom_module,
name=name,
updateMask=scc_util.CleanUpUserMaskInput(update_mask),
validateOnly=validate_only,
)
response = self._client.Patch(req)
if validate_only:
log.status.Print('Request is valid.')
return response
log.UpdatedResource(name)
return response
def Create(
self,
parent: str,
validate_only: bool,
custom_config: messages.CustomConfig,
enablement_state: messages.SecurityHealthAnalyticsCustomModule.EnablementStateValueValuesEnum,
display_name: str,
) -> messages.SecurityHealthAnalyticsCustomModule:
"""Create an SHA custom module."""
security_health_analytics_custom_module = (
messages.SecurityHealthAnalyticsCustomModule(
customConfig=custom_config,
enablementState=enablement_state,
displayName=display_name,
)
)
req = messages.SecuritycentermanagementProjectsLocationsSecurityHealthAnalyticsCustomModulesCreateRequest(
securityHealthAnalyticsCustomModule=security_health_analytics_custom_module,
parent=parent,
validateOnly=validate_only,
)
response = self._client.Create(req)
if validate_only:
log.status.Print('Request is valid.')
return response
log.CreatedResource(display_name)
return response
def Delete(self, name: str, validate_only: bool):
"""Delete a SHA custom module."""
req = messages.SecuritycentermanagementProjectsLocationsSecurityHealthAnalyticsCustomModulesDeleteRequest(
name=name, validateOnly=validate_only
)
response = self._client.Delete(req)
if validate_only:
log.status.Print('Request is valid.')
return response
log.DeletedResource(name)
return response
def List(
self, page_size: int, parent: str, limit: int
) -> Generator[
messages.SecurityHealthAnalyticsCustomModule,
None,
messages.ListSecurityHealthAnalyticsCustomModulesResponse,
]:
"""List the details of a SHA custom module."""
req = messages.SecuritycentermanagementProjectsLocationsSecurityHealthAnalyticsCustomModulesListRequest(
pageSize=page_size, parent=parent
)
return list_pager.YieldFromList(
self._client,
request=req,
limit=limit,
field='securityHealthAnalyticsCustomModules',
batch_size=page_size,
batch_size_attribute='pageSize',
)
def ListDescendant(
self, page_size: int, parent: str, limit: int
) -> Generator[
messages.SecurityHealthAnalyticsCustomModule,
None,
messages.ListDescendantSecurityHealthAnalyticsCustomModulesResponse,
]:
"""List the details of the resident and descendant SHA custom modules."""
req = messages.SecuritycentermanagementProjectsLocationsSecurityHealthAnalyticsCustomModulesListDescendantRequest(
pageSize=page_size, parent=parent
)
return list_pager.YieldFromList(
self._client,
method='ListDescendant',
request=req,
limit=limit,
field='securityHealthAnalyticsCustomModules',
batch_size=page_size,
batch_size_attribute='pageSize',
)
class EffectiveSHACustomModuleClient(object):
"""Client for SHA effective custom module interaction with the Security Center Management API."""
def __init__(self):
self._client = apis.GetClientInstance(
'securitycentermanagement', 'v1'
).projects_locations_effectiveSecurityHealthAnalyticsCustomModules
def Get(self,
name: str) -> messages.EffectiveSecurityHealthAnalyticsCustomModule:
"""Get a SHA effective custom module."""
req = messages.SecuritycentermanagementProjectsLocationsEffectiveSecurityHealthAnalyticsCustomModulesGetRequest(
name=name
)
return self._client.Get(req)
def List(
self, page_size: int, parent: str, limit: int
) -> Generator[
messages.EffectiveSecurityHealthAnalyticsCustomModule,
None,
messages.ListEffectiveSecurityHealthAnalyticsCustomModulesResponse,
]:
"""List the details of the resident and descendant SHA effective custom modules."""
req = messages.SecuritycentermanagementProjectsLocationsEffectiveSecurityHealthAnalyticsCustomModulesListRequest(
pageSize=page_size, parent=parent
)
return list_pager.YieldFromList(
self._client,
request=req,
limit=limit,
field='effectiveSecurityHealthAnalyticsCustomModules',
batch_size=page_size,
batch_size_attribute='pageSize',
)

View File

@@ -0,0 +1,36 @@
# -*- 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.
"""Utilities for Posture related commands to call Security Posture API."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.util import apis
from googlecloudsdk.calliope import base
API_NAME = 'securityposture'
VERSION_MAP = {base.ReleaseTrack.ALPHA: 'v1alpha', base.ReleaseTrack.GA: 'v1'}
# The messages module can also be accessed from client.MESSAGES_MODULE
def GetMessagesModule(release_track=base.ReleaseTrack.GA):
api_version = VERSION_MAP.get(release_track)
return apis.GetMessagesModule(API_NAME, api_version)
def GetClientInstance(release_track=base.ReleaseTrack.GA):
api_version = VERSION_MAP.get(release_track)
return apis.GetClientInstance(API_NAME, api_version)

View File

@@ -0,0 +1,72 @@
# -*- coding: utf-8 -*- #
# Copyright 2025 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.
"""Constants used in the Remediation Intent related commands."""
# Max retry count for doing the remediation of the intents.
REMEDIATION_RETRY_COUNT = 3
TF_CMD_TIMEOUT = 10
TF_FMT_ERROR_MSG = (
"Following error occurred while formatting the terraform file: {file_path}"
"\n STDOUT: {stdout}"
"\n STDERR: {stderr}"
)
TF_VALIDATE_ERROR_MSG = (
"Following error occurred while validating the terraform files: "
"\n STDOUT: {stdout}"
"\n STDERR: {stderr}"
)
BLOCK_SEPARATOR = "---------------------------------------------------------\n"
PR_FAILURE_MSG = (
"Following error occurred while creating the PR: "
"\n STDOUT: {stdout}"
"\n STDERR: {stderr}"
)
IAM_RECOMMENDER_FINDINGS = (
# go/keep-sorted start
"IAM_ROLE_HAS_EXCESSIVE_PERMISSIONS",
"IAM_ROLE_REPLACEMENT",
"SERVICE_AGENT_GRANTED_BASIC_ROLE",
"SERVICE_AGENT_ROLE_REPLACED_WITH_BASIC_ROLE",
"UNUSED_IAM_ROLE",
# go/keep-sorted end
)
FIREWALL_FINDINGS = (
# go/keep-sorted start
"OPEN_FIREWALL",
# go/keep-sorted end
)
COMMIT_MSG = (
"[Gemini Generated] [SCC] Remediation for finding: {finding_id}, Project:"
" {project_id}, Category {category}"
)
PR_TITLE = (
"[Gemini Generated] [SCC] Remediation for finding: {finding_id}, Project:"
" {project_id}, Category {category}"
)
PR_DESC = (
"Remediation explanation: {remediation_explanation}\n"
"Last file modifiers:\n{file_modifiers}\n"
"File owners:\n{file_owners}\n"
)

View File

@@ -0,0 +1,65 @@
# -*- coding: utf-8 -*- #
# Copyright 2025 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.
"""Module for storing converters to be used in the remediation intents."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from typing import Mapping, Sequence, Any, Dict
from googlecloudsdk.api_lib.scc.remediation_intents import sps_api
from googlecloudsdk.calliope import base
class RemediationIntentConverter():
"""Converter related to the Remediation Intent resource."""
def __init__(self, release_track=base.ReleaseTrack.ALPHA):
"""Initializes the RemediationIntentConverter.
Args:
release_track: The release track to use for the API version.
"""
self.messages = sps_api.GetMessagesModule(release_track)
def DictFilesToMessage(self, files_dict: Mapping[str, str]) -> Sequence[Any]:
"""Converts a dictionary of files with their content to the message.
Args:
files_dict: A dictionary of files with their content. [path: content]
Returns:
List of message of type [securityposture.messages.FileData]
"""
return [
self.messages.FileData(filePath=path, fileContent=content)
for path, content in files_dict.items()
]
def MessageFilesToDict(self, files_data: Sequence[Any]) -> Mapping[str, str]:
"""Converts a list of file messages to a dictionary.
Args:
files_data: A list of file messages. [securityposture.messages.FileData]
Returns:
A dictionary of files with their content. [path: content]
"""
result: Dict[str, str] = {
file_data.filePath: file_data.fileContent
for file_data in files_data
}
return result

View File

@@ -0,0 +1,137 @@
# -*- coding: utf-8 -*- #
# Copyright 2025 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.
"""Module containing the extended wrappers for Remediation Intents service."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from typing import Any
from googlecloudsdk.api_lib.scc.remediation_intents import sps_api
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.scc.remediation_intents import errors
class ExtendedSPSClient():
"""Extended client for the SPS Service (wrappers for specific API calls).
Attributes:
release_track: The Gcloud release track to use, like ALPHA, GA.
org_id: The organization ID for which the API methods are called.
api_version: The API version to use like v1alpha, main etc.
client: The client for the SPS Service.
messages: The messages module for the SPS Service.
"""
def __init__(self, org_id: str, release_track=base.ReleaseTrack.ALPHA):
"""Initializes the ExtendedSPSClient.
Args:
org_id: The organization ID for which the API methods are called.
release_track: The release track to use for the API version.
"""
self.release_track = release_track
self.org_id = org_id
self.api_version = sps_api.VERSION_MAP.get(release_track)
self.client = sps_api.GetClientInstance(release_track)
self.messages = sps_api.GetMessagesModule(release_track)
def fetch_enqueued_remediation_intent(self) -> Any:
"""Fetches a Remediation Intent resource in ENQUEUED state in given org.
Returns:
A Remediation Intent resource in ENQUEUED state for the given org. If no
such resource is found, returns None.
Return format is of class (securityposture.messages.RemediationIntent).
Raises:
APICallError: An error while calling the SPS Service.
"""
request = self.messages.SecuritypostureOrganizationsLocationsRemediationIntentsListRequest(
parent=f'organizations/{self.org_id}/locations/global',
filter='state : REMEDIATION_INTENT_ENQUEUED',
)
try:
response = ( # List API call.
self.client.organizations_locations_remediationIntents.List(request)
)
except Exception as e: # Any error like network or system failure.
raise errors.APICallError('List', str(e))
remediation_intents = response.remediationIntents
if remediation_intents is None or len(remediation_intents) < 1:
return None
return remediation_intents[0]
def create_semi_autonomous_remediation_intent(self) -> None:
"""Creates a Semi Autonomous type Remediation Intent resource.
Raises:
APICallError: An error while calling the SPS Service.
"""
request = self.messages.SecuritypostureOrganizationsLocationsRemediationIntentsCreateRequest(
parent=f'organizations/{self.org_id}/locations/global',
createRemediationIntentRequest=self.messages.CreateRemediationIntentRequest(
workflowType=self.messages.CreateRemediationIntentRequest.WorkflowTypeValueValuesEnum.WORKFLOW_TYPE_SEMI_AUTONOMOUS,
),
)
try: # Create API call.
operation = self.client.organizations_locations_remediationIntents.Create(
request=request
)
_ = sps_api.WaitForOperation( # Polling the LRO.
operation_ref=sps_api.GetOperationsRef(operation.name),
message='Waiting for remediation intent to be created',
has_result=True,
)
except Exception as e:
raise errors.APICallError('Create', str(e))
def update_remediation_intent(
self,
ri_name: str, update_mask: str,
remediation_intent: Any,
) -> Any:
"""Updates a Remediation Intent resource.
Args:
ri_name: The name of the Remediation Intent resource to be updated.
update_mask: The update mask for the update operation.
remediation_intent: The updated Remediation Intent resource.
Returns:
The updated Remediation Intent resource.
Return format is of class (securityposture.messages.RemediationIntent).
Raises:
APICallError: An error while calling the SPS Service.
"""
request = self.messages.SecuritypostureOrganizationsLocationsRemediationIntentsPatchRequest(
name=ri_name,
updateMask=update_mask,
remediationIntent=remediation_intent
)
try:
operation = self.client.organizations_locations_remediationIntents.Patch(
request=request
)
return sps_api.WaitForOperation( # Polling the LRO.
operation_ref=sps_api.GetOperationsRef(operation.name),
message='Waiting for remediation intent to be updated',
has_result=True,
)
except Exception as e:
raise errors.APICallError('Update', str(e))

View File

@@ -0,0 +1,245 @@
# -*- coding: utf-8 -*- #
# Copyright 2025 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.
"""Helper functions to interact with git and github for remediation intents orchestration."""
import os
import subprocess
import tempfile
from typing import Mapping, Tuple
from googlecloudsdk.api_lib.scc.remediation_intents import const
from googlecloudsdk.command_lib.code import run_subprocess
from googlecloudsdk.core.util import files
def is_git_repo():
"""Check whether the current directory is a git repo or not.
Returns:
True, repo_root_path if the current directory is a git repo
False, None otherwise.
"""
try:
git_check_cmd = ('git rev-parse --show-toplevel')
result = subprocess.run(
git_check_cmd,
shell=True, check=True, cwd=os.getcwd(),
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
text=True,
)
return True, result.stdout.strip()
except subprocess.CalledProcessError:
return False, None
def branch_remote_exists(remote_name, branch_name):
"""Helper function to check if a branch exists in the remote.
Args:
remote_name: Name of the remote of the repo at which to check.
branch_name: Name of the branch to check.
Returns:
Boolean indicating whether the branch exists in the remote.
"""
result = subprocess.run(
['git', 'ls-remote', '--heads', remote_name, branch_name],
check=False,
cwd=os.getcwd(),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
) # Output: <256hash> refs/heads/branch_name | empty string if not found.
return bool(result.stdout.strip())
def get_working_tree_dir(*, remote_name, branch_name):
"""Returns the working tree directory for the branch.
Will create a new working tree if one doesn't exist
Args:
remote_name: Name of the remote of the repo at which to check.
branch_name: Name of the branch for which the working tree directory is
required.
Returns:
Working tree directory path for the branch in string format.
"""
worktree_dir = None
# Check if there is a worktree already for the branch.
existing_worktrees = subprocess.check_output(
['git', 'worktree', 'list'] # output format is: <path> <branch>
).decode('utf-8')
for line in existing_worktrees.splitlines():
if branch_name in line:
# If worktree found for the branch, set it
worktree_dir = line.split()[0]
break
if worktree_dir is None: # else create a new worktree
worktree_dir = tempfile.mkdtemp()
subprocess.run(
['git', 'worktree', 'add', worktree_dir, '-B', branch_name],
check=True, cwd=os.getcwd(),
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
)
# Check if the branch exists in the remote and push the branch if not.
if not branch_remote_exists(remote_name, branch_name):
subprocess.run(
['git', 'push', '--set-upstream', remote_name, branch_name],
check=False, cwd=worktree_dir,
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
)
# Pull the latest changes from the remote for the branch.
subprocess.run(
['git', 'pull'],
check=False, cwd=worktree_dir,
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
)
return worktree_dir
def push_commit(files_data, commit_message, remote_name, branch_name):
"""Pushes the commit to the given branch with the given files data and commit message.
Args:
files_data: Dictionary of file path (relative path of the files in original
repo) and file data in string format to be written
commit_message: Message to be added to the commit.
remote_name: Name of the remote of the repo at which to check.
branch_name: Name of the branch where commit needs to be pushed.
"""
worktree_dir = get_working_tree_dir(
remote_name=remote_name, branch_name=branch_name
)
# Overwrite the files in the worktree dir's for the branch.
for file_path, file_data in files_data.items():
abs_file_path = os.path.join(worktree_dir, file_path)
files.WriteFileContents(abs_file_path, file_data)
subprocess.run( # add them to the git index
['git', 'add', abs_file_path],
check=True,
cwd=worktree_dir,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
subprocess.run(
['git', 'commit', '-m', commit_message],
check=False,
cwd=worktree_dir,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
# Push the commit.
subprocess.run(
['git', 'push'],
check=False,
cwd=worktree_dir,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
def create_pr(
title, desc, remote_name, branch_name, base_branch, reviewers
) -> Tuple[bool, str]:
"""Creates a PR for the given branch to the main base branch.
Args:
title: PR title
desc: PR description
remote_name: Name of the remote of the repo at which to check.
branch_name: The branch from which PR needs to be created.
base_branch: The main branch name to be which PR needs to be merged.
reviewers: List of reviewers to be added to the PR.
Returns:
Boolean indicating whether the PR was created successfully or not.
PR link if created successfully, otherwise error message.
"""
worktree_dir = get_working_tree_dir(
remote_name=remote_name,
branch_name=branch_name
)
pr_command = [
'gh',
'pr',
'create',
'--base',
base_branch,
'--head',
branch_name,
'--title',
title,
'--body',
desc,
'--assignee',
reviewers,
]
try:
p = subprocess.run( # If successful, output will be the PR link.
pr_command,
check=True,
cwd=worktree_dir,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
pr_link = p.stdout.strip()
except subprocess.CalledProcessError as e:
return False, const.PR_FAILURE_MSG.format(stdout=e.stdout, stderr=e.stderr)
subprocess.run( # cleanup the worktree
['git', 'worktree', 'remove', '--force', worktree_dir],
check=False,
cwd=worktree_dir,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
return True, pr_link
def get_file_modifiers(
files_data: Mapping[str, str], first: bool = False
) -> Mapping[str, str]:
"""Returns the first (creators) or last modifiers for the given files.
By default, it returns the last modifiers.
Args:
files_data: Dictionary of file path and file contents.
first: If True, returns the first modifiers for the files. Otherwise,
returns the last modifiers for the files.
Returns:
A dictionary of file path and the modifiers for the given files. If the
command fails, an empty dictionary is returned. ["file_path" : "modifier"]
"""
file_modifiers = {}
base_cmd = [
'git', 'log',
*(['--reverse'] if first else []),
'-s', '-n1', '--pretty=format:%ae%n',
] # By default, the cmd gives the last modifier for the file.
try:
for file_path, _ in files_data.items():
cmd = base_cmd + [file_path]
# If successful, cmd_output: <email_address> followed by \n
file_modifiers[file_path] = run_subprocess.GetOutputLines(
cmd, timeout_sec=const.TF_CMD_TIMEOUT, strip_output=True
)[0] # first line of the output is the email address.
return file_modifiers
except subprocess.CalledProcessError:
return {}

View File

@@ -0,0 +1,109 @@
# -*- coding: utf-8 -*- #
# Copyright 2025 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.
"""Module for storing the functions for parsing the tfstate files for dfifferent findings."""
import json
from typing import Sequence, Mapping, Any
from googlecloudsdk.api_lib.scc.remediation_intents import const
from googlecloudsdk.command_lib.util.apis import arg_utils
def iam_recommender_parser(
all_resources: Sequence[Mapping[str, Any]], finding_data
) -> str:
"""Parses the terraform state file for IAM recommender findings.
Args:
all_resources: List of resources from the tfstate file. Resource Format:
{
"type": "google_project_iam_member",
"value": {
"member": "user:test@google.com",
"role": "roles/owner"
}
}
finding_data: SCC Finding data in form of class
(securityposture.messages.Finding).
Returns:
A string containing the terraform resource data blocks in structured format
for the given finding data.
Format: (Data block as json string + SEPARATOR ...)
If any error occurs, returns an empty string.
"""
data_blocks = []
try:
iam_bindings = arg_utils.GetFieldValueFromMessage(
finding_data, "findingMetadata.iamBindingsList.iamBindings"
)
for resource in all_resources:
# If the resource is of valid type, then check if the resource contains
# blocks.
is_relevant_resource = False
if resource["type"] == "google_project_iam_member":
for binding in iam_bindings:
if (
resource["value"]["member"] == binding.member
and resource["value"]["role"] == binding.role
):
is_relevant_resource = True
break
elif resource["type"] == "google_project_iam_binding":
for binding in iam_bindings:
if (
resource["value"]["role"] == binding.role
and (binding.member in resource["value"]["members"])
):
is_relevant_resource = True
break
# Add resource+separator to the data blocks if it's relevant.
if is_relevant_resource:
data_blocks.append(json.dumps(resource, indent=2))
data_blocks.append(const.BLOCK_SEPARATOR)
return "".join(data_blocks)
except (KeyError, arg_utils.InvalidFieldPathError) as _:
return ""
def firewall_parser(
all_resources: Sequence[Mapping[str, Any]], finding_data
) -> str:
"""Parses the terraform state file for firewall findings.
Args:
all_resources: List of resources from the tfstate file. Resource Format: {
"type": "google_compute_firewall", "value": { "name": "default-allow-ssh"
} }
finding_data: SCC Finding data in form of class
(securityposture.messages.Finding).
Returns:
A string containing the terraform resource data block in json format
for the given finding data.
If any error occurs, returns an empty string.
"""
try:
firewall_name = arg_utils.GetFieldValueFromMessage(
finding_data, "findingMetadata.firewallRule.name"
)
for resource in all_resources:
if (
resource["type"] == "google_compute_firewall"
and resource["value"]["name"] == firewall_name
):
return json.dumps(resource, indent=2)
except (KeyError, arg_utils.InvalidFieldPathError) as _:
return ""

View File

@@ -0,0 +1,102 @@
# -*- coding: utf-8 -*- #
# Copyright 2025 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.
"""Utility functions to call Security Posture API."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import datetime
from googlecloudsdk.api_lib.util import apis
from googlecloudsdk.api_lib.util import waiter
from googlecloudsdk.calliope import base
from googlecloudsdk.core import resources
API_NAME = 'securityposture'
VERSION_MAP = {
base.ReleaseTrack.ALPHA: 'v1alpha',
base.ReleaseTrack.GA: 'v1'
}
def GetMessagesModule(release_track=base.ReleaseTrack.ALPHA):
api_version = VERSION_MAP.get(release_track)
return apis.GetMessagesModule(API_NAME, api_version)
def GetClientInstance(release_track=base.ReleaseTrack.ALPHA):
api_version = VERSION_MAP.get(release_track)
return apis.GetClientInstance(API_NAME, api_version)
def GetOperationsRef(operation_id, release_track=base.ReleaseTrack.ALPHA):
"""Operations to Resource used for `waiter.WaitFor`.
Args:
operation_id: The operation ID for which resource reference is required.
release_track: The release track to use for the API version.
Returns:
The resource reference for the operation.
"""
return resources.REGISTRY.ParseRelativeName(
operation_id,
collection='securityposture.organizations.locations.operations',
api_version=VERSION_MAP.get(release_track),
)
def WaitForOperation(
operation_ref,
message,
has_result=False,
release_track=base.ReleaseTrack.ALPHA,
max_wait=datetime.timedelta(seconds=600),
):
"""Waits for an operation to complete.
Polls the Security Posture Operations service until the operation completes,
fails, or max_wait_seconds elapses.
Args:
operation_ref: A Resource created by GetOperationRef describing the
Operation.
message: The message to display to the user while they wait.
has_result: If True, the function will return the target of the operation
when it completes. If False, nothing will be returned.
release_track: The release track to use for the API version.
max_wait: The time to wait for the operation to succeed before timing out.
Returns:
if has_result = True, a RemediationIntent entity.
Otherwise, None.
"""
client = GetClientInstance(release_track)
resource_client = client.organizations_locations_remediationIntents
operations_client = client.organizations_locations_operations
if has_result:
poller = waiter.CloudOperationPoller(
resource_client, operations_client
)
else:
# For no result expectations, just operations service client is required.
poller = waiter.CloudOperationPollerNoResources(operations_client)
response = waiter.WaitFor(
poller, operation_ref, message, max_wait_ms=max_wait.seconds * 1000
)
return response

View File

@@ -0,0 +1,227 @@
# -*- coding: utf-8 -*- #
# Copyright 2025 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.
"""Module for interacting with Terraform files."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import collections
import json
import os
import subprocess
from typing import Dict, List, Any
from googlecloudsdk.api_lib.scc.remediation_intents import const
from googlecloudsdk.api_lib.scc.remediation_intents import parsers
from googlecloudsdk.command_lib.code import run_subprocess
from googlecloudsdk.command_lib.scc.remediation_intents import errors
from googlecloudsdk.core.util import files
def fetch_tf_files(root_dir: str) -> Dict[str, str]:
"""Fetches all the relevant TF files in the given directory recusively and returns a dictionary of the file paths and contents.
Args:
root_dir: The path of the directory to find the TF files in.
Returns:
A dictionary of the TF files in the given directory {path: contents}.
"""
tf_files: Dict[str, str] = {}
dir_queue = collections.deque([root_dir])
# Algo: Recursively search the items in the current directory, if it's a
# directory, add it to the queue. If it is a tf file, read it to the
# dictionary.
while dir_queue:
current_dir = dir_queue.popleft()
for item in os.listdir(current_dir):
item_path = os.path.join(current_dir, item)
if os.path.isdir(item_path):
if not item.startswith("."): # Ignore hidden directories
dir_queue.append(item_path)
elif os.path.isfile(item_path) and (
item_path.endswith(".tf") or item_path.endswith(".tfvars")
and not item_path.startswith(".")
):
tf_files[item_path] = files.ReadFileContents(item_path)
return tf_files
def fetch_tfstate(dir_path: str)-> json:
"""Fetches the TfState json for the given directory and returns in json format.
Args:
dir_path: The path of the directory to fetch the TfState data from.
Returns:
The json of the TfState data or throws an exception if there is an error.
"""
# Fetching tfState data is a two step process:
# 1. Run terraform init command to initialize the terraform directory.
# 2. Run terraform show command to fetch the tfstate data.
try:
org_dir = os.getcwd()
os.chdir(dir_path)
cmd = ["terraform", "init"] # Step 1
run_subprocess.GetOutputLines(cmd, timeout_sec=const.TF_CMD_TIMEOUT)
except Exception as e:
raise errors.TfStateFetchingError(str(e))
try:
cmd = ["terraform", "show", "-json"] # Step 2
tfstate_data = run_subprocess.GetOutputLines(
cmd, timeout_sec=const.TF_CMD_TIMEOUT, strip_output=True
)
os.chdir(org_dir)
return json.loads(tfstate_data[0])
except Exception as e:
raise errors.TfStateFetchingError(str(e))
def validate_tf_files(modified_tf_files: Dict[str, str]) -> str:
"""Validates the given TF files and returns the appropriate error message if any.
Args:
modified_tf_files: The dictionary of the modified TF files {path: contents}.
Returns:
The error message if any in string format, otherwise None.
"""
# Save the original contents of just the modified TF files.
original_tf_files: Dict[str, str] = {}
for file_path, _ in modified_tf_files.items():
original_tf_files[file_path] = files.ReadFileContents(file_path)
# Format the modified TF files one by one.
for file_path, file_content in modified_tf_files.items():
files.WriteFileContents(file_path, file_content)
try:
cmd = ["terraform", "fmt", "-write=true", file_path]
_ = subprocess.run(
cmd,
text=True,
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
except subprocess.CalledProcessError as e:
# Restore the original contents of the files in case of error.
_ = [
files.WriteFileContents(fp, fc)
for fp, fc in original_tf_files.items()
]
return const.TF_FMT_ERROR_MSG.format(
file_path=file_path, stdout=e.stdout, stderr=e.stderr
)
# Validate the modified TF files by running terraform validate command.
cmd = ["terraform", "validate"]
try:
_ = subprocess.run(
cmd,
text=True,
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
except subprocess.CalledProcessError as e:
# Restore the original contents of the files in case of error.
_ = [
files.WriteFileContents(fp, fc)
for fp, fc in original_tf_files.items()
]
return const.TF_VALIDATE_ERROR_MSG.format(
stdout=e.stdout, stderr=e.stderr
)
# Restore the original contents of the files finally.
_ = [files.WriteFileContents(fp, fc) for fp, fc in original_tf_files.items()]
return None
def get_resources_from_tfstate(
tfstate_json: Dict[str, Any],
) -> List[Dict[str, Any]]:
"""Traverses the TfState json and returns a list of resources in json format.
Args:
tfstate_json: The json of the TfState data. Structure:
{
"values": {
"root_module": {
"resources": [ ... ], # List of resources
"child_modules": [ # List of nested modules
{
"resources": [ ... ],
"child_modules": [ ... ]
}
]
}
}
}
Returns:
A list of json objects, each representing a resource in the TfState.
or an empty list if there are no resources in the TfState or if the TfState
is not in the expected format.
"""
all_resources = []
# Recursive function to traverse the tfstate data and extract the resources.
def traverse(module: Dict[str, Any]):
if "resources" in module:
all_resources.extend(module["resources"])
if "child_modules" in module:
for child in module["child_modules"]:
traverse(child)
root_module = tfstate_json.get("values", {}).get("root_module", {})
traverse(root_module)
return all_resources
def parse_tf_file(dir_path: str, finding_data) -> str:
"""Parses the tfstate file for the given finding.
Args:
dir_path: The path of the directory to parse the tfstate file from.
finding_data: SCC Finding data in form of class
(securityposture.messages.Finding).
Returns:
The structured data depending on the finding category, in string format. If
the finding category is not supported, returns an empty string.
"""
tftstate_json = fetch_tfstate(dir_path)
resources = get_resources_from_tfstate(tftstate_json)
# Mapping of finding category to the parser function.
parser_map = { # category: parser_function
**{
category: parsers.iam_recommender_parser
for category in const.IAM_RECOMMENDER_FINDINGS
},
**{
category: parsers.firewall_parser
for category in const.FIREWALL_FINDINGS
},
}
# Each parser function takes the list of resources and the finding data as
# input and returns the structured data in string format.
if finding_data.category in parser_map:
return parser_map[finding_data.category](resources, finding_data)
return ""

View File

@@ -0,0 +1,65 @@
# -*- coding: utf-8 -*- #
# Copyright 2025 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.
"""Module for storing the functions related to validation of data."""
import pathlib
from typing import Any, Mapping
from googlecloudsdk.command_lib.scc.remediation_intents import errors
def validate_git_config(git_config_file: Mapping[str, Any]):
"""Validates the git config file, raises an error if it is invalid.
Args:
git_config_file: The git config file data in dict format to be validated.
"""
# Check if the required fields are present in the git config file.
# The fields are: [remote, main-branch-name, branch-prefix]
if git_config_file.get('remote', None) is None:
raise errors.InvalidGitConfigError('remote')
if git_config_file.get('main-branch-name', None) is None:
raise errors.InvalidGitConfigError('main-branch-name')
if git_config_file.get('branch-prefix', None) is None:
raise errors.InvalidGitConfigError('branch-prefix')
if git_config_file.get('reviewers', None) is None:
raise errors.InvalidGitConfigError('reviewers')
def validate_relative_dir_path(rel_dir_path: str):
"""Validates the relative directory path, raises an error if it is invalid.
Args:
rel_dir_path: The relative directory path to be validated.
"""
# Default to current directory if empty
rel_dir_path = rel_dir_path.strip() or '.'
path_obj = pathlib.Path(rel_dir_path)
if path_obj.is_absolute():
raise errors.InvalidDirectoryPathError(
rel_dir_path, 'Directory path must be relative not absolute.'
)
if not path_obj.exists():
raise errors.InvalidDirectoryPathError(
rel_dir_path, 'Directory path does not exist.'
)
if not path_obj.is_dir():
raise errors.InvalidDirectoryPathError(
rel_dir_path, 'Given path is not a directory.'
)

View File

@@ -0,0 +1,65 @@
# -*- 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.
"""Useful commands for interacting with the Cloud SCC API."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.util import apis
from googlecloudsdk.core import exceptions
API_NAME = 'securitycenter'
BETA_API_VERSION = 'v1beta1'
V1_API_VERSION = 'v1'
V1P1BETA1_API_VERSION = 'v1p1beta1'
V2_API_VERSION = 'v2'
def GetClient(version=V1_API_VERSION):
"""Import and return the appropriate Cloud SCC client.
Args:
version: str, the version of the API desired.
Returns:
Cloud SCC client for the appropriate release track.
"""
return apis.GetClientInstance(API_NAME, version)
def GetMessages(version=V1_API_VERSION):
"""Import and return the appropriate Cloud SCC messages module."""
return apis.GetMessagesModule(API_NAME, version)
class AssetsClient(object):
"""Client for Security Center service in the for the Asset APIs."""
def __init__(self, client=None, messages=None):
self.client = client or GetClient()
self.messages = messages or GetMessages()
self._assetservice = self.client.organizations_assets
def List(self, parent, request_filter=None):
list_req_type = (self.messages.SecuritycenterOrganizationsAssetsListRequest)
list_req = list_req_type(parent=parent, filter=request_filter)
return self._assetservice.List(list_req)
class Error(exceptions.Error):
"""Base class for exceptions in this module."""

View File

@@ -0,0 +1,14 @@
# -*- coding: utf-8 -*- #
# Copyright 2022 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.

View File

@@ -0,0 +1,185 @@
# -*- coding: utf-8 -*- #
# Copyright 2022 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.
"""Overwatch Object to run all commands under overwatch."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.command_lib.scc.slz_overwatch import instances
class SLZOverwatchClient(object):
"""Implements overwatch commands using API Client."""
def __init__(self):
self._messages = instances.get_overwatch_message()
self._overwatch_service = instances.get_overwatch_service()
self._organization_service = instances.get_organization_service()
self._operation_service = instances.get_operations_service()
def List(self, parent, page_size=None, page_token=None):
"""Implements method for the overwatch command `list`.
Args:
parent: The organization ID and location in the format
organizations/<ORG_ID>/locations/<LOCATION-ID>.
page_size: The entries required at a time.
page_token: The page token for the specific page. If the page_token is
empty, then it indicates to return results from the start.
Returns:
response: The response from the List method of API client.
"""
if page_size is not None:
page_size = int(page_size)
request = self._messages.SecuredlandingzoneOrganizationsLocationsOverwatchesListRequest(
parent=parent, pageSize=page_size, pageToken=page_token)
response = self._overwatch_service.List(request)
return response
def Get(self, overwatch_path):
"""Implements method for the overwatch command `get`.
Args:
overwatch_path: The complete overwatch path. Format:
organizations/<ORG_ID>/locations/<LOCATION_ID>/overwatches/<OVERWATCH_ID>.
Returns:
response: The json response from the Get method of API client.
"""
request = self._messages.SecuredlandingzoneOrganizationsLocationsOverwatchesGetRequest(
name=overwatch_path)
response = self._overwatch_service.Get(request)
return response
def Delete(self, overwatch_path):
"""Implements method for the overwatch command `delete`.
Args:
overwatch_path: The complete overwatch path. Format:
organizations/<ORG_ID>/locations/<LOCATION_ID>/overwatches/<OVERWATCH_ID>.
Returns:
response: The json response from the Delete method of API client.
"""
request = self._messages.SecuredlandingzoneOrganizationsLocationsOverwatchesDeleteRequest(
name=overwatch_path)
response = self._overwatch_service.Delete(request)
return response
def Create(self, overwatch, blueprint_plan):
"""Implements method for the overwatch command `create`.
Args:
overwatch: The overwatch resource.
blueprint_plan: Base64 encoded blueprint plan message.
Returns:
response: The json response from the Create method of API client.
"""
req_overwatch = self._messages.GoogleCloudSecuredlandingzoneV1betaOverwatch(
name=overwatch.RelativeName(),
# Any overwatch created by default is set to ACTIVE
state=self._messages.GoogleCloudSecuredlandingzoneV1betaOverwatch
.StateValueValuesEnum.ACTIVE,
planData=blueprint_plan)
request = self._messages.SecuredlandingzoneOrganizationsLocationsOverwatchesCreateRequest(
parent=overwatch.Parent().RelativeName(),
googleCloudSecuredlandingzoneV1betaOverwatch=req_overwatch,
overwatchId=overwatch.Name())
response = self._overwatch_service.Create(request)
return response
def Suspend(self, overwatch_path):
"""Implements method for the overwatch command `suspend`.
Args:
overwatch_path: The complete overwatch path. Format:
organizations/<ORG_ID>/locations/<LOCATION_ID>/overwatches/<OVERWATCH_ID>.
Returns:
response: The json response from the Suspend method of API client.
"""
request = self._messages.SecuredlandingzoneOrganizationsLocationsOverwatchesSuspendRequest(
name=overwatch_path)
response = self._overwatch_service.Suspend(request)
return response
def Activate(self, overwatch_path):
"""Implements method for the overwatch command `activate`.
Args:
overwatch_path: The complete overwatch path. Format:
organizations/<ORG_ID>/locations/<LOCATION_ID>/overwatches/<OVERWATCH_ID>.
Returns:
response: The json response from the Activate method of API client.
"""
request = self._messages.SecuredlandingzoneOrganizationsLocationsOverwatchesActivateRequest(
name=overwatch_path)
response = self._overwatch_service.Activate(request)
return response
def Patch(self, overwatch_path, blueprint_plan, update_mask):
"""Implements method for the overwatch command `update`.
Args:
overwatch_path: The complete overwatch path. Format:
organizations/<ORG_ID>/locations/<LOCATION_ID>/overwatches/<OVERWATCH_ID>.
blueprint_plan: Base64 encoded blueprint plan message.
update_mask: The name of the field that will be updated.
Returns:
response: The json response from the Update method of API client.
"""
overwatch = self._messages.GoogleCloudSecuredlandingzoneV1betaOverwatch(
name=overwatch_path, planData=blueprint_plan)
request = self._messages.SecuredlandingzoneOrganizationsLocationsOverwatchesPatchRequest(
name=overwatch_path,
googleCloudSecuredlandingzoneV1betaOverwatch=overwatch,
updateMask=update_mask)
response = self._overwatch_service.Patch(request)
return response
def Enable(self, parent):
"""Implements method for the overwatch command `enable`.
Args:
parent: The parent where overwatch service needs to be enabled. Format:
organizations/<ORG_ID>/locations/<LOCATION_ID>
Returns:
response: The json response from the Enable method of API client.
"""
request = self._messages.SecuredlandingzoneOrganizationsLocationsEnableOverwatchRequest(
organization=parent)
response = self._organization_service.EnableOverwatch(request)
return response
def Operation(self, operation_id):
"""Implements method for the overwatch command `operation`.
Args:
operation_id: The operation ID of google.lonrunning.operation. Format:
organizations/<ORG_ID>/locations/<LOCATION_ID>/operations/<OPERATION_ID>.
Returns:
response: The json response from the Operation method of API client.
"""
request = self._messages.SecuredlandingzoneOrganizationsLocationsOperationsGetRequest(
name=operation_id)
response = self._operation_service.Get(request)
return response