AWSTemplateFormatVersion: 2010-09-09
Description: AIML Single-Account Security Assessment Solution

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: Assessment Options
        Parameters:
          - EmailAddress
      - Label:
          default: Advanced Options
        Parameters:
          - CodeBuildTimeout
          - GitHubRepoUrl
          - GitHubBranch

Parameters:
  GitHubRepoUrl:
    Type: String
    Description: GitHub repository URL
    Default: https://github.com/aws-samples/sample-aiml-security-assessment
    AllowedPattern: ^https:\/\/github\.com\/[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+(\.git)?$
    ConstraintDescription: Must be a valid GitHub repository URL (e.g., https://github.com/owner/repo or https://github.com/owner/repo.git)

  GitHubBranch:
    Type: String
    Description: GitHub branch to build from
    Default: main

  EmailAddress:
    Description: "Email address to receive assessment completion notifications (optional)"
    Type: String
    Default: ""
    AllowedPattern: ^$|^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$
    ConstraintDescription: Must be a valid email address or leave empty

  CodeBuildTimeout:
    Description: "Timeout for the CodeBuild job in minutes"
    Type: Number
    MinValue: 5
    MaxValue: 480
    Default: 60

Conditions:
  CreateEmailNotification: !Not [!Equals [!Ref EmailAddress, ""]]

Resources:
  # S3 bucket for assessment results
  AssessmentBucket:
    Type: AWS::S3::Bucket
    Properties:
      VersioningConfiguration:
        Status: Enabled
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: aws:kms
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true

  AssessmentBucketPolicy:
    Type: "AWS::S3::BucketPolicy"
    Properties:
      Bucket: !Ref AssessmentBucket
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Sid: AllowSSLRequestsOnly
            Principal: "*"
            Action: "s3:*"
            Effect: Deny
            Resource:
              - !Sub "${AssessmentBucket.Arn}"
              - !Sub "${AssessmentBucket.Arn}/*"
            Condition:
              Bool:
                aws:SecureTransport: false

  # Local member role for single account assessments
  MemberRole:
    Type: "AWS::IAM::Role"
    Properties:
      Path: "/service-role/"
      RoleName: AIMLSecurityMemberRole
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              AWS: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:root"
            Action:
              - "sts:AssumeRole"
            Condition:
              ArnEquals:
                aws:PrincipalArn: !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:role/service-role/CodeBuildRole
      Policies:
        - PolicyName: AIMLSecurityAssessmentPermissions
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - iam:ListRoles
                  - iam:ListUsers
                  - iam:ListAttachedRolePolicies
                  - iam:ListAttachedUserPolicies
                  - iam:ListRolePolicies
                  - iam:ListUserPolicies
                  - iam:GetRolePolicy
                  - iam:GetUserPolicy
                  - iam:GetPolicy
                  - iam:GetPolicyVersion
                  - iam:GenerateServiceLastAccessedDetails
                  - iam:GetServiceLastAccessedDetails
                  - iam:GetRole
                  - sts:GetCallerIdentity
                  # Bedrock Permissions
                  - bedrock:ListGuardrails
                  - bedrock:GetGuardrail
                  - bedrock:GetModelInvocationLoggingConfiguration
                  - bedrock:ListPrompts
                  - bedrock:GetPrompt
                  - bedrock:ListAgents
                  - bedrock:GetAgent
                  - bedrock:ListCustomModels
                  - bedrock:GetCustomModel
                  - bedrock:GetModelCustomizationJob
                  - bedrock:ListFlows
                  - bedrock:GetFlow
                  # Bedrock Agent Permissions (Knowledge Bases, Flows)
                  - bedrock-agent:ListKnowledgeBases
                  - bedrock-agent:GetKnowledgeBase
                  - bedrock-agent:ListFlows
                  - bedrock-agent:GetFlow
                  # SageMaker Permissions
                  - sagemaker:ListNotebookInstances
                  - sagemaker:DescribeNotebookInstance
                  - sagemaker:ListDomains
                  - sagemaker:DescribeDomain
                  - sagemaker:ListTrainingJobs
                  - sagemaker:DescribeTrainingJob
                  - sagemaker:ListModelPackageGroups
                  - sagemaker:ListModelPackages
                  - sagemaker:DescribeModelPackage
                  - sagemaker:ListFeatureGroups
                  - sagemaker:DescribeFeatureGroup
                  - sagemaker:ListPipelines
                  - sagemaker:ListPipelineExecutions
                  - sagemaker:ListProcessingJobs
                  - sagemaker:DescribeProcessingJob
                  - sagemaker:ListMonitoringSchedules
                  - sagemaker:DescribeMonitoringSchedule
                  - sagemaker:ListModels
                  - sagemaker:DescribeModel
                  - sagemaker:ListEndpoints
                  - sagemaker:DescribeEndpoint
                  - sagemaker:ListDataQualityJobDefinitions
                  - sagemaker:DescribeDataQualityJobDefinition
                  # Additional SageMaker Security Hub Controls (SM.10-15)
                  - sagemaker:ListTransformJobs
                  - sagemaker:DescribeTransformJob
                  - sagemaker:ListHyperParameterTuningJobs
                  - sagemaker:DescribeHyperParameterTuningJob
                  - sagemaker:ListCompilationJobs
                  - sagemaker:DescribeCompilationJob
                  - sagemaker:ListAutoMLJobs
                  - sagemaker:DescribeAutoMLJob
                  # Model Governance Permissions
                  - sagemaker:ListExperiments
                  - sagemaker:DescribeExperiment
                  - sagemaker:ListTrials
                  - sagemaker:DescribeTrial
                  - sagemaker:ListAssociations
                  # CloudTrail Permissions
                  - cloudtrail:ListTrails
                  - cloudtrail:GetTrail
                  - cloudtrail:GetEventSelectors
                  - cloudtrail:GetTrailStatus
                  - lambda:ListFunctions
                  - ecs:ListClusters
                  - ecs:ListTasks
                  - ecs:DescribeTasks
                  - ec2:DescribeVpcEndpoints
                  - ec2:DescribeVpcs
                  - kms:DescribeKey
                  - kms:ListAliases
                  # GuardDuty Permissions
                  - guardduty:GetFindings
                  - guardduty:ListFindings
                  - guardduty:ListDetectors
                  - guardduty:GetDetector
                  - guardduty:GetFindingsStatistics
                  - guardduty:ListPublishingDestinations
                  - guardduty:ListTagsForResource
                  - guardduty:GetMemberDetectors
                  - guardduty:DescribeMalwareScans
                  - guardduty:GetRemainingFreeTrialDays
                  - guardduty:GetUsageStatistics
                  # Bedrock AgentCore Permissions
                  - bedrock-agentcore:ListAgentRuntimes
                  - bedrock-agentcore:GetAgentRuntime
                  - bedrock-agentcore:ListMemories
                  - bedrock-agentcore:GetMemory
                  - bedrock-agentcore:ListGateways
                  - bedrock-agentcore:GetGateway
                  - bedrock-agentcore:ListPolicyEngines
                  - bedrock-agentcore:GetPolicyEngine
                  - bedrock-agentcore:GetAgentRuntimeResourcePolicy
                  - bedrock-agentcore:GetGatewayResourcePolicy
                  # ECR Permissions for AgentCore encryption checks
                  - ecr:DescribeRepositories
                  - ecr:GetRepositoryPolicy
                  # CloudWatch Logs Permissions for AgentCore observability checks
                  - logs:DescribeLogGroups
                  - logs:DescribeLogStreams
                  # Additional EC2 Permissions for AgentCore VPC checks
                  - ec2:DescribeSubnets
                  - ec2:DescribeRouteTables
                  - ec2:DescribeNatGateways
                  # S3 Permissions for encryption checks
                  - s3:GetBucketEncryption
                  - s3:ListBucket
                  - s3:GetBucketVersioning
                  - s3:GetBucketTagging
                  - s3:HeadBucket
                Resource: "*"

  # CodeBuild role for running assessments
  CodeBuildRole:
    Type: AWS::IAM::Role
    Properties:
      Path: "/service-role/"
      RoleName: CodeBuildRole
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Action: "sts:AssumeRole"
            Effect: Allow
            Principal:
              Service:
                - codebuild.amazonaws.com
      Policies:
        - PolicyName: LogGroup
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Effect: Allow
                Resource: !Sub "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/*"
        - PolicyName: AssumeRole
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Action:
                  - sts:AssumeRole
                Effect: Allow
                Resource: !GetAtt MemberRole.Arn
        - PolicyName: CloudFormationPermissions
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Action:
                  - cloudformation:CreateStack
                  - cloudformation:UpdateStack
                  - cloudformation:DeleteStack
                  - cloudformation:DescribeStacks
                  - cloudformation:DescribeStackEvents
                  - cloudformation:DescribeStackResources
                  - cloudformation:GetTemplate
                  - cloudformation:GetTemplateSummary
                  - cloudformation:ListStackResources
                  - cloudformation:CreateChangeSet
                  - cloudformation:DescribeChangeSet
                  - cloudformation:ExecuteChangeSet
                  - cloudformation:DeleteChangeSet
                  - cloudformation:ListStacks
                Effect: Allow
                Resource:
                  - !Sub "arn:${AWS::Partition}:cloudformation:*:${AWS::AccountId}:stack/aiml-security-*/*"
                  - !Sub "arn:${AWS::Partition}:cloudformation:*:${AWS::AccountId}:stack/aiml-sec-*/*"
                  - !Sub "arn:${AWS::Partition}:cloudformation:*:${AWS::AccountId}:stack/aws-sam-cli-managed-default/*"
              - Action:
                  - cloudformation:GetTemplateSummary
                  - cloudformation:CreateChangeSet
                Effect: Allow
                Resource:
                  - !Sub "arn:${AWS::Partition}:cloudformation:*:aws:transform/Serverless-2016-10-31"
              - Action:
                  - cloudformation:GetTemplateSummary
                  - cloudformation:ListStacks
                Effect: Allow
                Resource: "*"
        - PolicyName: SAMDeploymentPermissions
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Sid: LambdaPermissions
                Action:
                  - lambda:CreateFunction
                  - lambda:DeleteFunction
                  - lambda:GetFunction
                  - lambda:GetFunctionConfiguration
                  - lambda:UpdateFunctionCode
                  - lambda:UpdateFunctionConfiguration
                  - lambda:AddPermission
                  - lambda:RemovePermission
                  - lambda:TagResource
                  - lambda:UntagResource
                  - lambda:ListTags
                  - lambda:InvokeFunction
                  - lambda:PublishVersion
                  - lambda:GetPolicy
                Effect: Allow
                Resource: "*"
              - Sid: IAMPermissions
                Action:
                  - iam:CreateRole
                  - iam:DeleteRole
                  - iam:GetRole
                  - iam:AttachRolePolicy
                  - iam:DetachRolePolicy
                  - iam:PutRolePolicy
                  - iam:DeleteRolePolicy
                  - iam:GetRolePolicy
                  - iam:TagRole
                  - iam:UntagRole
                  - iam:ListRolePolicies
                  - iam:ListAttachedRolePolicies
                Effect: Allow
                Resource: '*'
              - Sid: IAMPassRole
                Action:
                  - iam:PassRole
                Effect: Allow
                Resource:
                  - !Sub 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/aiml-security-*'
                  - !Sub 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/aiml-sec-*'
              - Sid: StepFunctionsPermissions
                Action:
                  - states:CreateStateMachine
                  - states:DeleteStateMachine
                  - states:DescribeStateMachine
                  - states:UpdateStateMachine
                  - states:TagResource
                  - states:UntagResource
                  - states:ListStateMachines
                  - states:StartExecution
                  - states:DescribeExecution
                Effect: Allow
                Resource: '*'
              - Sid: S3Permissions
                Action:
                  - s3:CreateBucket
                  - s3:DeleteBucket
                  - s3:PutBucketPolicy
                  - s3:GetBucketPolicy
                  - s3:DeleteBucketPolicy
                  - s3:PutBucketVersioning
                  - s3:GetBucketVersioning
                  - s3:PutBucketPublicAccessBlock
                  - s3:GetBucketPublicAccessBlock
                  - s3:PutBucketTagging
                  - s3:GetBucketTagging
                  - s3:PutEncryptionConfiguration
                  - s3:GetEncryptionConfiguration
                  - s3:GetBucketLocation
                  - s3:ListBucket
                  - s3:PutObject
                  - s3:GetObject
                  - s3:DeleteObject
                Effect: Allow
                Resource: '*'
        - PolicyName: UploadtoS3
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Action:
                  - s3:PutObject
                  - s3:GetObject
                Effect: Allow
                Resource: !Sub "${AssessmentBucket.Arn}/*"
              - Action:
                  - s3:GetObject
                  - s3:ListBucket
                Effect: Allow
                Resource:
                  - !Sub "arn:${AWS::Partition}:s3:::aiml-security-*"
                  - !Sub "arn:${AWS::Partition}:s3:::aiml-security-*/*"

  # CodeBuild project for running assessments
  CodeBuild:
    Type: AWS::CodeBuild::Project
    Properties:
      Name: AIMLSecurityCodeBuild
      Environment:
        ComputeType: BUILD_GENERAL1_SMALL
        Image: "aws/codebuild/amazonlinux2-x86_64-standard:4.0"
        Type: "LINUX_CONTAINER"
        EnvironmentVariables:
          - Name: BUCKET_REPORT
            Value: !Ref AssessmentBucket
            Type: PLAINTEXT
          - Name: GITHUB_REPO_URL
            Value: !Ref GitHubRepoUrl
            Type: PLAINTEXT
          - Name: GITHUB_BRANCH
            Value: !Ref GitHubBranch
            Type: PLAINTEXT
          - Name: MEMBER_ROLE
            Value: AIMLSecurityMemberRole
            Type: PLAINTEXT
          - Name: MULTI_ACCOUNT_SCAN
            Value: "false"
            Type: PLAINTEXT
          - Name: AWS_PARTITION
            Value: !Sub ${AWS::Partition}
            Type: PLAINTEXT
          - Name: AWS_ACCOUNT_ID
            Value: !Sub ${AWS::AccountId}
            Type: PLAINTEXT
      Description: Run AIML Security single-account assessment
      ServiceRole: !GetAtt CodeBuildRole.Arn
      TimeoutInMinutes: !Ref CodeBuildTimeout
      Artifacts:
        Type: NO_ARTIFACTS
      SourceVersion: !Ref GitHubBranch
      Source:
        Type: GITHUB
        Location: !Ref GitHubRepoUrl
        GitCloneDepth: 1

  # Lambda function to start CodeBuild
  CodeBuildStartBuildLambdaRole:
    Type: "AWS::IAM::Role"
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action:
              - "sts:AssumeRole"
      Path: /
      ManagedPolicyArns:
        - !Sub arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: StartBuildInline
          PolicyDocument:
            Statement:
              - Effect: Allow
                Action:
                  - "codebuild:StartBuild"
                Resource: !GetAtt CodeBuild.Arn

  CodeBuildStartBuildLambda:
    Type: AWS::Lambda::Function
    Properties:
      Handler: index.lambda_handler
      Role: !GetAtt CodeBuildStartBuildLambdaRole.Arn
      Timeout: 120
      Runtime: python3.12
      Code:
        ZipFile: |
          import boto3
          import logging
          from botocore.exceptions import ClientError
          import cfnresponse

          logger = logging.getLogger()
          logger.setLevel(logging.INFO)

          def startBuild(event, context):
            responseData = {}
            try:
              project_name = event.get('ResourceProperties', {}).get('ProjectName')
              if not project_name:
                raise ValueError("ProjectName is required")

              cb = boto3.client('codebuild')
              logger.info(f"Starting build for project: {project_name}")
              response = cb.start_build(projectName=project_name)
              responseData['buildID'] = response['build']['id']
              logger.info(f"Build started: {responseData['buildID']}")
              cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData)
            except ClientError as e:
              error_msg = e.response['Error']['Message']
              logger.error(f"AWS error: {error_msg}")
              cfnresponse.send(event, context, cfnresponse.FAILED, {'Error': error_msg})
            except Exception as e:
              logger.error(f"Error: {str(e)}")
              cfnresponse.send(event, context, cfnresponse.FAILED, {'Error': str(e)})

          def no_op(event, context):
            cfnresponse.send(event, context, cfnresponse.SUCCESS, {})

          def lambda_handler(event, context):
            logger.info(f"Request type: {event.get('RequestType')}")
            try:
              if event['RequestType'] == 'Create':
                startBuild(event, context)
              else:
                no_op(event, context)
            except Exception as e:
              logger.error(f"Unhandled error: {str(e)}")
              cfnresponse.send(event, context, cfnresponse.FAILED, {'Error': str(e)})

  CodeBuildStartBuild:
    Type: Custom::CodeBuildStartBuild
    DependsOn: AssessmentBucketPolicy
    Properties:
      ServiceToken: !GetAtt CodeBuildStartBuildLambda.Arn
      ProjectName: !Ref CodeBuild

  # SNS topic for notifications (optional)
  NotificationTopic:
    Condition: CreateEmailNotification
    Type: AWS::SNS::Topic
    Properties:
      Subscription:
        - Endpoint: !Ref EmailAddress
          Protocol: email

  SnsNotificationsPolicy:
    Condition: CreateEmailNotification
    Type: AWS::SNS::TopicPolicy
    Properties:
      PolicyDocument:
        Id: snsNotificationsPolicy
        Version: "2012-10-17"
        Statement:
          - Sid: Allow events
            Effect: Allow
            Principal:
              Service: "events.amazonaws.com"
            Action:
              - sns:Publish
            Resource: !Ref NotificationTopic
      Topics:
        - !Ref NotificationTopic

  CodeBuildStateChangeRule:
    Condition: CreateEmailNotification
    Type: AWS::Events::Rule
    Properties:
      EventBusName: default
      EventPattern:
        source:
          - aws.codebuild
        detail-type:
          - CodeBuild Build State Change
        detail:
          build-status:
            - SUCCEEDED
            - FAILED
            - STOPPED
          project-name:
            - !Ref CodeBuild
      State: ENABLED
      Targets:
        - Id: toSNS
          Arn: !Ref NotificationTopic
          InputTransformer:
            InputPathsMap:
              build-status: $.detail.build-status
              bucket: $.detail.additional-information.environment.environment-variables[0].value
            InputTemplate: >-
              "The AIML Security assessment has <build-status>."

              "You can view your results in the <bucket> bucket."

Outputs:
  CodeBuildProjectName:
    Description: CodeBuild project name for running assessments
    Value: !Ref CodeBuild

  AssessmentBucket:
    Description: S3 bucket containing assessment results (HTML reports and CSV data)
    Value: !Ref AssessmentBucket
