src/modules/wara/runbook/runbook.psm1 (640 lines of code) (raw):
using module ../utils/utils.psd1
<#
.FUNCTION
Get-RunbookSchema
.SYNOPSIS
Retrieves the JSON schema for a runbook.
.DESCRIPTION
The `Get-RunbookSchema` function returns the JSON schema that defines the structure
of a Well-Architected Reliability Assessment (WARA) runbook. This schema ensures consistency
in the configuration and validation of runbook content.
.OUTPUTS
[string]
The JSON schema as a string.
.EXAMPLE
$schema = Get-RunbookSchema
Retrieves the JSON schema for a runbook, ensuring it adheres to the expected structure.
.NOTES
Author: Casey Watson
Date: 2025-02-27
#>
function Get-RunbookSchema {
@"
{
"title": "Runbook",
"description": "A well-architected reliability assessment (WARA) runbook",
"type": "object",
"properties": {
"parameters": {
"type": "object"
},
"variables": {
"type": "object"
},
"selectors": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"checks": {
"type": "object",
"additionalProperties": {
"type": "object",
"additionalProperties": {
"oneOf": [
{
"type": "string"
},
{
"type": "object",
"properties": {
"selector": {
"type": "string"
},
"parameters": {
"type": "object"
},
"tags": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"selector"
]
}
]
}
}
}
},
"required": [
"selectors",
"checks"
]
}
"@
}
<#
.FUNCTION
New-RunbookFactory
.SYNOPSIS
Instantiates a new RunbookFactory object.
.DESCRIPTION
The `New-RunbookFactory` function creates an instance of the `RunbookFactory` class,
which is responsible for parsing runbook files and generating `Runbook` instances.
.OUTPUTS
[RunbookFactory]
A new instance of the RunbookFactory class.
.EXAMPLE
$factory = New-RunbookFactory
Creates a new instance of the RunbookFactory class, which can then be used
to parse runbook content.
.NOTES
Author: Casey Watson
Date: 2025-02-27
#>
function New-RunbookFactory {
return [RunbookFactory]::new()
}
<#
.FUNCTION
New-Runbook
.SYNOPSIS
Creates a new Runbook instance.
.DESCRIPTION
The `New-Runbook` function returns a new instance of the `Runbook` class.
It can optionally be initialized from a JSON string (-FromJson) or a JSON file (-FromJsonFile),
but not both.
.PARAMETER FromJson
JSON content to initialize the runbook. Cannot be used with -FromJsonFile.
.PARAMETER FromJsonFile
Path to a JSON file to initialize the runbook. Cannot be used with -FromJson.
.OUTPUTS
[Runbook]
A new `Runbook` instance.
.EXAMPLE
$runbook = New-Runbook
Creates an empty `Runbook` instance.
.EXAMPLE
$runbook = New-Runbook -FromJson $jsonContent
Initializes a `Runbook` instance from JSON content.
.EXAMPLE
$runbook = New-Runbook -FromJsonFile "C:\runbook.json"
Initializes a `Runbook` instance from a JSON file.
.NOTES
- If neither `-FromJson` nor `-FromJsonFile` is specified, an empty `Runbook` instance is returned.
- If both `-FromJson` and `-FromJsonFile` are specified, the function throws an error.
- The provided JSON must be valid and match the expected `Runbook` schema.
Author: Casey Watson
Date: 2025-02-27
#>
function New-Runbook {
param(
[Parameter(Mandatory = $false)]
[string] $FromJson,
[Parameter(Mandatory = $false)]
[ValidateScript({ Test-FileExists -Path $_ })]
[string] $FromJsonFile
)
if ($FromJson -or $FromJsonFile) {
if ($FromJson -and $FromJsonFile) {
throw "Cannot specify both -FromJson and -FromJsonFile."
}
else {
$runbookFactory = New-RunbookFactory
if ($FromJson) {
return $runbookFactory.ParseRunbookContent($FromJson)
}
elseif ($(Test-RunbookFile -Path $FromJsonFile)) {
return $runbookFactory.ParseRunbookFile($FromJsonFile)
}
}
}
else {
return [Runbook]::new()
}
}
<#
.FUNCTION
New-Recommendation
.SYNOPSIS
Creates a new `Recommendation` instance.
.DESCRIPTION
The `New-Recommendation` function initializes and returns a new instance of the `Recommendation` class.
This object contains metadata and evaluation logic for a specific runbook check.
.OUTPUTS
[Recommendation]
A new `Recommendation` object.
.EXAMPLE
$recommendation = New-Recommendation
Creates a new `Recommendation` instance.
.NOTES
Author: Casey Watson
Date: 2025-02-27
#>
function New-Recommendation {
return [Recommendation]::new()
}
<#
.FUNCTION
New-RunbookRecommendation
.SYNOPSIS
Creates a new `RunbookRecommendation` instance.
.DESCRIPTION
The `New-RunbookRecommendation` function initializes and returns a new instance of
the `RunbookRecommendation` class, which encapsulates metadata for a specific runbook
check, including its associated recommendation.
.OUTPUTS
[RunbookRecommendation]
A new `RunbookRecommendation` object.
.EXAMPLE
$runbookRecommendation = New-RunbookRecommendation
Creates a new `RunbookRecommendation` instance.
.NOTES
Author: Casey Watson
Date: 2025-02-27
#>
function New-RunbookRecommendation {
return [RunbookRecommendation]::new()
}
<#
.FUNCTION
New-RunbookCheckSet
.SYNOPSIS
Creates a new `RunbookCheckSet` instance.
.DESCRIPTION
The `New-RunbookCheckSet` function returns a new instance of the `RunbookCheckSet` class,
which represents a logical grouping of related runbook checks.
.OUTPUTS
[RunbookCheckSet]
A new `RunbookCheckSet` instance.
.EXAMPLE
$checkSet = New-RunbookCheckSet
Creates a new `RunbookCheckSet` instance.
.NOTES
Author: Casey Watson
Date: 2025-02-27
#>
function New-RunbookCheckSet {
return [RunbookCheckSet]::new()
}
<#
.FUNCTION
New-RunbookCheck
.SYNOPSIS
Creates a new `RunbookCheck` instance.
.DESCRIPTION
The `New-RunbookCheck` function returns a new instance of the `RunbookCheck` class,
representing an individual check within a runbook. Each check is associated with a selector
and contains parameterized logic for evaluating resource compliance.
.OUTPUTS
[RunbookCheck]
A new `RunbookCheck` instance.
.EXAMPLE
$check = New-RunbookCheck
Creates a new `RunbookCheck` instance.
.NOTES
Author: Casey Watson
Date: 2025-02-27
#>
function New-RunbookCheck {
return [RunbookCheck]::new()
}
<#
.FUNCTION
Build-RunbookQueries
.SYNOPSIS
Constructs queries for runbook checks.
.DESCRIPTION
The `Build-RunbookQueries` function generates a list of queries based on the check sets
and associated recommendations within a runbook. It dynamically merges parameters, variables,
and selectors to construct accurate queries for evaluation.
.PARAMETER Runbook
The `Runbook` object containing check sets, parameters, and selectors.
.PARAMETER Recommendations
An array of `RunbookRecommendation` objects defining the checks to be executed.
.PARAMETER ProgressId
(Optional) A progress indicator ID for `Write-Progress`.
.OUTPUTS
[RunbookQuery[]]
An array of `RunbookQuery` objects, each containing a check name, query, tags, and recommendation.
.EXAMPLE
$queries = Build-RunbookQueries -Runbook $runbook -Recommendations $recommendations
Constructs queries for the given runbook and recommendations.
.NOTES
- Queries are created by combining parameters, variables, and selectors with recommendation queries.
- Throws an error if a check references a missing selector.
- Ensures that only recommendations matching the runbook's check sets are processed.
Author: Casey Watson
Date: 2025-02-27
#>
function Build-RunbookQueries {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[Runbook] $Runbook,
[Parameter(Mandatory = $true)]
[RunbookRecommendation[]] $Recommendations,
[Parameter(Mandatory = $false)]
[int] $ProgressId = 30
)
$checkCount = 0
$checkIndex = 0
foreach ($checkSetKey in $Runbook.CheckSets.Keys) {
$checkSet = $Runbook.CheckSets[$checkSetKey]
$checkCount += $checkSet.Checks.Count
}
$queries = @()
$globalParameters = @{}
if ($Runbook.Parameters) {
foreach ($globalParameterKey in $Runbook.Parameters.Keys) {
$globalParameters[$globalParameterKey] = $Runbook.Parameters[$globalParameterKey].ToString()
}
}
if ($Runbook.Variables) {
foreach ($variableKey in $Runbook.Variables.Keys) {
$variableValue = $Runbook.Variables[$variableKey].ToString()
$globalParameters[$variableKey] = Merge-ParametersIntoString -Parameters $globalParameters -Into $variableValue
}
}
$recommendationsMap = @{}
foreach ($recommendation in $Recommendations) {
if (-not ($recommendationsMap.ContainsKey($recommendation.CheckSetName))) {
$recommendationsMap[$recommendation.CheckSetName] = @{}
}
$recommendationsMap[$recommendation.CheckSetName][$recommendation.CheckName] = $recommendation.Recommendation
}
foreach ($checkSetKey in $Runbook.CheckSets.Keys) {
if (-not ($recommendationsMap.ContainsKey($checkSetKey))) {
throw "No recommendations found for check set [$checkSetKey]."
}
$checkSet = $Runbook.CheckSets[$checkSetKey]
foreach ($checkKey in $checkSet.Checks.Keys) {
$checkIndex++
$pctComplete = (($checkIndex / $checkCount) * 100)
Write-Progress `
-Activity "Building runbook queries" `
-Status "$checkKey" `
-PercentComplete $pctComplete `
-Id $ProgressId
if (-not ($recommendationsMap[$checkSetKey].ContainsKey($checkKey))) {
throw "No recommendation found for check [$checkSetKey]:[$checkKey]."
}
$check = $checkSet.Checks[$checkKey]
$recommendation = $recommendationsMap[$checkSetKey][$checkKey]
$checkParameters = @{}
$checkParameters += $globalParameters
foreach ($checkParameterKey in $check.Parameters.Keys) {
$checkParameterValue = $check.Parameters[$checkParameterKey].ToString()
$checkParameters[$checkParameterKey] = Merge-ParametersIntoString -Parameters $globalParameters -Into $checkParameterValue
}
if ($Runbook.Selectors.ContainsKey($check.SelectorName)) {
$query = Merge-ParametersIntoString -Parameters $checkParameters -Into $recommendation.Query
foreach ($selectorKey in $Runbook.Selectors.Keys) {
$selector = $Runbook.Selectors[$selectorKey]
$selector = Merge-ParametersIntoString -Parameters $checkParameters -Into $selector
$query = $($query -replace "//\s*selector:$($selectorKey)", "| where $selector")
}
$selector = Merge-ParametersIntoString -Parameters $checkParameters -Into $Runbook.Selectors[$check.SelectorName]
$query = $($query -replace "//\s*selector", "| where $selector")
$queries += [RunbookQuery]@{
CheckSetName = $checkSetKey
CheckName = $checkKey
SelectorName = $check.SelectorName
Query = $query
Tags = $check.Tags
Recommendation = $recommendation
}
}
}
}
Write-Progress -Id $ProgressId -Completed
return $queries
}
<#
.FUNCTION
Merge-ParametersIntoString
.SYNOPSIS
Replaces placeholders in a string with parameter values.
.DESCRIPTION
The `Merge-ParametersIntoString` function iterates through a hashtable of parameters
and replaces placeholders in the input string with corresponding values.
Placeholders must follow the `{{Key}}` format.
.PARAMETER Parameters
A hashtable containing key-value pairs for replacement.
.PARAMETER Into
The string containing placeholders to be replaced.
.OUTPUTS
[string]
A string with placeholders replaced by their corresponding values.
.EXAMPLE
$params = @{ "Region" = "eastus"; "Env" = "Production" }
$result = Merge-ParametersIntoString -Parameters $params -Into "Deploying to {{Region}} in {{Env}}."
Returns: "Deploying to eastus in Production."
.NOTES
- Only placeholders matching keys in the hashtable are replaced.
- Uses simple string replacement logic.
Author: Casey Watson
Date: 2025-02-27
#>
function Merge-ParametersIntoString {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[hashtable] $Parameters,
[Parameter(Mandatory = $true)]
[string] $Into
)
foreach ($parameterKey in $Parameters.Keys) {
$Into = $Into.Replace("{{$parameterKey}}", $Parameters[$parameterKey])
}
return $Into
}
<#
.FUNCTION
Read-RunbookFile
.SYNOPSIS
Reads and parses a runbook file.
.DESCRIPTION
The `Read-RunbookFile` function validates and loads a runbook from a JSON file.
If the file is valid, it returns a parsed `Runbook` instance.
.PARAMETER Path
The file path to the runbook JSON file.
.OUTPUTS
[Runbook]
A parsed `Runbook` object.
.EXAMPLE
$runbook = Read-RunbookFile -Path "C:\runbook.json"
Reads and parses the specified runbook file.
.NOTES
- Uses `Test-RunbookFile` to validate the file before parsing.
- If validation fails, an error is thrown.
- The runbook is parsed using `RunbookFactory`.
Author: Casey Watson
Date: 2025-02-27
#>
function Read-RunbookFile {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[ValidateScript({ Test-FileExists $_ })]
[string] $Path
)
if (Test-RunbookFile -Path $Path) {
$runbookFactory = New-RunbookFactory
return $runbookFactory.ParseRunbookFile($Path)
}
}
<#
.FUNCTION
Write-RunbookFile
.SYNOPSIS
Saves a `Runbook` object to a JSON file.
.DESCRIPTION
The `Write-RunbookFile` function validates the provided `Runbook` object
and serializes it into a JSON file at the specified path.
.PARAMETER Runbook
The `Runbook` object to be saved.
.PARAMETER Path
The file path where the `Runbook` JSON should be written.
.OUTPUTS
None
.EXAMPLE
Write-RunbookFile -Runbook $myRunbook -Path "C:\runbook.json"
Saves the provided `Runbook` object to "C:\runbook.json".
.NOTES
- Ensures the `Runbook` is valid before saving.
- The output file is formatted in JSON.
Author: Casey Watson
Date: 2025-02-27
#>
function Write-RunbookFile {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[Runbook] $Runbook,
[Parameter(Mandatory = $true)]
[string] $Path
)
$Runbook.Validate()
$runbookFileContents = @{
query_paths = $Runbook.QueryPaths
parameters = $Runbook.Parameters
variables = $Runbook.Variables
selectors = $Runbook.Selectors
checks = @{}
}
foreach ($checkSetKey in $Runbook.CheckSets.Keys) {
$checkSet = $Runbook.CheckSets[$checkSetKey]
$checkSetContents = @{}
foreach ($checkKey in $checkSet.Checks.Keys) {
$check = $checkSet.Checks[$checkKey]
$checkContents = @{
parameters = ($check.Parameters ?? @{})
selector = $check.SelectorName
tags = ($check.Tags ?? @())
}
$checkSetContents[$checkKey] = $checkContents
}
$runbookFileContents.checks[$checkSetKey] = $checkSetContents
}
$runbookFileJson = $runbookFileContents | ConvertTo-Json -Depth 15
$runbookFileJson | Out-File -FilePath $Path -Force
}
<#
.FUNCTION
Test-RunbookFile
.SYNOPSIS
Validates a runbook file.
.DESCRIPTION
The `Test-RunbookFile` function checks whether a specified runbook JSON file is
valid according to the runbook schema. It ensures the JSON structure is correct
and adheres to expected schema requirements.
.PARAMETER Path
The full file path to the runbook JSON file.
.OUTPUTS
[bool]
Returns `$true` if the file is valid; otherwise, an error is thrown.
.EXAMPLE
$isValid = Test-RunbookFile -Path "C:\runbook.json"
Returns `$true` if the runbook is valid.
.NOTES
- Uses `Test-Json` to validate JSON structure.
- If validation fails, an error is thrown.
Author: Casey Watson
Date: 2025-02-27
#>
function Test-RunbookFile {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[ValidateScript({ Test-FileExists $_ })]
[string] $Path
)
$fileContent = Get-Content -Path $Path -Raw
if (-not ($fileContent | Test-Json -ErrorAction SilentlyContinue)) {
throw "[$Path] is not a valid JSON file."
}
if (-not ($fileContent | Test-Json -ErrorAction SilentlyContinue -Schema $(Get-RunbookSchema))) {
throw "[$Path] does not adhere to the runbook JSON schema. Run [Get-RunbookSchema] to get the schema."
}
$runbookFactory = New-RunbookFactory
$runbookFactory.ParseRunbookContent($fileContent).Validate()
return $true
}
<#
.FUNCTION
Build-RunbookSelectorReview
.SYNOPSIS
Builds a selector review for a runbook.
.DESCRIPTION
The `Build-RunbookSelectorReview` function evaluates each selector in a runbook,
resolves parameters, and executes queries to identify matching resources across
specified subscriptions.
.PARAMETER Runbook
The `Runbook` object containing selectors, parameters, and variables.
.PARAMETER SubscriptionIds
(Optional) An array of subscription IDs to scope the queries.
.OUTPUTS
[SelectorReview]
A `SelectorReview` object mapping each selector to its resolved query and matched resources.
.EXAMPLE
$review = Build-RunbookSelectorReview -Runbook $runbook -SubscriptionIds @("sub1", "sub2")
Generates a selector review to verify correct resource scoping.
.NOTES
- Selectors define which resources are included in a runbook.
- Misconfigured selectors may cause missing or incorrect results.
- Uses `Invoke-WAFQuery` to fetch matching resources.
Author: Casey Watson
Date: 2025-02-27
#>
function Build-RunbookSelectorReview {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[Runbook] $Runbook,
[Parameter(Mandatory = $false)]
[AllowEmptyCollection()]
[string[]] $SubscriptionIds
)
$selectorReview = [SelectorReview]::new()
$globalParameters = @{}
if ($Runbook.Parameters) {
foreach ($globalParameterKey in $Runbook.Parameters.Keys) {
$globalParameters[$globalParameterKey] = $Runbook.Parameters[$globalParameterKey].ToString()
}
}
if ($Runbook.Variables) {
foreach ($variableKey in $Runbook.Variables.Keys) {
$variableValue = $Runbook.Variables[$variableKey].ToString()
$globalParameters[$variableKey] = Merge-ParametersIntoString -Parameters $globalParameters -Into $variableValue
}
}
for ($i = 0; $i -lt $Runbook.Selectors.Keys.Count; $i++) {
$selectorKey = $Runbook.Selectors.Keys[$i]
$selector = Merge-ParametersIntoString -Parameters $globalParameters -Into $Runbook.Selectors[$selectorKey]
$pctComplete = ((($i + 1) / $Runbook.Selectors.Keys.Count) * 100)
Write-Progress `
-Activity "Building selector review..." `
-Status "$pctComplete% - Processing selector [$selectorKey]" `
-PercentComplete $pctComplete
$selectedResourceSet = [SelectedResourceSet]@{
Selector = $selector
ResourceGraphQuery = Build-SelectorResourceGraphQuery -Selector $selector
}
$selectedResources = Invoke-WAFQuery `
-Query $selectedResourceSet.SelectorResourceGraphQuery `
-SubscriptionIds $SubscriptionIds
foreach ($selectedResource in $selectedResources) {
$selectedResourceSet.Resources += [SelectedResource]@{
ResourceId = $selectedResource.id
ResourceType = $selectedResource.type
ResourceName = $selectedResource.name
ResourceLocation = $selectedResource.location
ResourceGroupName = $selectedResource.resourceGroup
ResourceTags = $(ConvertTo-Json $selectedResource.tags | ConvertFrom-Json -AsHashtable)
}
}
$selectorReview.Selectors[$selectorKey] = $selectedResourceSet
}
Write-Progress -Activity "Selector review built." -Completed
return $selectorReview
}
<#
.FUNCTION
Build-SelectorResourceGraphQuery
.SYNOPSIS
Constructs an Azure Resource Graph query from a selector.
.DESCRIPTION
The `Build-SelectorResourceGraphQuery` function creates a Resource Graph query
that filters resources based on the provided selector expression.
.PARAMETER Selector
The filter expression used to scope resources in the query.
.OUTPUTS
[string]
The formatted Azure Resource Graph query.
.EXAMPLE
$query = Build-SelectorResourceGraphQuery -Selector "type == 'Microsoft.Compute/virtualMachines'"
Generates a query to filter virtual machines.
.NOTES
- The selector should be a valid KQL (Kusto Query Language) expression.
- The output query can be executed using Azure Resource Graph API.
Author: Casey Watson
Date: 2025-02-27
#>
function Build-SelectorResourceGraphQuery {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[string] $Selector
)
return @"
resources
| where $Selector
| project id, type, location, name, resourceGroup, tags
"@
}