AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::LanguageExtensions
Description: 'Cloud-Operator Lambda Scanning Infrastructure - Service Account Resources'

Metadata:
  QualysTemplateVersion: 1.0.0-0

Parameters:
  ServiceAccountId:
    Type: String
    Description: AWS Account ID where this stack is deployed (service account). Leave empty to auto-detect.
    Default: ''
  
  CloudOperatorImageUri:
    Type: String
    Description: ECR image URI for Lambda orchestrator (e.g., 123456789012.dkr.ecr.us-east-1.amazonaws.com/cloud-operator:latest)
  
  TargetAccountIds:
    Type: CommaDelimitedList
    Description: Comma-separated list of target AWS account IDs allowed to send events to the service account SQS queue
    Default: ''
  
  SecondaryRegions:
    Type: CommaDelimitedList
    Description: "Additional regions to deploy EventBridge rules for cross-region Lambda detection (e.g., sa-east-1,eu-west-1). WARNING: Must NOT include the stack deploy region - doing so will cause a deployment error."
    Default: ''
  
  QualysAccessToken:
    Type: String
    Description: Qualys access token for qscanner (reads from QUALYS_ACCESS_TOKEN env var)
    NoEcho: true
    Default: ''

  QualysClientId:
    Type: String
    Description: Optional OAuth client ID for qscanner (reads from QUALYS_CLIENT_ID env var)
    Default: ''

  QualysClientSecret:
    Type: String
    Description: Optional OAuth client secret for qscanner (reads from QUALYS_CLIENT_SECRET env var)
    Default: ''
    NoEcho: true

  ScanTypes:
    Type: String
    Description: Optional comma-separated scan types passed to qscanner (leave empty to use qscanner default)
    Default: 'pkg,fileinsight,aiinsight'

  Proxy:
    Type: String
    Description: Optional HTTP/HTTPS proxy URL (e.g., http://proxy.example.com:3128)
    Default: ''

  CACertificate:
    Type: String
    Description: Optional base64-encoded CA certificate
    Default: ''
    NoEcho: true

  LogLevel:
    Type: String
    Description: Log level for Lambda functions (debug, info, warn, error, fatal)
    Default: 'info'
    AllowedValues: ['debug', 'info', 'warn', 'error', 'fatal']

  GatewayURL:
    Type: String
    Description: Qualys gateway URL passed to qscanner via --gateway-url (e.g., https://gateway.qg1.apps.qualys.com)
  
  CloudOperatorMemorySize:
    Type: Number
    Description: Memory size for orchestrator Lambda (MB)
    Default: 2048
    MinValue: 128
    MaxValue: 10240
  
  CloudOperatorTimeout:
    Type: Number
    Description: Timeout for orchestrator Lambda (seconds)
    Default: 900
    MinValue: 30
    MaxValue: 900
  
  LambdaSQSBatchSize:
    Type: Number
    Description: Number of SQS messages to process in batch for Lambda workload queue
    Default: 1
    MinValue: 1
    MaxValue: 10

  CloudOperatorLambdaArchitecture:
    Type: String
    Description: Architecture for Lambda function (x86_64 or arm64)
    Default: x86_64
    AllowedValues: ['x86_64', 'arm64']

  CloudOperatorEphemeralStorageSize:
    Type: Number
    Description: Ephemeral storage size for orchestrator Lambda in MB (/tmp directory, min 512, max 10240)
    Default: 5120
    MinValue: 512
    MaxValue: 10240

  ScanServiceAccount:
    Type: String
    Description: Scan Lambda functions in the service account itself. Set to 'false' if you only want to scan target accounts.
    Default: 'true'
    AllowedValues: ['true', 'false']

  OrganizationId:
    Type: String
    Description: >
      Optional. AWS Organizations ID (e.g. o-xxxxxxxxxx). When provided, the SQS queue policy
      will allow EventBridge from any account in the organization that holds the required role,
      in addition to (or instead of) the explicit TargetAccountIds list. Used ONLY for SQS
      queue policy validation — not passed to cloud-operator.
    Default: ''

Conditions:
  UseProvidedServiceAccountId: !Not [!Equals [!Ref ServiceAccountId, '']]
  ScanServiceAccountLambdas: !Equals [!Ref ScanServiceAccount, 'true']
  HasSecondaryRegions: !Not [!Equals [!Join ['', !Ref SecondaryRegions], '']]
  HasTargetAccounts: !Not [!Equals [!Join ['', !Ref TargetAccountIds], '']]
  HasOrganizationId: !Not [!Equals [!Ref OrganizationId, '']]

Resources:
  # ========================================
  # DynamoDB Tables
  # ========================================
  
  LambdaScanJobsTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: qualys-lambda-scan-jobs
      BillingMode: PAY_PER_REQUEST
      AttributeDefinitions:
        - AttributeName: function_arn
          AttributeType: S
      KeySchema:
        - AttributeName: function_arn
          KeyType: HASH
      TimeToLiveSpecification:
        Enabled: true
        AttributeName: ttl
      PointInTimeRecoverySpecification:
        PointInTimeRecoveryEnabled: true
      SSESpecification:
        SSEEnabled: true
      Tags:
        - Key: Name
          Value: lambda-scan-jobs
        - Key: ManagedBy
          Value: CloudFormation

  OnboardedAccountsTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: qualys-onboarded-accounts
      BillingMode: PAY_PER_REQUEST
      AttributeDefinitions:
        - AttributeName: account_id
          AttributeType: S
      KeySchema:
        - AttributeName: account_id
          KeyType: HASH
      PointInTimeRecoverySpecification:
        PointInTimeRecoveryEnabled: true
      SSESpecification:
        SSEEnabled: true
      Tags:
        - Key: Name
          Value: onboarded-accounts
        - Key: ManagedBy
          Value: CloudFormation

  # ========================================
  # SQS Queues
  # ========================================

  OnboardingEventsQueue:
    Type: AWS::SQS::Queue
    Properties:
      QueueName: qualys-onboarding-events
      VisibilityTimeout: 1800  # 2x max Lambda timeout (900s) for safety
      MessageRetentionPeriod: 1209600  # 14 days
      ReceiveMessageWaitTimeSeconds: 20  # Long polling
      SqsManagedSseEnabled: true
      RedrivePolicy:
        deadLetterTargetArn: !GetAtt OnboardingEventsDLQ.Arn
        maxReceiveCount: 5

  OnboardingEventsDLQ:
    Type: AWS::SQS::Queue
    Properties:
      QueueName: qualys-onboarding-events-dlq
      MessageRetentionPeriod: 1209600  # 14 days
      SqsManagedSseEnabled: true

  OnboardingEventsQueuePolicy:
    Type: AWS::SQS::QueuePolicy
    Properties:
      Queues:
        - !Ref OnboardingEventsQueue
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - !If
            - HasTargetAccounts
            - Sid: AllowOnboardingNotifierFromTargetAccounts
              Effect: Allow
              Principal:
                AWS: '*'
              Action:
                - sqs:SendMessage
              Resource: !GetAtt OnboardingEventsQueue.Arn
              Condition:
                StringLike:
                  aws:PrincipalArn:
                    - !Sub 'arn:${AWS::Partition}:iam::*:role/qualys-cloud-scanning-notifier-role'
                ForAnyValue:StringEquals:
                  aws:PrincipalAccount: !Ref TargetAccountIds
            - !Ref AWS::NoValue
          - !If
            - HasOrganizationId
            - Sid: AllowOnboardingNotifierFromOrganization
              Effect: Allow
              Principal:
                AWS: '*'
              Action:
                - sqs:SendMessage
              Resource: !GetAtt OnboardingEventsQueue.Arn
              Condition:
                StringLike:
                  aws:PrincipalArn:
                    - !Sub 'arn:${AWS::Partition}:iam::*:role/qualys-cloud-scanning-notifier-role'
                StringEquals:
                  aws:PrincipalOrgID: !Ref OrganizationId
            - !Ref AWS::NoValue

  LambdaEventsQueue:
    Type: AWS::SQS::Queue
    Properties:
      QueueName: qualys-lambda-events
      VisibilityTimeout: 1800  # 2x max Lambda timeout (900s) for safety
      MessageRetentionPeriod: 1209600  # 14 days
      ReceiveMessageWaitTimeSeconds: 20  # Long polling
      SqsManagedSseEnabled: true
      RedrivePolicy:
        deadLetterTargetArn: !GetAtt LambdaEventsDLQ.Arn
        maxReceiveCount: 10

  LambdaEventsDLQ:
    Type: AWS::SQS::Queue
    Properties:
      QueueName: qualys-lambda-events-dlq
      MessageRetentionPeriod: 1209600  # 14 days
      SqsManagedSseEnabled: true

  LambdaEventsQueuePolicy:
    Type: AWS::SQS::QueuePolicy
    Properties:
      Queues:
        - !Ref LambdaEventsQueue
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Sid: AllowEventBridgeFromServiceAccount
            Effect: Allow
            Principal:
              Service: events.amazonaws.com
            Action:
            - sqs:SendMessage
            Resource: !GetAtt LambdaEventsQueue.Arn
            Condition:
              StringEquals:
                aws:SourceAccount: !Ref AWS::AccountId
              ArnLike:
                aws:SourceArn: !Sub 'arn:${AWS::Partition}:events:*:${AWS::AccountId}:rule/qualys-lambda-function-events*'
          # Allow cross-account EventBridge/notifier from an explicit account list (optional).
          # When TargetAccountIds is provided, only those accounts' roles can send to SQS.
          - !If
            - HasTargetAccounts
            - Sid: AllowEventBridgeFromTargetAccounts
              Effect: Allow
              Principal:
                AWS: '*'
              Action:
                - sqs:SendMessage
              Resource: !GetAtt LambdaEventsQueue.Arn
              Condition:
                StringLike:
                  aws:PrincipalArn:
                    - !Sub 'arn:${AWS::Partition}:iam::*:role/qualys-cloud-scanning-eventbridge-to-sqs-role'
                ForAnyValue:StringEquals:
                  aws:PrincipalAccount: !Ref TargetAccountIds
            - !Ref AWS::NoValue
          # Allow cross-account EventBridge from any account in the AWS Organization (optional).
          # When OrganizationId is provided, any org account holding the required role can send to SQS.
          - !If
            - HasOrganizationId
            - Sid: AllowEventBridgeFromOrganization
              Effect: Allow
              Principal:
                AWS: '*'
              Action:
                - sqs:SendMessage
              Resource: !GetAtt LambdaEventsQueue.Arn
              Condition:
                StringLike:
                  aws:PrincipalArn:
                    - !Sub 'arn:${AWS::Partition}:iam::*:role/qualys-cloud-scanning-eventbridge-to-sqs-role'
                StringEquals:
                  aws:PrincipalOrgID: !Ref OrganizationId
            - !Ref AWS::NoValue
  # ========================================
  # EventBridge Rule
  # ========================================
  # Note: This rule uses AWS default CloudTrail management events
  # No explicit CloudTrail resource or S3 bucket is required

  CrossRegionEventBusPolicy:
    Type: AWS::Events::EventBusPolicy
    Condition: HasSecondaryRegions
    Properties:
      StatementId: AllowCrossRegionFromSameAccount
      Action: events:PutEvents
      Principal: !Ref AWS::AccountId

  'Fn::ForEach::TargetAccountEventBusPolicies':
    - AccountId
    - !If [HasTargetAccounts, !Ref TargetAccountIds, []]
    - 'TargetAccountEventBusPolicy${AccountId}':
        Type: AWS::Events::EventBusPolicy
        Properties:
          StatementId: !Sub 'AllowTargetAccount${AccountId}PutEvents'
          Action: events:PutEvents
          Principal: !Ref AccountId

  OrganizationEventBusPolicy:
    Type: AWS::Events::EventBusPolicy
    Condition: HasOrganizationId
    Properties:
      StatementId: AllowOrganizationPutEvents
      Action: events:PutEvents
      Principal: '*'
      Condition:
        Type: StringEquals
        Key: aws:PrincipalOrgID
        Value: !Ref OrganizationId

  CrossRegionForwardingRule:
    Type: AWS::Events::Rule
    Condition: HasSecondaryRegions
    Properties:
      Name: qualys-lambda-function-events-cross-region
      Description: Receive Lambda events forwarded from secondary regions via event bus and route to SQS
      State: ENABLED
      EventPattern:
        source:
          - aws.lambda
        detail-type:
          - AWS API Call via CloudTrail
        detail:
          eventSource:
            - lambda.amazonaws.com
          eventName:
            - CreateFunction20150331
            - UpdateFunctionCode20150331v2
            - DeleteFunction20150331
          errorCode:
            - exists: false
      Targets:
        - Arn: !GetAtt LambdaEventsQueue.Arn
          Id: LambdaEventsQueueCrossRegion

  LambdaFunctionEventsRule:
    Type: AWS::Events::Rule
    Properties:
      Name: qualys-lambda-function-events
      Description: Capture Lambda CreateFunction, UpdateFunctionCode, and DeleteFunction events
      State: !If [ScanServiceAccountLambdas, ENABLED, DISABLED]
      EventPattern:
        source:
          - aws.lambda
        detail-type:
          - AWS API Call via CloudTrail
        detail:
          eventName:
            - CreateFunction20150331
            - UpdateFunctionCode20150331v2
            - DeleteFunction20150331
      Targets:
        - Arn: !GetAtt LambdaEventsQueue.Arn
          Id: LambdaEventsQueue
          RoleArn: !GetAtt EventBridgeToSQSRole.Arn

  # ========================================
  # IAM Roles
  # ========================================
  
  EventBridgeToSQSRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: events.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: EventBridgeToSQSPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - sqs:SendMessage
                Resource: !GetAtt LambdaEventsQueue.Arn
  
  OrchestratorExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: QualysCloudOperatorOrchestratorRole
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: CloudWatchLogsPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource: !GetAtt CloudOperatorLogGroup.Arn
        - PolicyName: OrchestratorPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - sts:AssumeRole
                Resource: 'arn:aws:iam::*:role/QualysCloudOperatorRole'
              - Effect: Allow
                Action:
                  - sqs:ReceiveMessage
                  - sqs:DeleteMessage
                  - sqs:GetQueueAttributes
                  - sqs:SendMessage
                Resource:
                  - !GetAtt LambdaEventsQueue.Arn
                  - !GetAtt OnboardingEventsQueue.Arn
              - Effect: Allow
                Action:
                  - dynamodb:GetItem
                  - dynamodb:PutItem
                  - dynamodb:UpdateItem
                  - dynamodb:DeleteItem
                  - dynamodb:Query
                  - dynamodb:Scan
                Resource:
                  - !GetAtt LambdaScanJobsTable.Arn
                  - !GetAtt OnboardedAccountsTable.Arn
              - Effect: Allow
                Action:
                  - lambda:GetFunction
                  - lambda:GetFunctionConfiguration
                  - lambda:ListFunctions
                Resource: '*'
                Sid: ScanLambdaFunctions
              - Effect: Allow
                Action:
                  - lambda:InvokeFunction
                Resource: !Sub 'arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:qualys-cloud-operator'
                Sid: InvokeSelfForInitialScan
              - Effect: Allow
                Action:
                  - ecr:GetAuthorizationToken
                  - ecr:BatchCheckLayerAvailability
                  - ecr:GetDownloadUrlForLayer
                  - ecr:BatchGetImage
                Resource: '*'
                Sid: ECRImagePull
      Tags:
        - Key: Name
          Value: QualysCloudOperatorOrchestratorRole
        - Key: ManagedBy
          Value: CloudFormation

  # ========================================
  # Lambda Functions
  # ========================================
  
  CloudOperator:
    Type: AWS::Lambda::Function
    DependsOn: CloudOperatorLogGroup
    Properties:
      FunctionName: qualys-cloud-operator
      PackageType: Image
      Code:
        ImageUri: !Ref CloudOperatorImageUri
      Role: !GetAtt OrchestratorExecutionRole.Arn
      Architectures:
        - !Ref CloudOperatorLambdaArchitecture
      Timeout: !Ref CloudOperatorTimeout
      MemorySize: !Ref CloudOperatorMemorySize
      EphemeralStorage:
        Size: !Ref CloudOperatorEphemeralStorageSize
      Environment:
        Variables:
          LAMBDA_SCAN_JOBS_TABLE: !Ref LambdaScanJobsTable
          ONBOARDED_ACCOUNTS_TABLE: !Ref OnboardedAccountsTable
          LAMBDA_SQS_QUEUE_URL: !Ref LambdaEventsQueue
          ROLE_NAME: QualysCloudOperatorRole
          SERVICE_ACCOUNT_ID: !If [UseProvidedServiceAccountId, !Ref ServiceAccountId, !Ref 'AWS::AccountId']
          REGIONS: !If
            - HasSecondaryRegions
            - !Sub
              - '${DeployRegion},${Secondary}'
              - DeployRegion: !Ref 'AWS::Region'
                Secondary: !Join [',', !Ref SecondaryRegions]
            - !Ref 'AWS::Region'
          SCAN_SERVICE_ACCOUNT: !Ref ScanServiceAccount
          QUALYS_ACCESS_TOKEN: !Ref QualysAccessToken
          QUALYS_CLIENT_ID: !Ref QualysClientId
          QUALYS_CLIENT_SECRET: !Ref QualysClientSecret
          SCAN_TYPES: !Ref ScanTypes
          GATEWAY_URL: !Ref GatewayURL
          PROXY: !Ref Proxy
          CA_CERTIFICATE: !Ref CACertificate
          LOG_LEVEL: !Ref LogLevel
      Tags:
        - Key: Name
          Value: qualys-cloud-operator
        - Key: ManagedBy
          Value: CloudFormation

  CloudOperatorEventSourceMapping:
    Type: AWS::Lambda::EventSourceMapping
    Properties:
      FunctionName: !Ref CloudOperator
      EventSourceArn: !GetAtt LambdaEventsQueue.Arn
      BatchSize: !Ref LambdaSQSBatchSize
      MaximumBatchingWindowInSeconds: 5
      FunctionResponseTypes:
        - ReportBatchItemFailures

  CloudOperatorOnboardingEventSourceMapping:
    Type: AWS::Lambda::EventSourceMapping
    Properties:
      FunctionName: !Ref CloudOperator
      EventSourceArn: !GetAtt OnboardingEventsQueue.Arn
      BatchSize: 1
      MaximumBatchingWindowInSeconds: 0
      FunctionResponseTypes:
        - ReportBatchItemFailures

  CloudOperatorLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: '/aws/lambda/qualys-cloud-operator'
      RetentionInDays: 7

  # ========================================
  # Initial Scan Trigger (Custom Resource)
  # ========================================
  
  InitialScanTrigger:
    Type: Custom::InitialScan
    DeletionPolicy: Retain  # Prevent stuck deletions - Custom Resource doesn't need cleanup
    DependsOn:
      - CloudOperator
      - OnboardedAccountsTable
      - LambdaEventsQueue
    Properties:
      ServiceToken: !GetAtt CloudOperator.Arn
      CloudOperatorFunctionName: !Ref CloudOperator
      TriggerInitialScan: true
      ScanServiceAccount: !Ref ScanServiceAccount
      # Regions is passed so the custom resource can detect region changes on stack Update
      # (e.g. when SecondaryRegions is modified) and trigger a fresh initial scan for new regions.
      # Uses the full region list (deploy region + secondary) to match the REGIONS env var.
      Regions: !If
        - HasSecondaryRegions
        - !Sub
          - '${DeployRegion},${Secondary}'
          - DeployRegion: !Ref 'AWS::Region'
            Secondary: !Join [',', !Ref SecondaryRegions]
        - !Ref 'AWS::Region'
      Timestamp: !Ref 'AWS::StackName'

  # ========================================
  # CloudWatch Alarms
  # ========================================
  
  OrchestratorErrorAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmName: qualys-cloud-operator-errors
      AlarmDescription: Alert when orchestrator Lambda has errors
      MetricName: Errors
      Namespace: AWS/Lambda
      Statistic: Sum
      Period: 300
      EvaluationPeriods: 1
      Threshold: 5
      ComparisonOperator: GreaterThanThreshold
      Dimensions:
        - Name: FunctionName
          Value: !Ref CloudOperator
      TreatMissingData: notBreaching

  SQSQueueDepthAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmName: qualys-lambda-events-queue-depth
      AlarmDescription: Alert when SQS queue has too many messages
      MetricName: ApproximateNumberOfMessagesVisible
      Namespace: AWS/SQS
      Statistic: Average
      Period: 300
      EvaluationPeriods: 2
      Threshold: 100
      ComparisonOperator: GreaterThanThreshold
      Dimensions:
        - Name: QueueName
          Value: !GetAtt LambdaEventsQueue.QueueName
      TreatMissingData: notBreaching

  DLQMessagesAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmName: qualys-lambda-events-dlq-messages
      AlarmDescription: Alert when messages appear in DLQ
      MetricName: ApproximateNumberOfMessagesVisible
      Namespace: AWS/SQS
      Statistic: Average
      Period: 60
      EvaluationPeriods: 1
      Threshold: 1
      ComparisonOperator: GreaterThanOrEqualToThreshold
      Dimensions:
        - Name: QueueName
          Value: !GetAtt LambdaEventsDLQ.QueueName
      TreatMissingData: notBreaching

  # ========================================
  # StackSet for Cross-Region EventBridge Rules
  # ========================================
  StackSetAdministrationRole:
    Type: AWS::IAM::Role
    Condition: HasSecondaryRegions
    Properties:
      RoleName: QualysCloudScanningStackSetAdminRole
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: cloudformation.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: AssumeStackSetExecutionRole
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - sts:AssumeRole
                Resource: !Sub 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/QualysCloudScanningStackSetExecutionRole'

  StackSetExecutionRole:
    Type: AWS::IAM::Role
    Condition: HasSecondaryRegions
    Properties:
      RoleName: QualysCloudScanningStackSetExecutionRole
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              AWS: !GetAtt StackSetAdministrationRole.Arn
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AWSCloudFormationFullAccess
      Policies:
        - PolicyName: StackSetExecutionPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - events:*
                Resource: '*'
              - Effect: Allow
                Action:
                  - iam:CreateRole
                  - iam:DeleteRole
                  - iam:GetRole
                  - iam:PutRolePolicy
                  - iam:DeleteRolePolicy
                  - iam:TagRole
                  - iam:UntagRole
                  - iam:ListRoleTags
                  - iam:PassRole
                Resource: !Sub 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/StackSet-*'

  SecondaryRegionEventBridgeStackSet:
    Type: AWS::CloudFormation::StackSet
    Condition: HasSecondaryRegions
    DependsOn:
      - StackSetAdministrationRole
      - StackSetExecutionRole
    Properties:
      StackSetName: qualys-lambda-scanning-eventbridge-secondary-regions
      Description: EventBridge rules for Lambda events in secondary regions (same-account cross-region)
      PermissionModel: SELF_MANAGED
      AdministrationRoleARN: !GetAtt StackSetAdministrationRole.Arn
      ExecutionRoleName: QualysCloudScanningStackSetExecutionRole
      Capabilities:
        - CAPABILITY_IAM
      Parameters:
        - ParameterKey: CentralEventBusArn
          ParameterValue: !Sub 'arn:${AWS::Partition}:events:${AWS::Region}:${AWS::AccountId}:event-bus/default'
      TemplateBody: |
        AWSTemplateFormatVersion: '2010-09-09'
        Description: 'EventBridge Rule for Lambda Events (Secondary Region)'

        Parameters:
          CentralEventBusArn:
            Type: String
            Description: ARN of the central event bus in the service account region

        Resources:
          EventBridgeToEventBusRole:
            Type: AWS::IAM::Role
            Properties:
              AssumeRolePolicyDocument:
                Version: '2012-10-17'
                Statement:
                  - Effect: Allow
                    Principal:
                      Service: events.amazonaws.com
                    Action: sts:AssumeRole
              Policies:
                - PolicyName: AllowPutEventsToServiceAccount
                  PolicyDocument:
                    Version: '2012-10-17'
                    Statement:
                      - Effect: Allow
                        Action:
                          - events:PutEvents
                        Resource: !Ref CentralEventBusArn

          LambdaFunctionEventsRule:
            Type: AWS::Events::Rule
            Properties:
              Name: qualys-lambda-function-events-to-service-account
              Description: !Sub 'Capture Lambda function events in ${AWS::Region} and forward to central event bus'
              State: ENABLED
              EventPattern:
                source:
                  - aws.lambda
                detail-type:
                  - AWS API Call via CloudTrail
                detail:
                  eventSource:
                    - lambda.amazonaws.com
                  eventName:
                    - CreateFunction20150331
                    - UpdateFunctionCode20150331v2
                    - DeleteFunction20150331
                  errorCode:
                    - exists: false
              Targets:
                - Arn: !Ref CentralEventBusArn
                  Id: CentralEventBusTarget
                  RoleArn: !GetAtt EventBridgeToEventBusRole.Arn

        Outputs:
          EventBridgeRuleArn:
            Description: ARN of the EventBridge rule
            Value: !GetAtt LambdaFunctionEventsRule.Arn
      StackInstancesGroup:
        - DeploymentTargets:
            Accounts:
              - !Ref AWS::AccountId
          Regions: !Ref SecondaryRegions

Outputs:
  LambdaScanJobsTableName:
    Description: DynamoDB table for Lambda scan jobs
    Value: !Ref LambdaScanJobsTable
    Export:
      Name: !Sub '${AWS::StackName}-LambdaScanJobsTable'

  OnboardedAccountsTableName:
    Description: DynamoDB table for onboarded accounts
    Value: !Ref OnboardedAccountsTable
    Export:
      Name: !Sub '${AWS::StackName}-OnboardedAccountsTable'

  LambdaEventsQueueUrl:
    Description: SQS queue URL for Lambda workload events
    Value: !Ref LambdaEventsQueue
    Export:
      Name: !Sub '${AWS::StackName}-LambdaEventsQueueUrl'

  LambdaEventsQueueArn:
    Description: SQS queue ARN for Lambda workload events
    Value: !GetAtt LambdaEventsQueue.Arn
    Export:
      Name: !Sub '${AWS::StackName}-LambdaEventsQueueArn'

  OnboardingEventsQueueUrl:
    Description: SQS queue URL for account onboarding events
    Value: !Ref OnboardingEventsQueue
    Export:
      Name: !Sub '${AWS::StackName}-OnboardingEventsQueueUrl'

  CloudOperatorArn:
    Description: Lambda orchestrator function ARN
    Value: !GetAtt CloudOperator.Arn
    Export:
      Name: !Sub '${AWS::StackName}-CloudOperatorArn'

  ServiceAccountId:
    Description: Service account ID (for external ID in cross-account roles)
    Value: !If [UseProvidedServiceAccountId, !Ref ServiceAccountId, !Ref 'AWS::AccountId']
    Export:
      Name: !Sub '${AWS::StackName}-ServiceAccountId'
