|
using namespace System.Collections |
|
using namespace System.Collections.Generic |
|
|
|
Connect-AzAccount -Identity -WA 0 | Out-Null |
|
|
|
# Days ago to rotate keys |
|
$dayLimit = 85 |
|
|
|
class ConnectionString { |
|
static [bool] TryParse([DictionaryEntry] $entry, [ref] $details) { |
|
if ($entry.Value -notmatch '^.+=.+;') { |
|
return $false |
|
} |
|
|
|
$data = $entry.Value -replace '\s*;\s*', "`n" | ConvertFrom-StringData |
|
if ($data.ContainsKey('accountName') -and $data.ContainsKey('accountKey')) { |
|
$details.Value = [pscustomobject]@{ |
|
StorageAccount = $data['accountName'] |
|
Name = $entry.Key |
|
Key = $data['accountKey'] |
|
} |
|
|
|
return $true |
|
} |
|
|
|
return $false |
|
} |
|
} |
|
|
|
function New-StorageAccountKey { |
|
param( |
|
[Parameter(Mandatory)] |
|
[string] $Id, |
|
|
|
[Parameter()] |
|
[ValidateNotNullOrEmpty()] |
|
[string[]] $KeyName) |
|
|
|
if ($KeyName.Count -eq 0) { |
|
$KeyName = 'key1', 'key2' |
|
} |
|
|
|
$uri = "https://management.azure.com${Id}/regenerateKey?api-version=2024-01-01" |
|
|
|
$result = [ordered]@{} |
|
foreach ($key in $KeyName) { |
|
$body = @{ keyName = $key } | ConvertTo-Json |
|
$req = Invoke-AzRestMethod $uri -Method POST -Payload $body |
|
|
|
if ($req.StatusCode -ne 200) { |
|
"Failed to rotate Key '$key' for Storage Account '$Id'. " + |
|
"Status Code $($req.StatusCode)." | Write-Warning |
|
} |
|
|
|
$json = $req.Content | ConvertFrom-Json -AsHashtable |
|
foreach ($keyResult in $json['keys']) { |
|
if ($keyResult['keyName'] -eq $key) { |
|
$result[$key] = $keyResult['value'] |
|
break |
|
} |
|
} |
|
} |
|
|
|
$result |
|
} |
|
|
|
function Get-FunctionAppSettings { |
|
param( |
|
[Parameter()] |
|
[ValidateNotNull()] |
|
[string[]] $SubscriptionScope) |
|
|
|
$uri = 'https://management.azure.com/{0}/config/appsettings/list?api-version=2024-04-01' |
|
$searchAzGraphSplat = @{ |
|
Query = " |
|
resources |
|
| where ['type'] == 'microsoft.web/sites' |
|
and ['kind'] has 'functionapp' |
|
| summarize functions = make_list(pack('id', id, 'name', name))" |
|
} |
|
|
|
if ($SubscriptionScope) { |
|
$searchAzGraphSplat['Subscription'] = $SubscriptionScope |
|
} |
|
|
|
$req = Search-AzGraph @searchAzGraphSplat |
|
|
|
foreach ($function in $req.Data[0].functions) { |
|
$req = Invoke-AzRestMethod ($uri -f $function.id) -Method POST |
|
|
|
if ($req.StatusCode -ne 200) { |
|
"Failed to get App Settings for Function App '$($function.name)'. " + |
|
"Status Code $($req.StatusCode)." | Write-Warning |
|
continue |
|
} |
|
|
|
$setting = $req.Content | ConvertFrom-Json -AsHashtable |
|
$stgAccounts = @{} |
|
foreach ($pair in $setting['properties'].GetEnumerator()) { |
|
$details = $null |
|
|
|
if (-not [ConnectionString]::TryParse($pair, [ref] $details)) { |
|
continue |
|
} |
|
|
|
foreach ($detail in $details) { |
|
if (-not $stgAccounts.ContainsKey($detail.StorageAccount)) { |
|
$stgAccounts[$detail.StorageAccount] = [List[object]]::new() |
|
} |
|
|
|
$stgAccounts[$detail.StorageAccount].Add($detail) |
|
} |
|
} |
|
|
|
[pscustomobject]@{ |
|
Id = $function.id |
|
Name = $function.name |
|
Settings = $setting |
|
StorageAccounts = $stgAccounts |
|
} |
|
} |
|
} |
|
|
|
function Get-StorageAccountsToRotate { |
|
param( |
|
[Parameter(Mandatory)] |
|
[int] $DayLimit, |
|
|
|
[Parameter()] |
|
[ValidateNotNull()] |
|
[string[]] $SubscriptionScope) |
|
|
|
$searchAzGraphSplat = @{ |
|
Query = " |
|
resources |
|
| where type == 'microsoft.storage/storageaccounts' |
|
| extend keyCreation = properties.keyCreationTime |
|
| extend |
|
key1 = now() - make_datetime(keyCreation.key1), |
|
key2 = now() - make_datetime(keyCreation.key2) |
|
| where key1 > ${DayLimit}d or key2 > ${DayLimit}d |
|
| project subscriptionId, resourceGroup, id, name, key1, key2" |
|
} |
|
|
|
if ($SubscriptionScope) { |
|
$searchAzGraphSplat['Subscription'] = $SubscriptionScope |
|
} |
|
|
|
do { |
|
$req = Search-AzGraph @searchAzGraphSplat |
|
$searchAzGraphSplat['SkipToken'] = $req.SkipToken |
|
if ($req.Count -gt 0) { |
|
$req.Data |
|
} |
|
} |
|
while ($req.SkipToken) |
|
} |
|
|
|
|
|
function Update-FunctionAppSettings { |
|
param( |
|
[Parameter(Mandatory)] |
|
[string] $Id, |
|
|
|
[Parameter(Mandatory)] |
|
[hashtable] $Properties) |
|
|
|
$retries = 10 |
|
while ($retries--) { |
|
$invokeAzRestMethodSplat = @{ |
|
Uri = "https://management.azure.com${Id}?api-version=2024-04-01" |
|
Method = 'PUT' |
|
Payload = @{ properties = $Properties } | ConvertTo-Json |
|
} |
|
|
|
$req = Invoke-AzRestMethod @invokeAzRestMethodSplat |
|
if ($req.StatusCode -eq 200) { |
|
return |
|
} |
|
|
|
Start-Sleep 1 |
|
} |
|
|
|
"Failed to update properties for '$Id'. Status Code $($req.StatusCode)." | |
|
Write-Warning |
|
} |
|
|
|
$getStorageAccountsToRotateSplat = @{ |
|
DayLimit = $dayLimit |
|
} |
|
|
|
$stgAccounts = Get-StorageAccountsToRotate @getStorageAccountsToRotateSplat |
|
|
|
if (-not $stgAccounts) { |
|
return 'No expiring Storage Account keys found.' |
|
} |
|
|
|
$apps = Get-FunctionAppSettings |
|
$rotatedAccounts = foreach ($account in $stgAccounts) { |
|
$newKeys = New-StorageAccountKey -Id $account.id |
|
$key = $newKeys.Values | Select-Object -First 1 |
|
|
|
foreach ($app in $apps) { |
|
if (-not $app.StorageAccounts.ContainsKey($account.name)) { |
|
continue |
|
} |
|
|
|
foreach ($variable in $app.StorageAccounts[$account.name]) { |
|
$connectionString = $app.Settings['properties'][$variable.Name].Replace( |
|
$variable.Key, $key) |
|
|
|
$app.Settings['properties'][$variable.Name] = $connectionString |
|
|
|
if (-not $app.Settings.ContainsKey('updatedVars')) { |
|
$app.Settings['updatedVars'] = [List[string]]::new() |
|
} |
|
|
|
$app.Settings['updatedVars'].Add($variable.Name) |
|
} |
|
} |
|
|
|
$account |
|
} |
|
|
|
@" |
|
Rotated Storage Accounts |
|
------------------------ |
|
$($rotatedAccounts | |
|
Format-Table subscriptionId, resourceGroup, name -AutoSize | |
|
Out-String) |
|
"@ |
|
|
|
$updatedVars = foreach ($app in $apps) { |
|
if ($app.Settings['updatedVars'].Count -eq 0) { |
|
continue |
|
} |
|
|
|
$updateFunctionAppSettingsSplat = @{ |
|
Id = $app.Settings['id'] |
|
Properties = $app.Settings['properties'] |
|
} |
|
|
|
Update-FunctionAppSettings @updateFunctionAppSettingsSplat |
|
|
|
[pscustomobject]@{ |
|
Name = $app.Name |
|
UpdatedEnvVars = $app.Settings.updatedVars -join ', ' |
|
} |
|
} |
|
|
|
if (-not $updatedVars) { |
|
return |
|
} |
|
|
|
@" |
|
Updated Variables for Function Apps |
|
----------------------------------- |
|
$($updatedVars | Format-Table -AutoSize | Out-String) |
|
"@ |