MinimalIAMConfiguration.java

package com.cloudforgeci.api.core.iam;

import com.cloudforge.core.enums.IAMProfile;
import com.cloudforge.core.enums.RuntimeType;
import com.cloudforge.core.enums.SecurityProfile;
import com.cloudforgeci.api.core.SystemContext;
import com.cloudforgeci.api.interfaces.IAMConfiguration;
import com.cloudforgeci.api.interfaces.Rule;
import io.github.cdklabs.cdknag.NagPackSuppression;
import io.github.cdklabs.cdknag.NagSuppressions;
import software.amazon.awscdk.services.iam.ManagedPolicy;
import software.amazon.awscdk.services.iam.PolicyStatement;
import software.amazon.awscdk.services.iam.Role;
import software.amazon.awscdk.services.iam.ServicePrincipal;

import java.util.List;

import static com.cloudforgeci.api.core.rules.RuleKit.require;

/**
 * Minimal IAM configuration with least privilege permissions.
 * Suitable for production environments with strict compliance requirements.
 *
 * Key principles:
 * - No administrative permissions
 * - Only essential read/write permissions
 * - Resource-specific permissions (no wildcards)
 * - Minimal CloudWatch permissions
 */
public final class MinimalIAMConfiguration implements IAMConfiguration {

    @Override
    public IAMProfile kind() {
        return IAMProfile.MINIMAL;
    }

    @Override
    public String id() {
        return "iam:MINIMAL";
    }

    @Override
    public List<Rule> rules(SystemContext c) {
        var rules = new java.util.ArrayList<Rule>();
        rules.add(require("vpc", x -> x.vpc));

        // Instance security group is only required for EC2 runtime
        if (c.runtime == RuntimeType.EC2) {
            rules.add(require("instance security group", x -> x.instanceSg));
        }

        rules.add(require("alb security group", x -> x.albSg));
        return rules;
    }

    @Override
    public void wire(SystemContext c) {
        // Create minimal IAM roles based on runtime type
        if (c.runtime == RuntimeType.EC2) {
            createMinimalEC2Role(c);
        } else if (c.runtime == RuntimeType.FARGATE) {
            createMinimalFargateRoles(c);
        }
    }

    private void createMinimalEC2Role(SystemContext c) {
        // Get account ID from stack
        String accountId = software.amazon.awscdk.Stack.of(c).getAccount();

        // Use stackName for application-aware log paths
        String appLogPattern = c.stackName != null && !c.stackName.isEmpty()
                ? "/aws/*/" + c.stackName + "*"
                : "/aws/*";

        // For PRODUCTION, use Customer Managed Policy (not inline, not AWS managed)
        if (c.security == SecurityProfile.PRODUCTION) {
            // Create Customer Managed SSM Policy (standalone, not inline)
            ManagedPolicy ssmPolicy = ManagedPolicy.Builder.create(c, "MinimalEc2SSMPolicy")
                    .description("Minimal SSM permissions for EC2 instances (HIPAA/PCI-DSS compliant)")
                    .statements(List.of(
                        PolicyStatement.Builder.create()
                            .sid("SSMCore")
                            .actions(List.of(
                                "ssm:UpdateInstanceInformation",
                                "ssmmessages:CreateControlChannel",
                                "ssmmessages:CreateDataChannel",
                                "ssmmessages:OpenControlChannel",
                                "ssmmessages:OpenDataChannel"
                            ))
                            .resources(List.of("*")) // SSM requires wildcard for these service endpoints
                            .build(),
                        PolicyStatement.Builder.create()
                            .sid("S3GetObject")
                            .actions(List.of("s3:GetObject"))
                            .resources(List.of(
                                "arn:aws:s3:::aws-ssm-" + c.cfc.region() + "/*",
                                "arn:aws:s3:::aws-windows-downloads-" + c.cfc.region() + "/*",
                                "arn:aws:s3:::amazon-ssm-" + c.cfc.region() + "/*",
                                "arn:aws:s3:::amazon-ssm-packages-" + c.cfc.region() + "/*"
                            ))
                            .build()
                    ))
                    .build();

            // Add CDK-NAG suppression for SSM S3 bucket wildcards
            NagSuppressions.addResourceSuppressions(
                ssmPolicy,
                List.of(
                    NagPackSuppression.builder()
                        .id("AwsSolutions-IAM5")
                        .reason("SSM requires s3:GetObject on AWS-managed SSM buckets (aws-ssm-*, amazon-ssm-*, aws-windows-downloads-*) - these are AWS service requirements, not application data")
                        .build()
                ),
                Boolean.TRUE
            );

            // Create Customer Managed CloudWatch Policy (standalone, not inline)
            ManagedPolicy cloudwatchPolicy = ManagedPolicy.Builder.create(c, "MinimalEc2CloudWatchPolicy")
                    .description("Minimal CloudWatch Logs permissions for EC2 instances")
                    .statements(List.of(
                        PolicyStatement.Builder.create()
                            .sid("MinimalCloudWatchLogs")
                            .actions(List.of(
                                "logs:CreateLogGroup",
                                "logs:CreateLogStream",
                                "logs:PutLogEvents",
                                "logs:DescribeLogGroups",
                                "logs:DescribeLogStreams"
                            ))
                            .resources(List.of(
                                // Log group ARN for CreateLogGroup/DescribeLogGroups (requires :* suffix)
                                "arn:aws:logs:" + c.cfc.region() + ":" + accountId + ":log-group:" + appLogPattern + ":*",
                                // Log stream ARN for CreateLogStream/PutLogEvents/DescribeLogStreams
                                "arn:aws:logs:" + c.cfc.region() + ":" + accountId + ":log-group:" + appLogPattern + ":*"
                            ))
                            .build()
                    ))
                    .build();

            // Add CDK-NAG suppression for CloudWatch Logs wildcard pattern
            NagSuppressions.addResourceSuppressions(
                cloudwatchPolicy,
                List.of(
                    NagPackSuppression.builder()
                        .id("AwsSolutions-IAM5")
                        .reason("CloudWatch Logs requires wildcard in log group path (/aws/*/) to allow logging to application-specific log groups created at runtime")
                        .build()
                ),
                Boolean.TRUE
            );

            // Create role and attach Customer Managed Policies
            Role ec2Role = Role.Builder.create(c, "MinimalEc2Role")
                    .assumedBy(new ServicePrincipal("ec2.amazonaws.com"))
                    .managedPolicies(List.of(ssmPolicy, cloudwatchPolicy))
                    .build();

            c.ec2InstanceRole.set(ec2Role);

        } else {
            // For DEV/STAGING, use AWS managed policy (simpler, less strict)
            Role ec2Role = Role.Builder.create(c, "MinimalEc2Role")
                    .assumedBy(new ServicePrincipal("ec2.amazonaws.com"))
                    .managedPolicies(List.of(
                        ManagedPolicy.fromAwsManagedPolicyName("AmazonSSMManagedInstanceCore")
                    ))
                    .build();

            NagSuppressions.addResourceSuppressions(
                ec2Role,
                List.of(
                    NagPackSuppression.builder()
                        .id("AwsSolutions-IAM4")
                        .reason("AmazonSSMManagedInstanceCore is an AWS-managed policy required for " +
                                "EC2 instances to use Systems Manager. It provides minimal permissions " +
                                "for SSM connectivity and is AWS-recommended for all EC2 deployments.")
                        .build()
                ),
                Boolean.TRUE
            );

            // Add CloudWatch permissions as inline policy for DEV/STAGING
            ec2Role.addToPolicy(PolicyStatement.Builder.create()
                    .sid("MinimalCloudWatchLogs")
                    .actions(List.of(
                            "logs:CreateLogGroup",
                            "logs:CreateLogStream",
                            "logs:PutLogEvents",
                            "logs:DescribeLogGroups",
                            "logs:DescribeLogStreams"
                    ))
                    .resources(List.of(
                            // Log group ARN for CreateLogGroup/DescribeLogGroups (requires :* suffix)
                            "arn:aws:logs:" + c.cfc.region() + ":" + accountId + ":log-group:" + appLogPattern + ":*",
                            // Log stream ARN for CreateLogStream/PutLogEvents/DescribeLogStreams
                            "arn:aws:logs:" + c.cfc.region() + ":" + accountId + ":log-group:" + appLogPattern + ":*"
                    ))
                    .build());

            c.ec2InstanceRole.set(ec2Role);
        }
    }

    private void createMinimalFargateRoles(SystemContext c) {
        // Get account ID from stack
        String accountId = software.amazon.awscdk.Stack.of(c).getAccount();

        // Use stackName for application-aware log paths
        String appLogPattern = c.stackName != null && !c.stackName.isEmpty()
                ? "/aws/*/" + c.stackName + "*"
                : "/aws/*";

        // For PRODUCTION, use Customer Managed Policies (not inline, not AWS managed)
        if (c.security == SecurityProfile.PRODUCTION) {
            // Create Customer Managed ECS Task Execution Policy
            ManagedPolicy executionPolicy = ManagedPolicy.Builder.create(c, "MinimalTaskExecutionPolicy")
                    .description("Minimal ECS task execution permissions (HIPAA/PCI-DSS compliant)")
                    .statements(List.of(
                        PolicyStatement.Builder.create()
                            .sid("ECRImagePull")
                            .actions(List.of(
                                "ecr:GetAuthorizationToken",
                                "ecr:BatchCheckLayerAvailability",
                                "ecr:GetDownloadUrlForLayer",
                                "ecr:BatchGetImage"
                            ))
                            .resources(List.of("*")) // ECR GetAuthorizationToken requires wildcard
                            .build(),
                        PolicyStatement.Builder.create()
                            .sid("CloudWatchLogs")
                            .actions(List.of(
                                "logs:CreateLogStream",
                                "logs:PutLogEvents"
                            ))
                            .resources(List.of(
                                // Log group ARN (requires :* suffix)
                                "arn:aws:logs:" + c.cfc.region() + ":" + accountId + ":log-group:/aws/ecs/" + c.stackName + "*:*",
                                // Log stream ARN for CreateLogStream/PutLogEvents
                                "arn:aws:logs:" + c.cfc.region() + ":" + accountId + ":log-group:/aws/ecs/" + c.stackName + "*:*"
                            ))
                            .build()
                    ))
                    .build();

            // Create execution role with Customer Managed Policy
            Role executionRole = Role.Builder.create(c, "MinimalTaskExecutionRole")
                    .assumedBy(new ServicePrincipal("ecs-tasks.amazonaws.com"))
                    .managedPolicies(List.of(executionPolicy))
                    .build();

            // Create Customer Managed Task Policy for application permissions
            ManagedPolicy.Builder taskPolicyBuilder = ManagedPolicy.Builder.create(c, "MinimalTaskPolicy")
                    .description("Minimal ECS task permissions for application runtime");

            // Build task policy statements
            var taskStatements = new java.util.ArrayList<PolicyStatement>();

            // Add EFS permissions if EFS is used
            if (c.efs.get().isPresent() && c.ap.get().isPresent()) {
                taskStatements.add(PolicyStatement.Builder.create()
                        .sid("MinimalEfsAccess")
                        .actions(List.of(
                                "elasticfilesystem:ClientMount",
                                "elasticfilesystem:ClientWrite"
                        ))
                        .resources(List.of(
                                c.efs.get().orElseThrow().getFileSystemArn(),
                                c.ap.get().orElseThrow().getAccessPointArn()
                        ))
                        .build());
            }

            // Add CloudWatch Logs permissions
            taskStatements.add(PolicyStatement.Builder.create()
                    .sid("MinimalCloudWatchLogs")
                    .actions(List.of(
                            "logs:CreateLogGroup",
                            "logs:CreateLogStream",
                            "logs:PutLogEvents"
                    ))
                    .resources(List.of(
                            // Log group ARN for CreateLogGroup
                            "arn:aws:logs:" + c.cfc.region() + ":" + accountId + ":log-group:" + appLogPattern,
                            // Log stream ARN for CreateLogStream/PutLogEvents
                            "arn:aws:logs:" + c.cfc.region() + ":" + accountId + ":log-group:" + appLogPattern + ":*"
                    ))
                    .build());

            ManagedPolicy taskPolicy = taskPolicyBuilder.statements(taskStatements).build();

            // Create task role with Customer Managed Policy
            Role taskRole = Role.Builder.create(c, "MinimalTaskRole")
                    .assumedBy(new ServicePrincipal("ecs-tasks.amazonaws.com"))
                    .managedPolicies(List.of(taskPolicy))
                    .build();

            // Add CDK-NAG suppressions for wildcards (PRODUCTION)
            NagSuppressions.addResourceSuppressions(
                executionPolicy,
                List.of(
                    NagPackSuppression.builder()
                        .id("AwsSolutions-IAM5")
                        .reason("ECR GetAuthorizationToken and CloudWatch Logs patterns require wildcards - AWS service requirements")
                        .build()
                ),
                Boolean.TRUE
            );

            NagSuppressions.addResourceSuppressions(
                taskPolicy,
                List.of(
                    NagPackSuppression.builder()
                        .id("AwsSolutions-IAM5")
                        .reason("CloudWatch Logs requires wildcard in log group path for application logging")
                        .build()
                ),
                Boolean.TRUE
            );

            // Store roles in SystemContext
            c.fargateExecutionRole.set(executionRole);
            c.fargateTaskRole.set(taskRole);

        } else {
            // For DEV/STAGING, use AWS managed policy and inline policies (simpler, less strict)
            Role executionRole = Role.Builder.create(c, "MinimalTaskExecutionRole")
                    .assumedBy(new ServicePrincipal("ecs-tasks.amazonaws.com"))
                    .managedPolicies(List.of(
                        ManagedPolicy.fromAwsManagedPolicyName("service-role/AmazonECSTaskExecutionRolePolicy")
                    ))
                    .build();

            NagSuppressions.addResourceSuppressions(
                executionRole,
                List.of(
                    NagPackSuppression.builder()
                        .id("AwsSolutions-IAM4")
                        .reason("AmazonECSTaskExecutionRolePolicy is an AWS-managed policy specifically " +
                                "designed for ECS task execution. It provides minimal required permissions " +
                                "for pulling container images from ECR and writing logs to CloudWatch.")
                        .build()
                ),
                Boolean.TRUE
            );

            Role taskRole = Role.Builder.create(c, "MinimalTaskRole")
                    .assumedBy(new ServicePrincipal("ecs-tasks.amazonaws.com"))
                    .build();

            // Add EFS permissions if EFS is used
            if (c.efs.get().isPresent() && c.ap.get().isPresent()) {
                taskRole.addToPolicy(PolicyStatement.Builder.create()
                        .sid("MinimalEfsAccess")
                        .actions(List.of(
                                "elasticfilesystem:ClientMount",
                                "elasticfilesystem:ClientWrite"
                        ))
                        .resources(List.of(
                                c.efs.get().orElseThrow().getFileSystemArn(),
                                c.ap.get().orElseThrow().getAccessPointArn()
                        ))
                        .build());
            }

            // Add CloudWatch Logs permissions
            taskRole.addToPolicy(PolicyStatement.Builder.create()
                    .sid("MinimalCloudWatchLogs")
                    .actions(List.of(
                            "logs:CreateLogGroup",
                            "logs:CreateLogStream",
                            "logs:PutLogEvents"
                    ))
                    .resources(List.of(
                            // Log group ARN for CreateLogGroup
                            "arn:aws:logs:" + c.cfc.region() + ":" + accountId + ":log-group:" + appLogPattern,
                            // Log stream ARN for CreateLogStream/PutLogEvents
                            "arn:aws:logs:" + c.cfc.region() + ":" + accountId + ":log-group:" + appLogPattern + ":*"
                    ))
                    .build());

            // Store roles in SystemContext
            c.fargateExecutionRole.set(executionRole);
            c.fargateTaskRole.set(taskRole);
        }
    }
}