variable "region_id" {
type = string
default = "cn-shenzhen"
}
# main.tf
provider "alicloud" {
region = var.region_id
}
resource "local_file" "python_script" {
content = <<EOF
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
import sys
sys.path.append('/opt/python')
import json
import logging
import jmespath # jmespath を jsonpath の代わりに使用します。
from alibabacloud_tea_openapi import models as open_api_models
from alibabacloud_tea_openapi.client import Client as OpenApiClient
from alibabacloud_openapi_util.client import Client as OpenApiUtilClient
from alibabacloud_tea_util import models as util_models
logger = logging.getLogger()
def handler(event, context):
logger.info(f"This is event: {str(event, encoding='utf-8')}")
get_resources_non_compliant(event, context)
def get_resources_non_compliant(event, context):
# 非準拠リソースに関する情報を取得します。
resources = parse_json(event)
# 非準拠リソースを走査して修復を実行します。
for resource in resources:
remediation(resource, context)
def parse_json(content):
"""
文字列を JSON オブジェクトに解析します。
:param content: JSON 文字列コンテンツ
:return: JSON オブジェクト
"""
try:
return json.loads(content)
except Exception as e:
logger.error('Parse content:{} to json error:{}.'.format(content, e))
return None
def remediation(resource, context):
logger.info(f"修復対象のリソース情報: {resource}")
region_id = resource['regionId']
account_id = resource['accountId']
resource_id = resource['resourceId']
resource_type = resource['resourceType']
if resource_type == 'ACS::ECS::SecurityGroup':
# 非準拠セキュリティグループの構成を取得し、再検証して評価の正確性を確保します。
resource_result = get_discovered_resource(context, resource_id, resource_type, region_id)
configuration = json.loads(resource_result["body"]["DiscoveredResourceDetail"]["Configuration"])
# セキュリティグループがマネージドセキュリティグループかどうかを確認します。
is_managed_security_group = configuration.get('ServiceManaged')
# jmespath を使用して、インバウンド方向で 0.0.0.0/0 へのアクセスを許可するセキュリティグループルールの ID を取得します。
delete_security_group_rule_ids = jmespath.search(
"Permissions.Permission[?SourceCidrIp=='0.0.0.0/0'].SecurityGroupRuleId",
configuration
)
# セキュリティグループがマネージドセキュリティグループではなく、0.0.0.0/0 へのアクセスを許可するインバウンドルールがある場合、そのルールを削除します。
if is_managed_security_group is False and delete_security_group_rule_ids:
logger.info(f"注意: セキュリティグループルール {region_id}:{resource_id}:{delete_security_group_rule_ids} を削除します")
revoke_security_group(context, region_id, resource_id, delete_security_group_rule_ids)
def revoke_security_group(context, region_id, resource_id, security_group_rule_ids):
creds = context.credentials
config = open_api_models.Config(
access_key_id=creds.access_key_id,
access_key_secret=creds.access_key_secret,
security_token=creds.security_token,
endpoint=f'ecs.{region_id}.aliyuncs.com'
)
client = OpenApiClient(config)
params = open_api_models.Params(
style='RPC', # API スタイル
version='2014-05-26', # API バージョン番号
action='RevokeSecurityGroup', # API 名
method='POST', # リクエストメソッド
pathname='/', # API パス。RPC API のデフォルトパスは "/" です。
protocol='HTTPS', # API プロトコル
auth_type='AK',
req_body_type='json', # リクエストボディの形式
body_type='json' # レスポンスボディの形式
)
query = {'RegionId': region_id, 'SecurityGroupId': resource_id, 'SecurityGroupRuleId': security_group_rule_ids}
# API リクエストオブジェクトを作成します。
request = open_api_models.OpenApiRequest(
query=OpenApiUtilClient.query(query),
)
runtime = util_models.RuntimeOptions()
response = client.call_api(params, request, runtime)
logger.info(f"削除結果: {response}")
# リソースの詳細を取得します。
def get_discovered_resource(context, resource_id, resource_type, region_id):
"""
API を呼び出してリソースの構成詳細を取得します。
:param context: Function Compute コンテキスト
:param resource_id: リソース ID
:param resource_type: リソースタイプ
:param region_id: リソースが存在するリージョンの ID
:return: リソースの詳細
"""
# Function Compute (FC) のサービスロールには AliyunConfigFullAccess 権限が必要です。
creds = context.credentials
config = open_api_models.Config(
access_key_id=creds.access_key_id,
access_key_secret=creds.access_key_secret,
security_token=creds.security_token,
endpoint='config.cn-shanghai.aliyuncs.com'
)
client = OpenApiClient(config)
params = open_api_models.Params(
style='RPC', # API スタイル
version='2020-09-07', # API バージョン番号
action='GetDiscoveredResource', # API 名
method='POST', # リクエストメソッド
pathname='/', # API パス。RPC API のデフォルトパスは "/" です。
protocol='HTTPS', # API プロトコル
auth_type='AK',
req_body_type='json', # リクエストボディの形式
body_type='json' # レスポンスボディの形式
)
query = {'ResourceId': resource_id, 'ResourceType': resource_type, 'Region': region_id}
# API リクエストオブジェクトを作成します。
request = open_api_models.OpenApiRequest(
query=OpenApiUtilClient.query(query),
)
runtime = util_models.RuntimeOptions()
try:
response = client.call_api(params, request, runtime)
return response
except Exception as e:
logger.error('GetDiscoveredResource error: %s' % e)
EOF
filename = "${path.module}/python/index.py"
}
resource "local_file" "requirements_txt" {
content = <<EOF
alibabacloud-tea-openapi
jmespath>= 0.10.0
EOF
filename = "${path.module}/python/requests/requirements.txt"
}
locals {
code_dir = "${path.module}/python/"
archive_output = "${path.module}/code.zip"
base64_output = "${path.module}/code_base64.txt"
}
data "archive_file" "code_package" {
type = "zip"
source_dir = local.code_dir
output_path = local.archive_output
depends_on = [
local_file.python_script,
local_file.requirements_txt,
]
}
resource "null_resource" "upload_code" {
provisioner "local-exec" {
command = <<EOT
base64 -w 0 ${local.archive_output} > ${local.base64_output}
EOT
interpreter = ["sh", "-c"]
}
depends_on = [data.archive_file.code_package]
}
data "local_file" "base64_encoded_code" {
filename = local.base64_output
depends_on = [null_resource.upload_code]
}
resource "alicloud_fcv3_function" "fc_function" {
runtime = "python3.10"
handler = "index.handler"
function_name = "HHM-FC-TEST"
role = alicloud_ram_role.role.arn
code {
zip_file = data.local_file.base64_encoded_code.content
}
lifecycle {
ignore_changes = [
code
]
}
# log_config を明示的に空に設定します。
log_config {}
depends_on = [data.local_file.base64_encoded_code]
}
resource "alicloud_config_rule" "default" {
rule_name = "SPM0014-sg-disallow-risky-ports-for-all-ips"
description = "セキュリティグループがすべての IP 範囲に対して脆弱なポート 22 および 3389 を開放することを禁止します。"
source_owner = "ALIYUN"
# (必須、ForceNew) ルールを所有および管理するのがユーザーか Alibaba Cloud かを指定します。有効な値: CUSTOM_FC: ユーザーが所有するカスタムルール。ALIYUN: Alibaba Cloud が所有するマネージドルール。
source_identifier = "sg-risky-ports-check"
# ルールの識別子。マネージドルールの場合、値はマネージドルールの名前です。カスタムルールの場合、値はカスタムルールの Alibaba Cloud Resource Name (ARN) です。(必須、ForceNew)
resource_types_scope = ["ACS::ECS::SecurityGroup"]
# 監視範囲から除外されるリソースの ID。複数の ID はカンマ (,) で区切ります。このパラメータは、マネージドルールに基づいて作成されたルールにのみ適用されます。カスタムルールの場合、このパラメータは空です。
config_rule_trigger_types = "ConfigurationItemChangeNotification" # 構成が変更されたときにルールがトリガーされます。
risk_level = 1 # 1: 重大、2: 警告、3: 情報
input_parameters = {
"ports" : "22,3389"
}
}
resource "alicloud_config_remediation" "default" {
config_rule_id = alicloud_config_rule.default.id
remediation_template_id = alicloud_fcv3_function.fc_function.function_arn
remediation_source_type = "CUSTOM"
invoke_type = "AUTO_EXECUTION"
params = "{}"
remediation_type = "FC"
}
resource "random_integer" "default" {
min = 10000
max = 99999
}
resource "alicloud_ram_role" "role" {
name = "tf-example-role-${random_integer.default.result}"
document = <<EOF
{
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": [
"fc.aliyuncs.com"
]
}
}
],
"Version": "1"
}
EOF
description = "Ecs ram role."
force = true
}
resource "alicloud_ram_policy" "policy" {
policy_name = "tf-example-ram-policy-${random_integer.default.result}"
policy_document = <<EOF
{
"Statement": [
{
"Action": [
"config:GetDiscoveredResource",
"ecs:RevokeSecurityGroup"
],
"Effect": "Allow",
"Resource": ["*"]
}
],
"Version": "1"
}
EOF
description = "This is a policy test."
force = true
}
resource "alicloud_ram_role_policy_attachment" "attach" {
policy_name = alicloud_ram_policy.policy.policy_name
policy_type = "Custom"
role_name = alicloud_ram_role.role.name
}