AWSTemplateFormatVersion: '2010-09-09'

# ---------------------------------------------------------------------------
# CloudFormation caps Description at 1024 characters and rejects the whole
# template with "Template format error: 'Description' length is greater than
# 1024" when it is longer. This one was 1475 and had never deployed. It went
# unnoticed because the quick-create link ALSO pointed at a non-S3 URL, so AWS
# refused the template before it ever parsed it - the first fault masked the
# second, and both are invisible from our side.
#
# Detail that does not fit in Description belongs in comments like this one:
# CloudFormation ignores them, and a customer reading the file before running
# it still sees everything.
#
# What the role can read, exactly (the inline policy below, 26 actions):
# iam:GetAccountSummary, iam:ListUsers, iam:ListMFADevices,
# iam:ListAttachedUserPolicies, iam:GetAccountPasswordPolicy,
# cloudtrail:DescribeTrails, cloudtrail:GetTrailStatus,
# s3:ListAllMyBuckets, s3:GetBucketPublicAccessBlock,
# s3:GetEncryptionConfiguration, s3:GetBucketPolicy,
# guardduty:ListDetectors, guardduty:GetDetector,
# inspector2:BatchGetAccountStatus, securityhub:DescribeHub,
# config:DescribeConfigurationRecorderStatus,
# ssm:DescribeInstanceInformation, ssm:DescribeInstancePatchStates,
# ec2:GetEbsEncryptionByDefault, ec2:DescribeSecurityGroups,
# ec2:DescribeSubnets, ec2:DescribeRouteTables,
# backup:ListBackupPlans, backup:ListBackupVaults,
# kms:ListKeys, kms:DescribeKey.
# Every one is a describe, list, get or batch-get operation that returns
# configuration, status or metadata. None reads an object, a file, a secret value or a log
# event, and none can use a KMS key. No AWS-managed policy is attached.
#
# Template version 1.1.0 granted ten of these: the four IAM actions other
# than GetAccountPasswordPolicy, the two CloudTrail and two GuardDuty actions,
# s3:ListAllMyBuckets and s3:GetBucketPublicAccessBlock. Version 1.2.0
# (2026-09-26) added the other sixteen so the extended checks can measure. A
# stack created from 1.1.0 keeps its ten actions until you update the stack
# with this template; the stack's TemplateVersion output shows which one you
# have.
# ---------------------------------------------------------------------------
Description: |
  ai4cmmc.ai CMMC Read-Only Cross-Account Role (template 1.2.0)

  Creates a read-only IAM role ai4cmmc.ai assumes to scan this account
  against NIST SP 800-171 Rev. 2 controls for your readiness report or package.

  CAN DO (26 read-only actions, one inline policy, no AWS-managed policies):
  read IAM users, MFA devices, attached policy names, account summary and
  password policy; CloudTrail, GuardDuty, Inspector, Security Hub and Config
  status; S3 bucket names, public access block, encryption and bucket policy;
  SSM patch state; EBS default encryption, security groups, subnets, route
  tables; Backup plans and vaults; KMS key metadata. Full list in this file.

  CANNOT DO: modify, create or delete any resource; decrypt with your KMS keys;
  change permissions. It never calls S3 GetObject - only configuration metadata
  is read, so no CUI is read or stored.

  TRUST: only the ai4cmmc.ai account may assume this role, and only with the
  unique External ID issued for your purchase.

  TO REVOKE: delete this CloudFormation stack.

Parameters:
  ExternalId:
    Type: String
    Description: |
      Unique per-customer External ID generated by ai4cmmc.ai when you
      started your AWS connection. This was pre-filled if you arrived
      via the ai4cmmc.ai one-click link. Do NOT change it. Without this
      exact value, no one can assume the role.
    AllowedPattern: '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
    ConstraintDescription: Must be a UUID (e.g. 550e8400-e29b-41d4-a716-446655440000)

  Ai4CmmcAwsAccountId:
    Type: String
    Description: |
      The AWS account ID for ai4cmmc.ai's platform. Used in the trust
      policy so only that specific account can assume this role.
      This is pre-filled when you arrived via the ai4cmmc.ai one-click
      link. Do not edit. If you arrived here from a manual download,
      contact hello@ai4cmmc.ai for the correct account ID.
    AllowedPattern: '^[0-9]{12}$'
    ConstraintDescription: Must be a 12-digit AWS account ID.

Resources:
  Ai4CmmcReadOnlyRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: ai4cmmc-sprs-readonly
      Description: Read-only role assumed by ai4cmmc.ai to run CMMC configuration-metadata evidence scans against this AWS account.
      MaxSessionDuration: 3600  # 1 hour, the lowest value IAM accepts for this setting
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              AWS: !Sub 'arn:aws:iam::${Ai4CmmcAwsAccountId}:root'
            Action: sts:AssumeRole
            Condition:
              StringEquals:
                sts:ExternalId: !Ref ExternalId
      # No managed policies. This role grants exactly the actions called by
      # the extractors in services/cmmc_aws_scanner.py and
      # services/cmmc_aws_extractors_ext.py, and nothing else.
      #
      # It previously attached SecurityAudit + ReadOnlyAccess. ReadOnlyAccess
      # includes s3:GetObject, so the credential could read the contents of a
      # customer's objects even though no code path ever asked for one. That is
      # the difference between "our software does not read your data" and "this
      # role cannot read your data". Only the second survives a future code
      # change or a compromise of the platform, and only the second is worth
      # anything to an assessor.
      #
      # See docs/cui-boundary.md. Widening this list is a CUI-boundary change,
      # not a convenience fix. tests/test_aws_role_grants_only_what_the_code_calls.py
      # reads the calls in services/cmmc_aws_scanner.py and
      # services/cmmc_aws_extractors_ext.py, and fails the build if a called
      # action is not granted here, if anything here is not called, or if
      # anything beyond version 1.1.0 lacks an owner decision (O-1, 2026-09-26).
      Policies:
        - PolicyName: ai4cmmc-config-read-only
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Sid: ConfigurationMetadataReadOnly
                Effect: Allow
                Action:
                  # Nothing for sts. The platform's own credentials call
                  # AssumeRole, and the trust policy above governs that.
                  # sts:GetCallerIdentity needs no permission.
                  # IAM posture: MFA enrollment, attached policy names, counts
                  - iam:GetAccountSummary
                  - iam:ListUsers
                  - iam:ListMFADevices
                  - iam:ListAttachedUserPolicies
                  # Password length, complexity and reuse settings (3.5.7, 3.5.8)
                  - iam:GetAccountPasswordPolicy
                  # Audit logging: whether trails exist and are running, and
                  # whether log file validation is on (3.3.1, 3.3.8)
                  - cloudtrail:DescribeTrails
                  - cloudtrail:GetTrailStatus
                  # S3: bucket NAMES and bucket-level SETTINGS only: public
                  # access block (3.1.20), default encryption (3.13.16) and the
                  # bucket policy, read to check that it requires TLS (3.13.8).
                  # Deliberately absent: s3:GetObject, s3:SelectObjectContent.
                  - s3:ListAllMyBuckets
                  - s3:GetBucketPublicAccessBlock
                  - s3:GetEncryptionConfiguration
                  - s3:GetBucketPolicy
                  # Threat detection: whether GuardDuty is enabled (3.14.6)
                  - guardduty:ListDetectors
                  - guardduty:GetDetector
                  # Whether Amazon Inspector scans EC2 and ECR (3.11.2)
                  - inspector2:BatchGetAccountStatus
                  # Whether Security Hub is enabled (3.3.5)
                  - securityhub:DescribeHub
                  # Whether AWS Config is recording (3.4.1)
                  - config:DescribeConfigurationRecorderStatus
                  # SSM-managed instance IDs and their missing or failed patch
                  # counts (3.14.1)
                  - ssm:DescribeInstanceInformation
                  - ssm:DescribeInstancePatchStates
                  # EBS encryption by default (3.13.16)
                  - ec2:GetEbsEncryptionByDefault
                  # Security group rules: management ports open to the internet
                  # (3.13.1) and default security groups (3.13.6)
                  - ec2:DescribeSecurityGroups
                  # Subnets and route tables: public and private subnet
                  # separation (3.13.5)
                  - ec2:DescribeSubnets
                  - ec2:DescribeRouteTables
                  # Whether AWS Backup plans and vaults exist (3.8.9)
                  - backup:ListBackupPlans
                  - backup:ListBackupVaults
                  # KMS key IDs and key metadata (manager, state) (3.13.10).
                  # Deliberately absent: kms:Decrypt and every other
                  # operation that uses a key.
                  - kms:ListKeys
                  - kms:DescribeKey
                Resource: '*'
      Tags:
        - Key: Purpose
          Value: ai4cmmc-sprs-reality-check
        - Key: ManagedBy
          Value: ai4cmmc-platform
        - Key: ExternalId
          Value: !Ref ExternalId

Outputs:
  RoleArn:
    Description: |
      ARN of the read-only role. Copy this and paste into the
      ai4cmmc.ai onboarding page (or POST it to
      /api/v1/cmmc/aws-connect/activate with your intake token).
    Value: !GetAtt Ai4CmmcReadOnlyRole.Arn
    Export:
      Name: Ai4CmmcReadOnlyRoleArn

  ExternalIdEcho:
    Description: External ID used in the trust policy (echoed for confirmation).
    Value: !Ref ExternalId

  TemplateVersion:
    Description: Template version. The role's permissions last changed on 2026-09-26, when 16 read-only metadata actions were added for the extended checks.
    Value: '1.2.0-2026-09-26'
