Kanji
・云架构工程师 / 自由职业 ・1993年出生 ・爱媛县出身 / 现居东京都涩谷区 ・5年 AWS 实战经验 个人资料详情
目录
<块引用> Prowler 是一款适用于 AWS、Azure、Google Cloud 和 Kubernetes 的开源安全工具,可执行安全最佳实践评估、审计、事件响应、持续监控、强化和取证准备,并执行修复。 有一个名为 Prowler Open Source 的 Prowler CLI(命令行界面)和一个构建在其之上的名为 Prowler SaaS 的 SaaS 服务。
来源:GitHub - prowler-cloud/prowler
官方文档的下页描述了自定义检查:
其他 - Prowler 文档
下面是翻译后的摘录,但官方文档没有提供有关如何创建自定义检查的详细信息。《开发者指南》据说描述了编写检查的详细过程,实际上包含了如何开发 Prowler 本身的信息,并没有提及自定义检查。
<块引用> 自定义检查文件夹必须包含每张检查一个子文件夹,每个子文件夹必须以检查命名并包含以下内容:
空的 init .py:这告诉 Python 将此检查文件夹视为一个包。 check_name.py:包含检查的逻辑。 check_name.metadata.json:包含检查的元数据。 检查名称必须以服务名称开头,后跟下划线(例如 ec2_instance_public_ip)。
有关如何编写支票的更多详细信息,请参阅“开发人员指南”。
来源:其他 - Prowler 文档
${parent_folder_name}/ └── ${AWS_service_name}_${check_name}/ ├── __init__.py ├── ${AWS_service_name}_${check_name}.py └── ${AWS_service_name}_${check_name}.metadata.json # Example file structure: Check if alternative contact information is registered for the account custom-checks-folder/ ∟ account_alternative_contact_information_is_registered/ ∟ __init__.py ∟ account_alternative_contact_information_is_registered.py ∟ account_alternative_contact_information_is_registered.metadata.json
${parent_folder_name} 指定适当的文件夹名称。直接在父文件夹下创建一个名为“ ${AWS_service_name}_$ {check_name}”的文件夹。
${parent_folder_name}
父文件夹可以放置在任何嵌套结构中。但是,指定为参数的文件夹必须是包含检查文件夹的父文件夹的路径。
可以创建多个“ ${AWS_service_name}_$ {check_name}”文件夹。
${AWS_service_name} 必须是以下之一。详细原因稍后解释。(从 v3.12.1 开始)
${AWS_service_name}
${check_name} 以小写蛇形形式指定检查的名称。
${check_name}
接下来,我将解释文件的内容。
文件的内容是参考 prowler/prowler/providers/aws/services at master · prowler-cloud/prowler 创建的。
__init__.py 是一个空文件。不需要写任何处理。
__init__.py
${AWS_service_name}_${check_name}.py 描述检查的逻辑。
${AWS_service_name}_${check_name}.py
例如,要创建检查以查看该帐户是否注册了替代联系信息,描述如下。
生成检查报告的模式有多种,但以下示例解释了将检查结果返回为“FAIL”或“PASS”的模式。
突出显示的处理基本上是必要的代码。生成“Check_Report_AWS”后,设置“region”、“resource_arn”、“resource_id”、“status”、“status_extended”和“resource_tags”的值。
有关详细信息,请参阅 检查 - Prowler 文档 。
要生成报告,请使用 Prowler 的标准客户端“account_client”。 account_client 在 Prowler GitHub 存储库的 prowler/providers/aws/services/${AWS_service_name}/${AWS_service_name}_client.py 和 prowler/providers/aws/services/${AWS_service_name}/${AWS_service_name}_service.py 中定义。通过查看 ${AWS_service_name}_service.py 中继承AWSService或BaseModel的类的内容,可以检查可以与 account_client 一起使用的变量。
account_client
prowler/providers/aws/services/${AWS_service_name}/${AWS_service_name}_client.py
prowler/providers/aws/services/${AWS_service_name}/${AWS_service_name}_service.py
${AWS_service_name}_service.py
如果您想使用此处未定义的变量,您需要创建自己的客户端。详细方法稍后解释。
import json from prowler.lib.logger import logger from prowler.lib.check.models import Check, Check_Report_AWS from prowler.providers.aws.services.account.account_client import account_client class account_alternative_contact_information_is_registered(Check): def execute(self): desired_alternative_contact_count = account_client.audit_config.get("desired_alternative_contact_count", 1) findings = [] report = Check_Report_AWS(self.metadata()) # For each Prowler check we MUST fill the following # Check_Report_AWS fields: # - region # - resource_id # - resource_arn # - status # - status_extended # - resource_tags (optional) logger.debug(f"account_client.contact_names: {account_client.contact_names}") logger.debug(f"account_client.contact_phone_numbers: {account_client.contact_phone_numbers}") logger.debug(f"account_client.contact_emails: {account_client.contact_emails}") contact_emails = account_client.contact_emails contact_emails.discard(None) report = Check_Report_AWS(self.metadata()) report.region = account_client.region report.resource_arn = account_client.audited_account_arn report.resource_id = account_client.audited_account if len(contact_emails) != desired_alternative_contact_count: report.status = "FAIL" else: report.status = "PASS" report.status_extended = json.dumps( { "desired_alternative_contact_count": desired_alternative_contact_count, "contact_emails_count": len(contact_emails), "contact_emails": list(contact_emails), }, ) findings.append(report) return findings
${AWS_service_name}_${check_name}.metadata.json
Provider
aws
CheckID
${AWS_service_name}_${check_name}
ServiceName
{ "Provider": "aws", "CheckID": "account_alternative_contact_information_is_registered", "CheckTitle": "Account alternative contact information is registered", "CheckType": [ "IAM" ], "ServiceName": "account", "SubServiceName": "", "ResourceIdTemplate": "arn:partition:access-recorder:region:account-id:recorder/resource-id", "Severity": "medium", "ResourceType": "Other", "Description": "", "Risk": "", "RelatedUrl": "", "Remediation": { "Code": { "CLI": "", "NativeIaC": "", "Other": "", "Terraform": "" }, "Recommendation": { "Text": "", "Url": "" } }, "Categories": [ "custom-checks" ], "DependsOn": [], "RelatedTo": [], "Notes": "" }
cst_*.py
${parent_folder_name}/ ∟ ${AWS_service_name}_${check_name} ∟ __init__.py ∟ ${AWS_service_name}_${check_name}.py ∟ ${AWS_service_name}_${check_name}.metadata.json ∟ cst_${AWS_service_name}_client.py ∟ cst_${AWS_service_name}_service.py # Example file structure: Check if the Config delivery channel is enabled custom-checks-folder/ ∟ config_delivery_channel_enabled/ ∟ __init__.py ∟ config_delivery_channel_enabled.py ∟ config_delivery_channel_enabled.metadata.json ∟ cst_config_client.py ∟ cst_config_service.py
${AWS_service_name}_${check_name}/
sys.path.append
from prowler.lib.logger import logger from prowler.lib.check.models import Check, Check_Report_AWS import os import sys sys.path.append(os.path.join(os.path.dirname(__file__))) from cst_config_client import config_client # noqa: E402 class config_delivery_channel_enabled(Check): def execute(self): findings = [] report = Check_Report_AWS(self.metadata()) // Omitted
cst_config_client.py
cst_config_service.py
from prowler.providers.aws.lib.audit_info.audit_info import current_audit_info from cst_config_service import Config config_client = Config(current_audit_info)
Config(AWSService)
__describe_delivery_channels__
regional_client.describe_delivery_channels()
DeliveryChannel
self.delivery_channels
DeliveryChannel(BaseModel)
from typing import Optional from pydantic import BaseModel from prowler.lib.logger import logger from prowler.lib.scan_filters.scan_filters import is_resource_filtered from prowler.providers.aws.lib.service.service import AWSService class Config(AWSService): def __init__(self, audit_info): super().__init__(__class__.__name__, audit_info) self.delivery_channels = [] self.__threading_call__(self.__describe_delivery_channels__) def __describe_delivery_channels__(self, regional_client): logger.info("Config - Listing Delivery Channels...") try: delivery_channel_count = 0 delivery_channels = regional_client.describe_delivery_channels()[ "DeliveryChannels" ] for delivery_channel in delivery_channels: if not self.audit_resources or ( is_resource_filtered(delivery_channel["name"], self.audit_resources) ): self.delivery_channels.append( DeliveryChannel( name=delivery_channel["name"], s3_bucket_name=delivery_channel["s3BucketName"], s3_key_prefix=delivery_channel["s3KeyPrefix"], sns_topic_arn=delivery_channel["snsTopicARN"], region=regional_client.region, ) ) delivery_channel_count += 1 # No delivery channels in region if delivery_channel_count == 0: self.delivery_channels.append( DeliveryChannel( name=self.audited_account, s3_bucket_name=None, s3_key_prefix=None, sns_topic_arn=None, region=regional_client.region, ) ) except Exception as error: logger.error( f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) class DeliveryChannel(BaseModel): name: str s3_bucket_name: Optional[str] s3_key_prefix: Optional[str] sns_topic_arn: Optional[str] region: str
--log-level
prowler aws \ --checks-folder ./custom-checks-folder/ \ --checks \ account_alternative_contact_information_is_registered \ --log-level ERROR \ -M csv \ --output-directory ./output
prowler aws \ --checks-folder ./custom-checks-folder/ \ --checks \ account_alternative_contact_information_is_registered \ --log-level DEBUG \ -M csv \ --output-directory ./output > debug.log 2>&1
prowler/__main__.py
prowler()
def prowler(): # Parse Arguments parser = ProwlerArgumentParser() args = parser.parse() # Save Arguments provider = args.provider checks = args.checks excluded_checks = args.excluded_checks excluded_services = args.excluded_services services = args.services categories = args.categories checks_file = args.checks_file checks_folder = args.checks_folder severities = args.severity compliance_framework = args.compliance custom_checks_metadata_file = args.custom_checks_metadata_file
# Import custom checks from folder if checks_folder: parse_checks_from_folder(audit_info, checks_folder, provider)
# Exclude checks if -e/--excluded-checks if excluded_checks: checks_to_execute = exclude_checks_to_run(checks_to_execute, excluded_checks) # Exclude services if --excluded-services if excluded_services: checks_to_execute = exclude_services_to_run( checks_to_execute, excluded_services, provider ) # Once the audit_info is set and we have the eventual checks based on the resource identifier, # it is time to check what Prowler's checks are going to be executed if audit_info.audit_resources: checks_from_resources = set_provider_execution_parameters(provider, audit_info) checks_to_execute = checks_to_execute.intersection(checks_from_resources) # Sort final check list checks_to_execute = sorted(checks_to_execute)
# Execute checks findings = [] if len(checks_to_execute): findings = execute_checks( checks_to_execute, provider, audit_info, audit_output_options, custom_checks_metadata, ) else: logger.error( "There are no checks to execute. Please, check your input arguments" )
# If custom checks were passed, remove the modules if checks_folder: remove_custom_checks_module(checks_folder, provider)
-“parse_checks_from_folder(audit_info,checks_folder,provider)”函数在“prowler/lib/check/check.py”中定义。 - 在第 131-133 行中,它确定 checks_folder 是否是 S3 URI。如果是 S3 URI,则检查指定 URI 前缀下的对象并下载它们。 - 第146-151行,遍历 checks_folder 下的文件夹并识别检查文件夹。如果识别到检查文件夹,则会将 checks_folder 下的检查文件夹复制到 Prowler 的路径 {prowler_dir[0]}/providers/{provider}/services/{check_service}/{check.name} 。例如,如果结构为“custom-checks-folder/account_alternative_contact_information_is_registered/”,它将被复制到“{prowler_dir[0]}/providers/aws/services/account/account_alternative_contact_information_is_registered/”。 - 如 check_service = check.name.split("_")[0] 中,snake_case 名称的第一个单词成为 AWS 服务名称。因此,如前所述, 您必须使用 Prowler 中已定义的 AWS 服务名称。 - 每个AWS服务客户端都在 {prowler_dir[0]}/providers/{provider}/services/{check_service}/ 下定义,但由于仅复制check文件夹,即使您将自定义客户端放置在父文件夹下,文件也不会被复制。 - 如果您想创建自定义客户端,则需要将其包含在检查文件夹中。_
checks_folder
{prowler_dir[0]}/providers/{provider}/services/{check_service}/{check.name}
check_service = check.name.split("_")[0]
{prowler_dir[0]}/providers/{provider}/services/{check_service}/
def parse_checks_from_folder(audit_info, input_folder: str, provider: str) -> int: try: imported_checks = 0 # Check if input folder is a S3 URI if provider == "aws" and re.search( "^s3://([^/]+)/(.*?([^/]+))/$", input_folder ): bucket = input_folder.split("/")[2] key = ("/").join(input_folder.split("/")[3:]) s3_resource = audit_info.audit_session.resource("s3") bucket = s3_resource.Bucket(bucket) for obj in bucket.objects.filter(Prefix=key): if not os.path.exists(os.path.dirname(obj.key)): os.makedirs(os.path.dirname(obj.key)) if obj.key[-1] == "/": continue bucket.download_file(obj.key, obj.key) input_folder = key # Import custom checks by moving the checks folders to the corresponding services with os.scandir(input_folder) as checks: for check in checks: if check.is_dir(): check_module = input_folder + "/" + check.name # Copy checks to specific provider/service folder check_service = check.name.split("_")[0] prowler_dir = prowler.__path__ prowler_module = f"{prowler_dir[0]}/providers/{provider}/services/{check_service}/{check.name}" if os.path.exists(prowler_module): shutil.rmtree(prowler_module) shutil.copytree(check_module, prowler_module) imported_checks += 1 return imported_checks except Exception as error: logger.critical( f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" ) sys.exit(1)
execute_checks(checks_to_execute,provider,audit_info,audit_output_options,custom_checks_metadata)
prowler/lib/check/check.py
--only-logs
# Default execution checks_num = len(checks_to_execute) plural_string = "checks" singular_string = "check" check_noun = plural_string if checks_num > 1 else singular_string print( f"{Style.BRIGHT}Executing {checks_num} {check_noun}, please wait...{Style.RESET_ALL}\n" ) with alive_bar( total=len(checks_to_execute), ctrl_c=False, bar="blocks", spinner="classic", stats=False, enrich_print=False, ) as bar: for check_name in checks_to_execute: # Recover service from check name service = check_name.split("_")[0] bar.title = ( f"-> Scanning {orange_color}{service}{Style.RESET_ALL} service" ) try: check_findings = execute( service, check_name, provider, audit_output_options, audit_info, services_executed, checks_executed, custom_checks_metadata, ) all_findings.extend(check_findings) # If check does not exists in the provider or is from another provider except ModuleNotFoundError: logger.error( f"Check '{check_name}' was not found for the {provider.upper()} provider" ) except Exception as error: logger.error( f"{check_name} - {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) bar() bar.title = f"-> {Fore.GREEN}Scan completed!{Style.RESET_ALL}" return all_findings