AWSTemplateFormatVersion: '2010-09-09'

# FinShift — Cloud Intelligence read-only user
#
# Creates ONE IAM user that FinShift authenticates as to enumerate your resources. It grants
# Describe, List and Get and NOTHING ELSE: there is no action in this template that can create,
# modify, tag or delete anything in your account, and FinShift has no code path that could use one if
# there were.
#
# The permissions map one-to-one onto the checks the product performs, so you can read this policy
# and know exactly what it will look at. If a statement here has no corresponding check, that is a
# bug in this template and not a permission FinShift wants.
#
# A USER RATHER THAN A ROLE, and the reason is worth knowing: assuming a role requires the caller to
# already hold an AWS identity, and FinShift has none — it runs outside AWS entirely. A trust policy
# would have to name an account that does not exist.
#
# THIS TEMPLATE DOES NOT CREATE AN ACCESS KEY, deliberately. CloudFormation outputs are not secret:
# anyone who can call describe-stacks in this account could read it for the life of the stack. After
# deploying, open the IAM console, create an access key for the user below, and send FinShift the key
# id and secret — the secret is shown once.
#
# WHAT THIS DOES NOT GRANT, deliberately:
#   - No billing access. FinShift prices findings from a published rate table, not from your bill.
#     Cost Explorer and Cost and Usage Reports are a separate decision you have not been asked for.
#   - No read of object CONTENT. s3:ListBucket and s3:ListMultipartUploadParts enumerate; there is
#     no s3:GetObject.
#   - No read of log CONTENT. logs:DescribeLogGroups returns names, sizes and retention settings;
#     there is no logs:GetLogEvents or logs:FilterLogEvents.
#   - No secrets, no KMS, no IAM read.

Description: >-
  FinShift read-only inventory user for Cloud Intelligence. Describe/List/Get only —
  no write, no billing, and no object or log content.

Parameters:
  UserName:
    Type: String
    Default: finshift-readonly
    Description: The name of the IAM user to create. Change it only if that name is already taken.

Resources:
  FinShiftReadOnlyUser:
    Type: AWS::IAM::User
    Properties:
      UserName: !Ref UserName
      Policies:
        - PolicyName: FinShiftInventoryRead
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              # EC2 and EBS. DescribeRegions is what tells FinShift where to look at all; the rest
              # back the unattached-volume, idle-address, orphaned-snapshot, unused-image,
              # stopped-instance, gp2 and previous-generation checks.
              - Sid: Ec2Inventory
                Effect: Allow
                Action:
                  - 'ec2:DescribeRegions'
                  - 'ec2:DescribeVolumes'
                  - 'ec2:DescribeAddresses'
                  - 'ec2:DescribeSnapshots'
                  - 'ec2:DescribeInstances'
                  - 'ec2:DescribeImages'
                Resource: '*'

              # Load balancers with no registered targets.
              - Sid: LoadBalancerInventory
                Effect: Allow
                Action:
                  - 'elasticloadbalancing:DescribeLoadBalancers'
                  - 'elasticloadbalancing:DescribeTargetGroups'
                  - 'elasticloadbalancing:DescribeTargetHealth'
                  - 'elasticloadbalancing:DescribeTags'
                Resource: '*'

              # EKS clusters with neither nodegroups nor Fargate profiles. Both are listed: a
              # cluster running everything on Fargate has zero nodegroups and is not empty.
              - Sid: EksInventory
                Effect: Allow
                Action:
                  - 'eks:ListClusters'
                  - 'eks:DescribeCluster'
                  - 'eks:ListNodegroups'
                  - 'eks:ListFargateProfiles'
                Resource: '*'

              # Manual RDS snapshots whose source instance no longer exists. DescribeDBInstances is
              # what establishes "no longer exists" — without it every manual snapshot would look
              # orphaned.
              - Sid: RdsInventory
                Effect: Allow
                Action:
                  - 'rds:DescribeDBSnapshots'
                  - 'rds:DescribeDBInstances'
                Resource: '*'

              # Incomplete multipart uploads. ENUMERATION ONLY — there is no s3:GetObject here, so
              # FinShift can see that abandoned upload parts exist and how large they are, and
              # cannot read the contents of anything.
              - Sid: S3UploadInventory
                Effect: Allow
                Action:
                  - 's3:ListAllMyBuckets'
                  - 's3:GetBucketLocation'
                  - 's3:ListBucketMultipartUploads'
                  - 's3:ListMultipartUploadParts'
                Resource: '*'

              # Log groups with no retention policy. METADATA ONLY — names, stored bytes, retention
              # and tags. There is no logs:GetLogEvents and no logs:FilterLogEvents, so FinShift
              # cannot read a single line of your logs.
              - Sid: LogGroupInventory
                Effect: Allow
                Action:
                  - 'logs:DescribeLogGroups'
                  - 'logs:ListTagsForResource'
                Resource: '*'

Outputs:
  UserArn:
    Description: >-
      The user FinShift will authenticate as. Not a secret — the access key is. Create one for this
      user in the IAM console and send FinShift the key id and the secret.
    Value: !GetAtt FinShiftReadOnlyUser.Arn

  AccountId:
    Description: >-
      Send this to FinShift with the key. It records which account the credential belongs to and
      REFUSES to collect if the key authenticates into a different one — so a key stored against the
      wrong customer is caught rather than collected.
    Value: !Ref 'AWS::AccountId'

  ExcludeATag:
    Description: >-
      How to take a resource off the table. Tag it in AWS and FinShift drops it from findings on the
      next run, with your reason shown beside it. FinShift reads the exclusion; it does not own it.
    Value: 'finshift:keep = <your reason>'
