Skip to content

Commit

Permalink
add waf logging policy
Browse files Browse the repository at this point in the history
define vpc queries
  • Loading branch information
aquamatthias committed Jan 2, 2024
1 parent f2e3d4a commit 6243003
Show file tree
Hide file tree
Showing 9 changed files with 132 additions and 107 deletions.
80 changes: 75 additions & 5 deletions plugins/aws/resoto_plugin_aws/resource/waf.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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(
Expand All @@ -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}")
Expand All @@ -834,13 +900,17 @@ 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]:
return [
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"),
]


Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
}
4 changes: 3 additions & 1 deletion plugins/aws/tools/aws_model_gen.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
],
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
},
{
Expand All @@ -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"
]
},
{
Expand All @@ -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"
]
},
{
Expand Down
15 changes: 15 additions & 0 deletions resotocore/resotocore/static/report/checks/aws/aws_ec2.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
]
}
18 changes: 18 additions & 0 deletions resotocore/resotocore/static/report/checks/aws/aws_elb.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
]
}
Loading

0 comments on commit 6243003

Please sign in to comment.