Framework/Core/CredHygiene/CredHygieneScript.ps1
Set-StrictMode -Version Latest class CredHygiene : CommandBase{ [string] $credName; [string] $credLocation; [int] $rotationInt; [int] $nextExpiry; [string] $comment; hidden [string] $AzSKTemp = (Join-Path $([Constants]::AzSKAppFolderPath) $([Constants]::RotationMetadataSubPath)); hidden [PSObject] $AzSKResourceGroup = $null; hidden [PSObject] $AzSKStorageAccount = $null; hidden [PSObject] $RotationMetadataContainer = $null; hidden [string] $RotationMetadataContainerName = [Constants]::RotationMetadataContainerName CredHygiene([string] $subscriptionId, [InvocationInfo] $invocationContext): Base($subscriptionId, $invocationContext) { $this.DoNotOpenOutputFolder = $true; } hidden [void] GetAzSKRotationMetadatContainer() { if($null -eq $this.AzSKStorageAccount) { $this.GetAzSKStorageAccount() } if($null -eq $this.AzSKStorageAccount) { return; } try { $containerObject = Get-AzStorageContainer -Context $this.AzSKStorageAccount.Context -Name $this.RotationMetadataContainerName -ErrorAction Stop $this.RotationMetadataContainer = $containerObject; } catch { try { New-AzStorageContainer -Context $this.AzSKStorageAccount.Context -Name $this.RotationMetadataContainerName -ErrorAction SilentlyContinue $containerObject = Get-AzStorageContainer -Context $this.AzSKStorageAccount.Context -Name $this.RotationMetadataContainerName -ErrorAction SilentlyContinue $this.RotationMetadataContainer = $containerObject; } catch { #Do nothing } } } hidden [void] GetAzSKStorageAccount() { if($null -eq $this.AzSKResourceGroup) { $this.GetAzSKRG(); } if($null -ne $this.AzSKResourceGroup) { $StorageAccount = Get-AzStorageAccount -ResourceGroupName $this.AzSKResourceGroup.ResourceGroupName | Where-Object {$_.StorageAccountName -like 'azsk*'} -ErrorAction SilentlyContinue #if no storage account found then it assumes that there is no control state feature is not used and if there are more than one storage account found it assumes the same $this.AzSKStorageAccount = $StorageAccount; } } hidden [PSObject] GetAzSKRG() { $azSKConfigData = [ConfigurationManager]::GetAzSKConfigData() $resourceGroup = Get-AzResourceGroup -Name $azSKConfigData.AzSKRGName -ErrorAction SilentlyContinue $this.AzSKResourceGroup = $resourceGroup return $resourceGroup; } [void] PrintDetails($credentialInfo,$messageType){ $this.PublishCustomMessage("`n") $this.PublishCustomMessage("Settings for the AzSK tracked credential [$($credentialInfo.credName)] `n`n", [MessageType]::Info) $this.PublishCustomMessage("`n") $table = $credentialInfo | Format-List @{Label = "Name"; Expression = { $_.credName }} , @{Label = "Location"; Expression = { $_.credLocation }}, @{Label = "Rotation interval (days)"; Expression = { $_.rotationInt }}, @{Label = "Credential Group"; Expression = { $_.credGroup }}, @{Label = "Created on"; Expression = { $_.firstUpdatedOn }}, @{Label = "Created by"; Expression = { $_.firstUpdatedBy }}, @{Label = "Last update"; Expression = { $_.lastUpdatedOn }}, @{Label = "Updated by"; Expression = { $_.lastUpdatedBy }}, @{Label = "Comment"; Expression = { $_.comment }} | Out-String $this.PublishCustomMessage($table, $messageType) if($credentialInfo.credLocation -eq "AppService"){ $this.PublishCustomMessage([Constants]::SingleDashLine); $this.PublishCustomMessage("Additional Details:"); $table = $credentialInfo | Format-List @{Label = "AppService Name"; Expression = { $_.resourceName }} , @{Label = "AppService config type"; Expression = { $_.appConfigType }}, @{Label = "AppService config name"; Expression = { $_.appConfigName }} | Out-String $this.PublishCustomMessage($table, $messageType) } if($credentialInfo.credLocation -eq "KeyVault"){ $this.PublishCustomMessage([Constants]::SingleDashLine); $this.PublishCustomMessage("Additional Details:"); $table = $credentialInfo | Format-List @{Label = "Key vault Name"; Expression = { $_.kvName }} , @{Label = "Credential type"; Expression = { $_.kvCredType }}, @{Label = "Credential name"; Expression = { $_.kvCredName }}, @{Label = "Expiry time"; Expression = { $_.expiryTime }}, @{Label = "Version"; Expression = { $_.version }} | Out-String $this.PublishCustomMessage($table, $messageType) } $this.PublishCustomMessage("`n") } [void] PrintInfo ($credentialInfo){ $this.PublishCustomMessage("`n") $this.PublishCustomMessage("Settings for the AzSK tracked credential [$($credentialInfo.credName)] `n`n", [MessageType]::Info) $this.PublishCustomMessage("`n") $table = $credentialInfo | Format-List @{Label = "Name"; Expression = { $_.credName }} , @{Label = "Location"; Expression = { $_.credLocation }}, @{Label = "Rotation interval (days)"; Expression = { $_.rotationInt }}, @{Label = "Credential Group"; Expression = { $_.credGroup }}, @{Label = "Comment"; Expression = { $_.comment }} | Out-String $this.PublishCustomMessage($table, [MessageType]::Default) if($credentialInfo.credLocation -eq "AppService"){ $this.PublishCustomMessage([Constants]::SingleDashLine); $this.PublishCustomMessage("Additional Details:"); $table = $credentialInfo | Format-List @{Label = "AppService Name"; Expression = { $_.resourceName }} , @{Label = "AppService config type"; Expression = { $_.appConfigType }}, @{Label = "AppService config name"; Expression = { $_.appConfigName }} | Out-String $this.PublishCustomMessage($table, [MessageType]::Default) } if($credentialInfo.credLocation -eq "KeyVault"){ $this.PublishCustomMessage([Constants]::SingleDashLine); $this.PublishCustomMessage("Additional Details:"); $table = $credentialInfo | Format-List @{Label = "Key vault Name"; Expression = { $_.kvName }} , @{Label = "Credential type"; Expression = { $_.kvCredType }}, @{Label = "Credential name"; Expression = { $_.kvCredName }}, @{Label = "Expiry time"; Expression = { $_.expiryTime }}, @{Label = "Version"; Expression = { $_.version }} | Out-String $this.PublishCustomMessage($table, [MessageType]::Default) } $this.PublishCustomMessage("`n") } [void] GetAlert($CredentialName,$DetailedView) { $file = Join-Path $($this.AzSKTemp) -ChildPath $($this.SubscriptionContext.SubscriptionId) | Join-Path -ChildPath $CredentialName $file += ".json" $this.GetAzSKRotationMetadatContainer() $tempSubPath = Join-Path $($this.AzSKTemp) $($this.SubscriptionContext.SubscriptionId) if(![string]::isnullorwhitespace($this.SubscriptionContext.SubscriptionId)){ if(-not (Test-Path $tempSubPath)) { New-Item -ItemType Directory -Path $tempSubPath -ErrorAction Stop | Out-Null } } else{ if(-not (Test-Path "$($this.AzSKTemp)")) { New-Item -ItemType Directory -Path "$($this.AzSKTemp)" -ErrorAction Stop | Out-Null } } $controlSettings = $this.LoadServerConfigFile("ControlSettings.json"); if($CredentialName){ $blobName = $CredentialName.ToLower() + ".json" $blobContent = Get-AzStorageBlobContent -Blob $blobName -Container $this.RotationMetadataContainerName -Context $this.AzSKStorageAccount.Context -Destination $file -Force -ErrorAction Ignore if($blobContent){ $credentialInfo = Get-ChildItem -Path $file -Force | Get-Content | ConvertFrom-Json $messageType = [MessageType]::Default $currentTime = [DateTime]::UtcNow; $lastRotatedTime = $credentialInfo.lastUpdatedOn; $expiryTime = $lastRotatedTime.AddDays($credentialInfo.rotationInt); if($expiryTime -le $currentTime.AddDays($controlSettings.SubscriptionCore.credHighTH)){ #Checking for expired/about to expire credentials $messageType = [MessageType]::Critical } elseif(($expiryTime -gt $currentTime.AddDays($controlSettings.SubscriptionCore.credHighTH)) -and ($expiryTime -le $currentTime.AddDays($controlSettings.SubscriptionCore.credModerateTH))){ #Checking for credentials nearing expiry. $messageType = [MessageType]::Warning } $this.PrintDetails($credentialInfo,$messageType); } else{ $this.PublishCustomMessage("Could not find an entry for credential [$CredentialName].",[MessageType]::Critical) $this.PublishCustomMessage("Please check the name or run Get-AzSKTrackedCredential to list all credentials currently being tracked for rotation/expiry.") } } else{ $blob = @(); $blob = Get-AzStorageBlob -Container $this.RotationMetadataContainerName -Context $this.AzSKStorageAccount.Context -ErrorAction Ignore if($blob){ $this.PublishCustomMessage("`n") $this.PublishCustomMessage("`nListing settings for all the credentials `n",[MessageType]::Update) # array to store cred info in ascending order of expiry time. $sortedBlob = @(); $blob | where { $_ | Get-AzStorageBlobContent -Destination $file -Force | Out-Null $credentialInfo = Get-ChildItem -Path $file -Force | Get-Content | ConvertFrom-Json $sortedBlob += $credentialInfo; } $sortedBlob = $sortedBlob | Sort-Object -Property @{Expression = {($_.lastUpdatedOn).AddDays($_.rotationInt)}; Descending = $False} $currentTime = [DateTime]::UtcNow; if($DetailedView){ $sortedBlob | where{ $lastRotatedTime = $_.lastUpdatedOn; $expiryTime = $lastRotatedTime.AddDays($_.rotationInt); $messageType = [MessageType]::Default if($expiryTime -le $currentTime.AddDays($controlSettings.SubscriptionCore.credHighTH)){ #Checking for expired/about to expire credentials $messageType = [MessageType]::Critical } elseif(($expiryTime -gt $currentTime.AddDays($controlSettings.SubscriptionCore.credHighTH)) -and ($expiryTime -le $currentTime.AddDays($controlSettings.SubscriptionCore.credModerateTH))){ #Checking for credentials nearing expiry. $messageType = [MessageType]::Warning } $this.PrintDetails($_,$messageType); } } else{ $table = $sortedBlob | Format-Table -AutoSize -Wrap @{Label = "Name"; Expression = { $_.credName }} , @{Label = "Location"; Expression = { $_.credLocation }}, @{Label = "Rotation interval (days)"; Expression = { $_.rotationInt }; align='left'}, @{Label = "Credential Group"; Expression = { $_.credGroup }}, @{Label = "Created on"; Expression = { $_.firstUpdatedOn }}, @{Label = "Created by"; Expression = { $_.firstUpdatedBy }}, @{Label = "Last update"; Expression = { $_.lastUpdatedOn }}, @{Label = "Updated by"; Expression = { $_.lastUpdatedBy }}, @{Label = "Comment"; Expression = { $_.comment }} | Out-String $this.PublishCustomMessage($table, [MessageType]::Default) } } else{ $this.PublishCustomMessage("No credential entries found for rotation/expiry alert.", [MessageType]::Critical) } } if(Test-Path $file) { Remove-Item -Path $file } } [void] NewAlert($CredentialLocation,$CredentialGroup) { $file = Join-Path $($this.AzSKTemp) -ChildPath $($this.SubscriptionContext.SubscriptionId) | Join-Path -ChildPath $($this.credName) $file += ".json" $this.GetAzSKRotationMetadatContainer() $blobName = ($this.credName).ToLower() + ".json" [bool] $found = $true; $tempSubPath = Join-Path $($this.AzSKTemp) $($this.SubscriptionContext.SubscriptionId) if(![string]::isnullorwhitespace($this.SubscriptionContext.SubscriptionId)){ if(-not (Test-Path $tempSubPath)) { New-Item -ItemType Directory -Path $tempSubPath -ErrorAction Stop | Out-Null } } else{ if(-not (Test-Path "$($this.AzSKTemp)")) { New-Item -ItemType Directory -Path "$($this.AzSKTemp)" -ErrorAction Stop | Out-Null } } $blobContent = Get-AzStorageBlobContent -Blob $blobName -Container $this.RotationMetadataContainerName -Context $this.AzSKStorageAccount.Context -Destination $file -Force -ErrorAction Ignore if($blobContent){ $this.PublishCustomMessage("Entry for the credential [$($this.credName)] already exists.", [MessageType]::Error); $this.PublishCustomMessage("Run Update-AzSKTrackedCredential to update alert configurations for the existing credential.", [MessageType]::Default) } else{ $startTime = [DateTime]::UtcNow $user = ([ContextHelper]::GetCurrentRMContext()).Account.Id $this.PublishCustomMessage("Onboarding the credential [$($this.credName)] for rotation/expiry notification", [MessageType]::Default) $credentialInfo = New-Object PSObject Add-Member -InputObject $credentialInfo -MemberType NoteProperty -Name credLocation -Value $this.credLocation Add-Member -InputObject $credentialInfo -MemberType NoteProperty -Name credName -Value $this.credName Add-Member -InputObject $credentialInfo -MemberType NoteProperty -Name rotationInt -Value $this.rotationInt Add-Member -InputObject $credentialInfo -MemberType NoteProperty -Name nextExpiry -Value $this.nextExpiry Add-Member -InputObject $credentialInfo -MemberType NoteProperty -Name firstUpdatedOn -Value $startTime Add-Member -InputObject $credentialInfo -MemberType NoteProperty -Name lastUpdatedOn -Value $startTime Add-Member -InputObject $credentialInfo -MemberType NoteProperty -Name comment -Value $this.comment Add-Member -InputObject $credentialInfo -MemberType NoteProperty -Name firstUpdatedBy -Value $user Add-Member -InputObject $credentialInfo -MemberType NoteProperty -Name lastUpdatedBy -Value $user if($credentialInfo.rotationInt -gt $credentialInfo.nextExpiry){ $credentialInfo.lastUpdatedOn = ($credentialInfo.lastUpdatedOn).AddDays($credentialInfo.nextExpiry - $credentialInfo.rotationInt) } if($CredentialLocation -eq "AppService"){ $appsvc = Get-AzWebApp -ErrorAction Ignore if(($appsvc|Measure-Object).Count -gt 0){ Write-Host "`nPlease select app service name from below:" -ForegroundColor Cyan $i=0; $appsvc | where{Write-Host "[$i] $($_.Name)" -ForegroundColor Cyan; $i++} $choice = Read-Host "App service name choice" while($choice -notin 0..($i-1)){ Write-Host "`nIncorrect value supplied." -ForegroundColor Red Write-Host "Please select app service name from below:" -ForegroundColor Cyan $i=0; $appsvc | where{Write-Host "[$i] $($_.Name)" -ForegroundColor Cyan; $i++} $choice = Read-Host "App service name choice" } $ResourceName = $appsvc[$choice].Name $rsc = Get-AzWebApp -Name $ResourceName $appConfig = $null $AppConfigType = $null Write-Host "`nPlease select app config type from below: `n[1]: Application Settings`n[2]: Connection Strings" -ForegroundColor Cyan $input = Read-Host "App config type" while(($input -ne 1) -and ($input -ne 2)){ Write-Host "`nIncorrect value supplied." -ForegroundColor Red Write-Host "Please select app config type from below: `n[1]: Application Settings`n[2]: Connection Strings" -ForegroundColor Cyan $input = Read-Host "App config type" } if($input -eq 1) { $AppConfigType = "Application Settings" $appConfig = $rsc.SiteConfig.AppSettings } elseif($input -eq 2) { $AppConfigType = "Connection Strings" $appConfig = $rsc.SiteConfig.ConnectionStrings } if(($appConfig|Measure-Object).Count -gt 0){ Write-Host "`nPlease select app config name from below:" -ForegroundColor Cyan $i=0; $appConfig | where{Write-Host "[$i] $($_.Name)" -ForegroundColor Cyan; $i++} $choice = Read-Host "App config name" while($choice -notin 0..($i-1)){ Write-Host "`nIncorrect value supplied." -ForegroundColor Red Write-Host "Please select app config name from below:" -ForegroundColor Cyan $i=0; $appConfig | where{Write-Host "[$i] $($_.Name)" -ForegroundColor Cyan; $i++} $choice = Read-Host "App config name" } $AppConfigName = $appConfig[$choice].Name Add-Member -InputObject $credentialInfo -MemberType NoteProperty -Name resourceName -Value $ResourceName Add-Member -InputObject $credentialInfo -MemberType NoteProperty -Name appConfigType -Value $AppConfigType Add-Member -InputObject $credentialInfo -MemberType NoteProperty -Name appConfigName -Value $AppConfigName } else{ $found = $false $this.PublishCustomMessage("There are no $AppConfigType in the app service [$ResourceName].", [MessageType]::Error) } } else{ $found = $false $this.PublishCustomMessage("There are no app services in your subscription.", [MessageType]::Error) } } if($CredentialLocation -eq "KeyVault"){ $keyVault = Get-AzKeyVault -ErrorAction Ignore if(($keyVault|Measure-Object).Count -gt 0){ Write-Host "`nPlease select key vault name from below:" -ForegroundColor Cyan $i=0; $keyVault | where{Write-Host "[$i] $($_.VaultName)" -ForegroundColor Cyan; $i++} $choice = Read-Host "Key vault name choice" while($choice -notin 0..($i-1)){ Write-Host "`nIncorrect value supplied." -ForegroundColor Red Write-Host "Please select key vault name from below:" -ForegroundColor Cyan $i=0; $keyVault | where{Write-Host "[$i] $($_.VaultName)" -ForegroundColor Cyan; $i++} $choice = Read-Host "Key vault name choice" } $KVName = $keyVault[$choice].VaultName Add-Member -InputObject $credentialInfo -MemberType NoteProperty -Name kvName -Value $KVName $KVCredentialType = $null $KVCredentialName = $null Write-Host "`nPlease select key vault credential type from below: `n[1]: Key`n[2]: Secret" -ForegroundColor Cyan $input = Read-Host "`Key Vault credential type" while(($input -ne 1) -and ($input -ne 2)){ Write-Host "`nIncorrect value supplied." -ForegroundColor Red Write-Host "Please select key vault credential type from below: `n[1]: Key`n[2]: Secret" -ForegroundColor Cyan $input = Read-Host "Key Vault credential type" } if($input -eq 1) { $KVCredentialType = "Key" Add-Member -InputObject $credentialInfo -MemberType NoteProperty -Name kvCredType -Value $KVCredentialType $keys = Get-AzKeyVaultKey -VaultName $KVName -ErrorAction Ignore if($keys){ Write-Host "`nPlease select key name from below:" -ForegroundColor Cyan $i=0; $keys | where{Write-Host "[$i] $($_.Name)" -ForegroundColor Cyan; $i++} $choice = Read-Host "Key name choice" while($choice -notin 0..($i-1)){ Write-Host "`nIncorrect value supplied." -ForegroundColor Red Write-Host "Please select key name from below:" -ForegroundColor Cyan $i=0; $keys | where{Write-Host "[$i] $($_.Name)" -ForegroundColor Cyan; $i++} $choice = Read-Host "Key name choice" } $KVCredentialName = $keys[$choice].Name Add-Member -InputObject $credentialInfo -MemberType NoteProperty -Name kvCredName -Value $KVCredentialName $key = Get-AzKeyVaultKey -VaultName $KVName -Name $KVCredentialName -ErrorAction Ignore Add-Member -InputObject $credentialInfo -MemberType NoteProperty -Name expiryTime -Value $key.Expires if($key.Expires){ if($startTime.AddDays($credentialInfo.rotationInt) -gt $key.Expires){ $credentialInfo.lastUpdatedOn = ($key.Expires).AddDays(-($credentialInfo.rotationInt)) } else{ $credentialInfo.lastUpdatedOn = $startTime } } Add-Member -InputObject $credentialInfo -MemberType NoteProperty -Name version -Value $key.Version } else{ $found = $false; $this.PublishCustomMessage("Either there are no keys in the key vault [$KVName] or you do not have sufficient permissions to access them.",[MessageType]::Error) } } elseif($input -eq 2) { $KVCredentialType = "Secret" Add-Member -InputObject $credentialInfo -MemberType NoteProperty -Name kvCredType -Value $KVCredentialType $secrets = Get-AzKeyVaultSecret -VaultName $KVName -ErrorAction Ignore if($secrets){ Write-Host "`nPlease select secret name from below:" -ForegroundColor Cyan $i=0; $secrets | where{Write-Host "[$i] $($_.Name)" -ForegroundColor Cyan; $i++} $choice = Read-Host "Secret name choice" while($choice -notin 0..($i-1)){ Write-Host "`nIncorrect value supplied." -ForegroundColor Red Write-Host "Please select secret name from below:" -ForegroundColor Cyan $i=0; $secrets | where{Write-Host "[$i] $($_.Name)" -ForegroundColor Cyan; $i++} $choice = Read-Host "Secret name choice" } $KVCredentialName = $secrets[$choice].Name Add-Member -InputObject $credentialInfo -MemberType NoteProperty -Name kvCredName -Value $KVCredentialName $secret = Get-AzKeyVaultSecret -VaultName $KVName -Name $KVCredentialName -ErrorAction Ignore Add-Member -InputObject $credentialInfo -MemberType NoteProperty -Name expiryTime -Value $secret.Expires if($secret.Expires){ if($startTime.AddDays($credentialInfo.rotationInt) -gt $secret.Expires){ $credentialInfo.lastUpdatedOn = ($secret.Expires).AddDays(-($credentialInfo.rotationInt)) } else{ $credentialInfo.lastUpdatedOn = $startTime } } Add-Member -InputObject $credentialInfo -MemberType NoteProperty -Name version -Value $secret.Version } else{ $found = $false; $this.PublishCustomMessage("Either there are no secrets in the key vault [$KVName] or you do not have sufficient permissions to access them.",[MessageType]::Error) } } } else{ $found = $false $this.PublishCustomMessage("There are no key vaults in your subscription.", [MessageType]::Error) } } $ag = $null; if($found -and $CredentialGroup){ $actionGroups = Get-AzActionGroup -ErrorAction Ignore -WarningAction Ignore if(($actionGroups|Measure-Object).Count -gt 0){ $ag = $actionGroups | where{$_.Name -eq $CredentialGroup} if(-not $ag){ $this.PublishCustomMessage("The action group [$CredentialGroup] does not exist in the subscription.",[MessageType]::Error) Write-Host "`nPlease select action group name from below:" -ForegroundColor Cyan Write-Host "[0] Create new credential group" -ForegroundColor Cyan $i=1; $actionGroups | where{Write-Host "[$i] $($_.Name)" -ForegroundColor Cyan; $i++} $choice = Read-Host "Credential group choice" while($choice -notin 0..$i){ Write-Host "`nIncorrect value supplied." -ForegroundColor Red Write-Host "Please select action group name from below:" -ForegroundColor Cyan Write-Host "[0] Create new credential group" -ForegroundColor Cyan $i=1; $actionGroups | where{Write-Host "[$i] $($_.Name)" -ForegroundColor Cyan; $i++} $choice = Read-Host "Credential group choice" } if($choice -ne 0){ $ag = $actionGroups[$choice-1] $CredentialGroup = $ag.Name } elseif($choice -eq 0){ $emailR = $null; $phoneR = $null; $rgName = [ConfigurationManager]::GetAzSKConfigData().AzSKRGName Write-Host "`nInitiating flow for creating a new credential group..." -ForegroundColor Yellow $name = Read-Host "Please enter the name of the credential group" $shortName = Read-Host "Please enter the short name (less than 12 characters) of the credential group" Write-Host "`nPlease enter: `n[0]: to skip email configuration `n[1]: to configure email for the credential group" -ForegroundColor Cyan $choice = Read-Host "User choice" while($choice -notin 0..1){ Write-Host "`nIncorrect value supplied." -ForegroundColor Red Write-Host "`nPlease enter: `n[0]: to skip email configuration `n[1]: to configure email for the credential group" -ForegroundColor Cyan $choice = Read-Host "User choice" } if($choice -eq 1){ $email = Read-Host "Enter a valid email address" $emailR = New-AzActionGroupReceiver -Name 'email' -EmailReceiver -EmailAddress $email -ErrorAction Ignore } Write-Host "`nPlease enter: `n[0]: to skip SMS configuration `n[1]: to configure SMS for the credential group" -ForegroundColor Cyan $choice = Read-Host "User choice" while($choice -notin 0..1){ Write-Host "`nIncorrect value supplied." -ForegroundColor Red Write-Host "`nPlease enter: `n[0]: to skip SMS configuration `n[1]: to configure SMS for the credential group" -ForegroundColor Cyan $choice = Read-Host "User choice" } if($choice -eq 1){ $countryCode = Read-Host "Enter a valid country code" $phone = Read-Host "Enter a valid contact number" $phoneR = New-AzActionGroupReceiver -Name 'SMS' -SmsReceiver -CountryCode $countryCode -PhoneNumber $phone -ErrorAction Ignore } if($emailR -and $phoneR){ $ag = Set-AzActionGroup -Name $name -ResourceGroupName $rgName -ShortName $shortName -Receiver $emailR,$phoneR -ErrorAction Ignore -WarningAction Ignore } elseif($emailR){ $ag = Set-AzActionGroup -Name $name -ResourceGroupName $rgName -ShortName $shortName -Receiver $emailR -ErrorAction Ignore -WarningAction Ignore } elseif($phoneR) { $ag = Set-AzActionGroup -Name $name -ResourceGroupName $rgName -ShortName $shortName -Receiver $phoneR -ErrorAction Ignore -WarningAction Ignore } } } if($ag){ $CredentialGroup = $ag.Name $this.InstallCredentialGroupAlert($ag); Add-Member -InputObject $credentialInfo -MemberType NoteProperty -Name credGroup -Value $CredentialGroup } else{ $this.PublishCustomMessage("Error occured while creating the new credential group.", [MessageType]::Error) } } else{ Write-Host "`nPlease enter [0] to create a new credential group" -ForegroundColor Cyan $choice = Read-Host "User choice" if($choice -eq 0){ $emailR = $null; $phoneR = $null; $rgName = [ConfigurationManager]::GetAzSKConfigData().AzSKRGName Write-Host "`nInitiating flow for creating a new credential group..." -ForegroundColor Yellow $name = Read-Host "Please enter the name of the credential group" $shortName = Read-Host "Please enter the short name (less than 12 characters) of the credential group" Write-Host "`nPlease enter: `n[0]: to skip email configuration `n[1]: to configure email for the credential group" -ForegroundColor Cyan $choice = Read-Host "User choice" while($choice -notin 0..1){ Write-Host "`nIncorrect value supplied." -ForegroundColor Red Write-Host "`nPlease enter: `n[0]: to skip email configuration `n[1]: to configure email for the credential group" -ForegroundColor Cyan $choice = Read-Host "User choice" } if($choice -eq 1){ $email = Read-Host "Enter a valid email address" $emailR = New-AzActionGroupReceiver -Name 'email' -EmailReceiver -EmailAddress $email -ErrorAction Ignore } Write-Host "`nPlease enter: `n[0]: to skip SMS configuration `n[1]: to configure SMS for the credential group" -ForegroundColor Cyan $choice = Read-Host "User choice" while($choice -notin 0..1){ Write-Host "`nIncorrect value supplied." -ForegroundColor Red Write-Host "`nPlease enter: `n[0]: to skip SMS configuration `n[1]: to configure SMS for the credential group" -ForegroundColor Cyan $choice = Read-Host "User choice" } if($choice -eq 1){ $countryCode = Read-Host "Enter a valid country code" $phone = Read-Host "Enter a valid contact number" $phoneR = New-AzActionGroupReceiver -Name 'SMS' -SmsReceiver -CountryCode $countryCode -PhoneNumber $phone -ErrorAction Ignore } if($emailR -and $phoneR){ $ag = Set-AzActionGroup -Name $name -ResourceGroupName $rgName -ShortName $shortName -Receiver $emailR,$phoneR -ErrorAction Ignore -WarningAction Ignore } elseif($emailR){ $ag = Set-AzActionGroup -Name $name -ResourceGroupName $rgName -ShortName $shortName -Receiver $emailR -ErrorAction Ignore -WarningAction Ignore } elseif($phoneR) { $ag = Set-AzActionGroup -Name $name -ResourceGroupName $rgName -ShortName $shortName -Receiver $phoneR -ErrorAction Ignore -WarningAction Ignore } if($ag){ $CredentialGroup = $ag.Name $this.InstallCredentialGroupAlert($ag); Add-Member -InputObject $credentialInfo -MemberType NoteProperty -Name credGroup -Value $CredentialGroup } else{ $this.PublishCustomMessage("Error occured while creating the new credential group.", [MessageType]::Error) } } else{ $this.PublishCustomMessage("There are no action groups in your subscription.", [MessageType]::Error) } } } if($found){ $credentialInfo | ConvertTo-Json -Depth 10 | Out-File $file -Force Set-AzStorageBlobContent -Blob $blobName -Container $this.RotationMetadataContainerName -Context $this.AzSKStorageAccount.Context -File $file -Force | Out-Null $this.PublishCustomMessage("Successfully onboarded the credential [$($this.credName)] for rotation/expiry notification", [MessageType]::Update) $this.PrintInfo($credentialInfo); } else{ $this.PublishCustomMessage("Could not onboard the credential [$($this.credName)] for rotation/expiry notification", [MessageType]::Error) } } if(Test-Path $file) { Remove-Item -Path $file } $this.PublishCustomMessage("Run Get-AzSKTrackedCredential to list all the credentials onboarded for rotation/expiry notification.") } [void] RemoveAlert($CredentialName,$Force) { $file = Join-Path $($this.AzSKTemp) -ChildPath $($this.SubscriptionContext.SubscriptionId) | Join-Path -ChildPath $CredentialName $file += ".json" $this.GetAzSKRotationMetadatContainer() $blobName = $CredentialName.ToLower() + ".json" $tempSubPath = Join-Path $($this.AzSKTemp) $($this.SubscriptionContext.SubscriptionId) if(![string]::isnullorwhitespace($this.SubscriptionContext.SubscriptionId)){ if(-not (Test-Path $tempSubPath)) { New-Item -ItemType Directory -Path $tempSubPath -ErrorAction Stop | Out-Null } } else{ if(-not (Test-Path "$($this.AzSKTemp)")) { New-Item -ItemType Directory -Path "$($this.AzSKTemp)" -ErrorAction Stop | Out-Null } } $blobContent = Get-AzStorageBlobContent -Blob $blobName -Container $this.RotationMetadataContainerName -Context $this.AzSKStorageAccount.Context -Destination $file -Force -ErrorAction Ignore if($blobContent){ $title = "Confirm the removal of credential rotation/expiry notification" $yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "This means Yes" $no = New-Object System.Management.Automation.Host.ChoiceDescription "&No", "This means No" $options = [System.Management.Automation.Host.ChoiceDescription[]]($yes, $no) #below is hack for removing error due to strict mode - host variable is not assigned in the method $host = $host # Ask for confirmation only if force switch is not present $result = 0 if(!$Force) { #user confirmation before deleting container $storageConfirmMsg = "Are you sure you want to remove expiry alerting on [$CredentialName] credential?" $result = $host.ui.PromptForChoice($title, $storageConfirmMsg, $options, 1) } if($result -eq 0) { #user selected yes $this.PublishCustomMessage("Removing the rotation/expiry notification on the credential [$CredentialName]", [MessageType]::Default) $blobContent | Remove-AzStorageBlob $this.PublishCustomMessage("Successfully removed the rotation/expiry notification on the credential [$CredentialName]", [MessageType]::Update) } #user selected no in confirmation box else { $this.PublishCustomMessage("You have chosen not to remove rotation/expiry notification on the credential [$CredentialName]", [MessageType]::Default) } } else{ $this.PublishCustomMessage("Could not find an entry for credential [$CredentialName].", [MessageType]::Critical) } if(Test-Path $file) { Remove-Item -Path $file } } [void] UpdateAlert($CredentialName,$RotationIntervalInDays,$CredentialGroup,$UpdateCredential,$ResetLastUpdate,$Comment) { $file = Join-Path $($this.AzSKTemp) -ChildPath $($this.SubscriptionContext.SubscriptionId) | Join-Path -ChildPath $CredentialName $file += ".json" $this.GetAzSKRotationMetadatContainer() $blobName = $CredentialName.ToLower() + ".json" $tempSubPath = Join-Path $($this.AzSKTemp) $($this.SubscriptionContext.SubscriptionId) if(![string]::isnullorwhitespace($this.SubscriptionContext.SubscriptionId)){ if(-not (Test-Path $tempSubPath)) { New-Item -ItemType Directory -Path $tempSubPath -ErrorAction Stop | Out-Null } } else{ if(-not (Test-Path "$($this.AzSKTemp)")) { New-Item -ItemType Directory -Path "$($this.AzSKTemp)" -ErrorAction Stop | Out-Null } } $blobContent = Get-AzStorageBlobContent -Blob $blobName -Container $this.RotationMetadataContainerName -Context $this.AzSKStorageAccount.Context -Destination $file -Force -ErrorAction Ignore if($blobContent){ $this.PublishCustomMessage("Updating settings for AzSK tracked credential [$CredentialName]", [MessageType]::Default) $credentialInfo = Get-ChildItem -Path $file -Force | Get-Content | ConvertFrom-Json $user = ([ContextHelper]::GetCurrentRMContext()).Account.Id; $currentTime = [DateTime]::UtcNow if ($RotationIntervalInDays) { $credentialInfo.rotationInt = $RotationIntervalInDays; } $ag = $null; if($CredentialGroup){ $actionGroups = Get-AzActionGroup -ErrorAction Ignore -WarningAction Ignore if(($actionGroups|Measure-Object).Count -gt 0){ $ag = $actionGroups | where{$_.Name -eq $CredentialGroup} if(-not $ag){ $this.PublishCustomMessage("The action group [$CredentialGroup] does not exist in the subscription.",[MessageType]::Error) Write-Host "`nPlease select action group name from below:" -ForegroundColor Cyan $i=0; $actionGroups | where{Write-Host "[$i] $($_.Name)" -ForegroundColor Cyan; $i++} $choice = Read-Host "Credential group choice" while($choice -notin 0..($i-1)){ Write-Host "`nIncorrect value supplied." -ForegroundColor Red Write-Host "Please select action group name from below:" -ForegroundColor Cyan $i=0; $actionGroups | where{Write-Host "[$i] $($_.Name)" -ForegroundColor Cyan; $i++} $choice = Read-Host "Credential group choice" } $ag = $actionGroups[$choice] $CredentialGroup = $ag.Name } $this.InstallCredentialGroupAlert($ag); if([Helpers]::CheckMember($credentialInfo,"credGroup")){ $credentialInfo.credGroup = $CredentialGroup } else{ Add-Member -InputObject $credentialInfo -MemberType NoteProperty -Name credGroup -Value $CredentialGroup } } else{ $this.PublishCustomMessage("Could not update the credential group for the credential [$CredentialName] as there are no action groups in your subscription.", [MessageType]::Error) } } if($UpdateCredential){ if($credentialInfo.credLocation -eq "AppService"){ $resource = Get-AzWebApp -Name $credentialInfo.resourceName $hash = @{} if($credentialInfo.appConfigType -eq "Application Settings"){ $this.PublishCustomMessage("Updating the application setting [$($credentialInfo.appConfigName)] in the app service [$($credentialInfo.resourceName)]", [MessageType]::Default) $appConfig = $resource.SiteConfig.AppSettings $appConfigValue = Read-Host "Enter the new secret for the application setting - [$($credentialInfo.appConfigName)]" -AsSecureString $appConfigValue = [System.Runtime.InteropServices.marshal]::PtrToStringAuto([System.Runtime.InteropServices.marshal]::SecureStringToBSTR($appConfigValue)) foreach ($setting in $appConfig) { if($setting.Name -eq $credentialInfo.appConfigName){ $hash[$setting.Name] = $appConfigValue } else{ $hash[$setting.Name] = $setting.Value } } } elseif($credentialInfo.appConfigType -eq "Connection Strings"){ $this.PublishCustomMessage("Updating the connection string [$($credentialInfo.appConfigName)] in the app service [$($credentialInfo.resourceName)]", [MessageType]::Default) $appConfig = $resource.SiteConfig.ConnectionStrings $appConfigValue = Read-Host "Enter the new secret for the connection string - [$($credentialInfo.appConfigName)]" -AsSecureString $appConfigValue = [System.Runtime.InteropServices.marshal]::PtrToStringAuto([System.Runtime.InteropServices.marshal]::SecureStringToBSTR($appConfigValue)) foreach ($setting in $appConfig) { if($setting.Name -eq $credentialInfo.appConfigName){ $hash[$setting.Name] = @{Type=$setting.Type.ToString(); Value=$appConfigValue} } else{ $hash[$setting.Name] = @{Type=$setting.Type.ToString(); Value=$setting.ConnectionString} } } } if($credentialInfo.appConfigType -eq "Application Settings"){ Set-AzWebApp -Name $credentialInfo.resourceName -ResourceGroupName $resource.ResourceGroup -AppSettings $hash | Out-Null $this.PublishCustomMessage("Successfully updated the application setting [$($credentialInfo.appConfigName)] in the app service [$($credentialInfo.resourceName)]", [MessageType]::Update) } elseif($credentialInfo.appConfigType -eq "Connection Strings"){ Set-AzWebApp -Name $credentialInfo.resourceName -ResourceGroupName $resource.ResourceGroup -ConnectionStrings $hash | Out-Null $this.PublishCustomMessage("Successfully updated connection string [$($credentialInfo.appConfigName)] in the app service [$($credentialInfo.resourceName)]", [MessageType]::Update) } } if($credentialInfo.credLocation -eq "KeyVault") { if($credentialInfo.kvCredType -eq "Key") { $currentKey = Get-AzKeyVaultKey -VaultName $credentialInfo.kvName -Name $credentialInfo.kvCredName -ErrorAction SilentlyContinue if(($currentKey | Measure-Object).Count -ne 0) { $expiryTime = $currentTime.AddDays($credentialInfo.rotationInt) $this.PublishCustomMessage("Updating the key [$($credentialInfo.kvCredName)] in the key vault [$($credentialInfo.kvName)]", [MessageType]::Default) $newKey = Add-AzKeyVaultKey -VaultName $credentialInfo.kvName -Name $credentialInfo.kvCredName -Expires $ExpiryTime -Destination Software $credentialInfo.expiryTime = $newKey.Expires $credentialInfo.version = $newKey.Version $this.PublishCustomMessage("Successfully updated the key [$($credentialInfo.kvCredName)] in the key vault [$($credentialInfo.kvName)]", [MessageType]::Update) } } elseif($credentialInfo.kvCredType -eq "Secret") { $currentSecret = Get-AzKeyVaultSecret -VaultName $credentialInfo.kvName -Name $credentialInfo.kvCredName -ErrorAction SilentlyContinue if(($currentSecret | Measure-Object).Count -ne 0) { $expiryTime = $currentTime.AddDays($credentialInfo.rotationInt) $this.PublishCustomMessage("Updating the secret [$($credentialInfo.kvCredName)] in the key vault [$($credentialInfo.kvName)]", [MessageType]::Default) $secret = Read-Host "Enter the new secret value for the key vault credential - [$($credentialInfo.kvCredName)]" -AsSecureString $newSecret = Set-AzKeyVaultSecret -VaultName $credentialInfo.kvName -Name $credentialInfo.kvCredName -SecretValue $secret -Expires $ExpiryTime $credentialInfo.expiryTime = $newSecret.Expires $credentialInfo.version = $newSecret.Version $this.PublishCustomMessage("Successfully updated the secret [$($credentialInfo.kvCredName)] in the key vault [$($credentialInfo.kvName)]", [MessageType]::Update) } } $this.PublishCustomMessage("To avoid impacting availability, the previous version of this $($credentialInfo.kvCredType) has been kept in enabled state.", [MessageType]::Warning) $this.PublishCustomMessage("If you are using key/secret URLs with specific version identifier in them, please update to the new version before disabling it.") } $credentialInfo.lastUpdatedOn = $currentTime $credentialInfo.lastUpdatedBy = $user } else{ if($ResetLastUpdate){ $credentialInfo.lastUpdatedOn = $currentTime $credentialInfo.lastUpdatedBy = $user } if([Helpers]::CheckMember($credentialInfo,'expiryTime')){ if($credentialInfo.expiryTime){ $credentialInfo.lastUpdatedOn = ($credentialInfo.expiryTime).AddDays(-$credentialInfo.rotationInt) } } } $credentialInfo.comment = $Comment $credentialInfo | ConvertTo-Json -Depth 10 | Out-File $file -Force Set-AzStorageBlobContent -Blob $blobName -Container $this.RotationMetadataContainerName -Context $this.AzSKStorageAccount.Context -File $file -Force | Out-Null $this.PublishCustomMessage("Successfully updated settings for AzSK tracked credential [$CredentialName]", [MessageType]::Update) $this.PrintInfo($credentialInfo); } else{ $this.PublishCustomMessage("Could not find an entry for credential [$CredentialName].", [MessageType]::Critical) } if(Test-Path $file) { Remove-Item -Path $file } } [void] InstallAlert($AlertEmail) { $rgName = [ConfigurationManager]::GetAzSKConfigData().AzSKRGName $credHygieneAGName = [Constants]::CredHygieneActionGroupName $credHygieneAGShortName = [Constants]::CredHygieneActionGroupShortName $email = New-AzActionGroupReceiver -Name $AlertEmail -EmailReceiver -EmailAddress $AlertEmail $actionGroup = Set-AzActionGroup -Name $credHygieneAGName -ResourceGroupName $rgName -ShortName $credHygieneAGShortName -Receiver $email -ErrorAction Ignore -WarningAction SilentlyContinue if($actionGroup){ # We are using LAWS from the same sub for Alert REST API call. $automationAccDetails= Get-AzAutomationAccount -ResourceGroupName $rgName -ErrorAction SilentlyContinue if($automationAccDetails) { #Fetch LAWS Id from CA variables $laWSId = Get-AzAutomationVariable -ResourceGroupName $automationAccDetails.ResourceGroupName -AutomationAccountName $automationAccDetails.AutomationAccountName -Name "LAWSId" -ErrorAction SilentlyContinue if($laWSId){ $laWS = Get-AzOperationalInsightsWorkspace | where{$_.CustomerId -eq $laWSId.Value} # Verify whether the LA resource ith the WS id exists. if($laWS){ $body = [ConfigurationManager]::LoadServerConfigFile("CredentialHygieneAlert.json"); $dataSourceId = $body.properties.source.dataSourceId | ConvertTo-Json -Depth 10 $dataSourceId = $dataSourceId.Replace("{0}",$this.SubscriptionContext.SubscriptionId).Replace("{1}",$laWS.ResourceGroupName).Replace("{2}",$laWS.CustomerId) | ConvertFrom-Json $body.properties.source.dataSourceId = $dataSourceId $ag = $body.properties.action.aznsAction.actionGroup[0] | ConvertTo-Json -Depth 10 $ag = $ag.Replace("{3}",$actionGroup.Id) | ConvertFrom-Json $body.properties.action.aznsAction.actionGroup[0] = $ag $ResourceAppIdURI = [WebRequestHelper]::GetResourceManagerUrl() $uri = $ResourceAppIdURI + "subscriptions/$($this.SubscriptionContext.SubscriptionId)/resourcegroups/$($laWS.ResourceGroupName)/providers/microsoft.insights/scheduledQueryRules/AzSK_CredHygiene_Alert?api-version=2018-04-16" [WebRequestHelper]::InvokeWebRequest([Microsoft.PowerShell.Commands.WebRequestMethod]::Put, $uri, $body); $this.PublishCustomMessage("Alert for the credential group [$credHygieneAGName] is successfully configured."); } else{ # LA resource not found. $this.PublishCustomMessage("Log Analytics resource with workspace Id [$($laWSId.Value)] provided in the CA automation account variables doesn't exist. Please verify the value of Log Analytics workspace Id.", [MessageType]::Error) $this.PublishCustomMessage("Couldn't create credential hygiene alert for the current subscription.", [MessageType]::Error) $this.PublishCustomMessage("Run Update-AzSKContinuousAssurance to update the Log Analytics workspace Id with the correct value in the CA automation account.") } } else{ # LAWS id variable not found. $this.PublishCustomMessage("Log Analytics workspace Id not found in the CA automation account variables.", [MessageType]::Error) $this.PublishCustomMessage("Couldn't create credential hygiene alert for the current subscription.", [MessageType]::Error) $this.PublishCustomMessage("Run Update-AzSKContinuousAssurance to update the Log Analytics workspace Id with the correct value in the CA automation account.") } } else{ # CA setup not found. $this.PublishCustomMessage("Continuous Assurance setup was not found in the current subscription.", [MessageType]::Error) $this.PublishCustomMessage("Couldn't create credential hygiene alert for the current subscription.", [MessageType]::Error) } } else{ # Action group not found/created. $this.PublishCustomMessage("Couldn't create action group [$credHygieneAGName] for credential hygiene alerts.", [MessageType]::Error) $this.PublishCustomMessage("Couldn't create credential hygiene alert for the current subscription.", [MessageType]::Error) } } [void] InstallCredentialGroupAlert($actionGroup) { $rgName = [ConfigurationManager]::GetAzSKConfigData().AzSKRGName # We are using LAWS from the same sub for Alert REST API call. $automationAccDetails= Get-AzAutomationAccount -ResourceGroupName $rgName -ErrorAction SilentlyContinue if($automationAccDetails) { #Fetch LAWS Id from CA variables $laWSId = Get-AzAutomationVariable -ResourceGroupName $automationAccDetails.ResourceGroupName -AutomationAccountName $automationAccDetails.AutomationAccountName -Name "LAWSId" -ErrorAction SilentlyContinue if($laWSId){ $laWS = Get-AzOperationalInsightsWorkspace | where{$_.CustomerId -eq $laWSId.Value} # Verify whether the LA resource ith the WS id exists. if($laWS){ $body = [ConfigurationManager]::LoadServerConfigFile("CredentialHygieneAlert_CredentialGroup.json"); $dataSourceId = $body.properties.source.dataSourceId | ConvertTo-Json -Depth 10 $dataSourceId = $dataSourceId.Replace("{0}",$this.SubscriptionContext.SubscriptionId).Replace("{1}",$laWS.ResourceGroupName).Replace("{2}",$laWS.CustomerId) | ConvertFrom-Json $body.properties.source.dataSourceId = $dataSourceId $ag = $body.properties.action.aznsAction.actionGroup[0] | ConvertTo-Json -Depth 10 $ag = $ag.Replace("{3}",$actionGroup.Id) | ConvertFrom-Json $body.properties.action.aznsAction.actionGroup[0] = $ag $cg = $body.properties.source.query | ConvertTo-Json -Depth 10 $cg = $cg.Replace("{4}",$actionGroup.Name) | ConvertFrom-Json $body.properties.source.query = $cg $ResourceAppIdURI = [WebRequestHelper]::GetResourceManagerUrl() $uri = $ResourceAppIdURI + "subscriptions/$($this.SubscriptionContext.SubscriptionId)/resourcegroups/$($laWS.ResourceGroupName)/providers/microsoft.insights/scheduledQueryRules/AzSK_CredHygiene_Alert_$($actionGroup.GroupShortName)?api-version=2018-04-16" [WebRequestHelper]::InvokeWebRequest([Microsoft.PowerShell.Commands.WebRequestMethod]::Put, $uri, $body); $this.PublishCustomMessage("Alert for the credential group [$($actionGroup.Name)] is successfully configured."); } else{ # LA resource not found. $this.PublishCustomMessage("Log Analytics resource with workspace Id [$($laWSId.Value)] provided in the CA automation account variables doesn't exist. Please verify the value of Log Analytics workspace Id.", [MessageType]::Error) $this.PublishCustomMessage("Couldn't create credential hygiene alert for the current subscription.", [MessageType]::Error) $this.PublishCustomMessage("Run Update-AzSKContinuousAssurance to update the Log Analytics workspace Id with the correct value in the CA automation account.") } } else{ # LAWS id variable not found. $this.PublishCustomMessage("Log Analytics workspace Id not found in the CA automation account variables.", [MessageType]::Error) $this.PublishCustomMessage("Couldn't create credential hygiene alert for the current subscription.", [MessageType]::Error) $this.PublishCustomMessage("Run Update-AzSKContinuousAssurance to update the Log Analytics workspace Id with the correct value in the CA automation account.") } } else{ # CA setup not found. $this.PublishCustomMessage("Continuous Assurance setup was not found in the current subscription.", [MessageType]::Error) $this.PublishCustomMessage("Couldn't create credential hygiene alert for the current subscription.", [MessageType]::Error) } } } |