diff --git a/apis/v1alpha1/ack-generate-metadata.yaml b/apis/v1alpha1/ack-generate-metadata.yaml index 1ebc598..75f6492 100755 --- a/apis/v1alpha1/ack-generate-metadata.yaml +++ b/apis/v1alpha1/ack-generate-metadata.yaml @@ -1,13 +1,13 @@ ack_generate_info: - build_date: "2025-05-02T16:43:55Z" + build_date: "2025-05-09T19:21:17Z" build_hash: f8dc5330705b3752ce07dce0ac831161fd4cb14f - go_version: go1.24.2 + go_version: go1.24.1 version: v0.45.0 -api_directory_checksum: bca9b169696a2fdbcaed4f136fb99f69dc0aaa46 +api_directory_checksum: 5c5f7434bec90e2240f6fc534ca9df68f53d9eac api_version: v1alpha1 aws_sdk_go_version: v1.32.6 generator_config_info: - file_checksum: 6e1c5e9e2ed67b741bf090875a02d722e9fe192f + file_checksum: eb88080f34a163c6e85c94f6d06df0656ed96416 original_file_name: generator.yaml last_modification: reason: API generation diff --git a/apis/v1alpha1/generator.yaml b/apis/v1alpha1/generator.yaml index 68da275..4717298 100644 --- a/apis/v1alpha1/generator.yaml +++ b/apis/v1alpha1/generator.yaml @@ -113,6 +113,10 @@ resources: type: string set: - ignore: "all" + LoggingConfiguration: + from: + operation: PutLoggingConfiguration + path: LoggingConfiguration hooks: sdk_read_one_pre_build_request: template_path: hooks/webacl/sdk_read_one_pre_build_request.go.tpl @@ -120,6 +124,8 @@ resources: template_path: hooks/webacl/sdk_read_one_post_set_output.go.tpl sdk_create_post_build_request: template_path: hooks/webacl/sdk_create_post_build_request.go.tpl + sdk_create_post_set_output: + template_path: hooks/webacl/sdk_create_post_set_output.go.tpl sdk_update_post_build_request: template_path: hooks/webacl/sdk_update_post_build_request.go.tpl sdk_file_end: diff --git a/apis/v1alpha1/types.go b/apis/v1alpha1/types.go index 80cddb9..0e02934 100644 --- a/apis/v1alpha1/types.go +++ b/apis/v1alpha1/types.go @@ -122,6 +122,11 @@ type AWSManagedRulesBotControlRuleSet struct { InspectionLevel *string `json:"inspectionLevel,omitempty"` } +// A single action condition for a Condition in a logging filter. +type ActionCondition struct { + Action *string `json:"action,omitempty"` +} + // The name of a field in the request payload that contains part or all of your // customer's primary physical address. // @@ -319,6 +324,14 @@ type ChallengeConfig struct { ImmunityTimeProperty *ImmunityTimeProperty `json:"immunityTimeProperty,omitempty"` } +// A single match condition for a Filter. +type Condition struct { + // A single action condition for a Condition in a logging filter. + ActionCondition *ActionCondition `json:"actionCondition,omitempty"` + // A single label name condition for a Condition in a logging filter. + LabelNameCondition *LabelNameCondition `json:"labelNameCondition,omitempty"` +} + // The filter to use to identify the subset of cookies to inspect in a web request. // // You must specify exactly one setting: either All, IncludedCookies, or ExcludedCookies. @@ -596,6 +609,13 @@ type FieldToMatch struct { URIPath map[string]*string `json:"uriPath,omitempty"` } +// A single logging filter, used in LoggingFilter. +type Filter struct { + Behavior *string `json:"behavior,omitempty"` + Conditions []*Condition `json:"conditions,omitempty"` + Requirement *string `json:"requirement,omitempty"` +} + // A rule group that's defined for an Firewall Manager WAF policy. type FirewallManagerRuleGroup struct { // The processing guidance for an Firewall Manager rule. This is like a regular @@ -975,8 +995,28 @@ type LabelSummary struct { // information (https://docs.aws.amazon.com/waf/latest/developerguide/logging.html) // in the WAF Developer Guide. type LoggingConfiguration struct { - ManagedByFirewallManager *bool `json:"managedByFirewallManager,omitempty"` - ResourceARN *string `json:"resourceARN,omitempty"` + LogDestinationConfigs []*string `json:"logDestinationConfigs,omitempty"` + LogScope *string `json:"logScope,omitempty"` + LogType *string `json:"logType,omitempty"` + // Filtering that specifies which web requests are kept in the logs and which + // are dropped, defined for a web ACL's LoggingConfiguration. + // + // You can filter on the rule action and on the web request labels that were + // applied by matching rules during web ACL evaluation. + LoggingFilter *LoggingFilter `json:"loggingFilter,omitempty"` + ManagedByFirewallManager *bool `json:"managedByFirewallManager,omitempty"` + RedactedFields []*FieldToMatch `json:"redactedFields,omitempty"` + ResourceARN *string `json:"resourceARN,omitempty"` +} + +// Filtering that specifies which web requests are kept in the logs and which +// are dropped, defined for a web ACL's LoggingConfiguration. +// +// You can filter on the rule action and on the web request labels that were +// applied by matching rules during web ACL evaluation. +type LoggingFilter struct { + DefaultBehavior *string `json:"defaultBehavior,omitempty"` + Filters []*Filter `json:"filters,omitempty"` } // The properties of a managed product, such as an Amazon Web Services Managed diff --git a/apis/v1alpha1/web_acl.go b/apis/v1alpha1/web_acl.go index cec9406..6ded619 100644 --- a/apis/v1alpha1/web_acl.go +++ b/apis/v1alpha1/web_acl.go @@ -75,7 +75,8 @@ type WebACLSpec struct { // +kubebuilder:validation:Required DefaultAction *DefaultAction `json:"defaultAction"` // A description of the web ACL that helps with identification. - Description *string `json:"description,omitempty"` + Description *string `json:"description,omitempty"` + LoggingConfiguration *LoggingConfiguration `json:"loggingConfiguration,omitempty"` // The name of the web ACL. You cannot change the name of a web ACL after you // create it. // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable once set" diff --git a/apis/v1alpha1/zz_generated.deepcopy.go b/apis/v1alpha1/zz_generated.deepcopy.go index e8b2111..7310d0a 100644 --- a/apis/v1alpha1/zz_generated.deepcopy.go +++ b/apis/v1alpha1/zz_generated.deepcopy.go @@ -150,6 +150,26 @@ func (in *AWSManagedRulesBotControlRuleSet) DeepCopy() *AWSManagedRulesBotContro return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActionCondition) DeepCopyInto(out *ActionCondition) { + *out = *in + if in.Action != nil { + in, out := &in.Action, &out.Action + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActionCondition. +func (in *ActionCondition) DeepCopy() *ActionCondition { + if in == nil { + return nil + } + out := new(ActionCondition) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AddressField) DeepCopyInto(out *AddressField) { *out = *in @@ -382,6 +402,31 @@ func (in *ChallengeConfig) DeepCopy() *ChallengeConfig { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Condition) DeepCopyInto(out *Condition) { + *out = *in + if in.ActionCondition != nil { + in, out := &in.ActionCondition, &out.ActionCondition + *out = new(ActionCondition) + (*in).DeepCopyInto(*out) + } + if in.LabelNameCondition != nil { + in, out := &in.LabelNameCondition, &out.LabelNameCondition + *out = new(LabelNameCondition) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Condition. +func (in *Condition) DeepCopy() *Condition { + if in == nil { + return nil + } + out := new(Condition) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CookieMatchPattern) DeepCopyInto(out *CookieMatchPattern) { *out = *in @@ -781,6 +826,42 @@ func (in *FieldToMatch) DeepCopy() *FieldToMatch { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Filter) DeepCopyInto(out *Filter) { + *out = *in + if in.Behavior != nil { + in, out := &in.Behavior, &out.Behavior + *out = new(string) + **out = **in + } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]*Condition, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(Condition) + (*in).DeepCopyInto(*out) + } + } + } + if in.Requirement != nil { + in, out := &in.Requirement, &out.Requirement + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Filter. +func (in *Filter) DeepCopy() *Filter { + if in == nil { + return nil + } + out := new(Filter) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FirewallManagerRuleGroup) DeepCopyInto(out *FirewallManagerRuleGroup) { *out = *in @@ -1513,11 +1594,48 @@ func (in *LabelSummary) DeepCopy() *LabelSummary { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LoggingConfiguration) DeepCopyInto(out *LoggingConfiguration) { *out = *in + if in.LogDestinationConfigs != nil { + in, out := &in.LogDestinationConfigs, &out.LogDestinationConfigs + *out = make([]*string, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(string) + **out = **in + } + } + } + if in.LogScope != nil { + in, out := &in.LogScope, &out.LogScope + *out = new(string) + **out = **in + } + if in.LogType != nil { + in, out := &in.LogType, &out.LogType + *out = new(string) + **out = **in + } + if in.LoggingFilter != nil { + in, out := &in.LoggingFilter, &out.LoggingFilter + *out = new(LoggingFilter) + (*in).DeepCopyInto(*out) + } if in.ManagedByFirewallManager != nil { in, out := &in.ManagedByFirewallManager, &out.ManagedByFirewallManager *out = new(bool) **out = **in } + if in.RedactedFields != nil { + in, out := &in.RedactedFields, &out.RedactedFields + *out = make([]*FieldToMatch, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(FieldToMatch) + (*in).DeepCopyInto(*out) + } + } + } if in.ResourceARN != nil { in, out := &in.ResourceARN, &out.ResourceARN *out = new(string) @@ -1535,6 +1653,37 @@ func (in *LoggingConfiguration) DeepCopy() *LoggingConfiguration { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LoggingFilter) DeepCopyInto(out *LoggingFilter) { + *out = *in + if in.DefaultBehavior != nil { + in, out := &in.DefaultBehavior, &out.DefaultBehavior + *out = new(string) + **out = **in + } + if in.Filters != nil { + in, out := &in.Filters, &out.Filters + *out = make([]*Filter, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(Filter) + (*in).DeepCopyInto(*out) + } + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LoggingFilter. +func (in *LoggingFilter) DeepCopy() *LoggingFilter { + if in == nil { + return nil + } + out := new(LoggingFilter) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ManagedProductDescriptor) DeepCopyInto(out *ManagedProductDescriptor) { *out = *in @@ -3829,6 +3978,11 @@ func (in *WebACLSpec) DeepCopyInto(out *WebACLSpec) { *out = new(string) **out = **in } + if in.LoggingConfiguration != nil { + in, out := &in.LoggingConfiguration, &out.LoggingConfiguration + *out = new(LoggingConfiguration) + (*in).DeepCopyInto(*out) + } if in.Name != nil { in, out := &in.Name, &out.Name *out = new(string) diff --git a/config/crd/bases/wafv2.services.k8s.aws_webacls.yaml b/config/crd/bases/wafv2.services.k8s.aws_webacls.yaml index 2546bfb..d8ac035 100644 --- a/config/crd/bases/wafv2.services.k8s.aws_webacls.yaml +++ b/config/crd/bases/wafv2.services.k8s.aws_webacls.yaml @@ -229,6 +229,395 @@ spec: description: description: A description of the web ACL that helps with identification. type: string + loggingConfiguration: + description: |- + Defines an association between logging destinations and a web ACL resource, + for logging from WAF. As part of the association, you can specify parts of + the standard logging fields to keep out of the logs and you can specify filters + so that you log only a subset of the logging records. + + You can define one logging destination per web ACL. + + You can access information about the traffic that WAF inspects using the + following steps: + + Create your logging destination. You can use an Amazon CloudWatch Logs log + group, an Amazon Simple Storage Service (Amazon S3) bucket, or an Amazon + Kinesis Data Firehose. + + The name that you give the destination must start with aws-waf-logs-. Depending + on the type of destination, you might need to configure additional settings + or permissions. + + For configuration requirements and pricing information for each destination + type, see Logging web ACL traffic (https://docs.aws.amazon.com/waf/latest/developerguide/logging.html) + in the WAF Developer Guide. + + Associate your logging destination to your web ACL using a PutLoggingConfiguration + request. + + When you successfully enable logging using a PutLoggingConfiguration request, + WAF creates an additional role or policy that is required to write logs to + the logging destination. For an Amazon CloudWatch Logs log group, WAF creates + a resource policy on the log group. For an Amazon S3 bucket, WAF creates + a bucket policy. For an Amazon Kinesis Data Firehose, WAF creates a service-linked + role. + + For additional information about web ACL logging, see Logging web ACL traffic + information (https://docs.aws.amazon.com/waf/latest/developerguide/logging.html) + in the WAF Developer Guide. + properties: + logDestinationConfigs: + items: + type: string + type: array + logScope: + type: string + logType: + type: string + loggingFilter: + description: |- + Filtering that specifies which web requests are kept in the logs and which + are dropped, defined for a web ACL's LoggingConfiguration. + + You can filter on the rule action and on the web request labels that were + applied by matching rules during web ACL evaluation. + properties: + defaultBehavior: + type: string + filters: + items: + description: A single logging filter, used in LoggingFilter. + properties: + behavior: + type: string + conditions: + items: + description: A single match condition for a Filter. + properties: + actionCondition: + description: A single action condition for a Condition + in a logging filter. + properties: + action: + type: string + type: object + labelNameCondition: + description: A single label name condition for + a Condition in a logging filter. + properties: + labelName: + type: string + type: object + type: object + type: array + requirement: + type: string + type: object + type: array + type: object + managedByFirewallManager: + type: boolean + redactedFields: + items: + description: |- + Specifies a web request component to be used in a rule match statement or + in a logging configuration. + + * In a rule statement, this is the part of the web request that you want + WAF to inspect. Include the single FieldToMatch type that you want to + inspect, with additional specifications as needed, according to the type. + You specify a single request component in FieldToMatch for each rule statement + that requires it. To inspect more than one component of the web request, + create a separate rule statement for each component. Example JSON for + a QueryString field to match: "FieldToMatch": { "QueryString": {} } Example + JSON for a Method field to match specification: "FieldToMatch": { "Method": + { "Name": "DELETE" } } + + * In a logging configuration, this is used in the RedactedFields property + to specify a field to redact from the logging records. For this use case, + note the following: Even though all FieldToMatch settings are available, + the only valid settings for field redaction are UriPath, QueryString, + SingleHeader, and Method. In this documentation, the descriptions of the + individual fields talk about specifying the web request component to inspect, + but for field redaction, you are specifying the component type to redact + from the logs. If you have request sampling enabled, the redacted fields + configuration for logging has no impact on sampling. The only way to exclude + fields from request sampling is by disabling sampling in the web ACL visibility + configuration. + properties: + allQueryArguments: + additionalProperties: + type: string + description: |- + Inspect all query arguments of the web request. + + This is used in the FieldToMatch specification for some web request component + types. + + JSON specification: "AllQueryArguments": {} + type: object + body: + description: |- + Inspect the body of the web request. The body immediately follows the request + headers. + + This is used to indicate the web request component to inspect, in the FieldToMatch + specification. + properties: + oversizeHandling: + type: string + type: object + cookies: + description: |- + Inspect the cookies in the web request. You can specify the parts of the + cookies to inspect and you can narrow the set of cookies to inspect by including + or excluding specific keys. + + This is used to indicate the web request component to inspect, in the FieldToMatch + specification. + + Example JSON: "Cookies": { "MatchPattern": { "All": {} }, "MatchScope": "KEY", + "OversizeHandling": "MATCH" } + properties: + matchPattern: + description: |- + The filter to use to identify the subset of cookies to inspect in a web request. + + You must specify exactly one setting: either All, IncludedCookies, or ExcludedCookies. + + Example JSON: "MatchPattern": { "IncludedCookies": [ "session-id-time", "session-id" + ] } + properties: + all: + additionalProperties: + type: string + description: |- + Inspect all of the elements that WAF has parsed and extracted from the web + request component that you've identified in your FieldToMatch specifications. + + This is used in the FieldToMatch specification for some web request component + types. + + JSON specification: "All": {} + type: object + excludedCookies: + items: + type: string + type: array + includedCookies: + items: + type: string + type: array + type: object + matchScope: + type: string + oversizeHandling: + type: string + type: object + headerOrder: + description: |- + Inspect a string containing the list of the request's header names, ordered + as they appear in the web requestthat WAF receives for inspection. WAF generates + the string and then uses that as the field to match component in its inspection. + WAF separates the header names in the string using colons and no added spaces, + for example host:user-agent:accept:authorization:referer. + properties: + oversizeHandling: + type: string + type: object + headers: + description: |- + Inspect all headers in the web request. You can specify the parts of the + headers to inspect and you can narrow the set of headers to inspect by including + or excluding specific keys. + + This is used to indicate the web request component to inspect, in the FieldToMatch + specification. + + If you want to inspect just the value of a single header, use the SingleHeader + FieldToMatch setting instead. + + Example JSON: "Headers": { "MatchPattern": { "All": {} }, "MatchScope": "KEY", + "OversizeHandling": "MATCH" } + properties: + matchPattern: + description: |- + The filter to use to identify the subset of headers to inspect in a web request. + + You must specify exactly one setting: either All, IncludedHeaders, or ExcludedHeaders. + + Example JSON: "MatchPattern": { "ExcludedHeaders": [ "KeyToExclude1", "KeyToExclude2" + ] } + properties: + all: + additionalProperties: + type: string + description: |- + Inspect all of the elements that WAF has parsed and extracted from the web + request component that you've identified in your FieldToMatch specifications. + + This is used in the FieldToMatch specification for some web request component + types. + + JSON specification: "All": {} + type: object + excludedHeaders: + items: + type: string + type: array + includedHeaders: + items: + type: string + type: array + type: object + matchScope: + type: string + oversizeHandling: + type: string + type: object + ja3Fingerprint: + description: |- + Available for use with Amazon CloudFront distributions and Application Load + Balancers. Match against the request's JA3 fingerprint. The JA3 fingerprint + is a 32-character hash derived from the TLS Client Hello of an incoming request. + This fingerprint serves as a unique identifier for the client's TLS configuration. + WAF calculates and logs this fingerprint for each request that has enough + TLS Client Hello information for the calculation. Almost all web requests + include this information. + + You can use this choice only with a string match ByteMatchStatement with + the PositionalConstraint set to EXACTLY. + + You can obtain the JA3 fingerprint for client requests from the web ACL logs. + If WAF is able to calculate the fingerprint, it includes it in the logs. + For information about the logging fields, see Log fields (https://docs.aws.amazon.com/waf/latest/developerguide/logging-fields.html) + in the WAF Developer Guide. + + Provide the JA3 fingerprint string from the logs in your string match statement + specification, to match with any future requests that have the same TLS configuration. + properties: + fallbackBehavior: + type: string + type: object + jsonBody: + description: |- + Inspect the body of the web request as JSON. The body immediately follows + the request headers. + + This is used to indicate the web request component to inspect, in the FieldToMatch + specification. + + Use the specifications in this object to indicate which parts of the JSON + body to inspect using the rule's inspection criteria. WAF inspects only the + parts of the JSON that result from the matches that you indicate. + + Example JSON: "JsonBody": { "MatchPattern": { "All": {} }, "MatchScope": + "ALL" } + + For additional information about this request component option, see JSON + body (https://docs.aws.amazon.com/waf/latest/developerguide/waf-rule-statement-fields-list.html#waf-rule-statement-request-component-json-body) + in the WAF Developer Guide. + properties: + invalidFallbackBehavior: + type: string + matchPattern: + description: |- + The patterns to look for in the JSON body. WAF inspects the results of these + pattern matches against the rule inspection criteria. This is used with the + FieldToMatch option JsonBody. + properties: + all: + additionalProperties: + type: string + description: |- + Inspect all of the elements that WAF has parsed and extracted from the web + request component that you've identified in your FieldToMatch specifications. + + This is used in the FieldToMatch specification for some web request component + types. + + JSON specification: "All": {} + type: object + includedPaths: + items: + type: string + type: array + type: object + matchScope: + type: string + oversizeHandling: + type: string + type: object + method: + additionalProperties: + type: string + description: |- + Inspect the HTTP method of the web request. The method indicates the type + of operation that the request is asking the origin to perform. + + This is used in the FieldToMatch specification for some web request component + types. + + JSON specification: "Method": {} + type: object + queryString: + additionalProperties: + type: string + description: |- + Inspect the query string of the web request. This is the part of a URL that + appears after a ? character, if any. + + This is used in the FieldToMatch specification for some web request component + types. + + JSON specification: "QueryString": {} + type: object + singleHeader: + description: |- + Inspect one of the headers in the web request, identified by name, for example, + User-Agent or Referer. The name isn't case sensitive. + + You can filter and inspect all headers with the FieldToMatch setting Headers. + + This is used to indicate the web request component to inspect, in the FieldToMatch + specification. + + Example JSON: "SingleHeader": { "Name": "haystack" } + properties: + name: + type: string + type: object + singleQueryArgument: + description: |- + Inspect one query argument in the web request, identified by name, for example + UserName or SalesRegion. The name isn't case sensitive. + + This is used to indicate the web request component to inspect, in the FieldToMatch + specification. + + Example JSON: "SingleQueryArgument": { "Name": "myArgument" } + properties: + name: + type: string + type: object + uriPath: + additionalProperties: + type: string + description: |- + Inspect the path component of the URI of the web request. This is the part + of the web request that identifies a resource. For example, /images/daily-ad.jpg. + + This is used in the FieldToMatch specification for some web request component + types. + + JSON specification: "UriPath": {} + type: object + type: object + type: array + resourceARN: + type: string + type: object name: description: |- The name of the web ACL. You cannot change the name of a web ACL after you diff --git a/generator.yaml b/generator.yaml index 68da275..4717298 100644 --- a/generator.yaml +++ b/generator.yaml @@ -113,6 +113,10 @@ resources: type: string set: - ignore: "all" + LoggingConfiguration: + from: + operation: PutLoggingConfiguration + path: LoggingConfiguration hooks: sdk_read_one_pre_build_request: template_path: hooks/webacl/sdk_read_one_pre_build_request.go.tpl @@ -120,6 +124,8 @@ resources: template_path: hooks/webacl/sdk_read_one_post_set_output.go.tpl sdk_create_post_build_request: template_path: hooks/webacl/sdk_create_post_build_request.go.tpl + sdk_create_post_set_output: + template_path: hooks/webacl/sdk_create_post_set_output.go.tpl sdk_update_post_build_request: template_path: hooks/webacl/sdk_update_post_build_request.go.tpl sdk_file_end: diff --git a/helm/crds/wafv2.services.k8s.aws_webacls.yaml b/helm/crds/wafv2.services.k8s.aws_webacls.yaml index 00adc83..fd35a7b 100644 --- a/helm/crds/wafv2.services.k8s.aws_webacls.yaml +++ b/helm/crds/wafv2.services.k8s.aws_webacls.yaml @@ -229,6 +229,395 @@ spec: description: description: A description of the web ACL that helps with identification. type: string + loggingConfiguration: + description: |- + Defines an association between logging destinations and a web ACL resource, + for logging from WAF. As part of the association, you can specify parts of + the standard logging fields to keep out of the logs and you can specify filters + so that you log only a subset of the logging records. + + You can define one logging destination per web ACL. + + You can access information about the traffic that WAF inspects using the + following steps: + + Create your logging destination. You can use an Amazon CloudWatch Logs log + group, an Amazon Simple Storage Service (Amazon S3) bucket, or an Amazon + Kinesis Data Firehose. + + The name that you give the destination must start with aws-waf-logs-. Depending + on the type of destination, you might need to configure additional settings + or permissions. + + For configuration requirements and pricing information for each destination + type, see Logging web ACL traffic (https://docs.aws.amazon.com/waf/latest/developerguide/logging.html) + in the WAF Developer Guide. + + Associate your logging destination to your web ACL using a PutLoggingConfiguration + request. + + When you successfully enable logging using a PutLoggingConfiguration request, + WAF creates an additional role or policy that is required to write logs to + the logging destination. For an Amazon CloudWatch Logs log group, WAF creates + a resource policy on the log group. For an Amazon S3 bucket, WAF creates + a bucket policy. For an Amazon Kinesis Data Firehose, WAF creates a service-linked + role. + + For additional information about web ACL logging, see Logging web ACL traffic + information (https://docs.aws.amazon.com/waf/latest/developerguide/logging.html) + in the WAF Developer Guide. + properties: + logDestinationConfigs: + items: + type: string + type: array + logScope: + type: string + logType: + type: string + loggingFilter: + description: |- + Filtering that specifies which web requests are kept in the logs and which + are dropped, defined for a web ACL's LoggingConfiguration. + + You can filter on the rule action and on the web request labels that were + applied by matching rules during web ACL evaluation. + properties: + defaultBehavior: + type: string + filters: + items: + description: A single logging filter, used in LoggingFilter. + properties: + behavior: + type: string + conditions: + items: + description: A single match condition for a Filter. + properties: + actionCondition: + description: A single action condition for a Condition + in a logging filter. + properties: + action: + type: string + type: object + labelNameCondition: + description: A single label name condition for + a Condition in a logging filter. + properties: + labelName: + type: string + type: object + type: object + type: array + requirement: + type: string + type: object + type: array + type: object + managedByFirewallManager: + type: boolean + redactedFields: + items: + description: |- + Specifies a web request component to be used in a rule match statement or + in a logging configuration. + + - In a rule statement, this is the part of the web request that you want + WAF to inspect. Include the single FieldToMatch type that you want to + inspect, with additional specifications as needed, according to the type. + You specify a single request component in FieldToMatch for each rule statement + that requires it. To inspect more than one component of the web request, + create a separate rule statement for each component. Example JSON for + a QueryString field to match: "FieldToMatch": { "QueryString": {} } Example + JSON for a Method field to match specification: "FieldToMatch": { "Method": + { "Name": "DELETE" } } + + - In a logging configuration, this is used in the RedactedFields property + to specify a field to redact from the logging records. For this use case, + note the following: Even though all FieldToMatch settings are available, + the only valid settings for field redaction are UriPath, QueryString, + SingleHeader, and Method. In this documentation, the descriptions of the + individual fields talk about specifying the web request component to inspect, + but for field redaction, you are specifying the component type to redact + from the logs. If you have request sampling enabled, the redacted fields + configuration for logging has no impact on sampling. The only way to exclude + fields from request sampling is by disabling sampling in the web ACL visibility + configuration. + properties: + allQueryArguments: + additionalProperties: + type: string + description: |- + Inspect all query arguments of the web request. + + This is used in the FieldToMatch specification for some web request component + types. + + JSON specification: "AllQueryArguments": {} + type: object + body: + description: |- + Inspect the body of the web request. The body immediately follows the request + headers. + + This is used to indicate the web request component to inspect, in the FieldToMatch + specification. + properties: + oversizeHandling: + type: string + type: object + cookies: + description: |- + Inspect the cookies in the web request. You can specify the parts of the + cookies to inspect and you can narrow the set of cookies to inspect by including + or excluding specific keys. + + This is used to indicate the web request component to inspect, in the FieldToMatch + specification. + + Example JSON: "Cookies": { "MatchPattern": { "All": {} }, "MatchScope": "KEY", + "OversizeHandling": "MATCH" } + properties: + matchPattern: + description: |- + The filter to use to identify the subset of cookies to inspect in a web request. + + You must specify exactly one setting: either All, IncludedCookies, or ExcludedCookies. + + Example JSON: "MatchPattern": { "IncludedCookies": [ "session-id-time", "session-id" + ] } + properties: + all: + additionalProperties: + type: string + description: |- + Inspect all of the elements that WAF has parsed and extracted from the web + request component that you've identified in your FieldToMatch specifications. + + This is used in the FieldToMatch specification for some web request component + types. + + JSON specification: "All": {} + type: object + excludedCookies: + items: + type: string + type: array + includedCookies: + items: + type: string + type: array + type: object + matchScope: + type: string + oversizeHandling: + type: string + type: object + headerOrder: + description: |- + Inspect a string containing the list of the request's header names, ordered + as they appear in the web requestthat WAF receives for inspection. WAF generates + the string and then uses that as the field to match component in its inspection. + WAF separates the header names in the string using colons and no added spaces, + for example host:user-agent:accept:authorization:referer. + properties: + oversizeHandling: + type: string + type: object + headers: + description: |- + Inspect all headers in the web request. You can specify the parts of the + headers to inspect and you can narrow the set of headers to inspect by including + or excluding specific keys. + + This is used to indicate the web request component to inspect, in the FieldToMatch + specification. + + If you want to inspect just the value of a single header, use the SingleHeader + FieldToMatch setting instead. + + Example JSON: "Headers": { "MatchPattern": { "All": {} }, "MatchScope": "KEY", + "OversizeHandling": "MATCH" } + properties: + matchPattern: + description: |- + The filter to use to identify the subset of headers to inspect in a web request. + + You must specify exactly one setting: either All, IncludedHeaders, or ExcludedHeaders. + + Example JSON: "MatchPattern": { "ExcludedHeaders": [ "KeyToExclude1", "KeyToExclude2" + ] } + properties: + all: + additionalProperties: + type: string + description: |- + Inspect all of the elements that WAF has parsed and extracted from the web + request component that you've identified in your FieldToMatch specifications. + + This is used in the FieldToMatch specification for some web request component + types. + + JSON specification: "All": {} + type: object + excludedHeaders: + items: + type: string + type: array + includedHeaders: + items: + type: string + type: array + type: object + matchScope: + type: string + oversizeHandling: + type: string + type: object + ja3Fingerprint: + description: |- + Available for use with Amazon CloudFront distributions and Application Load + Balancers. Match against the request's JA3 fingerprint. The JA3 fingerprint + is a 32-character hash derived from the TLS Client Hello of an incoming request. + This fingerprint serves as a unique identifier for the client's TLS configuration. + WAF calculates and logs this fingerprint for each request that has enough + TLS Client Hello information for the calculation. Almost all web requests + include this information. + + You can use this choice only with a string match ByteMatchStatement with + the PositionalConstraint set to EXACTLY. + + You can obtain the JA3 fingerprint for client requests from the web ACL logs. + If WAF is able to calculate the fingerprint, it includes it in the logs. + For information about the logging fields, see Log fields (https://docs.aws.amazon.com/waf/latest/developerguide/logging-fields.html) + in the WAF Developer Guide. + + Provide the JA3 fingerprint string from the logs in your string match statement + specification, to match with any future requests that have the same TLS configuration. + properties: + fallbackBehavior: + type: string + type: object + jsonBody: + description: |- + Inspect the body of the web request as JSON. The body immediately follows + the request headers. + + This is used to indicate the web request component to inspect, in the FieldToMatch + specification. + + Use the specifications in this object to indicate which parts of the JSON + body to inspect using the rule's inspection criteria. WAF inspects only the + parts of the JSON that result from the matches that you indicate. + + Example JSON: "JsonBody": { "MatchPattern": { "All": {} }, "MatchScope": + "ALL" } + + For additional information about this request component option, see JSON + body (https://docs.aws.amazon.com/waf/latest/developerguide/waf-rule-statement-fields-list.html#waf-rule-statement-request-component-json-body) + in the WAF Developer Guide. + properties: + invalidFallbackBehavior: + type: string + matchPattern: + description: |- + The patterns to look for in the JSON body. WAF inspects the results of these + pattern matches against the rule inspection criteria. This is used with the + FieldToMatch option JsonBody. + properties: + all: + additionalProperties: + type: string + description: |- + Inspect all of the elements that WAF has parsed and extracted from the web + request component that you've identified in your FieldToMatch specifications. + + This is used in the FieldToMatch specification for some web request component + types. + + JSON specification: "All": {} + type: object + includedPaths: + items: + type: string + type: array + type: object + matchScope: + type: string + oversizeHandling: + type: string + type: object + method: + additionalProperties: + type: string + description: |- + Inspect the HTTP method of the web request. The method indicates the type + of operation that the request is asking the origin to perform. + + This is used in the FieldToMatch specification for some web request component + types. + + JSON specification: "Method": {} + type: object + queryString: + additionalProperties: + type: string + description: |- + Inspect the query string of the web request. This is the part of a URL that + appears after a ? character, if any. + + This is used in the FieldToMatch specification for some web request component + types. + + JSON specification: "QueryString": {} + type: object + singleHeader: + description: |- + Inspect one of the headers in the web request, identified by name, for example, + User-Agent or Referer. The name isn't case sensitive. + + You can filter and inspect all headers with the FieldToMatch setting Headers. + + This is used to indicate the web request component to inspect, in the FieldToMatch + specification. + + Example JSON: "SingleHeader": { "Name": "haystack" } + properties: + name: + type: string + type: object + singleQueryArgument: + description: |- + Inspect one query argument in the web request, identified by name, for example + UserName or SalesRegion. The name isn't case sensitive. + + This is used to indicate the web request component to inspect, in the FieldToMatch + specification. + + Example JSON: "SingleQueryArgument": { "Name": "myArgument" } + properties: + name: + type: string + type: object + uriPath: + additionalProperties: + type: string + description: |- + Inspect the path component of the URI of the web request. This is the part + of the web request that identifies a resource. For example, /images/daily-ad.jpg. + + This is used in the FieldToMatch specification for some web request component + types. + + JSON specification: "UriPath": {} + type: object + type: object + type: array + resourceARN: + type: string + type: object name: description: |- The name of the web ACL. You cannot change the name of a web ACL after you diff --git a/pkg/resource/web_acl/delta.go b/pkg/resource/web_acl/delta.go index 4635b4a..bac0374 100644 --- a/pkg/resource/web_acl/delta.go +++ b/pkg/resource/web_acl/delta.go @@ -146,6 +146,70 @@ func newResourceDelta( delta.Add("Spec.Description", a.ko.Spec.Description, b.ko.Spec.Description) } } + if ackcompare.HasNilDifference(a.ko.Spec.LoggingConfiguration, b.ko.Spec.LoggingConfiguration) { + delta.Add("Spec.LoggingConfiguration", a.ko.Spec.LoggingConfiguration, b.ko.Spec.LoggingConfiguration) + } else if a.ko.Spec.LoggingConfiguration != nil && b.ko.Spec.LoggingConfiguration != nil { + if len(a.ko.Spec.LoggingConfiguration.LogDestinationConfigs) != len(b.ko.Spec.LoggingConfiguration.LogDestinationConfigs) { + delta.Add("Spec.LoggingConfiguration.LogDestinationConfigs", a.ko.Spec.LoggingConfiguration.LogDestinationConfigs, b.ko.Spec.LoggingConfiguration.LogDestinationConfigs) + } else if len(a.ko.Spec.LoggingConfiguration.LogDestinationConfigs) > 0 { + if !ackcompare.SliceStringPEqual(a.ko.Spec.LoggingConfiguration.LogDestinationConfigs, b.ko.Spec.LoggingConfiguration.LogDestinationConfigs) { + delta.Add("Spec.LoggingConfiguration.LogDestinationConfigs", a.ko.Spec.LoggingConfiguration.LogDestinationConfigs, b.ko.Spec.LoggingConfiguration.LogDestinationConfigs) + } + } + if ackcompare.HasNilDifference(a.ko.Spec.LoggingConfiguration.LogScope, b.ko.Spec.LoggingConfiguration.LogScope) { + delta.Add("Spec.LoggingConfiguration.LogScope", a.ko.Spec.LoggingConfiguration.LogScope, b.ko.Spec.LoggingConfiguration.LogScope) + } else if a.ko.Spec.LoggingConfiguration.LogScope != nil && b.ko.Spec.LoggingConfiguration.LogScope != nil { + if *a.ko.Spec.LoggingConfiguration.LogScope != *b.ko.Spec.LoggingConfiguration.LogScope { + delta.Add("Spec.LoggingConfiguration.LogScope", a.ko.Spec.LoggingConfiguration.LogScope, b.ko.Spec.LoggingConfiguration.LogScope) + } + } + if ackcompare.HasNilDifference(a.ko.Spec.LoggingConfiguration.LogType, b.ko.Spec.LoggingConfiguration.LogType) { + delta.Add("Spec.LoggingConfiguration.LogType", a.ko.Spec.LoggingConfiguration.LogType, b.ko.Spec.LoggingConfiguration.LogType) + } else if a.ko.Spec.LoggingConfiguration.LogType != nil && b.ko.Spec.LoggingConfiguration.LogType != nil { + if *a.ko.Spec.LoggingConfiguration.LogType != *b.ko.Spec.LoggingConfiguration.LogType { + delta.Add("Spec.LoggingConfiguration.LogType", a.ko.Spec.LoggingConfiguration.LogType, b.ko.Spec.LoggingConfiguration.LogType) + } + } + if ackcompare.HasNilDifference(a.ko.Spec.LoggingConfiguration.LoggingFilter, b.ko.Spec.LoggingConfiguration.LoggingFilter) { + delta.Add("Spec.LoggingConfiguration.LoggingFilter", a.ko.Spec.LoggingConfiguration.LoggingFilter, b.ko.Spec.LoggingConfiguration.LoggingFilter) + } else if a.ko.Spec.LoggingConfiguration.LoggingFilter != nil && b.ko.Spec.LoggingConfiguration.LoggingFilter != nil { + if ackcompare.HasNilDifference(a.ko.Spec.LoggingConfiguration.LoggingFilter.DefaultBehavior, b.ko.Spec.LoggingConfiguration.LoggingFilter.DefaultBehavior) { + delta.Add("Spec.LoggingConfiguration.LoggingFilter.DefaultBehavior", a.ko.Spec.LoggingConfiguration.LoggingFilter.DefaultBehavior, b.ko.Spec.LoggingConfiguration.LoggingFilter.DefaultBehavior) + } else if a.ko.Spec.LoggingConfiguration.LoggingFilter.DefaultBehavior != nil && b.ko.Spec.LoggingConfiguration.LoggingFilter.DefaultBehavior != nil { + if *a.ko.Spec.LoggingConfiguration.LoggingFilter.DefaultBehavior != *b.ko.Spec.LoggingConfiguration.LoggingFilter.DefaultBehavior { + delta.Add("Spec.LoggingConfiguration.LoggingFilter.DefaultBehavior", a.ko.Spec.LoggingConfiguration.LoggingFilter.DefaultBehavior, b.ko.Spec.LoggingConfiguration.LoggingFilter.DefaultBehavior) + } + } + if len(a.ko.Spec.LoggingConfiguration.LoggingFilter.Filters) != len(b.ko.Spec.LoggingConfiguration.LoggingFilter.Filters) { + delta.Add("Spec.LoggingConfiguration.LoggingFilter.Filters", a.ko.Spec.LoggingConfiguration.LoggingFilter.Filters, b.ko.Spec.LoggingConfiguration.LoggingFilter.Filters) + } else if len(a.ko.Spec.LoggingConfiguration.LoggingFilter.Filters) > 0 { + if !reflect.DeepEqual(a.ko.Spec.LoggingConfiguration.LoggingFilter.Filters, b.ko.Spec.LoggingConfiguration.LoggingFilter.Filters) { + delta.Add("Spec.LoggingConfiguration.LoggingFilter.Filters", a.ko.Spec.LoggingConfiguration.LoggingFilter.Filters, b.ko.Spec.LoggingConfiguration.LoggingFilter.Filters) + } + } + } + if ackcompare.HasNilDifference(a.ko.Spec.LoggingConfiguration.ManagedByFirewallManager, b.ko.Spec.LoggingConfiguration.ManagedByFirewallManager) { + delta.Add("Spec.LoggingConfiguration.ManagedByFirewallManager", a.ko.Spec.LoggingConfiguration.ManagedByFirewallManager, b.ko.Spec.LoggingConfiguration.ManagedByFirewallManager) + } else if a.ko.Spec.LoggingConfiguration.ManagedByFirewallManager != nil && b.ko.Spec.LoggingConfiguration.ManagedByFirewallManager != nil { + if *a.ko.Spec.LoggingConfiguration.ManagedByFirewallManager != *b.ko.Spec.LoggingConfiguration.ManagedByFirewallManager { + delta.Add("Spec.LoggingConfiguration.ManagedByFirewallManager", a.ko.Spec.LoggingConfiguration.ManagedByFirewallManager, b.ko.Spec.LoggingConfiguration.ManagedByFirewallManager) + } + } + if len(a.ko.Spec.LoggingConfiguration.RedactedFields) != len(b.ko.Spec.LoggingConfiguration.RedactedFields) { + delta.Add("Spec.LoggingConfiguration.RedactedFields", a.ko.Spec.LoggingConfiguration.RedactedFields, b.ko.Spec.LoggingConfiguration.RedactedFields) + } else if len(a.ko.Spec.LoggingConfiguration.RedactedFields) > 0 { + if !reflect.DeepEqual(a.ko.Spec.LoggingConfiguration.RedactedFields, b.ko.Spec.LoggingConfiguration.RedactedFields) { + delta.Add("Spec.LoggingConfiguration.RedactedFields", a.ko.Spec.LoggingConfiguration.RedactedFields, b.ko.Spec.LoggingConfiguration.RedactedFields) + } + } + if ackcompare.HasNilDifference(a.ko.Spec.LoggingConfiguration.ResourceARN, b.ko.Spec.LoggingConfiguration.ResourceARN) { + delta.Add("Spec.LoggingConfiguration.ResourceARN", a.ko.Spec.LoggingConfiguration.ResourceARN, b.ko.Spec.LoggingConfiguration.ResourceARN) + } else if a.ko.Spec.LoggingConfiguration.ResourceARN != nil && b.ko.Spec.LoggingConfiguration.ResourceARN != nil { + if *a.ko.Spec.LoggingConfiguration.ResourceARN != *b.ko.Spec.LoggingConfiguration.ResourceARN { + delta.Add("Spec.LoggingConfiguration.ResourceARN", a.ko.Spec.LoggingConfiguration.ResourceARN, b.ko.Spec.LoggingConfiguration.ResourceARN) + } + } + } if ackcompare.HasNilDifference(a.ko.Spec.Name, b.ko.Spec.Name) { delta.Add("Spec.Name", a.ko.Spec.Name, b.ko.Spec.Name) } else if a.ko.Spec.Name != nil && b.ko.Spec.Name != nil { diff --git a/pkg/resource/web_acl/hooks.go b/pkg/resource/web_acl/hooks.go index 99f5f0f..3bac454 100644 --- a/pkg/resource/web_acl/hooks.go +++ b/pkg/resource/web_acl/hooks.go @@ -1,10 +1,19 @@ package web_acl import ( + "context" + "errors" + "github.com/ghodss/yaml" + "github.com/aws/aws-sdk-go-v2/aws" svcsdktypes "github.com/aws/aws-sdk-go-v2/service/wafv2/types" - "github.com/aws/aws-sdk-go/aws" + + ackcompare "github.com/aws-controllers-k8s/runtime/pkg/compare" + ackrtlog "github.com/aws-controllers-k8s/runtime/pkg/runtime/log" + svcsdk "github.com/aws/aws-sdk-go-v2/service/wafv2" + + svcapitypes "github.com/aws-controllers-k8s/wafv2-controller/apis/v1alpha1" ) type Statement interface { @@ -33,3 +42,353 @@ func stringToStatement[T Statement](cfg *string) (*T, error) { return &config, nil } + +// setLoggingConfiguration populates the WebACL's logging configuration +func setLoggingConfiguration( + ko *svcapitypes.WebACL, + loggingConfig *svcsdktypes.LoggingConfiguration, +) { + if ko.Spec.LoggingConfiguration == nil { + ko.Spec.LoggingConfiguration = &svcapitypes.LoggingConfiguration{} + } + + if loggingConfig.LogDestinationConfigs != nil { + ko.Spec.LoggingConfiguration.LogDestinationConfigs = aws.StringSlice(loggingConfig.LogDestinationConfigs) + } + + if loggingConfig.ResourceArn != nil { + ko.Spec.LoggingConfiguration.ResourceARN = loggingConfig.ResourceArn + } + + if loggingConfig.LogScope != "" { + ko.Spec.LoggingConfiguration.LogScope = aws.String(string(loggingConfig.LogScope)) + } + + if loggingConfig.LogType != "" { + ko.Spec.LoggingConfiguration.LogType = aws.String(string(loggingConfig.LogType)) + } + + ko.Spec.LoggingConfiguration.ManagedByFirewallManager = aws.Bool(loggingConfig.ManagedByFirewallManager) + + if loggingConfig.LoggingFilter != nil { + filter := &svcapitypes.LoggingFilter{} + + if loggingConfig.LoggingFilter.DefaultBehavior != "" { + filter.DefaultBehavior = aws.String(string(loggingConfig.LoggingFilter.DefaultBehavior)) + } + + if loggingConfig.LoggingFilter.Filters != nil { + var filters []*svcapitypes.Filter + + for _, f := range loggingConfig.LoggingFilter.Filters { + filter := &svcapitypes.Filter{} + + if f.Behavior != "" { + filter.Behavior = aws.String(string(f.Behavior)) + } + + if f.Requirement != "" { + filter.Requirement = aws.String(string(f.Requirement)) + } + + if f.Conditions != nil { + var conditions []*svcapitypes.Condition + + for _, c := range f.Conditions { + condition := &svcapitypes.Condition{} + + if c.ActionCondition != nil { + actionCondition := &svcapitypes.ActionCondition{} + if c.ActionCondition.Action != "" { + actionCondition.Action = aws.String(string(c.ActionCondition.Action)) + } + condition.ActionCondition = actionCondition + } + + if c.LabelNameCondition != nil { + labelNameCondition := &svcapitypes.LabelNameCondition{} + if c.LabelNameCondition.LabelName != nil { + labelNameCondition.LabelName = c.LabelNameCondition.LabelName + } + condition.LabelNameCondition = labelNameCondition + } + + conditions = append(conditions, condition) + } + + filter.Conditions = conditions + } + + filters = append(filters, filter) + } + + filter.Filters = filters + } + + ko.Spec.LoggingConfiguration.LoggingFilter = filter + } + + if loggingConfig.RedactedFields != nil { + var redactedFields []*svcapitypes.FieldToMatch + + for _, field := range loggingConfig.RedactedFields { + redactedField := &svcapitypes.FieldToMatch{} + + if field.AllQueryArguments != nil { + redactedField.AllQueryArguments = map[string]*string{} + } + + if field.Body != nil { + body := &svcapitypes.Body{} + if field.Body.OversizeHandling != "" { + body.OversizeHandling = aws.String(string(field.Body.OversizeHandling)) + } + redactedField.Body = body + } + + if field.Method != nil { + redactedField.Method = map[string]*string{} + } + + if field.QueryString != nil { + redactedField.QueryString = map[string]*string{} + } + + if field.SingleHeader != nil { + singleHeader := &svcapitypes.SingleHeader{} + if field.SingleHeader.Name != nil { + singleHeader.Name = field.SingleHeader.Name + } + redactedField.SingleHeader = singleHeader + } + + if field.UriPath != nil { + redactedField.URIPath = map[string]*string{} + } + + redactedFields = append(redactedFields, redactedField) + } + + ko.Spec.LoggingConfiguration.RedactedFields = redactedFields + } +} + +// syncLoggingConfiguration syncs the WebACL's logging configuration by sending a PutLoggingConfiguration request +func syncLoggingConfiguration( + ctx context.Context, + rm *resourceManager, + desired *resource, + delta *ackcompare.Delta, +) error { + rlog := ackrtlog.FromContext(ctx) + exit := rlog.Trace("syncLoggingConfiguration") + defer func() { + exit(nil) + }() + + ko := desired.ko + if ko.Spec.LoggingConfiguration == nil { + return nil + } + + // Check if we have the ARN available - it might not be during creation + if ko.Status.ACKResourceMetadata == nil || ko.Status.ACKResourceMetadata.ARN == nil { + return nil + } + + sdkLoggingConfig := &svcsdktypes.LoggingConfiguration{ + ResourceArn: aws.String(string(*ko.Status.ACKResourceMetadata.ARN)), + } + + if ko.Spec.LoggingConfiguration.LogDestinationConfigs != nil { + sdkLoggingConfig.LogDestinationConfigs = aws.ToStringSlice(ko.Spec.LoggingConfiguration.LogDestinationConfigs) + } + + if ko.Spec.LoggingConfiguration.LogScope != nil { + sdkLoggingConfig.LogScope = svcsdktypes.LogScope(*ko.Spec.LoggingConfiguration.LogScope) + } + + if ko.Spec.LoggingConfiguration.LogType != nil { + sdkLoggingConfig.LogType = svcsdktypes.LogType(*ko.Spec.LoggingConfiguration.LogType) + } + + if ko.Spec.LoggingConfiguration.ManagedByFirewallManager != nil { + sdkLoggingConfig.ManagedByFirewallManager = *ko.Spec.LoggingConfiguration.ManagedByFirewallManager + } + + if ko.Spec.LoggingConfiguration.LoggingFilter != nil { + filter := &svcsdktypes.LoggingFilter{} + + if ko.Spec.LoggingConfiguration.LoggingFilter.DefaultBehavior != nil { + filter.DefaultBehavior = svcsdktypes.FilterBehavior(*ko.Spec.LoggingConfiguration.LoggingFilter.DefaultBehavior) + } + + if ko.Spec.LoggingConfiguration.LoggingFilter.Filters != nil { + var filters []svcsdktypes.Filter + + for _, f := range ko.Spec.LoggingConfiguration.LoggingFilter.Filters { + filter := svcsdktypes.Filter{} + + if f.Behavior != nil { + filter.Behavior = svcsdktypes.FilterBehavior(*f.Behavior) + } + + if f.Requirement != nil { + filter.Requirement = svcsdktypes.FilterRequirement(*f.Requirement) + } + + if f.Conditions != nil { + var conditions []svcsdktypes.Condition + + for _, c := range f.Conditions { + condition := svcsdktypes.Condition{} + + if c.ActionCondition != nil && c.ActionCondition.Action != nil { + condition.ActionCondition = &svcsdktypes.ActionCondition{ + Action: svcsdktypes.ActionValue(*c.ActionCondition.Action), + } + } + + if c.LabelNameCondition != nil && c.LabelNameCondition.LabelName != nil { + condition.LabelNameCondition = &svcsdktypes.LabelNameCondition{ + LabelName: c.LabelNameCondition.LabelName, + } + } + + conditions = append(conditions, condition) + } + + filter.Conditions = conditions + } + + filters = append(filters, filter) + } + + filter.Filters = filters + } + + sdkLoggingConfig.LoggingFilter = filter + } + + if ko.Spec.LoggingConfiguration.RedactedFields != nil { + var redactedFields []svcsdktypes.FieldToMatch + + for _, field := range ko.Spec.LoggingConfiguration.RedactedFields { + redactedField := svcsdktypes.FieldToMatch{} + + if field.AllQueryArguments != nil { + redactedField.AllQueryArguments = &svcsdktypes.AllQueryArguments{} + } + + if field.Body != nil { + body := &svcsdktypes.Body{} + if field.Body.OversizeHandling != nil { + body.OversizeHandling = svcsdktypes.OversizeHandling(*field.Body.OversizeHandling) + } + redactedField.Body = body + } + + if field.Method != nil { + redactedField.Method = &svcsdktypes.Method{} + } + + if field.QueryString != nil { + redactedField.QueryString = &svcsdktypes.QueryString{} + } + + if field.SingleHeader != nil && field.SingleHeader.Name != nil { + redactedField.SingleHeader = &svcsdktypes.SingleHeader{ + Name: field.SingleHeader.Name, + } + } + + if field.URIPath != nil { + redactedField.UriPath = &svcsdktypes.UriPath{} + } + + redactedFields = append(redactedFields, redactedField) + } + + sdkLoggingConfig.RedactedFields = redactedFields + } + + // Construct the input for PutLoggingConfiguration + input := &svcsdk.PutLoggingConfigurationInput{ + LoggingConfiguration: sdkLoggingConfig, + } + + // Call the PutLoggingConfiguration API + resp, err := rm.sdkapi.PutLoggingConfiguration(ctx, input) + if err != nil { + return err + } + + // Update the resource with the response + if resp.LoggingConfiguration != nil { + setLoggingConfiguration(ko, resp.LoggingConfiguration) + } + + return nil +} + +// setResourceAdditionalFields is called after the ReadOne operation to set +// additional resource fields like LockToken, Rules and LoggingConfiguration +func (rm *resourceManager) setResourceAdditionalFields( + ctx context.Context, + ko *svcapitypes.WebACL, + resp *svcsdk.GetWebACLOutput, +) error { + rlog := ackrtlog.FromContext(ctx) + exit := rlog.Trace("setResourceAdditionalFields") + defer func() { + exit(nil) + }() + + if resp.LockToken != nil { + ko.Status.LockToken = resp.LockToken + } + + if err := rm.setOutputRulesNestedStatements(ko.Spec.Rules, resp); err != nil { + return err + } + + err := customSetOutputGetLoggingConfiguration(ctx, rm, ko) + if err != nil { + return err + } + + return nil +} + +// customSetOutputGetLoggingConfiguration fetches and sets the logging configuration for a WebACL. +func customSetOutputGetLoggingConfiguration( + ctx context.Context, + rm *resourceManager, + ko *svcapitypes.WebACL, +) error { + rlog := ackrtlog.FromContext(ctx) + if ko.Status.ACKResourceMetadata != nil && ko.Status.ACKResourceMetadata.ARN != nil { + loggingConfigInput := &svcsdk.GetLoggingConfigurationInput{ + ResourceArn: aws.String(string(*ko.Status.ACKResourceMetadata.ARN)), + } + loggingConfigResp, err := rm.sdkapi.GetLoggingConfiguration(ctx, loggingConfigInput) + if err != nil { + var nfe *svcsdktypes.WAFNonexistentItemException + if errors.As(err, &nfe) { + // WAFNonexistentItemException is not a fatal error for a read operation. + // It implies that Logging has not been enabled using the PutLoggingConfiguration call. + // We log it and proceed, loggingConfigResp will be nil in this case. + rlog.Info("Logging has not been enabled for the WebACL", "WebACL", *ko.Status.ACKResourceMetadata.ARN) + } else { + // For any other error, it's genuinely an issue with the GetLoggingConfiguration call. + return err + } + } + + if loggingConfigResp != nil && loggingConfigResp.LoggingConfiguration != nil { + // Populate the logging configuration fields in ko. + setLoggingConfiguration(ko, loggingConfigResp.LoggingConfiguration) + } + } + return nil +} diff --git a/pkg/resource/web_acl/sdk.go b/pkg/resource/web_acl/sdk.go index 353ee5a..4167f7a 100644 --- a/pkg/resource/web_acl/sdk.go +++ b/pkg/resource/web_acl/sdk.go @@ -2106,11 +2106,8 @@ func (rm *resourceManager) sdkFind( } rm.setStatusDefaults(ko) - if resp.LockToken != nil { - ko.Status.LockToken = resp.LockToken - } - if err := rm.setOutputRulesNestedStatements(ko.Spec.Rules, resp); err != nil { - return nil, err + if err := rm.setResourceAdditionalFields(ctx, ko, resp); err != nil { + return &resource{ko}, err } return &resource{ko}, nil @@ -2206,6 +2203,13 @@ func (rm *resourceManager) sdkCreate( } rm.setStatusDefaults(ko) + // After creation, sync the logging configuration if specified + if ko.Spec.LoggingConfiguration != nil { + if err = syncLoggingConfiguration(ctx, rm, &resource{ko}, nil); err != nil { + return nil, err + } + } + return &resource{ko}, nil } @@ -4352,6 +4356,16 @@ func (rm *resourceManager) sdkUpdate( if err := rm.setInputRulesNestedStatements(input.Rules, desired); err != nil { return nil, err } + if delta.DifferentAt("Spec.LoggingConfiguration") { + // Call the syncLoggingConfiguration function to update the logging configuration + err = syncLoggingConfiguration(ctx, rm, desired, delta) + if err != nil { + return nil, err + } + } + if !delta.DifferentExcept("Spec.LoggingConfiguration") { + return + } var resp *svcsdk.UpdateWebACLOutput _ = resp diff --git a/templates/hooks/webacl/sdk_create_post_set_output.go.tpl b/templates/hooks/webacl/sdk_create_post_set_output.go.tpl new file mode 100644 index 0000000..f998917 --- /dev/null +++ b/templates/hooks/webacl/sdk_create_post_set_output.go.tpl @@ -0,0 +1,7 @@ + // After creation, sync the logging configuration if specified + if ko.Spec.LoggingConfiguration != nil { + if err = syncLoggingConfiguration(ctx, rm, &resource{ko}, nil); err != nil { + return nil, err + } + } + \ No newline at end of file diff --git a/templates/hooks/webacl/sdk_read_one_post_set_output.go.tpl b/templates/hooks/webacl/sdk_read_one_post_set_output.go.tpl index 3b7c38a..dc74291 100644 --- a/templates/hooks/webacl/sdk_read_one_post_set_output.go.tpl +++ b/templates/hooks/webacl/sdk_read_one_post_set_output.go.tpl @@ -1,7 +1,3 @@ - if resp.LockToken != nil { - ko.Status.LockToken = resp.LockToken - } - if err := rm.setOutputRulesNestedStatements(ko.Spec.Rules, resp); err != nil { - return nil, err + if err := rm.setResourceAdditionalFields(ctx, ko, resp); err != nil { + return &resource{ko}, err } - \ No newline at end of file diff --git a/templates/hooks/webacl/sdk_update_post_build_request.go.tpl b/templates/hooks/webacl/sdk_update_post_build_request.go.tpl index 6bf65f1..36904ee 100644 --- a/templates/hooks/webacl/sdk_update_post_build_request.go.tpl +++ b/templates/hooks/webacl/sdk_update_post_build_request.go.tpl @@ -1,3 +1,13 @@ if err := rm.setInputRulesNestedStatements(input.Rules, desired); err != nil { return nil, err - } \ No newline at end of file + } + if delta.DifferentAt("Spec.LoggingConfiguration") { + // Call the syncLoggingConfiguration function to update the logging configuration + err = syncLoggingConfiguration(ctx, rm, desired, delta) + if err != nil { + return nil, err + } + } + if !delta.DifferentExcept("Spec.LoggingConfiguration") { + return + } \ No newline at end of file diff --git a/test/e2e/bootstrap_resources.py b/test/e2e/bootstrap_resources.py index 450a769..0c3c904 100644 --- a/test/e2e/bootstrap_resources.py +++ b/test/e2e/bootstrap_resources.py @@ -17,11 +17,12 @@ from dataclasses import dataclass from acktest.bootstrapping import Resources +from acktest.bootstrapping.s3 import Bucket from e2e import bootstrap_directory @dataclass class BootstrapResources(Resources): - pass + WAFLoggingBucket: Bucket _bootstrap_resources = None diff --git a/test/e2e/requirements.txt b/test/e2e/requirements.txt index 2df0111..bdf9c5e 100644 --- a/test/e2e/requirements.txt +++ b/test/e2e/requirements.txt @@ -1 +1 @@ -acktest @ git+https://github.com/aws-controllers-k8s/test-infra.git@3aedc6b0bf8bbcfdaf0bddb322d5c6adf04c329a +acktest @ git+https://github.com/aws-controllers-k8s/test-infra.git@62b132c217bd912960e268962664c545a6fa824d diff --git a/test/e2e/resources/web_acl_with_logging.yaml b/test/e2e/resources/web_acl_with_logging.yaml new file mode 100644 index 0000000..90aac0c --- /dev/null +++ b/test/e2e/resources/web_acl_with_logging.yaml @@ -0,0 +1,31 @@ +apiVersion: wafv2.services.k8s.aws/v1alpha1 +kind: WebACL +metadata: + name: $WEB_ACL_NAME +spec: + name: $WEB_ACL_NAME + description: "WAF ACL with S3 logging destinations" + scope: REGIONAL + defaultAction: + allow: {} + visibilityConfig: + cloudWatchMetricsEnabled: true + metricName: example-webacl-metric + sampledRequestsEnabled: true + loggingConfiguration: + logDestinationConfigs: + - "$S3_BUCKET_ARN" + logType: "WAF_LOGS" + redactedFields: + - singleHeader: + name: "authorization" + - singleHeader: + name: "cookie" + loggingFilter: + defaultBehavior: "KEEP" + filters: + - behavior: "KEEP" + requirement: "MEETS_ANY" + conditions: + - actionCondition: + action: "BLOCK" \ No newline at end of file diff --git a/test/e2e/service_bootstrap.py b/test/e2e/service_bootstrap.py index 0fcf0e7..dc87e6f 100644 --- a/test/e2e/service_bootstrap.py +++ b/test/e2e/service_bootstrap.py @@ -15,20 +15,68 @@ import logging from acktest.bootstrapping import Resources, BootstrapFailureException +from acktest.bootstrapping.s3 import Bucket from e2e import bootstrap_directory from e2e.bootstrap_resources import BootstrapResources +# WAF logging S3 bucket policy that allows AWS WAF to write logs +WAF_LOGGING_BUCKET_POLICY = """{ + "Version": "2012-10-17", + "Id": "AWSLogDeliveryWrite20150319", + "Statement": [ + { + "Sid": "AWSLogDeliveryWrite", + "Effect": "Allow", + "Principal": { + "Service": "delivery.logs.amazonaws.com" + }, + "Action": "s3:PutObject", + "Resource": "arn:aws:s3:::$NAME/AWSLogs/*", + "Condition": { + "StringEquals": { + "s3:x-amz-acl": "bucket-owner-full-control", + "aws:SourceAccount": "$ACCOUNT_ID" + }, + "ArnLike": { + "aws:SourceArn": "arn:aws:logs:$REGION:$ACCOUNT_ID:*" + } + } + }, + { + "Sid": "AWSLogDeliveryAclCheck", + "Effect": "Allow", + "Principal": { + "Service": "delivery.logs.amazonaws.com" + }, + "Action": "s3:GetBucketAcl", + "Resource": "arn:aws:s3:::$NAME", + "Condition": { + "StringEquals": { + "aws:SourceAccount": "$ACCOUNT_ID" + }, + "ArnLike": { + "aws:SourceArn": "arn:aws:logs:$REGION:$ACCOUNT_ID:*" + } + } + } + ] +}""" + def service_bootstrap() -> Resources: logging.getLogger().setLevel(logging.INFO) resources = BootstrapResources( - # TODO: Add bootstrapping when you have defined the resources + WAFLoggingBucket=Bucket( + name_prefix="aws-waf-logs-", + policy=WAF_LOGGING_BUCKET_POLICY + ) ) try: resources.bootstrap() except BootstrapFailureException as ex: + logging.error(f"Failed to bootstrap resources: {str(ex)}") exit(254) return resources diff --git a/test/e2e/tests/test_web_acl.py b/test/e2e/tests/test_web_acl.py index 38d0218..c9cd55c 100644 --- a/test/e2e/tests/test_web_acl.py +++ b/test/e2e/tests/test_web_acl.py @@ -15,6 +15,7 @@ import time import pytest +import boto3 from acktest.k8s import condition from acktest.k8s import resource as k8s @@ -22,12 +23,13 @@ from e2e import service_marker, CRD_GROUP, CRD_VERSION, load_wafv2_resource from e2e.replacement_values import REPLACEMENT_VALUES from e2e import web_acl +from e2e.bootstrap_resources import get_bootstrap_resources WEB_ACL_RESOURCE_PLURAL = "webacls" -CREATE_WAIT_SECONDS = 10 -MODIFY_WAIT_SECONDS = 10 -DELETE_WAIT_SECONDS = 10 +CREATE_WAIT_SECONDS = 30 +MODIFY_WAIT_SECONDS = 20 +DELETE_WAIT_SECONDS = 20 @pytest.fixture(scope="module") @@ -98,6 +100,58 @@ def nested_statement_web_acl(): pass +@pytest.fixture(scope="module") +def web_acl_with_logging(): + web_acl_name = random_suffix_name("webacl-logging", 24) + + # Get the bootstrap resources + bootstrap_resources = get_bootstrap_resources() + s3_bucket_arn = f"arn:aws:s3:::{bootstrap_resources.WAFLoggingBucket.name}" + + replacements = REPLACEMENT_VALUES.copy() + replacements["WEB_ACL_NAME"] = web_acl_name + replacements["S3_BUCKET_ARN"] = s3_bucket_arn + + resource_data = load_wafv2_resource( + "web_acl_with_logging", + additional_replacements=replacements, + ) + + ref = k8s.CustomResourceReference( + CRD_GROUP, + CRD_VERSION, + WEB_ACL_RESOURCE_PLURAL, + web_acl_name, + namespace="default", + ) + k8s.create_custom_resource(ref, resource_data) + cr = k8s.wait_resource_consumed_by_controller(ref) + + assert cr is not None + assert k8s.get_resource_exists(ref) + + yield (ref, cr) + + try: + # First delete the logging configuration + wafv2_client = boto3.client("wafv2") + if cr and "status" in cr and "ackResourceMetadata" in cr["status"] and "arn" in cr["status"]["ackResourceMetadata"]: + try: + wafv2_client.delete_logging_configuration( + ResourceArn=cr["status"]["ackResourceMetadata"]["arn"] + ) + except: + pass + + # Then delete the WebACL resource + _, deleted = k8s.delete_custom_resource(ref, DELETE_WAIT_SECONDS) + assert deleted + if "spec" in cr and "name" in cr["spec"] and "status" in cr and "id" in cr["status"]: + web_acl.wait_until_deleted(cr["spec"]["name"], cr["status"]["id"]) + except: + pass + + @service_marker @pytest.mark.canary class TestWebACL: @@ -187,6 +241,95 @@ def nested_statement(self, nested_statement_web_acl): assert deleted web_acl.wait_until_deleted(web_acl_name, web_acl_id) + def test_logging_configuration(self, web_acl_with_logging): + ref, _ = web_acl_with_logging + + time.sleep(CREATE_WAIT_SECONDS) + condition.assert_synced(ref) + + cr = k8s.get_resource(ref) + + assert "spec" in cr + assert "name" in cr["spec"] + web_acl_name = cr["spec"]["name"] + + assert "status" in cr + assert "id" in cr["status"] + web_acl_id = cr["status"]["id"] + + latest = web_acl.get(web_acl_name, web_acl_id) + assert latest is not None + + # Check logging configuration is present + logging_config = None + try: + wafv2_client = boto3.client("wafv2") + response = wafv2_client.get_logging_configuration( + ResourceArn=cr["status"]["ackResourceMetadata"]["arn"] + ) + logging_config = response.get("LoggingConfiguration") + except: + pass + + assert logging_config is not None + assert "LogDestinationConfigs" in logging_config + assert len(logging_config["LogDestinationConfigs"]) == 1 + + assert "LogType" in logging_config + assert logging_config["LogType"] == "WAF_LOGS" + + # Verify initial redacted fields + assert "RedactedFields" in logging_config + assert len(logging_config["RedactedFields"]) == 2 + redacted_fields = [ + field.get("SingleHeader", {}).get("Name") + for field in logging_config["RedactedFields"] + if "SingleHeader" in field + ] + assert "authorization" in redacted_fields + assert "cookie" in redacted_fields + + # Update redacted fields (remove cookie and add user-agent) + updates = { + "spec": { + "loggingConfiguration": { + "redactedFields": [ + {"singleHeader": {"name": "authorization"}}, + {"singleHeader": {"name": "user-agent"}} + ] + } + } + } + k8s.patch_custom_resource(ref, updates) + time.sleep(MODIFY_WAIT_SECONDS) + + # Check updated logging configuration + try: + response = wafv2_client.get_logging_configuration( + ResourceArn=cr["status"]["ackResourceMetadata"]["arn"] + ) + logging_config = response.get("LoggingConfiguration") + except: + pass + + assert logging_config is not None + assert "RedactedFields" in logging_config + assert len(logging_config["RedactedFields"]) == 2 + + redacted_fields = [ + field.get("SingleHeader", {}).get("Name") + for field in logging_config["RedactedFields"] + if "SingleHeader" in field + ] + assert "authorization" in redacted_fields + assert "user-agent" in redacted_fields + assert "cookie" not in redacted_fields + + # Verify logging filter is still intact + assert "LoggingFilter" in logging_config + assert logging_config["LoggingFilter"]["DefaultBehavior"] == "KEEP" + assert len(logging_config["LoggingFilter"]["Filters"]) == 1 + ADDITIONAL_RULE = { "name": "rule-3",