SecurityHubVerificationFactory.java

package com.cloudforgeci.api.observability;

import com.cloudforgeci.api.core.annotation.BaseFactory;
import com.cloudforge.core.annotation.DeploymentContext;
import software.amazon.awscdk.Stack;
import software.amazon.awscdk.customresources.AwsCustomResource;
import software.amazon.awscdk.customresources.AwsCustomResourcePolicy;
import software.amazon.awscdk.customresources.AwsSdkCall;
import software.amazon.awscdk.customresources.PhysicalResourceId;
import software.amazon.awscdk.customresources.SdkCallsPolicyOptions;
import software.constructs.Construct;

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

/**
 * Verifies Security Hub is already enabled in this account/Region, instead of enabling it from
 * the application stack.
 *
 * <p>Security Hub (and its standards subscriptions) is an account/Region singleton, and the org
 * consuming CFC may already own its enablement centrally -- via AWS Control Tower, an
 * Organizations delegated administrator, Landing Zone Accelerator, or a hand-rolled baseline
 * stack. An application deploy role enabling and configuring an account-wide security service
 * is a real IAM-blast-radius concern for that setup, on top of the multi-stack collision risk
 * {@link SecurityHubFactory} already solves with idempotent adoption. This factory instead makes
 * the compliance claim honest without taking on either: it calls {@code securityhub:DescribeHub},
 * which throws (failing this stack's deploy) when the account/Region isn't subscribed, and
 * succeeds silently otherwise. Nothing here enables Security Hub or manages standards
 * subscriptions.
 *
 * <p><b>Scope</b>: this only confirms the hub itself is enabled, not that a specific standard
 * (e.g. PCI DSS v4.0.1) is subscribed -- {@code DescribeHub}'s response doesn't include standards,
 * and asserting a specific standard from {@code GetEnabledStandards}' response list isn't
 * expressible with this SDK-call-only custom resource (no conditional-fail-on-value primitive
 * without a Lambda-backed custom resource, which is a bigger lift than this check justifies on
 * its own). Revisit if per-standard verification becomes a real requirement.
 *
 * <p>{@link SecurityHubFactory} is left in place, unused, for the case a future deployment model
 * wants CFC to own provisioning again.
 */
public class SecurityHubVerificationFactory extends BaseFactory {

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

    @DeploymentContext("securityHubEnabled")
    private Boolean securityHubEnabled;

    /** @param scope parent construct
     *  @param id construct ID */
    public SecurityHubVerificationFactory(Construct scope, String id) {
        super(scope, id);
    }

    /** Resolves whether Security Hub is required, then fails this stack's deploy if the
     *  account/Region hub isn't already enabled centrally. */
    @Override
    public void create() {
        if (securityHubEnabled == null) {
            var securityProfileConfig = ctx.securityProfileConfig.get().orElse(null);
            if (securityProfileConfig != null) {
                securityHubEnabled = securityProfileConfig.isSecurityHubEnabled();
                LOG.info("Security Hub requirement inherited from security profile: " + securityHubEnabled);
            }
        }

        if (!Boolean.TRUE.equals(securityHubEnabled)) {
            LOG.info("Security Hub not required for this deployment -- skipping verification");
            return;
        }

        AwsSdkCall describeHub = AwsSdkCall.builder()
            .service("SecurityHub")
            .action("describeHub")
            .physicalResourceId(PhysicalResourceId.of("securityhub-verify-" + Stack.of(this).getStackName()))
            .build();

        AwsCustomResource.Builder.create(this, "SecurityHubVerification")
            .onCreate(describeHub)
            .onUpdate(describeHub)
            .policy(AwsCustomResourcePolicy.fromSdkCalls(
                SdkCallsPolicyOptions.builder()
                    .resources(List.of("*"))
                    .build()
            ))
            .build();

        LOG.info("Security Hub verification resource created -- deploy fails if the hub isn't already enabled");
    }
}