InspectorVerificationFactory.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.Map;
import java.util.logging.Logger;

/**
 * Verifies Amazon Inspector is reachable for this account, instead of enabling it from the
 * application stack.
 *
 * <p>Same reasoning as {@link SecurityHubVerificationFactory}/{@link MacieVerificationFactory}:
 * Inspector is an account-wide setting an org may already own centrally, and an application
 * deploy role enabling it is a real IAM-blast-radius concern on top of the collision risk
 * {@link InspectorFactory} already solves.
 *
 * <p><b>This check is weaker than the Security Hub/Macie ones -- read before relying on it.</b>
 * Unlike {@code securityhub:DescribeHub}/{@code macie2:GetMacieSession}, which throw when the
 * service isn't enabled, {@code inspector2:BatchGetAccountStatus} always succeeds and reports
 * {@code resourceState.ec2.status}/{@code resourceState.ecr.status} as {@code ENABLED},
 * {@code DISABLED}, or {@code SUSPENDED} in its response body -- there's no API that throws on
 * "not enabled" the way Security Hub/Macie's do. An {@link AwsSdkCall}-only custom resource (the
 * pattern every other factory in this package uses) can only fail on the API call itself
 * failing, not on a value inside a successful response, so this cannot yet gate a deploy on
 * Inspector actually being disabled. Closing that gap needs a Lambda-backed custom resource --
 * real added scope (function code, bundling, its own IAM role) beyond what this check justifies
 * on its own. This resource still verifies the account is one Inspector's API recognizes at all
 * (fails on account/permission errors), which is a real, if narrower, guarantee.
 *
 * <p>{@link InspectorFactory} is left in place, unused, for the case a future deployment model
 * wants CFC to own provisioning again.
 */
public class InspectorVerificationFactory extends BaseFactory {

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

    @DeploymentContext("inspectorEnabled")
    private Boolean inspectorEnabled;

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

    /** Resolves whether Inspector is required, then checks the account is reachable via
     *  Inspector's API -- see the class javadoc for why this can't yet gate on the account's
     *  actual enabled/disabled status. */
    @Override
    public void create() {
        if (inspectorEnabled == null) {
            var securityProfileConfig = ctx.securityProfileConfig.get().orElse(null);
            if (securityProfileConfig != null) {
                inspectorEnabled = securityProfileConfig.isInspectorEnabled();
                LOG.info("Inspector requirement inherited from security profile: " + inspectorEnabled);
            }
        }

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

        String account = Stack.of(this).getAccount();

        AwsSdkCall batchGetAccountStatus = AwsSdkCall.builder()
            .service("Inspector2")
            .action("batchGetAccountStatus")
            .parameters(Map.of("accountIds", List.of(account)))
            .physicalResourceId(PhysicalResourceId.of("inspector-verify-" + Stack.of(this).getStackName()))
            .build();

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

        LOG.info("Inspector verification resource created -- see class javadoc for its narrower guarantee");
    }
}