From 62430032379dd82ec1de2a9ff2d56a7514d4534d Mon Sep 17 00:00:00 2001 From: Matthias Veit Date: Tue, 2 Jan 2024 17:45:59 +0100 Subject: [PATCH] add waf logging policy define vpc queries --- plugins/aws/resoto_plugin_aws/resource/waf.py | 80 ++++++++++++++++-- ...afv2_us_east_1_test_regional_deleteme.json | 9 ++ .../files/wafv2/list-web-acls__REGIONAL.json | 2 +- plugins/aws/tools/aws_model_gen.py | 4 +- .../benchmark/aws/aws_waf_security.json | 17 ++-- .../static/report/checks/aws/aws_ec2.json | 15 ++++ .../static/report/checks/aws/aws_elb.json | 18 ++++ .../static/report/checks/aws/aws_vpc.json | 82 ------------------- .../static/report/checks/aws/aws_wafv2.json | 12 +-- 9 files changed, 132 insertions(+), 107 deletions(-) create mode 100644 plugins/aws/test/resources/files/wafv2/get-logging-configuration__arn_aws_wafv2_us_east_1_test_regional_deleteme.json delete mode 100644 resotocore/resotocore/static/report/checks/aws/aws_vpc.json diff --git a/plugins/aws/resoto_plugin_aws/resource/waf.py b/plugins/aws/resoto_plugin_aws/resource/waf.py index 9979a35eab..580d0a08c7 100644 --- a/plugins/aws/resoto_plugin_aws/resource/waf.py +++ b/plugins/aws/resoto_plugin_aws/resource/waf.py @@ -1,12 +1,13 @@ from __future__ import annotations import logging +from contextlib import suppress from typing import ClassVar, Dict, Optional, List, Type from attrs import define, field from boto3.exceptions import Boto3Error -from resoto_plugin_aws.resource.base import AwsApiSpec, AwsResource, GraphBuilder +from resoto_plugin_aws.resource.base import AwsApiSpec, AwsResource, GraphBuilder, parse_json from resoto_plugin_aws.utils import ToDict from resotolib.baseresources import ModelReference from resotolib.json_bender import Bender, S, Bend, ForallBend, ParseJson, MapDict @@ -746,6 +747,56 @@ class AwsWafCustomResponseBody: content: Optional[str] = field(default=None, metadata={"description": "The payload of the custom response."}) # fmt: skip +@define(eq=False, slots=False) +class AwsWafCondition: + kind: ClassVar[str] = "aws_waf_condition" + mapping: ClassVar[Dict[str, Bender]] = { + "action_condition": S("ActionCondition", "Action"), + "label_name_condition": S("LabelNameCondition", "LabelName"), + } + action_condition: Optional[str] = field(default=None, metadata={"description": "A single action condition. This is the action setting that a log record must contain in order to meet the condition."}) # fmt: skip + label_name_condition: Optional[str] = field(default=None, metadata={"description": "A single label name condition. This is the fully qualified label name that a log record must contain in order to meet the condition. Fully qualified labels have a prefix, optional namespaces, and label name. The prefix identifies the rule group or web ACL context of the rule that added the label."}) # fmt: skip + + +@define(eq=False, slots=False) +class AwsWafFilter: + kind: ClassVar[str] = "aws_waf_filter" + mapping: ClassVar[Dict[str, Bender]] = { + "behavior": S("Behavior"), + "requirement": S("Requirement"), + "conditions": S("Conditions", default=[]) >> ForallBend(AwsWafCondition.mapping), + } + behavior: Optional[str] = field(default=None, metadata={"description": "How to handle logs that satisfy the filter's conditions and requirement."}) # fmt: skip + requirement: Optional[str] = field(default=None, metadata={"description": "Logic to apply to the filtering conditions. You can specify that, in order to satisfy the filter, a log must match all conditions or must match at least one condition."}) # fmt: skip + conditions: Optional[List[AwsWafCondition]] = field(factory=list, metadata={"description": "Match conditions for the filter."}) # fmt: skip + + +@define(eq=False, slots=False) +class AwsWafLoggingFilter: + kind: ClassVar[str] = "aws_waf_logging_filter" + mapping: ClassVar[Dict[str, Bender]] = { + "filters": S("Filters", default=[]) >> ForallBend(AwsWafFilter.mapping), + "default_behavior": S("DefaultBehavior"), + } + filters: Optional[List[AwsWafFilter]] = field(factory=list, metadata={"description": "The filters that you want to apply to the logs."}) # fmt: skip + default_behavior: Optional[str] = field(default=None, metadata={"description": "Default handling for logs that don't match any of the specified filtering conditions."}) # fmt: skip + + +@define(eq=False, slots=False) +class AwsWafLoggingConfiguration: + kind: ClassVar[str] = "aws_waf_logging_configuration" + mapping: ClassVar[Dict[str, Bender]] = { + "log_destination_configs": S("LogDestinationConfigs", default=[]), + "redacted_fields": S("RedactedFields", default=[]) >> ForallBend(AwsWafFieldToMatch.mapping), + "managed_by_firewall_manager": S("ManagedByFirewallManager"), + "logging_filter": S("LoggingFilter") >> Bend(AwsWafLoggingFilter.mapping), + } + log_destination_configs: Optional[List[str]] = field(factory=list, metadata={"description": "The logging destination configuration that you want to associate with the web ACL. You can associate one logging destination to a web ACL."}) # fmt: skip + redacted_fields: Optional[List[AwsWafFieldToMatch]] = field(factory=list, metadata={"description": "The parts of the request that you want to keep out of the logs. For example, if you redact the SingleHeader field, the HEADER field in the logs will be REDACTED for all rules that use the SingleHeader FieldToMatch setting. Redaction applies only to the component that's specified in the rule's FieldToMatch setting, so the SingleHeader redaction doesn't apply to rules that use the Headers FieldToMatch. You can specify only the following fields for redaction: UriPath, QueryString, SingleHeader, and Method."}) # fmt: skip + managed_by_firewall_manager: Optional[bool] = field(default=None, metadata={"description": "Indicates whether the logging configuration was created by Firewall Manager, as part of an WAF policy configuration. If true, only Firewall Manager can modify or delete the configuration."}) # fmt: skip + logging_filter: Optional[AwsWafLoggingFilter] = field(default=None, metadata={"description": "Filtering that specifies which web requests are kept in the logs and which are dropped. You can filter on the rule action and on the web request labels that were applied by matching rules during web ACL evaluation."}) # fmt: skip + + @define(eq=False, slots=False) class AwsWafWebACL(AwsResource): kind: ClassVar[str] = "aws_waf_web_acl" @@ -790,14 +841,28 @@ class AwsWafWebACL(AwsResource): challenge_config: Optional[AwsWafChallengeConfig] = field(default=None, metadata={"description": "Specifies how WAF should handle challenge evaluations for rules that don't have their own ChallengeConfig settings."}) # fmt: skip token_domains: Optional[List[str]] = field(factory=list, metadata={"description": "Specifies the domains that WAF should accept in a web request token. This enables the use of tokens across multiple protected websites."}) # fmt: skip association_inspection_limit: Optional[str] = field(default=None, metadata={"description": "Specifies the maximum size of the web request body component that an associated CloudFront distribution should send to WAF for inspection."}) # fmt: skip + logging_configuration: Optional[AwsWafLoggingConfiguration] = None _associated_resources: Optional[List[str]] = None @classmethod def collect_resources(cls: Type[AwsResource], builder: GraphBuilder) -> None: def fetch_acl_resources(acl: AwsWafWebACL) -> None: - acl._associated_resources = builder.client.list( - service_name, "list-resources-for-web-acl", "ResourceArns", WebACLArn=acl.arn - ) + with suppress(Exception): + acl._associated_resources = builder.client.list( + service_name, "list-resources-for-web-acl", "ResourceArns", WebACLArn=acl.arn + ) + + def fetch_logging_configuration(acl: AwsWafWebACL) -> None: + with suppress(Exception): + if logging_configuration := builder.client.get( + aws_service=service_name, + action="get-logging-configuration", + result_name="LoggingConfiguration", + ResourceArn=acl.arn, + ): + acl.logging_configuration = parse_json( + logging_configuration, AwsWafLoggingConfiguration, builder, AwsWafLoggingConfiguration.mapping + ) def fetch_web_acl(entry: Json) -> None: if web_acl := builder.client.get( @@ -809,8 +874,9 @@ def fetch_web_acl(entry: Json) -> None: Name=entry["Name"], ): if instance := AwsWafWebACL.from_api(web_acl, builder): - builder.submit_work(service_name, fetch_acl_resources, instance) builder.add_node(instance) + builder.submit_work(service_name, fetch_acl_resources, instance) + builder.submit_work(service_name, fetch_logging_configuration, instance) # Default behavior: in case the class has an ApiSpec, call the api and call collect. log.debug(f"Collecting {cls.__name__} in region {builder.region.name}") @@ -834,6 +900,9 @@ def fetch_web_acl(entry: Json) -> None: def connect_in_graph(self, builder: GraphBuilder, source: Json) -> None: for arn in self._associated_resources or []: builder.add_edge(self, arn=arn) + if (lc := self.logging_configuration) and (lcdcs := lc.log_destination_configs): + for arn in lcdcs: + builder.add_edge(self, arn=arn) @classmethod def called_collect_apis(cls) -> List[AwsApiSpec]: @@ -841,6 +910,7 @@ def called_collect_apis(cls) -> List[AwsApiSpec]: AwsApiSpec(service_name, "list-web-acls"), AwsApiSpec(service_name, "get-web-acl"), AwsApiSpec(service_name, "list-resources-for-web-acl"), + AwsApiSpec(service_name, "get-logging-configuration"), ] diff --git a/plugins/aws/test/resources/files/wafv2/get-logging-configuration__arn_aws_wafv2_us_east_1_test_regional_deleteme.json b/plugins/aws/test/resources/files/wafv2/get-logging-configuration__arn_aws_wafv2_us_east_1_test_regional_deleteme.json new file mode 100644 index 0000000000..7cc335ab99 --- /dev/null +++ b/plugins/aws/test/resources/files/wafv2/get-logging-configuration__arn_aws_wafv2_us_east_1_test_regional_deleteme.json @@ -0,0 +1,9 @@ +{ + "LoggingConfiguration": { + "ResourceArn": "arn:aws:wafv2:us-east-1:test:regional/deleteme", + "LogDestinationConfigs": [ + "arn:aws:logs:us-east-1:625596817853:log-group:aws-waf-logs-deleteme" + ], + "ManagedByFirewallManager": false + } +} diff --git a/plugins/aws/test/resources/files/wafv2/list-web-acls__REGIONAL.json b/plugins/aws/test/resources/files/wafv2/list-web-acls__REGIONAL.json index f4f2f675e2..cb2968d852 100644 --- a/plugins/aws/test/resources/files/wafv2/list-web-acls__REGIONAL.json +++ b/plugins/aws/test/resources/files/wafv2/list-web-acls__REGIONAL.json @@ -6,7 +6,7 @@ "Id": "acl1", "Description": "", "LockToken": "04e465af-501f-45b8-9d8a-39668cfc6674", - "ARN": "arn:aws:wafv2:us-east-1:test:regional/webacl/deleteme/9550dcbe-2009-456f-8093-8ce81b4e2245" + "ARN": "arn:aws:wafv2:us-east-1:test:regional/deleteme" } ] } diff --git a/plugins/aws/tools/aws_model_gen.py b/plugins/aws/tools/aws_model_gen.py index a549771ede..9f930577e7 100644 --- a/plugins/aws/tools/aws_model_gen.py +++ b/plugins/aws/tools/aws_model_gen.py @@ -1010,7 +1010,9 @@ def default_imports() -> str: "acm": [ # AwsResotoModel("describe-certificate", "Certificate", "CertificateDetail", prefix="Acm", name="AcmCertificate") ], - "wafv2": [AwsResotoModel("get-web-acl", "WebACL", "Statement", prefix="Waf")], + "wafv2": [ + # AwsResotoModel("get-logging-configuration", "LoggingConfigurations", "LoggingConfiguration", prefix="Waf") + ], } diff --git a/resotocore/resotocore/static/report/benchmark/aws/aws_waf_security.json b/resotocore/resotocore/static/report/benchmark/aws/aws_waf_security.json index 69f9e6647e..fb23ad4046 100644 --- a/resotocore/resotocore/static/report/benchmark/aws/aws_waf_security.json +++ b/resotocore/resotocore/static/report/benchmark/aws/aws_waf_security.json @@ -337,7 +337,7 @@ "aws_sagemaker_model_vpc_settings_enabled", "aws_sagemaker_notebook_vpc_settings_enabled", "aws_sagemaker_training_job_vpc_settings_enabled", - "aws_vpc_elb_application_lb_waf_enabled" + "aws_elb_waf_enabled" ] }, { @@ -346,13 +346,13 @@ "checks": [ "aws_apigateway_authorizers_enabled", "aws_dms_public_ip_address", + "aws_ec2_default_restrict_traffic", "aws_ec2_public_ip_address", "aws_ec2_snapshot_encrypted", + "aws_ec2_subnet_auto_assign_public_ip_disabled", "aws_s3_account_level_public_access_blocks", "aws_s3_bucket_no_mfa_delete", - "aws_sagemaker_notebook_with_direct_internet_access", - "aws_vpc_default_security_group_restricts_all_traffic", - "aws_vpc_subnet_auto_assign_public_ip_disabled" + "aws_sagemaker_notebook_with_direct_internet_access" ] }, { @@ -377,12 +377,9 @@ "aws_ec2_allow_ingress_from_internet_to_telnet_ports", "aws_ec2_default_restrict_traffic", "aws_dms_public_ip_address", - "aws_vpc_security_group_allows_ingress_authorized_ports", - "aws_vpc_subnet_auto_assign_public_ip_disabled", - "aws_vpc_default_security_group_restricts_all_traffic", - "aws_vpc_elb_application_lb_waf_enabled", - "aws_apigateway_waf_acl_attached", - "aws_vpc_default_security_group_restricts_all_traffic" + "aws_ec2_subnet_auto_assign_public_ip_disabled", + "aws_elb_waf_enabled", + "aws_apigateway_waf_acl_attached" ] }, { diff --git a/resotocore/resotocore/static/report/checks/aws/aws_ec2.json b/resotocore/resotocore/static/report/checks/aws/aws_ec2.json index c4f121f7f2..93afe674f0 100644 --- a/resotocore/resotocore/static/report/checks/aws/aws_ec2.json +++ b/resotocore/resotocore/static/report/checks/aws/aws_ec2.json @@ -690,6 +690,21 @@ "text": "Always ensure that User data picks up secrets from a managed service like Parameter store or Secrets manager, rather than having it hardcoded in the actual script", "url": "https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/user-data.html" } + }, + { + "name": "subnet_auto_assign_public_ip_disabled", + "title": "Ensure that subnets have public ip disabled by default to prevent accidental public access", + "result_kinds": ["aws_ec2_subnet"], + "categories": [ "security", "compliance" ], + "risk": "Disabling auto-assign public IP in subnets is important for security because it reduces the exposure of instances to the public internet, minimizing the risk of external attacks. This setting is crucial in controlling the network accessibility of EC2 instances and maintaining a secure environment within the VPC.", + "severity": "medium", + "detect": { + "resoto": "is(aws_ec2_subnet) and subnet_map_public_ip_on_launch=true" + }, + "remediation": { + "text": "select subnet -> click 'Actions' -> 'Modify auto-assign IP settings' -> Uncheck 'Auto-assign IPv4' and save.", + "url": "https://docs.aws.amazon.com/vpc/latest/userguide/vpc-ip-addressing.html" + } } ] } diff --git a/resotocore/resotocore/static/report/checks/aws/aws_elb.json b/resotocore/resotocore/static/report/checks/aws/aws_elb.json index 351ed0dab6..3bd6f0717e 100644 --- a/resotocore/resotocore/static/report/checks/aws/aws_elb.json +++ b/resotocore/resotocore/static/report/checks/aws/aws_elb.json @@ -64,6 +64,24 @@ "text": "In the load balancer settings, click 'View/edit rules' for 443 and associate a valid ssl certificate.", "url": "https://docs.aws.amazon.com/elasticloadbalancing/latest/application/listener-update-certificates.html" } + }, + { + "name": "waf_enabled", + "title": "Ensure that Load balancer has WAF enabled to provide protection against many of the common exploits and attacks ", + "result_kinds": ["aws_alb"], + "categories": [ + "security", + "compliance" + ], + "risk": "Enabling WAF (Web Application Firewall) on an Application Load Balancer is important for security as it provides a layer of protection against common web exploits and attacks. WAF can filter, monitor, and block harmful traffic before it reaches your applications, enhancing the overall security posture.", + "severity": "medium", + "detect": { + "resoto": "is(aws_alb) with (empty, <-- is(aws_waf_web_acl))" + }, + "remediation": { + "text": "Open the AWS WAF & Shield console, Go to 'Web ACLs', create or select an existing Web ACL.", + "url": "https://docs.aws.amazon.com/elasticloadbalancing/latest/application/application-load-balancers.html" + } } ] } diff --git a/resotocore/resotocore/static/report/checks/aws/aws_vpc.json b/resotocore/resotocore/static/report/checks/aws/aws_vpc.json deleted file mode 100644 index 8b69b1bb5f..0000000000 --- a/resotocore/resotocore/static/report/checks/aws/aws_vpc.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "provider": "aws", - "service": "vpc", - "checks": [ - { - "name": "elb_application_lb_waf_enabled", - "title": "Ensure that Load balancer has WAF enabled to provide protection against many of the common exploits and attacks ", - "result_kinds": ["aws_elb"], - "categories": [ - "security", - "compliance" - ], - "risk": "Enabling WAF (Web Application Firewall) on an ELB Application Load Balancer is important for security as it provides a layer of protection against common web exploits and attacks. WAF can filter, monitor, and block harmful traffic before it reaches your applications, enhancing the overall security posture.", - "severity": "medium", - "detect": { - "manual": "Navigate to EC2 -> select Load balancers -> go to the listener tab -> check if there are rules associated with a WAF Web ACL." - }, - "remediation": { - "text": "Open the AWS WAF & Shield console, Go to 'Web ACLs', create or select an existing Web ACL.", - "url": "https://docs.aws.amazon.com/elasticloadbalancing/latest/application/application-load-balancers.html" - }, - "source": "saad" - }, - { - "name": "subnet_auto_assign_public_ip_disabled", - "title": "Ensure that subnets have public ip disabled by default to prevent accidental public access", - "result_kinds": ["aws_subnet"], - "categories": [ - "security", - "compliance" - ], - "risk": "Disabling auto-assign public IP in subnets is important for security because it reduces the exposure of instances to the public internet, minimizing the risk of external attacks. This setting is crucial in controlling the network accessibility of EC2 instances and maintaining a secure environment within the VPC.", - "severity": "medium", - "detect": { - "manual": "Navigate to VPC -> select subnets -> check a subnet and inspect 'Auto-assign public IP' settings in details" - }, - "remediation": { - "text": "select subnet -> click 'Actions' -> 'Modify auto-assign IP settings' -> Uncheck 'Auto-assign IPv4' and save.", - "url": "https://docs.aws.amazon.com/vpc/latest/userguide/vpc-ip-addressing.html" - }, - "source": "saad" - }, - { - "name": "default_security_group_restricts_all_traffic", - "title": "Ensure that security groups provide a baseline of 'Deny all' ingress and egress access by restricting all traffic", - "result_kinds": ["aws_securitygroup"], - "categories": [ - "security", - "compliance" - ], - "risk": "Having the default VPC security group restrict all traffic is important for security because it provides a baseline of 'deny all' ingress and egress, minimizing the risk of unauthorized access. This approach adheres to the principle of least privilege, ensuring that only explicitly allowed traffic can flow through the network, thereby enhancing security. ", - "severity": "medium", - "detect": { - "manual": "Navigate to VPC -> select 'Security Groups' -> identify the default security group (usually named 'default') -> Review the Ingress and Egress rules to verify if all traffic is restricted." - }, - "remediation": { - "text": "Modify the Ingress and Egress rules to deny all traffic -> Remove all existing rules. -> Add a deny all rule if necessary (by default, security groups deny all inbound traffic if no rules are set).", - "url": "https://docs.aws.amazon.com/vpc/latest/userguide/vpc-security-groups.html" - }, - "source": "saad" - }, - { - "name": "security_group_allows_ingress_authorized_ports", - "title": "Ensuring that VPC security groups allow only authorized ports for ingress traffic is crucial for security.", - "result_kinds": ["aws_securitygroup"], - "categories": [ - "security", - "compliance" - ], - "risk": "Restricting security groups to only those ports and IP's that need them, helps control and restrict the flow of traffic into your VPC, reducing the attack surface and minimizing the risk of unauthorized access or potential security vulnerabilities.", - "severity": "medium", - "detect": { - "manual": "Navigate to VPC -> select 'Security Groups' -> Choose the relevant security group -> Check the inbound rules to ensure that only authorized ports are allowed." - }, - "remediation": { - "text": "Modify the Ingress and Egress rules -> Edit the inbound rules to allow only authorized ports -> Remove any unnecessary or overly permissive rules.", - "url": "https://docs.aws.amazon.com/vpc/latest/userguide/vpc-security-groups.html" - }, - "source": "saad" - } - ] -} diff --git a/resotocore/resotocore/static/report/checks/aws/aws_wafv2.json b/resotocore/resotocore/static/report/checks/aws/aws_wafv2.json index a5012f7ef3..307fe154fe 100644 --- a/resotocore/resotocore/static/report/checks/aws/aws_wafv2.json +++ b/resotocore/resotocore/static/report/checks/aws/aws_wafv2.json @@ -5,21 +5,17 @@ { "name": "web_acl_logging_enabled", "title": "Ensure that every Web ACL has logging enabled to provide insight into environment especially the requests being handled and blocked by the ACL", - "result_kinds": ["aws_waf"], - "categories": [ - "security", - "compliance" - ], + "result_kinds": ["aws_waf_web_acl"], + "categories": [ "security", "compliance" ], "risk": "Having WAFv2 Web ACL logging enabled is crucial for security because it provides detailed information about the traffic that is inspected by your Web ACL, including any requests that are blocked. This data is vital for security analysis, auditing, and identifying potential threats or misconfigurations", "severity": "medium", "detect": { - "manual": "aws wafv2 get-logging-configuration --resource-arn YOUR_WEB_ACL_ARN" + "resoto": "is(aws_waf_web_acl) and logging_configuration==null" }, "remediation": { "text": "in 'WAF and Shield' service in AWS -> select the relevent ACL -> goto 'Logging' -> click 'edit' and configure logging by selecting an S3 bucket", "url": "https://docs.aws.amazon.com/waf/latest/developerguide/logging.html" - }, - "source": "saad" + } } ] }