DomainFactory.java

package com.cloudforgeci.api.network;

import com.cloudforgeci.api.core.annotation.BaseFactory;
import com.cloudforge.core.annotation.DeploymentContext;
import com.cloudforge.core.annotation.SystemContext;
import com.cloudforge.core.enums.SecurityProfile;
import software.amazon.awscdk.RemovalPolicy;
import software.amazon.awscdk.services.iam.PolicyStatement;
import software.amazon.awscdk.services.iam.ServicePrincipal;
import software.amazon.awscdk.services.kms.Key;
import software.amazon.awscdk.services.logs.LogGroup;
import software.amazon.awscdk.services.route53.CfnHostedZone;
import software.amazon.awscdk.services.route53.HostedZone;
import software.amazon.awscdk.services.route53.HostedZoneProviderProps;
import software.amazon.awscdk.services.route53.IHostedZone;
import software.constructs.Construct;

import java.util.List;
import java.util.logging.Logger;


/**
 * Domain Factory using annotation-based context extraction.
 * Fields annotated with @DeploymentContext automatically extract values from the context.
 */
public class DomainFactory extends BaseFactory {

    private static final Logger LOG = Logger.getLogger(DomainFactory.class.getName());

    @DeploymentContext("domain")
    private String domain;

    @DeploymentContext("subdomain")
    private String subdomain;

    @DeploymentContext("createZone")
    private boolean createZone;

    @SystemContext("security")
    private SecurityProfile security;

    public DomainFactory(Construct scope, String id) {
        super(scope, id);
        // Values are automatically injected by BaseFactory via annotations
    }


    @Override
    public void create() {
        if (domain != null && !domain.isBlank()) {
            IHostedZone zone = createHostedZone(domain);
            ctx.zone.set(zone);
            ctx.domain.set(domain);
            ctx.subdomain.set(subdomain);
        }
    }

    private IHostedZone createHostedZone(String domainName) {
        if (createZone) {
            // Create a new hosted zone resource when createZone = true
            HostedZone zone = HostedZone.Builder.create(this, getNode().getId() + "Zone")
                    .zoneName(domainName)
                    .build();

            // Set removal policy based on security profile
            // PRODUCTION: RETAIN (keep DNS records for safety)
            // DEV/STAGING: DESTROY (clean up test resources)
            RemovalPolicy policy = (security == SecurityProfile.PRODUCTION)
                ? RemovalPolicy.RETAIN
                : RemovalPolicy.DESTROY;
            zone.applyRemovalPolicy(policy);

            LOG.info("Created hosted zone for " + domainName + " with removal policy: " + policy);

            // Configure Route53 query logging when required by compliance frameworks (SOC2, NIST)
            if (config.isRoute53QueryLoggingEnabled()) {
                configureQueryLogging(zone, domainName, policy);
            }

            return zone;
        } else {
            // Use existing hosted zone lookup when createZone = false (normal behavior)
            LOG.info("Looking up existing hosted zone for " + domainName);
            return HostedZone.fromLookup(this, getNode().getId() + "Zone",
                    HostedZoneProviderProps.builder()
                            .privateZone(false)
                            .domainName(domainName)
                            .build());
        }
    }

    /**
     * Configure Route53 DNS query logging for compliance monitoring.
     *
     * <p>DNS query logging captures all DNS queries made to the hosted zone,
     * providing network visibility for security monitoring and forensics.</p>
     *
     * <p>Required for SOC2 CC7.2 (network monitoring) and NIST AU-2 (audit events).</p>
     *
     * <p>Note: Route53 query logs must be in us-east-1 region. The log group
     * must have the prefix "/aws/route53/" for Route53 to write to it.</p>
     *
     * <p>KMS encryption is enabled for HIPAA/PCI-DSS compliance (log data at rest).</p>
     *
     * @param zone The hosted zone to configure logging for
     * @param domainName The domain name for logging identification
     * @param removalPolicy The removal policy to apply to the log group
     */
    private void configureQueryLogging(HostedZone zone, String domainName, RemovalPolicy removalPolicy) {
        LOG.info("Enabling Route53 query logging for " + domainName + " (SOC2/NIST compliance)");

        // Create CloudWatch Log Group for DNS query logs
        // Note: Route53 requires the log group name to start with /aws/route53/
        String logGroupName = "/aws/route53/" + domainName.replace(".", "-");

        // Create KMS key for log encryption (HIPAA/PCI-DSS compliance requirement)
        Key queryLogsKmsKey = Key.Builder.create(this, "Route53QueryLogsKmsKey")
                .description("KMS key for Route53 query logs encryption")
                .enableKeyRotation(true)
                .removalPolicy(removalPolicy)
                .build();

        // Grant CloudWatch Logs permission to use the KMS key
        queryLogsKmsKey.grantEncryptDecrypt(new ServicePrincipal("logs.amazonaws.com"));

        LogGroup queryLogGroup = LogGroup.Builder.create(this, "Route53QueryLogs")
                .logGroupName(logGroupName)
                .retention(config.getLogRetentionDays())
                .removalPolicy(removalPolicy)
                .encryptionKey(queryLogsKmsKey)
                .build();

        LOG.info("Route53 query logs KMS encryption enabled");

        // Grant Route53 permission to write to the log group
        queryLogGroup.addToResourcePolicy(PolicyStatement.Builder.create()
                .principals(List.of(new ServicePrincipal("route53.amazonaws.com")))
                .actions(List.of(
                        "logs:CreateLogStream",
                        "logs:PutLogEvents"
                ))
                .resources(List.of(queryLogGroup.getLogGroupArn()))
                .build());

        // Configure query logging on the hosted zone using L1 construct
        CfnHostedZone cfnZone = (CfnHostedZone) zone.getNode().getDefaultChild();
        cfnZone.addPropertyOverride("QueryLoggingConfig", java.util.Map.of(
                "CloudWatchLogsLogGroupArn", queryLogGroup.getLogGroupArn()
        ));

        LOG.info("Route53 query logging configured for " + domainName + " -> " + logGroupName);
    }

}