MacieFactory.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;
/**
* Factory for Amazon Macie (sensitive-data discovery), account/region-scoped like GuardDuty.
*
* <p>An AWS account allows only one Macie session per Region, and {@code macie2:EnableMacie} is
* not idempotent -- it throws {@code ConflictException} when a session already exists. Multiple
* CloudForgeCI stacks in the same account/Region (e.g. two PRODUCTION applications) would
* otherwise race to create {@code AWS::Macie::Session} and the second deploy would fail outright.
* This uses an {@link AwsCustomResource} calling {@code macie2:EnableMacie} directly and ignores
* that specific conflict, so a second stack's deploy adopts the existing session instead of
* failing. No {@code onDelete}: Macie is an account/Region singleton another stack may still
* depend on, so deleting this stack must not disable it for everyone else.
*
* <p>Automated discovery jobs ({@code macieAutomatedDiscoveryEnabled}) are a separate concern from
* the session itself and aren't modeled here yet -- there's no {@code CfnClassificationJob}
* equivalent for "continuous discovery" wired up; this factory only turns Macie on.
*/
public class MacieFactory extends BaseFactory {
private static final Logger LOG = Logger.getLogger(MacieFactory.class.getName());
@DeploymentContext("macieEnabled")
private Boolean macieEnabled;
/** @param scope parent construct
* @param id construct ID */
public MacieFactory(Construct scope, String id) {
super(scope, id);
}
/** Resolves whether Macie is required, then enables the account/Region session, adopting an
* existing one if another stack already created it. */
@Override
public void create() {
if (macieEnabled == null) {
var securityProfileConfig = ctx.securityProfileConfig.get().orElse(null);
if (securityProfileConfig != null) {
macieEnabled = securityProfileConfig.isMacieEnabled();
LOG.info("Macie inherited from security profile: " + macieEnabled);
}
}
if (!Boolean.TRUE.equals(macieEnabled)) {
LOG.info("Macie disabled");
return;
}
String account = Stack.of(this).getAccount();
String region = Stack.of(this).getRegion();
AwsSdkCall enableCall = AwsSdkCall.builder()
.service("Macie2")
.action("enableMacie")
.parameters(Map.of(
"findingPublishingFrequency", "FIFTEEN_MINUTES",
"status", "ENABLED"
))
.physicalResourceId(PhysicalResourceId.of("macie-enable-" + account + "-" + region))
.ignoreErrorCodesMatching("ConflictException")
.build();
AwsCustomResource.Builder.create(this, "MacieSession")
.onCreate(enableCall)
.onUpdate(enableCall)
.policy(AwsCustomResourcePolicy.fromSdkCalls(
SdkCallsPolicyOptions.builder()
.resources(List.of("*"))
.build()
))
.build();
LOG.info("Macie enabled (account-region session, adopts an existing session if present)");
}
}