aws-cloudformation / cfn-lint

CloudFormation Linter
MIT No Attribution
2.43k stars 589 forks source link

Possible bug when evaluating Sub parameters with Serverless Framework #3006

Open scottrmercer opened 8 months ago

scottrmercer commented 8 months ago

CloudFormation Lint Version

0.83.8

What operating system are you using?

MacOS

Describe the bug

When evaluating a Serverless.yml file, Sub parameters that use dot notation are not matched with valid_params, leading to false E1019 errors.

E1019 Parameter SiteBucket.Arn for Fn::Sub not found at resources/Resources/SiteBucketPolicy/Properties/PolicyDocument/Statement/0/Resource/Fn::Sub

E1019 Parameter Distribution.Id for Fn::Sub not found at resources/Resources/SiteBucketPolicy/Properties/PolicyDocument/Statement/0/Condition/StringEquals/AWS:SourceArn/Fn::Sub

I managed to make 2 adjustments that "fixed" the symptoms, although I am not familiar enough with the codebase to know if these qualify as anything more than hacks.

  1. Serverless files embed the Resources node under another node named resources (see my example). Because of this, Template.get_resource_names() returns an empty list. As a hack, I changed get_resource_names so that resources = self.template.get("resources", {}) is replaced with resources = self.template.get("resources", {}).get("Resources", {}), returning the full list.
  2. Once the resource names are resolved, Sub params that use dot notation do not match, as only the first segment of the parameter is in the valid_params list. In order to use dot notation to refer to properties on the resource param, I had to change line 131 of Sub.py from if parameter not in valid_params: to if parameter.split('.')[0] not in valid_params:. This resolves all issues, although it does seem hacky, like #1.

Hopefully this is just something that I am doing wrong, rather than a real bug.

Expected behavior

E1019 is not returned, and linting passes.

Reproduction template

---
service: test-service

configValidationMode: error

plugins:
  - serverless-python-requirements
  - serverless-wsgi
  - serverless-plugin-datadog
  - serverless-plugin-ifelse

params:
  prod:
    integrations_event_bus_arn: !ImportValue :infrastructure:${opt:stage}:events:arn
  default:
    integrations_event_bus_arn: arn:aws:events:${aws:region}:${param:account}:event-bus/${opt:stage}-events

provider:
  name: aws
  runtime: python3.11
  stage: ${opt:stage}
  region: us-west-2
  logs:
    restApi: true
  tracing:
    lambda: true
  tags:
    service: test
    env: ${opt:stage}
  versionFunctions: false
  memorySize: ${file(sls/variables/default.yml):memorySize, 1024}
  timeout: 30
  deploymentBucket:
    name: ${opt:stage}-slsdeploy-${param:account}-${aws:region}
    serverSideEncryption: AES256
  environment:
    AWS_ACCOUNT_ID: ${param:account}
    SERVERLESS_STAGE: ${opt:stage}
    SERVERLESS_REGION: { Ref: "AWS::Region" }
  apiGateway:
    shouldStartNameWithService: true

package:
  excludeDevDependencies: true

  patterns:
    - 'package*.json'
    - '!node_modules/**'
    - '!tests/**'

functions:

  flask_proxy:
    vpc:
      securityGroupIds:
        - !Ref SecurityGroup
      subnetIds:
        !Split
        - ","
        - !ImportValue :infrastructure:${opt:stage}:vpc:privateSubnets
    handler: wsgi.handler
    events:
      - http:
          method: get
          path: /
          cors:
            origin: "*"
            headers:
              - Content-Type
              - X-Amz-Date
              - Authorization
              - X-Api-Key
              - X-Amz-Security-Token
              - X-Amz-User-Agent
              - X-User-Role
              - X-Impersonated-User
              - X-Impersonated-User-Role
              - x-datadog-origin
              - x-datadog-parent-id
              - x-datadog-sampling-priority
              - x-datadog-trace-id
            allowCredentials: false

resources:
  Resources:
    Distribution:
      Type: AWS::CloudFront::Distribution
      DeletionPolicy: Retain
      Properties:
        DistributionConfig:
          Enabled: true
          Aliases:
            - ${self:custom.cloudfront.alias.${opt:stage}, self:custom.cloudfront.alias.default}
          DefaultRootObject: index.html
          PriceClass: PriceClass_All
          HttpVersion: http2
          Origins:
            - DomainName: !GetAtt SiteBucket.DomainName
              Id: S3Origin
              S3OriginConfig:
                OriginAccessIdentity: ''
              OriginAccessControlId: !GetAtt AccessIdentity.Id
            - DomainName: !Join
                - ''
                - - !Ref ApiGatewayRestApi
                  - .execute-api.
                  - !Ref AWS::Region
                  - .amazonaws.com
              Id: API
              CustomOriginConfig:
                OriginProtocolPolicy: https-only
          CacheBehaviors:
            - AllowedMethods:
                - GET
                - HEAD
                - PUT
                - POST
                - DELETE
                - PATCH
                - OPTIONS
              CachedMethods:
                - GET
                - HEAD
              CachePolicyId: !Ref CachePolicyAPI
              Compress: true
              PathPattern: api/*
              ResponseHeadersPolicyId: !Ref ResponseHeadersPolicy
              TargetOriginId: API
              ViewerProtocolPolicy: redirect-to-https
          DefaultCacheBehavior:
            TargetOriginId: S3Origin
            LambdaFunctionAssociations:
              - EventType: origin-request
                LambdaFunctionARN: '{{resolve:ssm:/infrastructure/${opt:stage}/edgeFunctions/frontController}}'
            AllowedMethods:
              - GET
              - HEAD
            CachePolicyId: !Ref CachePolicyS3
            Compress: true
            MaxTTL: 300
            DefaultTTL: 90
            MinTTL: 60
            ViewerProtocolPolicy: redirect-to-https
            ResponseHeadersPolicyId: !Ref ResponseHeadersPolicy
          Logging:
            Bucket: !GetAtt LogsBucket.DomainName
            Prefix: logs/cloudfront/${self:custom.cloudfront.alias.${opt:stage}, self:custom.cloudfront.alias.default}
            IncludeCookies: false
          ViewerCertificate:
            AcmCertificateArn: !ImportValue ':infrastructure:${opt:stage}:cert:cloudFrontArn'
            SslSupportMethod: sni-only
            MinimumProtocolVersion: TLSv1.2_2021
    SiteBucket:
      Type: AWS::S3::Bucket
      DeletionPolicy: Retain
      UpdateReplacePolicy: Retain
      Properties: null
    SiteBucketPolicy:
      Type: AWS::S3::BucketPolicy
      Properties:
        Bucket: !Ref SiteBucket
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
            - Sid: CloudFrontOAC
              Action: s3:GetObject
              Effect: Allow
              Resource: !Sub ${SiteBucket.Arn}/*
              Principal:
                Service: cloudfront.amazonaws.com
              Condition:
                StringEquals:
                  AWS:SourceArn: !Sub arn:${AWS::Partition}:cloudfront::${AWS::AccountId}:distribution/${Distribution.Id}
    LogsBucket:
      Type: AWS::S3::Bucket
      Properties:
        AccessControl: LogDeliveryWrite
        OwnershipControls:
          Rules:
            - ObjectOwnership: BucketOwnerPreferred
      DeletionPolicy: Retain
      UpdateReplacePolicy: Retain
Hatter1337 commented 3 months ago

I have the same problem. SAM template:

Conditions:
  UseProductionSettings: !Equals [!Ref Env, "prod"]

Parameters:
  Env:
    Description: Deploying environment
    Type: String
    Default: dev
  ProductionProvisionedConcurrency:
    Description: Number of Lambda provisioned concurrency for production environment
    Type: Number
    Default: 2

...

  MyFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: !Sub "my-function-${Env}"
      CodeUri: my_function/
      Handler: app.lambda_handler
      Role: !Ref LambdaRoleARN
      AutoPublishAlias: live
      ProvisionedConcurrencyConfig:
        ProvisionedConcurrentExecutions:
          !If [
            UseProductionSettings,
            !Sub "${ProductionProvisionedConcurrency}",
            1,
          ]
E1019 {'Fn::Sub': '${ProductionProvisionedConcurrency}'} is not of type 'integer'
template.yaml:104:13
kddejong commented 3 months ago

I need to validate this (E1019 {'Fn::Sub': '${ProductionProvisionedConcurrency}'} is not of type 'integer'). There is an odd issue when doing a GetAtt to an integer but I think in the case of a parameter it works as you have pointed out.