utilities/pipelines/staticValidation/compliance/helper/helper.psm1 (325 lines of code) (raw):

############################## # Load general functions # ############################## $repoRootPath = (Get-Item -Path $PSScriptRoot).Parent.Parent.Parent.Parent.Parent.FullName . (Join-Path $repoRootPath 'utilities' 'pipelines' 'sharedScripts' 'Get-NestedResourceList.ps1') . (Join-Path $repoRootPath 'utilities' 'pipelines' 'sharedScripts' 'Get-ScopeOfTemplateFile.ps1') . (Join-Path $repoRootPath 'utilities' 'pipelines' 'sharedScripts' 'Get-PipelineFileName.ps1') . (Join-Path $repoRootPath 'utilities' 'pipelines' 'sharedScripts' 'helper' 'Get-IsParameterRequired.ps1') . (Join-Path $repoRootPath 'utilities' 'pipelines' 'sharedScripts' 'helper' 'ConvertTo-OrderedHashtable.ps1') . (Join-Path $repoRootPath 'utilities' 'pipelines' 'sharedScripts' 'helper' 'Get-CrossReferencedModuleList.ps1') . (Join-Path $repoRootPath 'utilities' 'pipelines' 'sharedScripts' 'Get-BRMRepositoryName.ps1') . (Join-Path $repoRootPath 'utilities' 'pipelines' 'publish' 'helper' 'Get-ModuleTargetVersion.ps1') #################################### # Load test-specific functions # #################################### <# .SYNOPSIS Get the index of a header in a given markdown array .DESCRIPTION Get the index of a header in a given markdown array .PARAMETER ReadMeContent Required. The content to search in .PARAMETER MarkdownSectionIdentifier Required. The header to search for. For example '*# Parameters' .EXAMPLE Get-MarkdownSectionStartIndex -ReadMeContent @('# Parameters', 'other content') -MarkdownSectionIdentifier '*# Parameters' Get the index of the '# Parameters' header in the given markdown array @('# Parameters', 'other content') #> function Get-MarkdownSectionStartIndex { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [array] $ReadMeContent, [Parameter(Mandatory = $true)] [string] $MarkdownSectionIdentifier ) $sectionStartIndex = 0 while ($ReadMeContent[$sectionStartIndex] -notlike $MarkdownSectionIdentifier -and -not ($sectionStartIndex -ge $ReadMeContent.count)) { $sectionStartIndex++ } return $sectionStartIndex } <# .SYNOPSIS Get the last index of a section in a given markdown array .DESCRIPTION Get the last index of a section in a given markdown array. The end of a section is identified by the start of a new header. .PARAMETER ReadMeContent Required. The content to search in .PARAMETER SectionStartIndex Required. The index where the section starts .EXAMPLE Get-MarkdownSectionEndIndex -ReadMeContent @('somrthing', '# Parameters', 'other content', '# Other header') -SectionStartIndex 2 Search for the end index of the section starting in index 2 in array @('somrthing', '# Parameters', 'other content', '# Other header'). Would return 3. #> function Get-MarkdownSectionEndIndex { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [array] $ReadMeContent, [Parameter(Mandatory = $true)] [int] $SectionStartIndex ) $sectionEndIndex = $sectionStartIndex + 1 while ($readMeContent[$sectionEndIndex] -notlike '*# *' -and -not ($sectionEndIndex -ge $ReadMeContent.count)) { $sectionEndIndex++ } return $sectionEndIndex } <# .SYNOPSIS Get the start & end index of a table in a given markdown section, indentified by a header .DESCRIPTION Get the start & end index of a table in a given markdown section, indentified by a header. .PARAMETER ReadMeContent Required. The content to search in .PARAMETER MarkdownSectionIdentifier Required. The header of the section containing the table to search for. For example '*# Parameters' .EXAMPLE $tableStartIndex, $tableEndIndex = Get-TableStartAndEndIndex -ReadMeContent @('# Parameters', '| a | b |', '| - | - |', '| 1 | 2 |', 'other content') -MarkdownSectionIdentifier '*# Parameters' Get the start & end index of the table in section '# Parameters' in the given ReadMe content. Would return @(1,3) #> function Get-TableStartAndEndIndex { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [array] $ReadMeContent, [Parameter(Mandatory = $true)] [string] $MarkdownSectionIdentifier ) $sectionStartIndex = Get-MarkdownSectionStartIndex -ReadMeContent $ReadMeContent -MarkdownSectionIdentifier $MarkdownSectionIdentifier $tableStartIndex = $sectionStartIndex + 1 while ($readMeContent[$tableStartIndex] -notlike '*|*' -and -not ($tableStartIndex -ge $readMeContent.count)) { $tableStartIndex++ } $tableEndIndex = $tableStartIndex + 2 while ($readMeContent[$tableEndIndex] -like '|*' -and -not ($tableEndIndex -ge $readMeContent.count)) { $tableEndIndex++ } return $tableStartIndex, $tableEndIndex } <# .SYNOPSIS Remove metadata blocks from given template object .DESCRIPTION Remove metadata blocks from given template object .PARAMETER TemplateObject The template object to remove the metadata from .EXAMPLE Remove-JSONMetadata -TemplateObject @{ metadata = 'a'; b = 'b' } Returns @{ b = 'b' } #> function Remove-JSONMetadata { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [hashtable] $TemplateObject ) $TemplateObject.Remove('metadata') # Differantiate case: With user defined types (resources property is hashtable) vs without user defined types (resources property is array) if ($TemplateObject.resources.GetType().BaseType.Name -eq 'Hashtable') { # Case: Hashtable $resourceIdentifiers = $TemplateObject.resources.Keys for ($index = 0; $index -lt $resourceIdentifiers.Count; $index++) { if ($TemplateObject.resources[$resourceIdentifiers[$index]].type -eq 'Microsoft.Resources/deployments' -and $TemplateObject.resources[$resourceIdentifiers[$index]].properties.template.GetType().BaseType.Name -eq 'Hashtable') { $TemplateObject.resources[$resourceIdentifiers[$index]] = Remove-JSONMetadata -TemplateObject $TemplateObject.resources[$resourceIdentifiers[$index]].properties.template } } } else { # Case: Array for ($index = 0; $index -lt $TemplateObject.resources.Count; $index++) { if ($TemplateObject.resources[$index].type -eq 'Microsoft.Resources/deployments' -and $TemplateObject.resources[$index].properties.template.GetType().BaseType.Name -eq 'Hashtable') { $TemplateObject.resources[$index] = Remove-JSONMetadata -TemplateObject $TemplateObject.resources[$index].properties.template } } } return $TemplateObject } <# .SYNOPSIS Get a flat list of all parameters in a given template .DESCRIPTION Get a flat list of all parameters in a given template .PARAMETER TemplateFileContent Mandatory. The template containing all the data .PARAMETER Properties Optional. Hashtable of the user defined properties .PARAMETER ParentName Optional. Name of the parameter, that has the user defined types .EXAMPLE Resolve-ReadMeParameterList -TemplateFileContent @{ resource = @{}; parameters = @{}; ... } Top-level invocation. Will start from the TemplateFile's parameters object and recursively crawl through all children. .EXAMPLE Resolve-ReadMeParameterList -TemplateFileContent @{ resource = @{}; parameters = @{}; ... } -Properties @{ @{ name = @{ type = 'string'; 'allowedValues' = @('A1','A2','A3','A4','A5','A6'); 'nullable' = $true; (...) } -ParentName 'diagnosticSettings' Child-level invocation during recursion. .NOTES The function is recursive and will also output grand, great grand children, ... . #> function Resolve-ReadMeParameterList { param ( [Parameter(Mandatory = $true)] [hashtable] $TemplateFileContent, [Parameter(Mandatory = $false)] [hashtable] $Properties, [Parameter(Mandatory = $false)] [string] $ParentName ) $parameterSet = @{} if (-not $Properties -and -not $TemplateFileContent.parameters) { # no Parameters / properties on this level or in the template return $parameterSet } elseif (-not $Properties) { # Top-level invocation # Add name as property for later reference $TemplateFileContent.parameters.Keys | ForEach-Object { $TemplateFileContent.parameters[$_]['name'] = $_ } [array] $parameters = $TemplateFileContent.parameters.Values | Sort-Object -Property 'Name' -Culture 'en-US' } else { # Add name as property for later reference $Properties.Keys | ForEach-Object { $Properties[$_]['name'] = $_ } $parameters = $Properties.Values | Sort-Object -Property 'Name' -Culture 'en-US' } foreach ($parameter in $parameters) { ###################### # Gather details # ###################### $paramIdentifier = (-not [String]::IsNullOrEmpty($ParentName)) ? '{0}.{1}' -f $ParentName, $parameter.name : $parameter.name # definition type (if any) if ($parameter.Keys -contains '$ref') { $identifier = Split-Path $parameter.'$ref' -Leaf $definition = $TemplateFileContent.definitions[$identifier] # $type = $definition['type'] } elseif ($parameter.Keys -contains 'items' -and $parameter.items.type -in @('object', 'array') -or $parameter.type -eq 'object') { # Array has nested non-primitive type (array/object) - and if array, the the UDT itself is declared as the array $definition = $parameter } elseif ($parameter.Keys -contains 'items' -and $parameter.items.keys -contains '$ref') { # Array has nested non-primitive type (array) - and the parameter is defined as an array of the UDT $identifier = Split-Path $parameter.items.'$ref' -Leaf $definition = $TemplateFileContent.definitions[$identifier] } else { $definition = $null } $parameterSet[$paramIdentifier] = $parameter #recursive call for children if ($definition -and $definition.keys -notcontains 'discriminator') { if ($definition.ContainsKey('items') -and $definition['items'].ContainsKey('properties')) { $childProperties = $definition['items']['properties'] } elseif ($definition.type -in @('object', 'secureObject') -and $definition['properties']) { $childProperties = $definition['properties'] } else { $childProperties = $null } if ($childProperties) { $parameterSet += Resolve-ReadMeParameterList -TemplateFileContent $TemplateFileContent -Properties $childProperties -ParentName $paramIdentifier } } elseif ($definition -and $definition.keys -contains 'discriminator') { $variants = $definition.discriminator.mapping.values.'$ref' | ForEach-Object { $variantName = Split-Path $_ -Leaf @{ name = $variantName properties = $TemplateFileContent.definitions[$variantName].properties } } foreach ($variant in $variants) { $paramIdentifier = (-not [String]::IsNullOrEmpty($ParentName)) ? '{0}.{1}.{2}' -f $ParentName, $parameter.name, $variant.name : '{0}.{1}' -f $parameter.name, $variant.name $parameterSet += Resolve-ReadMeParameterList -TemplateFileContent $TemplateFileContent -Properties $variant.properties -ParentName $paramIdentifier } } } return $parameterSet } <# Get a hashtable of all environment variables in the given GitHub workflow .DESCRIPTION Get a hashtable of all environment variables in the given GitHub workflow .PARAMETER WorkflowPath Mandatory. The path of the workflow to get the environment variables from .EXAMPLE Get-WorkflowEnvVariablesAsObject -WorkflowPath 'C:/bicep-registry-modules/.github/workflows/test.yml' Get the environment variables from the given workflow #> function Get-WorkflowEnvVariablesAsObject { [CmdletBinding()] param ( [Parameter(Mandatory)] [string] $WorkflowPath ) $contentFileContent = Get-Content -Path $workflowPath $envStartIndex = $contentFileContent.IndexOf('env:') if (-not $envStartIndex) { # No env variables defined in the given workflow return @{} } $searchIndex = $envStartIndex + 1 $envVars = @{} while ($searchIndex -lt $contentFileContent.Count) { $line = $contentFileContent[$searchIndex] if ($line -match "^\s+(\w+): (?:`"|')*([^`"'\s]+)(?:`"|')*$") { $envVars[($Matches[1])] = $Matches[2] } else { break } $searchIndex++ } return $envVars } <# Get a list of all versioned parents of a module .DESCRIPTION Get a list of all versioned parents of a module. The function will recursively search the parent directories of the given path until it finds a directory containing a version.json file or reaches the root path. The function will return a list of all the directories in the parent hierarchy that contain a version.json file, including the given path. The function will return the list in the order from the root path to the given path. .PARAMETER Path Mandatory. The path of the module to search for versioned parents. .PARAMETER UpperBoundPath Optional. The root path to stop the search at. The function will not search above this path. .PARAMETER Filter Optional. Only include module folders in the list (i.e., modules with a `main.json` file), or versioned modules folders (i.e., modules with `version.json` files). The default is to include all folders). .EXAMPLE Get-ParentFolderPathList -Path 'C:/bicep-registry-modules/avm/res/storage/storage-account/blob-service/container/immutability-policy' Get all parent folders of the 'immutability-policy' folder up to 'res'. Returns - <repoPath>\avm\res\storage - <repoPath>\avm\res\storage\storage-account - <repoPath>\avm\res\storage\storage-account\blob-service - <repoPath>\avm\res\storage\storage-account\blob-service\container .EXAMPLE Get-ParentFolderPathList -Path 'C:/bicep-registry-modules/avm/res/storage/storage-account/blob-service/container/immutability-policy' -RootPath 'C:/bicep-registry-modules/avm/res' -Filter 'OnlyVersionedModules' Get all versioned parent module folders of the 'immutability-policy' folder up to 'res'. Returns, e.g., - <repoPath>\avm\res\storage\storage-account #> function Get-ParentFolderPathList { param ( [Parameter(Mandatory = $true)] [string] $Path, [Parameter(Mandatory = $false)] [string] $UpperBoundPath = $repoRootPath, [Parameter(Mandatory = $false)] [ValidateSet('OnlyModules', 'OnlyVersionedModules', 'All')] [string] $Filter = 'All' ) $Item = Get-Item -Path $Path $result = @() if ($Item.FullName -ne $UpperBoundPath) { $result += Get-ParentFolderPathList -Path $Item.Parent.FullName -UpperBoundPath $UpperBoundPath -Filter $Filter switch ($Filter) { 'OnlyModules' { if (Test-Path (Join-Path -Path $Item.FullName 'main.json')) { $result += $Item.FullName } break } 'OnlyVersionedModules' { if ((Test-Path (Join-Path -Path $Item.FullName 'version.json')) -and (Test-Path (Join-Path -Path $Item.FullName 'main.json'))) { $result += $Item.FullName } break } 'All' { $result += $Item.FullName break } } } return $result }