AWSTemplateFormatVersion: '2010-09-09'
Description: 'CloudArq Universal Read-Only IAM Role for AWS Security Auditing'

# ─────────────────────────────────────────────────────────────────────────────
# 2026-07-25 — REBUILT so that shipping a new check almost never requires you to
# touch this stack again.
#
# BEFORE: 205 individually enumerated actions. Every new check that called a new
# AWS API meant a new template and a console visit for EVERY customer. Replaying
# the last 12 action-adding commits, all 12 would have required a redeploy.
# AFTER:  an AWS-maintained baseline + service-scoped read shapes + an explicit
# deny. Replaying the same 12 commits, 11 need NO customer action at all.
#
# The safety story is STRONGER, not weaker. Previously "read-only" rested on an
# allow-list being correct — and it silently was not: three S3 action names in
# the old list did not exist, so two checks could never run and reported PASS.
# Now there is an explicit Deny over every data-plane, secret, credential and
# log-content action. Deny is evaluated FIRST in IAM and cannot be overridden by
# any Allow, including anything AWS adds to SecurityAudit in future. It is a
# negative guarantee you can verify in your own account, not a promise from us.
#
# WHAT WE CAN NEVER DO, enforced below: read an S3 object, read a database row,
# read a secret or parameter, decrypt with KMS, read log or console contents,
# read an AI prompt / agent memory / model output, or download function code.
# ─────────────────────────────────────────────────────────────────────────────

Parameters:
  ExternalId:
    Type: String
    Description: 'External ID provided by CloudArq for secure role assumption'
    MinLength: 1

Resources:
  CloudArqReadOnlyRole:
    Type: 'AWS::IAM::Role'
    Properties:
      RoleName: CloudArqReadOnly
      # CloudFormation does NOT propagate its stack tags to IAM roles (open AWS
      # issue: aws-cloudformation/cloudformation-coverage-roadmap#277), so we
      # stamp them ourselves. CloudArq reads these back with iam:ListRoleTags
      # (granted by SecurityAudit) to tell you whether your role is current —
      # and to find your stack when it is named something other than the
      # default. Without this, stack discovery silently always failed.
      Tags:
        - Key: 'cloudarq:template-version'
          Value: '2026-07-25.1'
        - Key: 'cloudarq:stack-id'
          Value: !Ref 'AWS::StackId'
        - Key: 'cloudarq:stack-name'
          Value: !Ref 'AWS::StackName'
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              AWS: 'arn:aws:iam::738556365655:root'
            Action: 'sts:AssumeRole'
            Condition:
              StringEquals:
                'sts:ExternalId': !Ref ExternalId

      # AWS maintains these and extends them as new services launch, which is
      # what removes the weekly redeploy. SecurityAudit is the policy AWS
      # publishes specifically for security-audit tooling; ViewOnlyAccess adds
      # a handful of List/Describe surfaces SecurityAudit omits (backup plans,
      # CloudWatch metric reads, EKS cluster versions).
      # NOTE: ReadOnlyAccess is deliberately NOT used — it grants s3:GetObject,
      # dynamodb:GetItem and similar data reads, which we must never hold.
      ManagedPolicyArns:
        - 'arn:aws:iam::aws:policy/SecurityAudit'
        - 'arn:aws:iam::aws:policy/job-function/ViewOnlyAccess'

      Policies:
        - PolicyName: CloudArqReadOnlyPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:

              # ── DELTA ────────────────────────────────────────────────────
              # What the AWS-managed policies do NOT cover, expressed as
              # service-scoped READ shapes rather than individual actions, so
              # new checks inside these services need no customer action.
              #
              # Organised by ALL SIX well-architected pillars plus the AI
              # Workload lens — every axis CloudArq ships checks along — so
              # growth in any of them is already authorised. SecurityAudit
              # covers the Security pillar densely; the gaps cluster in cost,
              # AI/ML and the newer reliability/performance services.
              #
              # These shapes are deliberately broad because the Deny block
              # below is what enforces safety. Deny is evaluated first and
              # cannot be overridden, so a wide read-shaped Allow cannot
              # become a data read.
              - Sid: CloudArqReadOnlyDelta
                Effect: Allow
                Resource: '*'
                Action:
                  # ── COST ── SecurityAudit deliberately excludes every
                  # billing surface, so all cost checks live here.
                  - 'budgets:Describe*'
                  - 'budgets:View*'
                  - 'ce:Describe*'
                  - 'ce:Get*'
                  - 'ce:List*'
                  - 'compute-optimizer:Describe*'
                  - 'compute-optimizer:Get*'
                  - 'cur:Describe*'
                  - 'pricing:Describe*'
                  - 'pricing:Get*'
                  - 'pricing:List*'
                  - 'savingsplans:Describe*'
                  - 'savingsplans:List*'

                  # ── AI WORKLOAD ── control-plane configuration only. Every
                  # data plane (prompts, agent memory, model output, vector
                  # contents, document analysis) is denied below.
                  - 'aoss:BatchGet*'
                  - 'aoss:List*'
                  - 'bedrock:Get*'
                  - 'bedrock:List*'
                  - 'bedrock-agentcore:Get*'
                  - 'bedrock-agentcore:List*'
                  - 'comprehend:Describe*'
                  - 'comprehend:List*'
                  - 'kendra:Describe*'
                  - 'kendra:List*'
                  - 'lex:Describe*'
                  - 'lex:List*'
                  - 'personalize:Describe*'
                  - 'personalize:List*'
                  - 'q:Get*'
                  - 'q:List*'
                  - 'rekognition:Describe*'
                  - 'rekognition:List*'
                  - 'sagemaker:Describe*'
                  - 'sagemaker:List*'
                  - 'textract:Get*'
                  - 'textract:List*'
                  - 'transcribe:Describe*'
                  - 'transcribe:List*'

                  # ── RELIABILITY ── backup, DR, resilience and quota surfaces.
                  - 'backup:Describe*'
                  - 'backup:Get*'
                  - 'backup:List*'
                  - 'dlm:Get*'
                  - 'drs:Describe*'
                  - 'drs:List*'
                  - 'eks:Describe*'
                  - 'eks:List*'
                  - 'fis:Get*'
                  - 'fis:List*'
                  - 'resiliencehub:Describe*'
                  - 'resiliencehub:List*'
                  - 'route53-recovery-readiness:Get*'
                  - 'route53-recovery-readiness:List*'
                  - 'servicequotas:Get*'
                  - 'servicequotas:List*'

                  # ── PERFORMANCE ── metrics and the acceleration/caching tier.
                  # CloudWatch is the backbone of every performance check.
                  - 'application-autoscaling:Describe*'
                  - 'autoscaling:Describe*'
                  - 'cloudwatch:Describe*'
                  - 'cloudwatch:Get*'
                  - 'cloudwatch:List*'
                  - 'dax:Describe*'
                  - 'globalaccelerator:Describe*'
                  - 'globalaccelerator:List*'

                  # ── OPERATIONAL EXCELLENCE ── change management, patching,
                  # service health and tagging hygiene.
                  - 'health:Describe*'
                  - 'servicecatalog:Describe*'
                  - 'servicecatalog:List*'
                  - 'ssm:Describe*'
                  - 'ssm:Get*'
                  - 'ssm:List*'
                  - 'support:Describe*'
                  - 'tag:Describe*'
                  - 'tag:Get*'

                  # ── SUSTAINABILITY ── storage-class, right-sizing and
                  # region-efficiency signals. Mostly rides on the cost and
                  # performance shapes above plus S3 bucket config below.
                  - 'ec2:GetInstanceTypesFromInstanceRequirements'

                  # ── S3 BUCKET CONFIGURATION ── deliberately never the object
                  # surface. `s3:Get*` would include GetObject, so we scope to
                  # bucket-level shapes only; `s3:Get*Configuration` matches no
                  # object action (no S3 object API ends in "Configuration").
                  - 's3:Get*Configuration'
                  - 's3:GetAccountPublicAccessBlock'
                  - 's3:GetBucket*'
                  - 's3:ListAllMyBuckets'
                  - 's3:ListBucketMultipartUploads'

                  # ── SINGLETONS ──
                  # lambda:GetFunction is required to inspect a function's
                  # configuration; note it also returns a short-lived presigned
                  # URL to the deployment package. CloudArq never fetches that
                  # URL, and lambda:GetLayerVersion* (the layer-code download)
                  # is denied below.
                  - 'lambda:GetFunction'
                  - 'sts:GetCallerIdentity'

              # ── THE GUARD ────────────────────────────────────────────────
              # An explicit Deny is evaluated BEFORE every Allow and cannot be
              # overridden — not by the statement above, not by the AWS-managed
              # policies, and not by anything AWS adds to them later. This is
              # the line to point at when someone asks what CloudArq can see.
              - Sid: CloudArqNeverReadsYourData
                Effect: Deny
                Resource: '*'
                Action:
                  # Object and blob contents
                  - 's3:GetObject*'
                  - 's3:GetDataAccess'
                  - 's3-object-lambda:*'
                  # Database rows, query results, stream payloads
                  - 'athena:GetQueryResults*'
                  - 'aoss:APIAccessAll'
                  - 'cassandra:Select'
                  - 'dynamodb:BatchGetItem'
                  - 'dynamodb:GetItem'
                  - 'dynamodb:PartiQLSelect'
                  - 'dynamodb:Query'
                  - 'dynamodb:Scan'
                  - 'es:ESHttp*'
                  - 'kinesis:Get*'
                  - 'kinesis:SubscribeToShard'
                  - 'neptune-db:*'
                  - 'qldb:PartiQL*'
                  - 'rds-data:*'
                  - 'redshift-data:GetStatementResult*'
                  - 'sdb:Select*'
                  - 'sqs:ReceiveMessage'
                  - 'timestream:Select'
                  # Secrets, keys and credentials
                  # ``GetResource*Token`` catches the Oauth2Token/PaymentToken
                  # siblings but NOT GetResourceApiKey, which the Allow side's
                  # ``bedrock-agentcore:Get*`` therefore left ALLOWED — an
                  # API-KEY read, on a page that invites customers to verify our
                  # Deny with simulate-principal-policy. Listed explicitly
                  # rather than widening to ``GetResource*``, which would also
                  # deny GetResourcePolicy (a legitimate config read).
                  - 'bedrock-agentcore:GetResourceApiKey'
                  - 'bedrock-agentcore:GetResource*Token'
                  - 'bedrock-agentcore:GetWorkloadAccessToken*'
                  - 'ec2:GetPasswordData'
                  - 'kms:Decrypt'
                  - 'kms:Encrypt'
                  - 'kms:GenerateDataKey*'
                  - 'kms:GenerateMac'
                  - 'kms:ReEncrypt*'
                  - 'kms:Sign'
                  - 'secretsmanager:BatchGetSecretValue'
                  - 'secretsmanager:GetSecretValue'
                  - 'ssm:GetCommandInvocation'
                  - 'ssm:GetParameter'
                  - 'ssm:GetParameters'
                  - 'ssm:GetParametersByPath'
                  - 'ssm:StartSession'
                  - 'sts:GetFederationToken'
                  - 'sts:GetSessionToken'
                  # Log, console and DB-log contents.
                  # ec2:DescribeInstanceAttribute returns UserData, and
                  # rds:DownloadDBLogFilePortion returns log bodies — both are
                  # granted by SecurityAudit, so this Deny is what keeps the
                  # "we never read your data" promise true.
                  - 'cloudtrail:GetQueryResults'
                  - 'ec2:DescribeInstanceAttribute'
                  - 'ec2:GetConsoleOutput'
                  - 'ec2:GetConsoleScreenshot'
                  - 'ec2:GetInstanceUefiData'
                  - 'ec2:GetLaunchTemplateData'
                  - 'logs:FilterLogEvents'
                  - 'logs:GetLogEvents'
                  - 'logs:GetLogRecord'
                  - 'logs:GetQueryResults'
                  - 'logs:StartQuery'
                  - 'rds:DownloadCompleteDBLogFile'
                  - 'rds:DownloadDBLogFilePortion'
                  # AI prompts, agent memory, model input/output
                  - 'bedrock:GetAgentMemory'
                  - 'bedrock:GetAsyncInvoke'
                  - 'bedrock:GetDocumentContent'
                  - 'bedrock:GetKnowledgeBaseDocuments'
                  - 'bedrock:GetSession'
                  - 'bedrock:InvokeAgent'
                  - 'bedrock:InvokeModel*'
                  - 'bedrock:Retrieve*'
                  - 'bedrock-agentcore:*MemoryRecord*'
                  - 'bedrock-agentcore:GetBrowserSession'
                  - 'bedrock-agentcore:GetCodeInterpreterSession'
                  - 'bedrock-agentcore:GetDataset'
                  - 'bedrock-agentcore:GetEvent'
                  - 'bedrock-agentcore:Invoke*'
                  - 'bedrock-agentcore:ListDataset*'
                  - 'bedrock-agentcore:ListEvents'
                  - 'sagemaker:InvokeEndpoint*'
                  # Other AI/ML data planes. The Describe*/List* shapes above
                  # authorise CONFIGURATION reads for these services; every
                  # action that would process or return CUSTOMER CONTENT —
                  # document text, images, speech, chat, search hits,
                  # recommendations — is denied here. textract:GetDocument*
                  # in particular returns the extracted text of a customer's
                  # documents, so a bare textract:Get* would have been a data
                  # read despite looking like metadata.
                  - 'comprehend:BatchDetect*'
                  - 'comprehend:Classify*'
                  - 'comprehend:Detect*'
                  - 'kendra:Query'
                  - 'kendra:Retrieve'
                  - 'lex:PostContent'
                  - 'lex:PostText'
                  - 'lex:Recognize*'
                  - 'personalize:GetPersonalizedRanking'
                  - 'personalize:GetRecommendations'
                  # Amazon Q. Verified against AWS's published action namespace
                  # on 2026-07-26 after the name-validation guard caught two
                  # entries here that DENIED NOTHING: 'q:ListMessages' is not a
                  # real IAM action and 'q:Chat*' matches none. Exactly the bug
                  # class this rebuild exists to kill — a plausible-looking name
                  # that silently does nothing. These are the real ones, and
                  # they also close a gap: q:Get* was granting GetArtifact,
                  # GetArtifactActionResult and GetTroubleshootingResults, which
                  # return generated content.
                  - 'q:GenerateCode*'
                  - 'q:GetArtifact*'
                  - 'q:GetConversation'
                  - 'q:GetTroubleshootingResults'
                  - 'q:ListConversations'
                  - 'q:SendMessage'
                  - 'q:StartConversation'
                  - 'q:UpdateConversation'
                  - 'rekognition:Compare*'
                  - 'rekognition:Detect*'
                  - 'rekognition:GetCelebrity*'
                  - 'rekognition:GetContentModeration'
                  - 'rekognition:GetFaceDetection'
                  - 'rekognition:GetFaceSearch'
                  - 'rekognition:GetLabelDetection'
                  - 'rekognition:GetPersonTracking'
                  - 'rekognition:GetSegmentDetection'
                  - 'rekognition:GetTextDetection'
                  - 'rekognition:Recognize*'
                  - 'rekognition:Search*'
                  - 'textract:Analyze*'
                  - 'textract:Detect*'
                  - 'textract:GetDocument*'
                  - 'textract:GetExpense*'
                  - 'textract:GetLending*'
                  - 'transcribe:Get*'
                  # Source code and filesystem contents
                  - 'codecommit:GetBlob'
                  - 'codecommit:GetFile'
                  - 'codecommit:GetFolder'
                  - 'ecr:BatchGetImage'
                  - 'ecr:GetDownloadUrlForLayer'
                  - 'elasticfilesystem:Client*'
                  - 'lambda:GetLayerVersion*'
                  # Read-shaped actions that actually MUTATE. These are present
                  # in SecurityAudit; denying them keeps the role genuinely
                  # read-only rather than merely read-named.
                  - 'config:Deliver*'
                  - 'datapipeline:EvaluateExpression'
                  - 'iam:GenerateOrganizationsAccessReport'

Outputs:
  RoleArn:
    Description: 'ARN of the CloudArq read-only role. Paste this into CloudArq.'
    Value: !GetAtt CloudArqReadOnlyRole.Arn
  TemplateVersion:
    Description: 'Template version this stack was deployed from.'
    Value: '2026-07-25.1'
