functions/New-AzureDevOpsBug.ps1

function New-AzureDevOpsBug {
<#
  .SYNOPSIS
  This PowerShell script creates a Bug when there are one or multiple failed Remediation Tasks.
 
  .DESCRIPTION
  The New-AzureDevOpsBug.ps1 PowerShell script creates a Bug on the current Iteration of a team when one or
  multiple Remediation Tasks failed. The Bug is formatted as an HTML table and contains information on the name
  and Url properties. As a result, the team can easily locate and resolve the Remediation Tasks that failed.
 
  .PARAMETER FailedPolicyRemediationTasksJsonString
  Specifies the JSON string that contains the objects of one or multiple failed Remediation Tasks.
 
  .PARAMETER ModuleName
  Specifies the name of the PowerShell module installed at the beginning of the PowerShell script. By default, this is the VSTeam PowerShell Module.
 
  .PARAMETER OrganizationName
  Specifies the name of the Azure DevOps Organization.
 
  .PARAMETER ProjectName
  Specifies the name of the Azure DevOps Project.
 
  .PARAMETER PersonalAccessToken
  Specifies the Personal Access Token that is used for authentication purposes. Make sure that you use the AzureKeyVault@2 task (link below) for this purpose.
 
  .PARAMETER TeamName
  Specifies the name of the Azure DevOps team.
 
  .EXAMPLE
  New-AzureDevOpsBug.ps1 `
    -FailedPolicyRemediationTasksJsonString '<JSON string>'`
    -ModuleName 'VSTeam' `
    -OrganizationName 'bavanben' `
    -ProjectName 'Contoso' `
    -PersonalAccessToken '<secret string>' `
    -TeamName 'Contoso Team'
 
  .INPUTS
  None.
 
  .OUTPUTS
  The Start-PolicyAssignmentRemediation.ps1 PowerShell script outputs multiple string values for logging purposes.
 
  .LINK
  https://learn.microsoft.com/en-us/azure/devops/pipelines/tasks/reference/azure-key-vault-v2?view=azure-pipelines
#>


[CmdLetBinding()]
Param (
  [Parameter (Mandatory = $true)]
  [string] $FailedPolicyRemediationTasksJsonString,

  [Parameter (Mandatory = $true)]
  [string] $ModuleName = 'VSTeam',

  [Parameter (Mandatory = $true)]
  [string] $OrganizationName,

  [Parameter (Mandatory = $true)]
  [string] $ProjectName,

  [Parameter (Mandatory = $true)]
  [string] $PersonalAccessToken,

  [Parameter (Mandatory = $true)]
  [string] $TeamName
)

$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest

#region Install and import the PowerShell module
Write-Output "`nInstall and import the '$($ModuleName)' PowerShell module"
if (Get-Module | Where-Object { $_.Name -eq $ModuleName }) {
  Write-Output "`The '$($ModuleName)' PowerShell module is already installed and imported"
}
else {
  if (Get-Module -ListAvailable | Where-Object { $_.Name -eq $ModuleName }) {
    Write-Output "`The '$($ModuleName)' PowerShell module is not yet imported"
    try {
      Import-Module $ModuleName -Force
      Write-Output "The '$($ModuleName)' PowerShell module has been imported succesfully"
    }
    catch {
      Write-Error $_
    }
  }
  else {
    Write-Output "`The '$($ModuleName)' PowerShell module is not yet installed and imported"
    try {
      Install-Module -Name $ModuleName -Force
      Import-Module $ModuleName -Force
      Write-Output "The '$($ModuleName)' PowerShell module has been installed and imported succesfully"
    }
    catch {
      Write-Error $_
    }
  }
}
#endregion

#region Authenticate to the Azure DevOps Organization and Project"
Write-Output "`nAuthenticate to the '$($ProjectName)' Project located in the '$($OrganizationName)' Organization"
try {
  Set-VSTeamAccount -Account $OrganizationName -PersonalAccessToken $PersonalAccessToken
  Set-VSTeamDefaultProject -Project $ProjectName
  Write-Output "Succesfully authenticated to the '$($ProjectName)' Project located in the '$($OrganizationName)' Organization"
}
catch {
  Write-Error $_
}
#endregion

#region Retrieve the Iteration Paths of the team
Write-Output "`nRetrieve the Iterations Paths of the '$($TeamName)' team"
try {
  $authenticationToken = [System.Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$PersonalAccessToken"))
  $headers = @{
    "Authorization" = [String]::Format("Basic {0}", $authenticationToken)
    "Content-Type"  = "application/json"
  }
  $uri = "https://dev.azure.com/{0}/{1}/{2}/_apis/work/teamsettings/iterations?api-version=5.1" -f $OrganizationName, $ProjectName, $TeamName
  $iterationPaths = (Invoke-RestMethod -Method Get -Headers $headers -Uri $uri).value
  Write-Output "Succesfully retrieved the Iterations Paths of the '$($TeamName)' team"
}
catch {
  Write-Error $_
}
#endregion

#region Create the HTML table that will be included in the Bug
Write-Output "`nCreate the HTML table that will be included in the Bug"
$failedPolicyRemediationTasks = ConvertFrom-Json -InputObject $FailedPolicyRemediationTasksJsonString

Write-Verbose "For each failed Remediation Task object, add the 'Remediation Task Url' property"
foreach ($failedPolicyRemediationTask in $failedPolicyRemediationTasks) {
  $staticUrlComponent = "https://portal.azure.com/#view/Microsoft_Azure_Policy/ManageRemediationTaskBlade/assignmentId/"
  $variableUrlComponent = "$($failedPolicyRemediationTask.'Policy Assignment Id'.Replace("/","%2F"))/remediationTaskId/$($failedPolicyRemediationTask.'Remediation Task Id'.Replace("/","%2F"))"
  Add-Member -InputObject $failedPolicyRemediationTask -NotePropertyName 'Remediation Task Url' -NotePropertyValue "$($staticUrlComponent)$($variableUrlComponent)"
}

Write-Verbose "Build the header, pre-content and post-content of the HTML table"
$header = @"
<style>
TABLE {border-width: 1px; border-style: solid; border-color: black; border-collapse: collapse;}
TH {text-align: left; border-width: 1px; padding: 3px; border-style: solid; border-color: black; background-color: #6495ED;}
TD {text-align: left; border-width: 1px; padding: 3px; border-style: solid; border-color: black;}
</style>
"@

$postContent = "<H4><i>Table 1: Failed Remediation Tasks</i></H4>"

Write-Verbose "Build the HTML table"
$htmlParams = @{
  'Property'    = 'Remediation Task Name', 'Remediation Task Url', 'Provisioning State'
  'PostContent' = $postContent
  'Head'        = $header
}
$htmlTable = $failedPolicyRemediationTasks | ConvertTo-Html @htmlParams

Write-Verbose "Add the HTML table to the Repro Steps of the Bug"
$ReproSteps = @"
$htmlTable
"@

Write-Output "Succesfully created the HTML table that will be included in the Bug"
#endregion

#region Create a Bug on the current Iteration of the team
Write-Verbose "Set the variables that are used during the creation of the Bug"
$title = ('Failed Remediation Tasks - {0}' -f $(Get-Date -Format 'yyyyMMdd'))
$description = 'As you can see in Table 1, one or more Remediation Tasks failed. Please investigate these in more detail.'
$additionalFields = @{'Microsoft.VSTS.TCM.ReproSteps' = $ReproSteps }
$currentIterationPath = $iterationPaths | Where-Object -FilterScript { $_.attributes.timeFrame -eq 'current' }

Write-Output "`nCreate a Bug on the '$($currentIterationPath.name)' Iteration of the '$($TeamName)' team"
try {
  $workItemParams = @{
    'Title'            = $title
    'Description'      = $description
    'WorkItemType'     = 'Bug'
    'AdditionalFields' = $additionalFields
    'IterationPath'    = $currentIterationPath.path
  }
  Add-VSTeamWorkItem @workItemParams | Out-Null
  Write-Output "Succesfully created a Bug on the '$($currentIterationPath.name)' Iteration of the '$($TeamName)' team"
}
catch {
  Write-Error $_
}
#endregion
}