Azure / template-analyzer

Template scanner for security misconfiguration and best practices
MIT License
129 stars 40 forks source link

[BUG] Bicep template which loads json content errors #345

Open NSTA2 opened 1 year ago

NSTA2 commented 1 year ago

Describe the bug

I have a number of templates which load configuration from a json file. Template Analyzer produces this error for templates which use this pattern (However, given the ambiguity of the exception, it's hard to tell if the issue is the usage of json content or not)

image

Exception details:
Microsoft.Azure.Templates.Analyzer.Core.TemplateAnalyzerException: Error compiling Bicep template
 ---> System.Exception: Bicep issues found:
Expected the "]" character at this location.
This declaration type is not recognized. Specify a metadata, parameter, variable, resource, or output declaration.
   at Microsoft.Azure.Templates.Analyzer.BicepProcessor.BicepTemplateProcessor.ConvertBicepToJson(String bicepPath)
   at Microsoft.Azure.Templates.Analyzer.Core.TemplateAnalyzer.AnalyzeTemplate(String template, String templateFilePath, String parameters)
   --- End of inner exception stack trace ---
   at Microsoft.Azure.Templates.Analyzer.Core.TemplateAnalyzer.AnalyzeTemplate(String template, String templateFilePath, String parameters)
   at Microsoft.Azure.Templates.Analyzer.Cli.CommandLineParser.AnalyzeTemplate(TemplateAndParams templateAndParameters)

Expected behavior

No error; template is analyzed

Reproduction Steps

config.json (referenced from cosmos.bicep - check relative path)

{
  // Defines the environment-independent settings for each service.

  // ... truncated ...

  "environments": {

    "development": {

        // container properties:
        // {
        //       "name":"cosmos container name",       (REQUIRED)
        //       "partitionKey":"/partitionKeyPath",   (REQUIRED)
        //       "ttl": 100,   // default: -1  (none)
        //       "indexing": "lazy",    // consistent, lazy or none  (default: consistent)
        //       "uniqueKeys": []  // default: none
        //       "permissions": []  // default: none
        // }

      "cosmos": {
        "enabled": true,
        "RUs": 1000, // minimum: 1000   (shared by all containers)
        "containers": [ // max: 25 containers
          {
            "name": "items", // stored procedure(s) added in infra/environment/cosmos-container.bicep
            "partitionKey": "/p",
            "indexing": "consistent",
            "uniqueKeys": [ "/vg", "/v", "/t", "/tv" ],
            "permissions": [
              "Microsoft.DocumentDB/databaseAccounts/readMetadata",
              "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/executeStoredProcedure", // writes are via stored procedure
              "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/executeQuery", // query only (no direct item access)
              "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/readChangeFeed",
              "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/replace"
            ]            
          },
          {
            "name": "changefeed",
            "partitionKey": "/id",
            "indexing": "consistent",
            "permissions": [
              "Microsoft.DocumentDB/databaseAccounts/readMetadata",
              "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/*", // item writes/deletes allowed
              "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/executeQuery"
            ]
          },
          {
            "name": "orleans",
            "partitionKey": "/ClusterId",
            "indexing": "consistent",
            "indexIncludes": [ "/*" ],
            "indexExcludes": [ "/Address/*", "/Port/*", "/Generation/*", "/Hostname/*", "/SiloName/*", "/\"SuspectingSilos\"/*", "/\"SuspectingTimes\"/*", "/StartTime/*", "/IAmAliveTime/*" ],
            "permissions": [
              "Microsoft.DocumentDB/databaseAccounts/readMetadata",
              "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/*", // item writes/deletes allowed
              "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/executeQuery"
            ]
          },
          {
            "name": "grains",
            "partitionKey": "/PartitionKey",
            "indexing": "consistent",
            "indexIncludes": [ "/*" ],
            "indexExcludes": [ "/\"State\"/*", "/\"_etag\"/?", "/StartAt/*", "/Period/*" ],
            "permissions": [
              "Microsoft.DocumentDB/databaseAccounts/readMetadata",
              "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/*", // item writes/deletes allowed
              "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/executeQuery"
            ]
          }
        ]
      }
    }
  }
}

cosmos.bicep:

param now string = utcNow()
param location string
param sublocations array
param databaseName string
param tags object

// Resource names

@maxLength(44)
param cosmosName string

@description('The default consistency level of the Cosmos DB account.')
@allowed([
  'Eventual'
  'ConsistentPrefix'
  'Session'
  'BoundedStaleness'
  'Strong'
])
param defaultConsistencyLevel string = 'Session'

@description('Max stale requests. Required for BoundedStaleness. Valid ranges, Single Region: 10 to 2147483647. Multi Region: 100000 to 2147483647.')
@minValue(10)
@maxValue(2147483647)
param maxStalenessPrefix int = 100000

@description('Max lag time (minutes). Required for BoundedStaleness. Valid ranges, Single Region: 5 to 84600. Multi Region: 300 to 86400.')
@minValue(5)
@maxValue(86400)
param maxIntervalInSeconds int = 300

@description('Enable system managed failover for regions')
param systemManagedFailover bool = true

param environment string

// Configuration
var config = loadJsonContent('./../config.json')
var envConfig = config.environments[environment]

var cosmosConfig = envConfig.cosmos

var RUs = contains(cosmosConfig, 'RUs') ? cosmosConfig.RUs : 1000

var containers = contains(cosmosConfig, 'containers') ? cosmosConfig.containers : []

// New resources

var consistencyPolicy = {
  Eventual: {
    defaultConsistencyLevel: 'Eventual'
  }
  ConsistentPrefix: {
    defaultConsistencyLevel: 'ConsistentPrefix'
  }
  Session: {
    defaultConsistencyLevel: 'Session'
  }
  BoundedStaleness: {
    defaultConsistencyLevel: 'BoundedStaleness'
    maxStalenessPrefix: maxStalenessPrefix
    maxIntervalInSeconds: maxIntervalInSeconds
  }
  Strong: {
    defaultConsistencyLevel: 'Strong'
  }
}

var locations = [
  {
    locationName: location
    failoverPriority: 0
    isZoneRedundant: false
  }
]

var sublocationsList = [for sublocation in sublocations: {
    locationName: sublocation
    failoverPriority: 1
    isZoneRedundant: false
  }
]

var allLocations = union(locations, sublocationsList)

resource account 'Microsoft.DocumentDB/databaseAccounts@2022-11-15-preview' = {  // old version to avail of burst feature - went to GA May 2023:  TODO: update version when added to a non-preview version
  name: toLower(cosmosName)
  kind: 'GlobalDocumentDB'
  location: location
  properties: {
    consistencyPolicy: consistencyPolicy[defaultConsistencyLevel]
    locations: allLocations
    databaseAccountOfferType: 'Standard'
    enableAutomaticFailover: systemManagedFailover
    enableMultipleWriteLocations: true
    disableKeyBasedMetadataWriteAccess: true  // only data readon/write operations are allowed when connected using an account key
    publicNetworkAccess: 'Disabled'
    minimalTlsVersion: 'Tls12'
    disableLocalAuth: true
    enableBurstCapacity: true
    backupPolicy: {
      type:'Periodic'

       periodicModeProperties: {
        backupRetentionIntervalInHours: 168  // 1 week retention
        backupIntervalInMinutes: 60  // hourly backup
        backupStorageRedundancy: 'Zone'
       }
    }
  }
  tags: tags
}

resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2022-05-15' = {
  parent: account
  name: databaseName
  properties: {
    resource: {
      id: databaseName       
    }
     options: {
       throughput: RUs  // database-shared throughput
     }
  }
}

@batchSize(1)
module containersModule 'cosmos-container.bicep' = [for container in containers: {
  name: 'env-mod-cosmos-container-${container.name}-${now}'
  params: {
    accountName: account.name
    containerName: container.name
    databaseName: database.name   // creates an implicit dependency on the database
    partitionKeys: [container.partitionKey]
    defaultTtl: contains(container, 'ttl') ? container.ttl : -1
    indexingMode: contains(container, 'indexing') ? container.indexing : 'consistent'
    uniqueKeys: contains(container, 'uniqueKeys') ? container.uniqueKeys : []
    indexExcludes: contains(container, 'indexExcludes') ? container.indexExcludes : []
    indexIncludes: contains(container, 'indexIncludes') ? container.indexIncludes : []
  }
}]

output accountName string = account.name
output accountId string = account.id

cosmos-container.bicep (referenced from cosmos.bicep)

param accountName string
param databaseName string
param containerName string

@allowed(['consistent', 'lazy', 'none'])
param indexingMode string = 'consistent'

param indexIncludes array = ['/*']
param indexExcludes array = ['/_etag/?']

@description('string array of paths which (combined) form a unique key')
param uniqueKeys array = []

param defaultTtl int = -1

param partitionKeys array

resource account 'Microsoft.DocumentDB/databaseAccounts@2023-04-15' existing = {
  name: accountName
}

resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2023-04-15' existing = {
  parent: account
  name: databaseName
}

resource container 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2022-05-15' = {
  parent: database
  name: containerName
  properties: {
    resource: {
      conflictResolutionPolicy: {
         mode: 'LastWriterWins'
      }
      id: containerName
      partitionKey: {
        paths: partitionKeys
        kind: 'Hash'
      }
      indexingPolicy: {
        indexingMode: indexingMode
        includedPaths: [for path in indexIncludes: { path: path}]
        excludedPaths: [for path in indexExcludes: { path: path}]
      }
      defaultTtl: defaultTtl
      uniqueKeyPolicy: {
        uniqueKeys: length(uniqueKeys) > 0 ? [
          {
            paths: uniqueKeys
          }
        ] : []
      }
    }
  }
}

Environment

No response

nonik0 commented 1 year ago

Thanks for the input, we will take a look!