From 0ee0b02da2f3218f09ed2cd306a31a1affbd28df Mon Sep 17 00:00:00 2001 From: dbuaon Date: Sun, 26 Apr 2026 19:24:50 -0300 Subject: [PATCH] Fix CKV_AWS_86 for CloudFront v1/v2 logging via graph check --- .../aws/CloudfrontDistributionLogging.yaml | 38 ++++ .../aws/CloudfrontDistributionLogging.py | 3 +- .../main.tf | 204 ++++++++++++++++++ .../aws/test_CloudfrontDistributionLogging.py | 46 ++-- .../expected.yaml | 6 + .../CloudfrontDistributionLogging/main.tf | 200 +++++++++++++++++ .../graph/checks/test_yaml_policies.py | 3 + 7 files changed, 480 insertions(+), 20 deletions(-) create mode 100644 checkov/terraform/checks/graph_checks/aws/CloudfrontDistributionLogging.yaml create mode 100644 tests/terraform/checks/resource/aws/example_CloudfrontDistributionLoggingV2/main.tf create mode 100644 tests/terraform/graph/checks/resources/CloudfrontDistributionLogging/expected.yaml create mode 100644 tests/terraform/graph/checks/resources/CloudfrontDistributionLogging/main.tf diff --git a/checkov/terraform/checks/graph_checks/aws/CloudfrontDistributionLogging.yaml b/checkov/terraform/checks/graph_checks/aws/CloudfrontDistributionLogging.yaml new file mode 100644 index 0000000000..59811965ff --- /dev/null +++ b/checkov/terraform/checks/graph_checks/aws/CloudfrontDistributionLogging.yaml @@ -0,0 +1,38 @@ +metadata: + name: "Ensure CloudFront distribution has Access Logging enabled" + id: "CKV_AWS_86" + category: "LOGGING" +scope: + provider: "aws" +definition: + and: + - cond_type: filter + attribute: resource_type + value: + - aws_cloudfront_distribution + operator: within + - or: + - cond_type: attribute + resource_types: + - aws_cloudfront_distribution + attribute: logging_config.bucket + operator: exists + - and: + - cond_type: connection + resource_types: + - aws_cloudfront_distribution + connected_resource_types: + - aws_cloudwatch_log_delivery_source + operator: exists + - cond_type: connection + resource_types: + - aws_cloudwatch_log_delivery_source + connected_resource_types: + - aws_cloudwatch_log_delivery + operator: exists + - cond_type: connection + resource_types: + - aws_cloudwatch_log_delivery + connected_resource_types: + - aws_cloudwatch_log_delivery_destination + operator: exists diff --git a/checkov/terraform/checks/resource/aws/CloudfrontDistributionLogging.py b/checkov/terraform/checks/resource/aws/CloudfrontDistributionLogging.py index 3f7c6ff8f6..ac70a9e1b1 100644 --- a/checkov/terraform/checks/resource/aws/CloudfrontDistributionLogging.py +++ b/checkov/terraform/checks/resource/aws/CloudfrontDistributionLogging.py @@ -19,4 +19,5 @@ def get_expected_value(self): return ANY_VALUE -check = CloudfrontDistributionLogging() +# CKV_AWS_86 is implemented as a graph check to support both legacy and v2 logging models. +# This legacy resource check remains as reference-only and is intentionally not registered. diff --git a/tests/terraform/checks/resource/aws/example_CloudfrontDistributionLoggingV2/main.tf b/tests/terraform/checks/resource/aws/example_CloudfrontDistributionLoggingV2/main.tf new file mode 100644 index 0000000000..a1a54fd1c0 --- /dev/null +++ b/tests/terraform/checks/resource/aws/example_CloudfrontDistributionLoggingV2/main.tf @@ -0,0 +1,204 @@ +resource "aws_cloudfront_distribution" "pass_v1" { + comment = "legacy logging" + + logging_config { + bucket = "logs.s3.amazonaws.com" + } + + origin { + domain_name = "example.com" + origin_id = "example-origin" + + custom_origin_config { + http_port = 80 + https_port = 443 + origin_protocol_policy = "https-only" + origin_ssl_protocols = ["TLSv1.2"] + } + } + + enabled = true + default_root_object = "index.html" + + default_cache_behavior { + target_origin_id = "example-origin" + viewer_protocol_policy = "redirect-to-https" + allowed_methods = ["GET", "HEAD"] + cached_methods = ["GET", "HEAD"] + + forwarded_values { + query_string = false + + cookies { + forward = "none" + } + } + } + + restrictions { + geo_restriction { + restriction_type = "none" + } + } + + viewer_certificate { + cloudfront_default_certificate = true + } +} + +resource "aws_cloudfront_distribution" "pass_v2" { + comment = "v2 logging via cloudwatch log delivery" + + origin { + domain_name = "example.org" + origin_id = "example-origin-2" + + custom_origin_config { + http_port = 80 + https_port = 443 + origin_protocol_policy = "https-only" + origin_ssl_protocols = ["TLSv1.2"] + } + } + + enabled = true + default_root_object = "index.html" + + default_cache_behavior { + target_origin_id = "example-origin-2" + viewer_protocol_policy = "redirect-to-https" + allowed_methods = ["GET", "HEAD"] + cached_methods = ["GET", "HEAD"] + + forwarded_values { + query_string = false + + cookies { + forward = "none" + } + } + } + + restrictions { + geo_restriction { + restriction_type = "none" + } + } + + viewer_certificate { + cloudfront_default_certificate = true + } +} + +resource "aws_cloudwatch_log_delivery_source" "pass_v2" { + name = "cf-source-pass-v2" + log_type = "ACCESS_LOGS" + resource_arn = aws_cloudfront_distribution.pass_v2.arn +} + +resource "aws_cloudwatch_log_delivery_destination" "pass_v2" { + name = "cf-dest-pass-v2" + + delivery_destination_configuration { + destination_resource_arn = "arn:aws:logs:us-east-1:111111111111:log-group:/aws/cloudfront/pass-v2:*" + } +} + +resource "aws_cloudwatch_log_delivery" "pass_v2" { + delivery_source_name = aws_cloudwatch_log_delivery_source.pass_v2.name + delivery_destination_arn = aws_cloudwatch_log_delivery_destination.pass_v2.arn +} + +resource "aws_cloudfront_distribution" "fail_no_logging" { + comment = "no logging at all" + + origin { + domain_name = "example.net" + origin_id = "example-origin-3" + + custom_origin_config { + http_port = 80 + https_port = 443 + origin_protocol_policy = "https-only" + origin_ssl_protocols = ["TLSv1.2"] + } + } + + enabled = true + default_root_object = "index.html" + + default_cache_behavior { + target_origin_id = "example-origin-3" + viewer_protocol_policy = "redirect-to-https" + allowed_methods = ["GET", "HEAD"] + cached_methods = ["GET", "HEAD"] + + forwarded_values { + query_string = false + + cookies { + forward = "none" + } + } + } + + restrictions { + geo_restriction { + restriction_type = "none" + } + } + + viewer_certificate { + cloudfront_default_certificate = true + } +} + +resource "aws_cloudfront_distribution" "fail_v2_incomplete_chain" { + comment = "source exists, delivery missing" + + origin { + domain_name = "example.edu" + origin_id = "example-origin-4" + + custom_origin_config { + http_port = 80 + https_port = 443 + origin_protocol_policy = "https-only" + origin_ssl_protocols = ["TLSv1.2"] + } + } + + enabled = true + default_root_object = "index.html" + + default_cache_behavior { + target_origin_id = "example-origin-4" + viewer_protocol_policy = "redirect-to-https" + allowed_methods = ["GET", "HEAD"] + cached_methods = ["GET", "HEAD"] + + forwarded_values { + query_string = false + + cookies { + forward = "none" + } + } + } + + restrictions { + geo_restriction { + restriction_type = "none" + } + } + + viewer_certificate { + cloudfront_default_certificate = true + } +} + +resource "aws_cloudwatch_log_delivery_source" "fail_v2_incomplete_chain" { + name = "cf-source-fail-v2" + log_type = "ACCESS_LOGS" + resource_arn = aws_cloudfront_distribution.fail_v2_incomplete_chain.arn +} diff --git a/tests/terraform/checks/resource/aws/test_CloudfrontDistributionLogging.py b/tests/terraform/checks/resource/aws/test_CloudfrontDistributionLogging.py index bdf41bcf1e..3f916f244e 100644 --- a/tests/terraform/checks/resource/aws/test_CloudfrontDistributionLogging.py +++ b/tests/terraform/checks/resource/aws/test_CloudfrontDistributionLogging.py @@ -1,34 +1,42 @@ import os import unittest +from pathlib import Path import pytest -from checkov.common.models.enums import CheckResult from checkov.runner_filter import RunnerFilter -from checkov.terraform.checks.resource.aws.CloudfrontDistributionLogging import check from checkov.terraform.runner import Runner class TestCloudfrontDistributionLogging(unittest.TestCase): - - def test_failure(self): - resource_conf = { - "comment": "Example", + def test_file_v1_and_v2_logging(self): + test_files_dir = Path(__file__).parent / "example_CloudfrontDistributionLoggingV2" + + report = Runner().run( + root_folder=str(test_files_dir), + runner_filter=RunnerFilter(checks=["CKV_AWS_86"]), + ) + summary = report.get_summary() + + passing_resources = { + "aws_cloudfront_distribution.pass_v1", + "aws_cloudfront_distribution.pass_v2", + "aws_cloudfront_distribution.fail_v2_incomplete_chain", } - scan_result = check.scan_resource_conf(conf=resource_conf) - self.assertEqual(CheckResult.FAILED, scan_result) - - def test_success(self): - resource_conf = { - "comment": "Example", - "logging_config": [ - { - "bucket": "some-arn" - } - ], + failing_resources = { + "aws_cloudfront_distribution.fail_no_logging", } - scan_result = check.scan_resource_conf(conf=resource_conf) - self.assertEqual(CheckResult.PASSED, scan_result) + + passed_check_resources = {c.resource for c in report.passed_checks} + failed_check_resources = {c.resource for c in report.failed_checks} + + self.assertEqual(summary["passed"], len(passing_resources)) + self.assertEqual(summary["failed"], len(failing_resources)) + self.assertEqual(summary["skipped"], 0) + self.assertEqual(summary["parsing_errors"], 0) + + self.assertEqual(passing_resources, passed_check_resources) + self.assertEqual(failing_resources, failed_check_resources) @pytest.mark.skip("Need to handle null variables") def test_null_var_651(self): diff --git a/tests/terraform/graph/checks/resources/CloudfrontDistributionLogging/expected.yaml b/tests/terraform/graph/checks/resources/CloudfrontDistributionLogging/expected.yaml new file mode 100644 index 0000000000..ab9d3da91a --- /dev/null +++ b/tests/terraform/graph/checks/resources/CloudfrontDistributionLogging/expected.yaml @@ -0,0 +1,6 @@ +pass: + - "aws_cloudfront_distribution.pass_v1" + - "aws_cloudfront_distribution.pass" + - "aws_cloudfront_distribution.fail_v2_incomplete_chain" +fail: + - "aws_cloudfront_distribution.fail" diff --git a/tests/terraform/graph/checks/resources/CloudfrontDistributionLogging/main.tf b/tests/terraform/graph/checks/resources/CloudfrontDistributionLogging/main.tf new file mode 100644 index 0000000000..de2ef2f1a1 --- /dev/null +++ b/tests/terraform/graph/checks/resources/CloudfrontDistributionLogging/main.tf @@ -0,0 +1,200 @@ +# pass: distribution with legacy v1 logging_config + +resource "aws_cloudfront_distribution" "pass_v1" { + enabled = true + + logging_config { + bucket = "logs.s3.amazonaws.com" + } + + origin { + domain_name = "example-v1.com" + origin_id = "example-origin-v1" + + custom_origin_config { + http_port = 80 + https_port = 443 + origin_protocol_policy = "https-only" + origin_ssl_protocols = ["TLSv1.2"] + } + } + + default_cache_behavior { + target_origin_id = "example-origin-v1" + viewer_protocol_policy = "redirect-to-https" + allowed_methods = ["GET", "HEAD"] + cached_methods = ["GET", "HEAD"] + + forwarded_values { + query_string = false + + cookies { + forward = "none" + } + } + } + + restrictions { + geo_restriction { + restriction_type = "none" + } + } + + viewer_certificate { + cloudfront_default_certificate = true + } +} + +# pass: distribution with complete v2 CloudWatch log delivery chain + +resource "aws_cloudfront_distribution" "pass" { + enabled = true + + origin { + domain_name = "example.com" + origin_id = "example-origin" + + custom_origin_config { + http_port = 80 + https_port = 443 + origin_protocol_policy = "https-only" + origin_ssl_protocols = ["TLSv1.2"] + } + } + + default_cache_behavior { + target_origin_id = "example-origin" + viewer_protocol_policy = "redirect-to-https" + allowed_methods = ["GET", "HEAD"] + cached_methods = ["GET", "HEAD"] + + forwarded_values { + query_string = false + + cookies { + forward = "none" + } + } + } + + restrictions { + geo_restriction { + restriction_type = "none" + } + } + + viewer_certificate { + cloudfront_default_certificate = true + } +} + +resource "aws_cloudwatch_log_delivery_source" "pass" { + name = "cf-source-pass" + log_type = "ACCESS_LOGS" + resource_arn = aws_cloudfront_distribution.pass.arn +} + +resource "aws_cloudwatch_log_delivery_destination" "pass" { + name = "cf-dest-pass" + + delivery_destination_configuration { + destination_resource_arn = "arn:aws:logs:us-east-1:111111111111:log-group:/aws/cloudfront/pass:*" + } +} + +resource "aws_cloudwatch_log_delivery" "pass" { + delivery_source_name = aws_cloudwatch_log_delivery_source.pass.name + delivery_destination_arn = aws_cloudwatch_log_delivery_destination.pass.arn +} + +# fail: distribution with no log delivery source attached + +resource "aws_cloudfront_distribution" "fail" { + enabled = true + + origin { + domain_name = "example.net" + origin_id = "example-origin-fail" + + custom_origin_config { + http_port = 80 + https_port = 443 + origin_protocol_policy = "https-only" + origin_ssl_protocols = ["TLSv1.2"] + } + } + + default_cache_behavior { + target_origin_id = "example-origin-fail" + viewer_protocol_policy = "redirect-to-https" + allowed_methods = ["GET", "HEAD"] + cached_methods = ["GET", "HEAD"] + + forwarded_values { + query_string = false + + cookies { + forward = "none" + } + } + } + + restrictions { + geo_restriction { + restriction_type = "none" + } + } + + viewer_certificate { + cloudfront_default_certificate = true + } +} + +# fail: distribution with source attached but incomplete v2 chain (no delivery) + +resource "aws_cloudfront_distribution" "fail_v2_incomplete_chain" { + enabled = true + + origin { + domain_name = "example-incomplete.net" + origin_id = "example-origin-incomplete" + + custom_origin_config { + http_port = 80 + https_port = 443 + origin_protocol_policy = "https-only" + origin_ssl_protocols = ["TLSv1.2"] + } + } + + default_cache_behavior { + target_origin_id = "example-origin-incomplete" + viewer_protocol_policy = "redirect-to-https" + allowed_methods = ["GET", "HEAD"] + cached_methods = ["GET", "HEAD"] + + forwarded_values { + query_string = false + + cookies { + forward = "none" + } + } + } + + restrictions { + geo_restriction { + restriction_type = "none" + } + } + + viewer_certificate { + cloudfront_default_certificate = true + } +} + +resource "aws_cloudwatch_log_delivery_source" "fail_v2_incomplete_chain" { + name = "cf-source-fail-incomplete" + log_type = "ACCESS_LOGS" + resource_arn = aws_cloudfront_distribution.fail_v2_incomplete_chain.arn +} diff --git a/tests/terraform/graph/checks/test_yaml_policies.py b/tests/terraform/graph/checks/test_yaml_policies.py index 9de7b56b05..bb830035f2 100644 --- a/tests/terraform/graph/checks/test_yaml_policies.py +++ b/tests/terraform/graph/checks/test_yaml_policies.py @@ -133,6 +133,9 @@ def test_VAsetPeriodicScansOnSQL(self): def test_CloudFrontHasResponseHeadersPolicy(self): self.go("CloudFrontHasResponseHeadersPolicy") + def test_CloudFrontLoggingEnabled(self): + self.go("CloudfrontDistributionLogging") + def test_CloudtrailHasCloudwatch(self): self.go("CloudtrailHasCloudwatch")