Framework/Configurations/ContinuousAssurance/Continuous_Assurance_ScanOnTrigger_Runbook.ps1
Param( [object] $WebHookData ) $AzSKRunbookVersion = "[#runbookVersion#]" #Telemetry functions -- start here function SetCommonProperties([psobject] $EventObj) { $notAvailable = "NA" $eventObj.data.baseData.properties.Add("JobId",$PsPrivateMetaData.JobId.Guid) $eventObj.data.baseData.properties.Add("SubscriptionId",$RunAsConnection.SubscriptionID) $eventObj.data.baseData.properties.Add("AzSKRunbookVersion", $AzSKRunbookVersion) } function GetEventBaseObject([string] $EventName) { $eventObj = "" | Select-Object data, iKey, name, tags, time $eventObj.iKey = $telemetryKey $eventObj.name = "Microsoft.ApplicationInsights." + $telemetryKey.Replace("-", "") + ".Event" $eventObj.time = [datetime]::UtcNow.ToString("o") $eventObj.tags = "" | Select-Object ai.internal.sdkVersion $eventObj.tags.'ai.internal.sdkVersion' = "dotnet: 2.1.0.26048" $eventObj.data = "" | Select-Object baseData, baseType $eventObj.data.baseType = "EventData" $eventObj.data.baseData = "" | Select-Object ver, name, measurements, properties $eventObj.data.baseData.ver = 2 $eventObj.data.baseData.name = $EventName $eventObj.data.baseData.measurements = New-Object 'system.collections.generic.dictionary[string,double]' $eventObj.data.baseData.properties = New-Object 'system.collections.generic.dictionary[string,string]' return $eventObj; } function PublishEvent([string] $EventName, [hashtable] $Properties, [hashtable] $Metrics) { try { #return if telemetry key is empty if ([string]::IsNullOrWhiteSpace($telemetryKey)) { return; }; $eventObj = GetEventBaseObject -EventName $EventName SetCommonProperties -EventObj $eventObj if ($null -ne $Properties) { $Properties.Keys | ForEach-Object { try { if (!$eventObj.data.baseData.properties.ContainsKey($_)) { $eventObj.data.baseData.properties.Add($_ , $Properties[$_].ToString()) } } catch { # Left blank intentionally # Error while sending CA events to telemetry. No need to break the execution. } } } if ($null -ne $Metrics) { $Metrics.Keys | ForEach-Object { try { $metric = $Metrics[$_] -as [double] if (!$eventObj.data.baseData.measurements.ContainsKey($_) -and $null -ne $metric) { $eventObj.data.baseData.measurements.Add($_ , $Metrics[$_]) } } catch { # Left blank intentionally # Error while sending CA events to telemetry. No need to break the execution. } } } $eventJson = ConvertTo-Json $eventObj -Depth 100 -Compress Invoke-WebRequest -Uri "https://dc.services.visualstudio.com/v2/track" ` -Method Post ` -ContentType "application/x-json-stream" ` -Body $eventJson ` -UseBasicParsing | Out-Null } catch { # Left blank intentionally # Error while sending CA events to telemetry. No need to break the execution. } } #Telemetry functions -- end here #function to invoke script from server function InvokeScript($accessToken, $policyStoreURL,$fileName, $version) { [System.Uri] $validatedURI = $null; $URI = $global:ExecutionContext.InvokeCommand.ExpandString($policyStoreURL) $result = "Write-Host 'Error connecting to AzSK policy store server.'" if([System.Uri]::TryCreate($URI, [System.UriKind]::Absolute, [ref] $validatedURI)) { if($accessToken) { $retry = 3 while($retry -gt 0) { $retry = $retry - 1 $result = Invoke-WebRequest $validatedUri -Headers @{"Authorization" = "Bearer $accessToken"} -UseBasicParsing if ($null -ne $result -and $result.StatusCode -ge 200 -and $result.StatusCode -le 399) { $retry = -1; } } } else { $retry = 3 while($retry -gt 0) { $retry = $retry - 1 $result = Invoke-WebRequest $validatedUri -UseBasicParsing if ($null -ne $result -and $result.StatusCode -ge 200 -and $result.StatusCode -le 399) { $retry = -1; } } } Invoke-Expression $result; } } function GetResourceDetailsfromWebhook($WebHookDataforResourceCreation) { #Getting required properties of WebhookData. $WebhookName = $WebHookDataforResourceCreation.WebhookName $WebhookBody = $WebHookDataforResourceCreation.RequestBody $WebhookHeaders = $WebHookDataforResourceCreation.RequestHeader # Obtain the WebhookBody containing the AlertContext $WebhookBody = (ConvertFrom-Json -InputObject $WebhookBody) Write-Output "`nWEBHOOK BODY" Write-Output "=============" Write-Output $WebhookBody # Obtain the AlertContext $AlertContext = [object]$WebhookBody.data.context $AlertContext if($alertcontext -ne $null -and ![string]::IsNullOrWhiteSpace($alertcontext.activityLog) -and ![string]::IsNullOrWhiteSpace($alertcontext.activityLog.resourceGroupName)) { $resourcedetails = @{ResourceGroupNamefromWebhook = $alertcontext.activityLog.resourceGroupName ; ResourceNamefromWebhook = ""} } else { $resourcedetails = @{ResourceGroupNamefromWebhook = "" ; ResourceNamefromWebhook = ""} } # Some selected AlertContext information #Write-Output "`nALERT CONTEXT DATA" #Write-Output "===================" #Write-Output $alertcontext.activityLog.eventSource #Write-Output $alertcontext.activityLog.subscriptionId #Write-Output $alertcontext.activityLog.resourceGroupName #Write-Output $alertcontext.activityLog.operationName #Write-Output $alertcontext.activityLog.resourceType #Write-Output $alertcontext.activityLog.resourceId #Write-Output $alertcontext.activityLog.eventTimestamp #$resourceidsplit = $alertcontext.activityLog.resourceId -split '/' #Write-Output $resourceidsplit[6] #$datafromdeployment = Get-AzResourceGroupDeployment -ResourceGroupName $alertcontext.activityLog.resourceGroupName -Name $resourceidsplit[6] | ConvertTo-Json -Depth 10 #if(-not [string]::IsNullOrWhiteSpace($datafromdeployment)) #{ # $datafromdeploymentbody = (ConvertFrom-Json -InputObject $datafromdeployment) # $resourcename = $datafromdeploymentbody.Parameters.name.Value # Write-Output $resourcename # $resourcedetails = @{ResourceGroupNamefromWebhook = $alertcontext.activityLog.resourceGroupName ; ResourceNamefromWebhook = $resourcename} #} #else #{ # $resourcedetails = @{ResourceGroupNamefromWebhook = "" ; ResourceNamefromWebhook = ""} #} return $resourcedetails; } ###################################################################################################################### #Core runbook code. #This is built using the runbook code template inside \Modules\AzSK\<version>\Framework\Configurations\Continuous_Assurance_ScanOnTrigger_Runbook #The placeholder values for various important variables are determined 'on the fly' based on the defaults that ship in AzSK.JSON #file in the \Modules\AzSK\<version>\Framework\Configurations folder and the local AzSKSettings.JSON file in the %localappdata%\Microsoft\AzSK #folder for the user setting up CA. #This Runbook gets triggered when resource is created in the subscription. #In an org-specific installation, various values from AzSK.JSON can be overridden in org policy and #are picked up from the org-specific AzSK.JSON obtained from the serverUrl location (in AzSKSettings.JSON). #In generic (org-neutral) setups these values are obtained from AzSK.JSON on a public CDN endpoint. ###################################################################################################################### try { #start job timer $jobTimer = [System.Diagnostics.Stopwatch]::StartNew(); #----------------------------------Config start------------------------------------------------------------------ $automationAccountRG = "[#automationAccountRG#]" $automationAccountName="[#automationAccountName#]" $telemetryKey ="[#telemetryKey#]" #This is the location from where policy is fetched at runtime. #This can be an org-specific URL (when org policy is set up) or, if generic org-neutral mode is used, it will just match the CoreSetupSrcUrl (below) #We will refer to this as org-policy store or org-policy url in comments below (with the above understanding) $onlinePolicyStoreUrl = "[#onlinePolicyStoreUrl#]" #This setting determines if the policy store enforces authentication. Generally 'false' for org-policy or OSS (org-neutral) context. $enableAADAuthForOnlinePolicyStore = "[#enableAADAuthForOnlinePolicyStore#]" $AzureEnv = "[#AzureEnvironment#]" #This is the script that is run to peform the actual scanning. This is fetched from the org-policy store if org-policy #is in use. If not, it is fetched from the default AzSK CDN. #This script basically allows orgs to customize/tweak the scripts that are run to perform the daily CA scans. $runbookScanAgentScript = "RunbookScanAgent.ps1" $azureRmResourceURI = "[#ManagementUri#]" #This is the Run-As (SPN) account for the runbook. It is read from the CA Automation account. $RunAsConnection = Get-AutomationConnection -Name "AzureRunAsConnection" #------------------------------------Execute RunbookCoreSetup.ps1 to download required modules------------------------- #Login to Azure if(!$RunAsConnection) { throw "Cannot login to Azure from AzSK CA runbook. Connection info for AzureRunAsConnection not found." } try { Write-Output("RB: Started runbook execution...") $appId = $RunAsConnection.ApplicationId Write-Output ("RB: Logging in to Azure for appId: [$appId]") $Azlogin = Get-Command -Name "Connect-AzAccount" -ErrorAction SilentlyContinue $loginCmdlets = Get-Command -Noun "AzureRmAccount" -ErrorAction SilentlyContinue if($Null -ne $Azlogin) { Connect-AzAccount ` -Environment $AzureEnv ` -ServicePrincipal ` -Tenant $RunAsConnection.TenantId ` -ApplicationId $RunAsConnection.ApplicationId ` -CertificateThumbprint $RunAsConnection.CertificateThumbprint | Out-Null Set-AzContext -SubscriptionId $RunAsConnection.SubscriptionID | Out-Null } else { if($Null -ne $loginCmdlets) { #AzureRm.profile version = 5.x.x if($Null -ne ($loginCmdlets | Where-Object{$_.Name -eq "Connect-AzureRmAccount"})) { Connect-AzureRmAccount ` -Environment $AzureEnv ` -ServicePrincipal ` -TenantId $RunAsConnection.TenantId ` -ApplicationId $RunAsConnection.ApplicationId ` -CertificateThumbprint $RunAsConnection.CertificateThumbprint | Out-Null } #AzureRm.profile version = 4.x.x elseif ($Null -ne ($loginCmdlets | Where-Object{$_.Name -eq "Add-AzureRmAccount"})) { Add-AzureRmAccount ` -EnvironmentName $AzureEnv ` -ServicePrincipal ` -TenantId $RunAsConnection.TenantId ` -ApplicationId $RunAsConnection.ApplicationId ` -CertificateThumbprint $RunAsConnection.CertificateThumbprint | Out-Null } else { throw "RB: Failed to login to Azure. Check if AzureRm.profile module is present." } Set-AzureRmContext -SubscriptionId $RunAsConnection.SubscriptionID | Out-Null } else { throw "RB: Failed to login to Azure. Check if AzureRm.profile module is present." } } } catch { Write-Output ("RB: Failed to login to Azure with AzSK AppId: [$appId].") throw $_.Exception } #This is a 'pseudo-version' and corresponds to the folder on the online policy store #from where the current CA core setup and scan agent scripts will be fetched. $caScriptsFolder = "1.0.0" $WebHookDataforResourceCreation = $WebHookData $ResourceGroupNamefromWebhook = "" $ResourceNamefromWebhook = "" #Fetching the webhook parameter and get resourcegroup name and resource name if($null -ne $WebHookDataforResourceCreation) { try { $resourcedetails = GetResourceDetailsfromWebhook -WebHookDataforResourceCreation $WebHookDataforResourceCreation } catch { Write-Output ("Failed to get the resource details from webhook.") throw $_.Exception } try { if(![string]::IsNullOrWhiteSpace($resourcedetails.ResourceGroupNamefromWebhook)) { $automationjoblist = Get-AzAutomationJob -RunbookName Continuous_Assurance_ScanOnTrigger_Runbook -ResourceGroupName $automationAccountRG -Status Running -AutomationAccountName $automationAccountName $automationjoblist | ForEach-Object { $jobdetails = Get-AzAutomationJob -AutomationAccountName $_.AutomationAccountName -ResourceGroupName $_.ResourceGroupName -Id $_.JobId $jobdetails = $jobdetails.JobParameters $jobdetailsBody = $jobdetails.webhookData.RequestBody $jobdetailsBody = (ConvertFrom-Json -InputObject $jobdetailsBody) $jobdetailsContext = [object]$jobdetailsBody.data.context $rgname = $jobdetailsContext.activityLog.resourceGroupName if($rgname -eq $resourcedetails.ResourceGroupNamefromWebhook) { $resourcedetails.ResourceGroupNamefromWebhook = "" } } } } catch { Write-Output ("Failed to get the Job List for Automation account.") throw $_.Exception } $ResourceGroupNamefromWebhook = $resourcedetails.ResourceGroupNamefromWebhook $ResourceNamefromWebhook = $resourcedetails.ResourceNamefromWebhook } #-----------------------------------Config end------------------------------------------------------------------------- #-----------------------------------Telemetry script------------------------------------------------------------------- PublishEvent -EventName "CA Job Started" -Properties @{ "OnlinePolicyStoreUrl"=$OnlinePolicyStoreUrl; ` "AzureADAppId"=$RunAsConnection.ApplicationId } #------------------------------------Execute RunbookScanAgent.ps1 to scan subscription and resources------------------- #We start with a check for 'Get-AzSKAccessToken' to ensure that AzSK module is ready (and loaded) if((Get-Command -Name "Get-AzSKAccessToken" -ErrorAction SilentlyContinue|Measure-Object).Count -gt 0) { #If policy store authN is set to true, get a token. (mostly for org policy/OSS, this will be 'false') if($enableAADAuthForOnlinePolicyStore -eq "true") { Write-Output("RB: Getting token for authN to online policy store.") $accessToken = Get-AzSKAccessToken -ResourceAppIdURI $azureRmResourceURI } $isAzAutomationAvailable = Get-Command -Name "Get-AzAutomationSchedule" -ErrorAction SilentlyContinue $isAzAccountsAvailable = Get-Module Az.Accounts if ((-not [string]::IsNullOrWhiteSpace($isAzAccountsAvailable)) -and (-not [string]::IsNullOrWhiteSpace($isAzAutomationAvailable))) { $Global:isAzAvailable = $true } PublishEvent -EventName "CA Job Invoke Scan Started" Write-Output ("RB: Invoking scan agent script. PolicyStoreURL: [" + $onlinePolicyStoreUrl.Substring(0,15) + "*****]") InvokeScript -accessToken $accessToken -policyStoreURL $onlinePolicyStoreUrl -fileName $runbookScanAgentScript -version $caScriptsFolder Write-Output ("RB: Scan agent script completed.") PublishEvent -EventName "CA Job Invoke Scan Completed" } else { Write-Output("RB: Not triggering a scan. AzSK module not yet ready in the automation account. Will retry in the next run.") } Write-Output("RB: Runbook execution completed...") PublishEvent -EventName "CA Job Completed" -Metrics @{ "TimeTakenInMs" = $jobTimer.ElapsedMilliseconds; ` "SuccessCount" = 1 } } catch { Write-Output ("RB: Exception occurred in CA runbook...`r`nError details: " + ($_ | Out-String)) PublishEvent -EventName "CA Job Error" -Properties @{ "ErrorRecord" = ($_ | Out-String) } -Metrics @{"TimeTakenInMs" =$jobTimer.ElapsedMilliseconds; "SuccessCount" = 0} throw; } #----------------------------------Runbook end------------------------------------------------------------------------- |