Public/Measure-ARMTemplate.ps1
function Measure-ARMTemplate { [CmdletBinding()] [Alias('marmt')] param ( # Specifies a path to one or more locations. [Parameter(Mandatory = $true, Position = 0, ParameterSetName = "ARM", ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, HelpMessage = "Path to a template")] [ValidateNotNullOrEmpty()] #[ValidatePattern('metadata|parameters|settings')] #[ValidateScript] [string] $TemplatePath, # [Parameter(Mandatory = $true, Position = 0, ParameterSetName = "Bicep", ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, HelpMessage = "Path to a template")] [ValidateNotNullOrEmpty()] #[ValidatePattern('metadata|parameters|settings')] #[ValidateScript] [hashtable] $TemplateAsHashtable ) #region HelperVariables # Hardcoded for now until I can work out a nicer way to extract these another way. $armfunctions = 'add', 'and', 'array', 'base64', 'base64ToJson', 'base64ToString', 'bool', 'coalesce', 'concat', 'contains', 'copyIndex', 'createArray', 'createObject', 'dataUri', 'dataUriToString', 'dateTimeAdd', 'deployment', 'div', 'empty', 'endsWith', 'environment', 'equals', 'extensionResourceId', 'false', 'first', 'float', 'format', 'greater', 'greaterOrEquals', 'guid', 'if', 'indexOf', 'int', 'intersection', 'json', 'last', 'lastIndexOf', 'length', 'less', 'lessOrEquals', 'list', 'listAccountSas', 'listAdminKeys', 'listAuthKeys', 'listCallbackUrl', 'listChannelWithKeys', 'listClusterAdminCredential', 'listConnectionStrings', 'listCredentials', 'listCredential', 'listKeys', 'listKeyValue', 'listPackage', 'listQueryKeys', 'listSecrets', 'listServiceSas', 'listSyncFunctionTriggerStatus', 'max', 'min', 'mod', 'mul', 'newGuid', 'not', 'null', 'or', 'padLeft', 'parameters', 'pickZones', 'providers', 'range', 'reference', 'replace', 'resourceGroup', 'resourceId', 'skip', 'split', 'startsWith', 'string', 'sub', 'subscription', 'subscriptionResourceId', 'substring', 'take', 'tenantResourceId', 'toLower', 'toUpper', 'trim', 'true', 'union', 'uniqueString', 'uri', 'uriComponent', 'uriComponentToString', 'utcNow', 'variables' # TODO - Export this to a configuration file elsewhere to allow for more flexible complexity scoring models in future as this is just a starter model. $baseScore = 10 $parameterScore = 10 $defaultValueScore = 5 $minValueScore = 5 $maxValueScore = 5 $allowedValuesScore = 10 $objectTypeScore = 10 $arrayTypeScore = 5 $secureTypeScore = 10 $totalParamScore = 0 $ARMFunctionScore = 10 $VariableStringScore = 5 $VariableObjectScore = 10 $ResourceScore = 10 $ResourceParameterScore = 5 $outputScore = 10 $customFunctionScore = 50 #endregion HelperVaraibles if ($TemplatePath) { $JsonTemplate = Get-Item $TemplatePath $content = Get-Content $JsonTemplate.FullName $json = $content | ConvertFrom-Json #-Depth 100 $measures = $content | Measure-Object -Line -Word -Character } elseif ($TemplateAsHashtable) { $json = $TemplateAsHashtable } if ($json.'$schema' -match 'schema.management.azure.com') { $TemplateComplexityScore = $baseScore $param = $json.parameters # Parameters aren't actually an array of objects - they are sub properties of the parameters object for reasons $parameters = [System.Collections.Generic.List[PSCustomObject]]::New() $var = $json.variables # Variables aren't actually an array of objects - they are sub properties/objects of the variables object for reasons $variables = [System.Collections.Generic.List[PSCustomObject]]::New() $out = $json.outputs # Outputs aren't actually an array of objects - they are sub objects of the Outputs object for reasons $outputs = [System.Collections.Generic.List[PSCustomObject]]::New() #Resources & functions are actually an array already - so we have no need to "transform" these $res = $json.resources $func = $json.functions #region Calculation $azfunctions = Foreach ($arm in $armfunctions) { [PSCustomObject]@{ PSTypeName = 'ARMTemplate.ARMFunction' Function = $arm Count = ( -split ($content | Out-String) | Where-Object { $_ -match $arm + '\(' } | Measure-Object).Count } } If ($param) { $totalParamScore = 0 $param | Get-Member | Where-Object MemberType -EQ 'NoteProperty' | ForEach-Object { $CurrentParam = $param.$($_.Name) $ParamName = $($_.Name) $calculatedParamScore = 0 $newParamObject = [PSCustomObject]@{ ParameterName = $($ParamName) ParameterValue = [pscustomobject]@{ type = $CurrentParam.type metadata = [pscustomobject]@{ description = if ($CurrentParam.metadata.description) { $CurrentParam.metadata.description } else { '' } parameterComplexity = 0 } } } $calculatedParamScore = $calculatedParamScore + $parameterScore ; if (! $CurrentParam.metadata.description ) { $calculatedParamScore = $calculatedParamScore + 3 } if ($CurrentParam.defaultValue) { $calculatedParamScore = $calculatedParamScore + $defaultValueScore $newParamObject.ParameterValue | Add-Member -Name defaultValue -Type NoteProperty -Value $CurrentParam.defaultValue if ($CurrentParam.defaultValue.gettype().Name -notmatch 'Boolean|Int') { $paramArmFunctions = Foreach ($arm in $armfunctions) { [PSCustomObject]@{ PSTypeName = 'ARMTemplate.ARMFunction' Function = $arm Count = ( $CurrentParam.defaultValue | Where-Object { $_ -match $arm + '\(' } | Measure-Object).Count } } $paramArmFunctions.where{ $_.count -gt 0 }.foreach{ $calculatedParamScore = $calculatedParamScore + $ARMFunctionScore } } } if ($CurrentParam.minValue) { $calculatedParamScore = $calculatedParamScore + $minValueScore $newParamObject.ParameterValue | Add-Member -Name minValue -Type NoteProperty -Value $CurrentParam.minValue } if ($CurrentParam.maxValue) { $calculatedParamScore = $calculatedParamScore + $maxValueScore $newParamObject.ParameterValue | Add-Member -Name maxValue -Type NoteProperty -Value $CurrentParam.maxValue } if ($CurrentParam.minLength) { $calculatedParamScore = $calculatedParamScore + $minValueScore $newParamObject.ParameterValue | Add-Member -Name minLength -Type NoteProperty -Value $CurrentParam.minLength } if ($CurrentParam.maxLength) { $calculatedParamScore = $calculatedParamScore + $maxValueScore $newParamObject.ParameterValue | Add-Member -Name maxLength -Type NoteProperty -Value $CurrentParam.maxLength } if ($CurrentParam.allowedValues) { $calculatedParamScore = $calculatedParamScore + $allowedValuesScore; $CurrentParam.allowedValues.foreach{ $calculatedParamScore = $calculatedParamScore + 1 } $newParamObject.ParameterValue | Add-Member -Name allowedValues -Type NoteProperty -Value $CurrentParam.allowedValues } # Check object type and for each if ($CurrentParam.type -imatch 'object') { $calculatedParamScore = $calculatedParamScore + $objectTypeScore } if ($CurrentParam.type -imatch 'array') { $calculatedParamScore = $calculatedParamScore + $arrayTypeScore } if ($CurrentParam.type -imatch 'secure') { $calculatedParamScore = $calculatedParamScore + $secureTypeScore } $newParamObject.ParameterValue.metadata.parameterComplexity = $calculatedParamScore $newParamObject.ParameterValue.metadata | Add-Member -Name parameterARMFunctions -Type NoteProperty -Value ($paramArmFunctions | Where-Object Count -GT 0) # Update total score for all parameters $totalParamScore = $totalParamScore + $calculatedParamScore $parameters.Add($newParamObject) #$CurrentParam,$newParamObject,$paramArmFunctions = $null } $TemplateComplexityScore = $TemplateComplexityScore + $totalParamScore } If ($var) { $totalVarScore = 0 $var | Get-Member | Where-Object MemberType -EQ 'NoteProperty' | ForEach-Object { $currentVar = $var.$($_.Name) $varName = $($_.Name) $calculatedVarScore = 0 $calculatedVarScore = $calculatedVarScore + $VariableScore $isInt = ((($content | Select-String -Pattern $varName -List | Select-Object -First 1) -split ':')[-1].Trim().GetType().Name) -match 'Int|Long|Double' $isObject = (($content | Select-String -Pattern $varName -List | Select-Object -First 1) -split ':')[-1].Trim().StartsWith('{') $isString = (($content | Select-String -Pattern $varName -List | Select-Object -First 1) -split ':')[-1].Trim().StartsWith('"') switch ($true) { $isInt { $variableType = 'int' ; $calculatedVarScore = $calculatedVarScore + 1 } $isString { $variableType = 'string' ; $calculatedVarScore = $calculatedVarScore + $VariableStringScore } $isObject { $variableType = 'object' ; $calculatedVarScore = $calculatedVarScore + $VariableObjectScore } Default { $variableType = 'unknown' ; $calculatedVarScore = $calculatedVarScore + 1 } } $newVarObject = [PSCustomObject]@{ VariableName = $($varName) VariableValue = $currentVar VariableType = $variableType VariableARMFunctions = [PSCustomObject]@{ } VariableComplexity = 0 } if ($variableType -eq 'string') { $varArmFunctions = Foreach ($arm in $armfunctions) { [PSCustomObject]@{ PSTypeName = 'ARMTemplate.ARMFunction' Function = $arm Count = ( $currentVar | Where-Object { $_ -match $arm + '\(' } | Measure-Object).Count } } $varArmFunctions = $varArmFunctions | Where-Object Count -GT 0 $varArmFunctions.foreach{ $calculatedVarScore = $calculatedVarScore + $_.Count } $newVarObject.VariableARMFunctions = $varArmFunctions } if ($variableType -eq 'object') { $varObjectARMFunctions = [System.Collections.Generic.List[PSCustomObject]]::New() $flatVarObject = ConvertTo-FlatObject $currentVar $reportingObject = $flatVarObject $flatVarObject | Get-Member -MemberType NoteProperty | ForEach-Object { $propName = $_.Name ; If (! $flatVarObject.$propName -eq $null) { If ($flatVarObject.$propName.gettype().Name -match 'string') { $propValue = $flatVarObject.$propName $varPropArmFunctions = Foreach ($arm in $armfunctions) { [PSCustomObject]@{ PSTypeName = 'ARMTemplate.ARMFunction' Function = $arm Count = ( $propValue | Where-Object { $_ -match $arm + '\(' } | Measure-Object).Count } } $varPropArmFunctions = $varPropArmFunctions | Where-Object Count -GT 0 $varPropArmFunctions.foreach{ $calculatedVarPropScore = $calculatedVarPropScore + $_.Count } $reportingObject | Add-Member -Name ($propName + '.Complexity') -Type NoteProperty -Value $calculatedVarPropScore } $varObjectARMFunctions.Add($varPropARMFunctions) } } $varObjectARMFunctions = $varObjectARMFunctions | Where-Object Count -GT 0 $varObjectARMFunctions.foreach{ $calculatedVarScore = $calculatedVarScore + $_.Count } $newVarObject.VariableARMFunctions = $varObjectARMFunctions $newVarObject | Add-Member -Name VarProp -Type NoteProperty -Value $reportingObject } $newVarObject.VariableComplexity = $calculatedVarScore $totalVarScore = $totalVarScore + $calculatedVarscore $variables.Add($newVarObject) } $TemplateComplexityScore = $TemplateComplexityScore + $totalVarScore } if($func) { ## TODO Properly $totalFuncScore = 0 $func.Foreach{ $currentFunction = $_ $FunctionName = $func.$($_.Name) $calculatedFuncScore = 0 $totalFuncScore = $calculatedFuncScore + $totalFuncScore + $customFunctionScore } } # Resources If ($res) { $totalResScore = 0 $res.Foreach{ $currentResource = $_ $ResourceName = $Res.$($_.Name) $calculatedResScore = 0 $calculatedResScore = $calculatedResScore + $ResourceScore $flatResObject = ConvertTo-FlatObject $currentResource $resObjectARMFunctions = [System.Collections.Generic.List[PSCustomObject]]::New() $flatResObject | Get-Member -MemberType NoteProperty | # We wont use the .properties objects as we have the properties of these objects already exposed in the flat object Where-Object { $_.name -Match '.*(?<!properties)$' } | ForEach-Object { $propName = $_.Name ; $calculatedResParamScore = 0 $calculatedResParamScore = $calculatedResParamScore + $ResourceParameterScore Write-Information "Working on $propname" If (! $flatResObject.$propName -eq $null) { If ($flatResObject.$propName.gettype().Name -match 'string') { $propValue = $flatResObject.$propName $resPropArmFunctions = Foreach ($arm in $armfunctions) { [PSCustomObject]@{ PSTypeName = 'ARMTemplate.ARMFunction' Function = $arm Count = ( $propValue | Where-Object { $_ -match $arm + '\(' } | Measure-Object).Count } } $resPropArmFunctions = $resPropArmFunctions | Where-Object Count -GT 0 $resPropArmFunctions.foreach{ $calculatedResPropScore = $calculatedResPropScore + $_.Count } $flatResObject | Add-Member -Name ($propName + '.Complexity') -Type NoteProperty -Value $calculatedResPropScore } } $ResObjectARMFunctions.Add($ResPropARMFunctions) $calculatedResScore = $calculatedResScore + $calculatedResParamScore } $resObjectARMFunctions = $resObjectARMFunctions | Where-Object Count -GT 0 $resObjectARMFunctions.foreach{ $calculatedResScore = $calculatedResScore + $_.Count } $flatResObject | Add-Member -Name ResourceARMFunctions -Type NoteProperty -Value $resObjectARMFunctions $flatResObject | Add-Member -Name ResourceComplexity -Type NoteProperty -Value $calculatedResScore $totalresScore = $totalresScore + $calculatedResScore } $TemplateComplexityScore = $TemplateComplexityScore + $totalResScore } # Outputs If ($Out) { $totalOutScore = 0 $out | Get-Member | Where-Object MemberType -EQ 'NoteProperty' | ForEach-Object { $currentOut = $out.$($_.Name) $outName = $($_.Name) $calculatedoutScore = 0 $calculatedoutScore = $calculatedoutScore + $outputScore $isInt = ((($content | Select-String -Pattern $outName -List | Select-Object -First 1) -split ':')[-1].Trim().GetType().Name) -match 'Int|Long|Double' $isObject = (($content | Select-String -Pattern $outName -List | Select-Object -First 1) -split ':')[-1].Trim().StartsWith('{') $isString = (($content | Select-String -Pattern $outName -List | Select-Object -First 1) -split ':')[-1].Trim().StartsWith('"') switch ($true) { $isInt { $outputType = 'int' ; $calculatedoutScore = $calculatedoutScore + 1 } $isString { $outputType = 'string' ; $calculatedoutScore = $calculatedoutScore + 1 } $isObject { $outputType = 'object' ; $calculatedoutScore = $calculatedoutScore + 3 } Default { $outputType = 'unknown' ; $calculatedoutScore = $calculatedoutScore + 1 } } $newoutObject = [PSCustomObject]@{ outputName = $($outName) outputValue = $currentOut outputType = $outputType outputARMFunctions = [PSCustomObject]@{ } outputComplexity = 0 } if ($outputType -eq 'string') { $outArmFunctions = Foreach ($arm in $armfunctions) { [PSCustomObject]@{ PSTypeName = 'ARMTemplate.ARMFunction' Function = $arm Count = ( $currentOut | Where-Object { $_ -match $arm + '\(' } | Measure-Object).Count } } $outArmFunctions = $outArmFunctions | Where-Object Count -GT 0 $outArmFunctions.foreach{ $calculatedoutScore = $calculatedoutScore + $_.Count } $newoutObject.outputARMFunctions = $outArmFunctions } if ($outputType -eq 'object') { $outObjectARMFunctions = [System.Collections.Generic.List[PSCustomObject]]::New() $flatOutObject = ConvertTo-FlatObject $currentout $reportingObject = $flatOutObject $flatOutObject | Get-Member -MemberType NoteProperty | ForEach-Object { $propName = $_.Name ; If (! $flatResObject.$propName -eq $null) { If ($flatOutObject.$propName.gettype().Name -match 'string') { $propValue = $flatOutObject.$propName $outPropArmFunctions = Foreach ($arm in $armfunctions) { [PSCustomObject]@{ PSTypeName = 'ARMTemplate.ARMFunction' Function = $arm Count = ( $propValue | Where-Object { $_ -match $arm + '\(' } | Measure-Object).Count } } $outPropArmFunctions = $outPropArmFunctions | Where-Object Count -GT 0 $outPropArmFunctions.foreach{ $calculatedoutPropScore = $calculatedoutPropScore + $_.Count } $reportingObject | Add-Member -Name ($propName + '.Complexity') -Type NoteProperty -Value $calculatedoutPropScore } $outObjectARMFunctions.Add($outPropARMFunctions) } } $outObjectARMFunctions = $outObjectARMFunctions | Where-Object Count -GT 0 $outObjectARMFunctions.foreach{ $calculatedoutScore = $calculatedoutScore + $_.Count } $newoutObject.OutputARMFunctions = $outObjectARMFunctions $newoutObject | Add-Member -Name outProp -Type NoteProperty -Value $reportingObject } $newOutObject.OutputComplexity = $calculatedoutScore $totaloutScore = $totaloutScore + $calculatedoutscore $Outputs.Add($newOutObject) } $TemplateComplexityScore = $TemplateComplexityScore + $totalOutScore } #endregion Calculation $ARMReport = [pscustomobject]@{ PSTypeName = 'JsonFile.ARMTemplate' Content = $content TemplateName = if ($JsonTemplate) {$JsonTemplate.BaseName} else {''} TemplatePath = if ($JsonTemplate) {$JsonTemplate.FullName} else {''} TemplateDirectory = if ($JsonTemplate) {$JsonTemplate.Directory.FullName} else {''} ContentVersion = $json.contentVersion Parameters = @{ Parameters = $param ParametersArray = $updatedparams Count = if ($param) { ($param | Get-Member -MemberType NoteProperty).Count } else { 0 } ComplexityScore = $totalParamScore } Variables = @{ Variables = $var VariablesArray = $updatedvar Count = if ($var) { ($var | Get-Member -MemberType NoteProperty).Count } else { 0 } ComplexityScore = $totalVarScore } Resources = @{ Resources = $res Count = if ($res) { $res.Count } else { 0 } ComplexityScore = $totalResScore } Outputs = @{ Outputs = $out OutputsArray = $updatedout Count = if ($out) { ($out | Get-Member -MemberType NoteProperty).Count } else { 0 } ComplexityScore = $totalOutScore } Functions = @{ CustomFucntions = $functions Count = if ($functions) { ($func | Get-Member -MemberType NoteProperty).Count } else { 0 } ComplexityScore = $totalFuncScore } ARMFunctions = $azfunctions | Where-Object Count -GT 0 FuncsUsed = ($azfunctions | Measure-Object -Property count -Sum).Sum LinesOfTemplate = $measures.Lines WordsInTemplate = $measures.Words CharsInTemplate = $measures.Characters ARMTemplateComplexityScore = $TemplateComplexityScore } } else { $ARMReport = [pscustomobject]@{ PSTypeName = 'JsonFile.OtherJsonFile' File = $File.Directory.Name Content = $content } } $ArmReport } |