functions/Install-HydrationEpac.ps1
<#
.SYNOPSIS This function deploys Enterprise Policy as Code locally, and configures the repo to be uploaded to the platform of your choice. .DESCRIPTION The Install-HydrationEpac function deploys the Enterprise Policy as Code. It takes several optional parameters: DefinitionsRootFolder, StarterKit, AnswerFile, Output, StopPoint, UseUtc, Interactive, and SkipTests. .PARAMETER TenantIntermediateRoot The path to the Tenant Intermediate Root. This parameter is mandatory. .PARAMETER DefinitionsRootFolder The path to the Definitions directory. Defaults to "./Definitions". .PARAMETER StarterKit The path to the StarterKit directory. Defaults to "./StarterKit". .PARAMETER Output The path to the Output directory. Defaults to "./Output". .PARAMETER AnswerFile The path to the Answer file. This parameter is optional and does not have a default value. .PARAMETER UseUtc Switch to use UTC time. .PARAMETER Interactive Switch to enable interactive mode. .PARAMETER SkipTests Switch to skip preliminary tests. .EXAMPLE Install-HydrationEpac -TenantIntermediateRoot "/path/to/root" This example deploys the Enterprise Policy as Code using the specified Tenant Intermediate Root and default directories, which is appropriate if being run from the root of the new repo. .LINK https://aka.ms/epac https://github.com/Azure/enterprise-azure-policy-as-code/tree/main/Docs/start-hydration-kit.md #> function Install-HydrationEpac { [CmdletBinding()] param ( [Parameter(Mandatory = $true, HelpMessage = "The path to the Tenant Intermediate Root. This parameter is mandatory.")] [string] $TenantIntermediateRoot, [Parameter(Mandatory = $false, HelpMessage = "The path to the Definitions directory. Defaults to './Definitions'.")] [string] $DefinitionsRootFolder = "./Definitions", [Parameter(Mandatory = $false, HelpMessage = "The path to the StarterKit directory. Defaults to './StarterKit'.")] [string] $StarterKit = "./StarterKit", [Parameter(Mandatory = $false, HelpMessage = "The path to the Output directory. Defaults to './Output'.")] [string] $Output = "./Output", [Parameter(Mandatory = $false, HelpMessage = "The path to the Answer file. This parameter is optional and does not have a default value.")] [string] $AnswerFile, [Parameter(Mandatory = $false, HelpMessage = "Switch to use UTC time.")] [switch] $UseUtc, [Parameter(Mandatory = $false, HelpMessage = "Switch to enable interactive mode.")] [switch] $Interactive, [Parameter(Mandatory = $false, HelpMessage = "Switch to skip preliminary tests.")] [switch] $SkipTests ) Clear-Host $sleepTime = 10 # Define Verbosity $InformationPreference = "Continue" if ($DebugPreference -eq "Continue") { $debug = $true } else { $Silent = $true } # Move to repo root as EPAC generally prefers to be run from this location $repoRootPath = Split-Path $DefinitionsRootFolder Set-Location $repoRootPath $logDirectory = Join-Path $Output "Logs" $logFilePath = Join-Path $logDirectory "Install-HydrationEpac.log" $questionsFilePath = Join-Path $StarterKit "HydrationKit" "questions.jsonc" $pathsToTest = @($DefinitionsRootFolder, ` $logDirectory, ` $Output) # Import, fail to import, or create the answer file if ($AnswerFile) { $answerFilePath = $AnswerFile if (!(Test-Path $AnswerFile)) { Write-Error "Answer file not found at $AnswerFile, exiting..." return } $allInterviewAnswers = Get-Content $AnswerFile ` | ConvertFrom-Json -Depth 10 -AsHashtable } else { $answerFilePath = Join-Path $Output ` "HydrationAnswer" ` "AnswerFile.json" if (!(Test-Path $(Split-Path $AnswerFilePath))) { $null = New-Item -Path $(Split-Path $AnswerFilePath) -ItemType Directory -Force } else { Write-Warning "A file already exists at $AnswerFilePath, and will be overwritten unless you use Ctrl+C to exit..." Read-Host "Press Enter to continue..." Write-HydrationLogFile -EntryType logEntryDataAsPresented ` -EntryData "Overwriting Answer File at $AnswerFilePath" ` -LogFilePath $logFilePath ` -UseUtc:$UseUtc ` -Silent } "" | Set-Content $AnswerFilePath -Force } $pathsToTest += $answerFilePath # Define UI Width for display automation if ($host.UI.RawUI.WindowSize.Width -lt 80 -or $host.UI.RawUI.WindowSize.Width -eq "" -or $null -eq $host.UI.RawUI.WindowSize.Width) { $TerminalWidth = 80 } else { $TerminalWidth = $host.UI.RawUI.WindowSize.Width } # Crete an ordered hashtable to hold log entries until the directories are tested $endSummary = [PSCustomObject]@{ preliminaryTestResults = [ordered]@{} gatherData = [ordered]@{} generateAnswerFile = [ordered]@{} importAnswerFile = [ordered]@{} populateRepoDefinitions = [ordered]@{} createRepo = [ordered]@{} testRepo = [ordered]@{} testAndPlan = [ordered]@{} buildRepo = [ordered]@{} deployRepo = [ordered]@{} deployPipelines = [ordered]@{} deployPolicyAssignments = [ordered]@{} deployPolicyDefinitions = [ordered]@{} deployPolicyRoles = [ordered]@{} } $interviewQuestionSets = [ordered]@{ initial = $true optionalCreatePrimaryIntermediateRoot = $false optionalCreateMainCaf3Hierarchy = $false tenantList = $false # Future state pacSelector = $true updatesByPacSelector = $true policyDecisionsByPacSelector = $true pipeline = $true } $roleMessages = @{ "PassedEpacAllDeploy" = "- The script will be unable to assist with building the new management groups for EPAC to test with during development. However, RBAC Authorization tests passed, and you have the necessary permissions to deploy the EPAC solution to existing management groups. It is recommended that you ensure that you already have your epac development hierarchy in place prior to continuing so that these initial test deployments can be run." "PassedEpacPolicyDeploy" = "- The script will be unable to deploy any roles to support policies that are deployed by EPAC. This can result in an unsupported state, so no deployments step will be offered." "PassedEpacRoleDeploy" = "- The script will be unable to deploy any policies used by the roles that will be deployed by EPAC. This can result in an unsupported state, so no deployment step will be offered." "PassedEpacPlan" = "- The script will be unable to deploy any aspect of the EPAC solution to Azure. Only a plan step will be offered." "Failed" = "- The script will be unable to plan a deployment to support the EPAC solution. No plan step will be offered, and there will be no attempt to gather data from Azure programatically to simplify this process." } $limitation = [ordered]@{ Write = @{ Status = $false Message = "The script will be unable to create the epac testing environment for testing as part of this process. Consider manually creating these management groups prior to continuing..." optionalCaf3Message = "This script will be unable to create the requisite management groups for the new caf3 management group hierarchy due to limitations of the account used for the current connection to Azure, these questions will be skipped. Consider creating the new Tenant Intermediate Root Group and restarting the process" optionalTirMessage = "This script will be unable to create the recommended management groups for the new caf3 management group hierarchy due to limitations of the account used for the current connection to Azure, these questions will be skipped. STRONGLY consider creating the new Tenant Intermediate Root Group and restarting the process." } Policy = @{ Status = $false Message = "The script will be unable to deploy policy, and will not continue beyond the plan phase." } Role = @{ Status = $false Message = "The script will be unable to deploy roles that are needed to support policy deployment, and will not continue beyond the plan phase." } Plan = @{ Status = $false Message = "The script is unable to read the environment to generate a plan, so deployment of Azure Policy will not be tested against the EPAC development environment specified." } Gather = @{ Status = $false Message = "The script will be unable to gather data from Azure programatically to simplify this process. No insights from Azure can be gained to assist in guiding you through this process... Stopping now and rerunning the tests after connecting to Azure with an account that has more comprehensive rights, at least Read, to Azure, is STRONGLY recommended." } Git = @{ Status = $false Message = "The script will be unable to programatically download the StarterKit from GitHub. Please ensure that you have an updated copy of StarterKit in the same directory as the Definitions folder, based on the choices made in this script." } } $stageBlocks = Get-Content $(Join-Path $StarterKit 'HydrationKit' 'blockDefinitions.jsonc') | ConvertFrom-Json -Depth 5 -AsHashtable foreach($key in $stageBlocks.keys) { $stageBlocks.$key.TerminalWidth = $TerminalWidth } Clear-Host ################################################################################ ################################################################################ # Initiate UI: Header $summary = [ordered]@{} $uiStart = $stageBlocks.uiStart New-HydrationSeparatorBlock @uiStart Write-HydrationLogFile -EntryType newStage ` -EntryData $stageBlocks.uiStart.DisplayText ` -LogFilePath $logFilePath ` -UseUtc:$UseUtc ` -Silent # Initiate UI: Body Write-Warning "This script is currently in Beta release. Please report any issues to the EPAC team." # TODO Add a welcome message here that is more informative than the current placeholder. Write-Host "Welcome to the Enterprise Policy as Code (EPAC) Hydration Kit. This script is intended to help guide you through the EPAC Deployment process." -ForegroundColor Yellow # Initiate UI: Footer New-HydrationContinuePrompt -Interactive:$Interactive -SleepTime:$sleepTime Clear-Host ################################################################################ ################################################################################ # Preliminary Tests: Path Header $summary = [ordered]@{} $runPreliminaryTests = $stageBlocks.runPreliminaryTests New-HydrationSeparatorBlock @runPreliminaryTests Write-HydrationLogFile -EntryType newStage ` -EntryData $stageBlocks.runPreliminaryTests.DisplayText ` -LogFilePath $logFilePath ` -UseUtc:$UseUtc ` -Silent ################################################################################ # Preliminary Tests: Path Body Write-Host "Beginning path tests to help ensure that the script can run successfully..." -ForegroundColor Yellow # Test for and create directories if they do not exist, this allows realtime logging to file to begin if ($SkipTests){ Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "Skipping tests..." -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Magenta }else { foreach ($path in $pathsToTest) { $pathTest = Test-HydrationPath -LocalPath $path ` -UseUtc:$UseUtc ` -LogFilePath $logFilePath ` -Silent if ($pathTest -eq "Failed") { Write-Error "The path $path could not be created. Please choose a location to work in that you have write access to and restart the process." $summary.Add($path, "Failed") } else { $summary.Add($path, "Passed") } } Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "Summary of $($stageblocks.runPreliminaryTests.DisplayText)" -LogFilePath $logFilePath -UseUtc:$UseUtc -Silent foreach ($entry in $summary.keys) { Write-HydrationLogFile -EntryType testResult -EntryData "$entry -- $($summary.$entry)" -UseUtc:$UseUtc -LogFilePath $logFilePath -Silent Start-Sleep -Seconds $sleepTime } # Begin Azure Network Connection Tests Write-Host "Beginning connectivity tests to help ensure that the script can run successfully..." -ForegroundColor Yellow $connectionTestUrlList = [ordered]@{ internetConnection = "13.107.42.14" # Used in Microsoft Automated Testing (per CoPilot) githubConnection = "www.github.com" azureManagement = (Get-AzContext).Environment.ResourceManagerUrl | Select-String -Pattern "https?://([^/]+)/?" | ForEach-Object { $_.Matches[0].Groups[1].Value } azureAuthentication = (Get-AzContext).Environment.ActiveDirectoryAuthority | Select-String -Pattern "https?://([^/]+)/?" | ForEach-Object { $_.Matches[0].Groups[1].Value } } foreach ($url in $connectionTestUrlList.Keys) { $connectionTest = Test-HydrationConnection -FullyQualifiedDomainName $connectionTestUrlList.$url -UseUtc:$UseUtc -LogFilePath $logFilePath -Silent if ($connectionTest -eq "Failed") { # Write-Error "The test for $url failed when tested with the value $($connectionTestUrlList.$url). Please ensure that you have an active internet connection and that the Azure services are available. This will not prevent continuing with the process, but it will prevent use of much of the accelerator, as well as gathering of data to provide guidance in this process, and is not recommended." $summary.Add($url, "Failed") } else { $summary.Add($url, "Passed") } Write-HydrationLogFile -EntryType testResult -EntryData "$url -- $($summary.$url)" -UseUtc:$UseUtc -LogFilePath $logFilePath -Silent } # Begin Azure Authorization Tests Write-Host "Beginning RBAC Authorization tests to help ensure that the script can run successfully..." -ForegroundColor Yellow Write-Host "- This will only check for direct assignments, and will not take into account group membership, as it requires API calls to Entra ID that are not within the scope of EPAC's mission." -ForegroundColor Yellow Write-Host " - Creation, and Removal, of a Management group at Tenant Root will effectively confirm privileges if the test must be bypassed/ignored for this reason." -ForegroundColor Yellow Write-Host "- If your terminal appears to hang during this process, it is likely that the script is waiting for an authentication prompt in the medium configured for your terminal session. This is generally the default web browser. Please check for hidden windows." -ForegroundColor Magenta Write-Host " - For example, VSCode's default Terminal behavior is to open a login window in the default web browser IN THE BACKGROUND, a pop-under. Please check for this window if you are using VSCode." -ForegroundColor Magenta $rbacTest = Test-HydrationRbacAssignment -Scope $(Get-AzContext).Tenant.Id -Output:$Output -UseUtc:$UseUtc -LogFilePath $logFilePath -Silent $summary.add("RbacAuthorization", $rbacTest) Write-Host "Beginning git tests to help ensure that the script can run successfully..." -ForegroundColor Yellow try { if (($(git --help))) { $summary.add("gitInstall", "Passed") } } catch { $summary.add("gitInstall", "Failed") } Write-Host "Reviewing Returns..." -ForegroundColor Yellow # Process failed tests for warning messages and script limitations calculations $failedTests = @{} foreach ($key in $summary.keys) { if ($summary.$key -like "Failed*") { $failedTests.Add($key, $summary.$key) } } if ($failedTests.keys.count -gt 0 -or (!($($summary.RBACAuthorization -eq "PassedHydrationDeploy")))) { Write-Host "The following items should be considered before continuing:" -ForegroundColor Red foreach ($key in $failedTests.keys) { switch ($key) { "githubConnection" { Write-Host " - The test for a connection to GitHub failed. You will need to ensure that the StarterKit folder has been downloaded to the same directory that your Definitions folder is now in, based on the choices made in this script.` `n - This is not a blocker if you have a fresh copy of the StarterKit in the same directory as the Definitions folder, and you already have access to the module. However, this script requires the presence of that folder in the repo to continue, either through automated or manual means.` `n - Download Location: https://github.com/Azure/enterprise-azure-policy-as-code/tree/main/StarterKit" -ForegroundColor Yellow } "azureManagement" { Write-Host " - The test for the management connection to Azure failed. The script will be unable to plan a deployment to support the EPAC solution. No plan step will be offered, and there will be no attempt to gather data from Azure programatically to simplify this process." -ForegroundColor Red $limitation.azureManagement.Status = $true } "azureAuthentication" { Write-Host " - The test for the authentication connection to Azure failed. The script will be unable to plan a deployment to support the EPAC solution. No plan step will be offered, and there will be no attempt to gather data from Azure programatically to simplify this process." -ForegroundColor Red $limitation.azureAuthentication.Status = $true } "gitInstall" { Write-Host " - The test for git software has failed. You will need to ensure that the StarterKit folder has been manually downloaded to the same directory that your Definitions folder is now in, based on the choices made in this script.` `n - This is not a blocker if you have a fresh copy of the StarterKit in the same directory as the Definitions folder, and you already have access to the module. However, this script requires the presence of that folder in the repo to continue, either through automated or manual means.` `n - Download Location: https://github.com/Azure/enterprise-azure-policy-as-code/tree/main/StarterKit" -ForegroundColor Yellow $limitation.git.Status = $true } } } switch ($summary.RbacAuthorization) { "PassedEpacAllDeploy" { Write-Host " $($roleMessages.PassedEpacAllDeploy)" -ForegroundColor Red } "PassedEpacPolicyDeploy" { Write-Host " $($roleMessages.PassedEpacAllDeploy)" -ForegroundColor Red Write-Host " $($roleMessages.PassedEpacPolicyDeploy)" -ForegroundColor Red $limitation.Write.Status = $true $limitation.Role.Status.Status = $true } "PassedEpacRoleDeploy" { Write-Host " $($roleMessages.PassedEpacRoleDeploy)" -ForegroundColor Red Write-Host " $($roleMessages.PassedEpacAllDeploy)" -ForegroundColor Red Write-Host " $($roleMessages.PassedEpacPolicyDeploy)" -ForegroundColor Red $limitation.Write.Status = $true $limitation.Policy.Status = $true } "PassedEpacPlan" { Write-Host " $($roleMessages.PassedEpacPlan)" -ForegroundColor Red Write-Host " $($roleMessages.PassedEpacRoleDeploy)" -ForegroundColor Red Write-Host " $($roleMessages.PassedEpacAllDeploy)" -ForegroundColor Red Write-Host " $($roleMessages.PassedEpacPolicyDeploy)" -ForegroundColor Red $limitation.Write.Status = $true $limitation.Policy.Status = $true $limitation.Role.Status = $true } "Failed" { Write-Host " $($roleMessages.PassedEpacAllDeploy)" -ForegroundColor Red Write-Host " $($roleMessages.PassedEpacPlan)" -ForegroundColor Red Write-Host " $($roleMessages.PassedEpacRoleDeploy)" -ForegroundColor Red Write-Host " $($roleMessages.PassedEpacAllDeploy)" -ForegroundColor Red Write-Host " $($roleMessages.PassedEpacPolicyDeploy)" -ForegroundColor Red $limitation.Write.Status = $true $limitation.Plan = $true $limitation.Policy.Status = $true $limitation.Role.Status = $true $limitation.Gather = $true } } } else { Write-Host "All tests indicate that the connection is in an optimal state for the EPAC installation process. Additional data will be gathered using these connections, and errors after this point will generally indicate a unique condition such as an inheritance block/override for RBAC authority." -ForegroundColor Green } ################################################################################ # Preliminary Tests: Footer # Summary separator block $displayPreliminaryTests = $stageBlocks.displayPreliminaryTests New-HydrationSeparatorBlock @displayPreliminaryTests Write-HydrationLogFile -EntryType newStage -EntryData $stageBlocks.displayPreliminaryTests.DisplayText -LogFilePath $logFilePath -UseUtc:$UseUtc -Silent # Display Summary $testSummarySeparator = "-" foreach ($hashkey in $summary.keys) { $hashString = $( -join ($hashKey, " ", ($testSummarySeparator * ($TerminalWidth - ($hashKey.Length + $($summary.$hashKey).Length + 2))), " ", $($summary.$hashKey))) if ($summary.$hashkey -like "Failed*") { Write-Host $hashString -ForegroundColor Red } else { Write-Host $hashString -ForegroundColor Green } Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "Summary: $hashString" -LogFilePath $logFilePath -UseUtc:$UseUtc -Silent } Write-Host "`n`n" # Pause for review based on interactive message from input New-HydrationContinuePrompt -Interactive:$Interactive -SleepTime:$sleepTime } ################################################################################ ################################################################################ # Begin Data Gather Process to Support EPAC Deployment $summary = [ordered]@{} $gatherData = $stageBlocks.gatherData New-HydrationSeparatorBlock @gatherData Write-HydrationLogFile -EntryType newStage -EntryData $stageBlocks.displayPreliminaryTests.DisplayText -LogFilePath $logFilePath -UseUtc:$UseUtc -Silent if ($limitation.Gather.Status) { Write-HydrationLogFile -EntryType answerRequested -EntryData $limitation.Gather.Message -LogFilePath $logFilePath -UseUtc:$UseUtc -Silent Write-Host limitation.Gather.Message -ForegroundColor Red $continueWithoutRead = New-HydrationAnswer ` -QuestionData "Would you like to continue without the ability to gather data from Azure?" ` -AnswerOptions "Yes", "No" ` -DefaultValue "No" if ($continueWithoutRead -eq "Yes") { Write-Host "Continuing without the ability to gather data from Azure..." -ForegroundColor DarkRed } else { Write-Host "Stopping the process to allow for a connection to Azure with an account that has more comprehensive rights, at least Read, to Azure." -ForegroundColor Red return } } else { Write-Host "Beginning data gathering process..." -ForegroundColor Yellow # Gather Data $gatherData = [ordered]@{} $environmentEntry = [ordered]@{ pacSelector = "" intermediateRootGroupName = $TenantIntermediateRoot tenantId = $(Get-AzContext).Tenant.Id cloud = $(Get-AzContext).Environment.Name keepDfcSecurityAssignments = $false caf3Status = "Untested" intermediateRootStatus = "Untested" } # Current Tenant Information try { $gatherData.Add('currentTenantPacSelector', $(Get-DeepCloneAsOrderedHashtable -InputObject $environmentEntry)) $gatherData.currentTenantPacSelector.pacSelector = "tenant01" # Set Default for primary tenant, primary root (that will be cloned to epac-dev environment) } catch { Write-Error "Cannot find the EPAC helper command `'Get-DeepCloneAsOrderedHashtable`', please ensure that the EPAC module or is installed and available, or that the scripts directory is available and the helpers have been dot sourced for use." return } try { $gatherData.Add('locationList', @((Get-AzLocation).Location) -join ", ") } catch { Write-Error "Failed to gather Azure Location List, please ensure that you have a connection to Azure and try again." return } try { $tenantIntermediateRootTestResult = Get-AzManagementGroupRestMethod -GroupID $TenantIntermediateRoot # Identify if the property tested below is correct } catch { Write-Warning "Error returned retrieving the Tenant Intermediate Root. This generally means that it has not been created, and the interview process will continue with the assumption that it needs to be created." } if (!($tenantIntermediateRootTestResult.Id)) { $gatherData.currentTenantPacSelector.intermediateRootStatus = $environmentEntry.intermediateRootStatus = "Available" Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "The Tenant Intermediate Root Management Group does not exist, and will need to be created to be used." -LogFilePath $logFilePath -UseUtc:$UseUtc -silent $interviewQuestionSets.optionalCreatePrimaryIntermediateRoot = $true Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "Create Intermediate Root: `"$TenantIntermediateRoot`" does not exist, adding discussion items..." -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Yellow } else { $gatherData.currentTenantPacSelector.intermediateRootStatus = $environmentEntry.intermediateRootStatus = "Passed" Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "The TenantIntermediateRoot Management Group `"$TenantIntermediateRoot`" is confirmed." -LogFilePath $logFilePath -UseUtc:$UseUtc -silent $interviewQuestionSets.optionalCreatePrimaryIntermediateRoot = $false Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "Create Intermediate Root Test: `"$TenantIntermediateRoot`" exists , no deployment is needed..." -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Green } try { $gatherData.currentTenantPacSelector.caf3Status = $environmentEntry.caf3Status = Test-HydrationCaf3Hierarchy -TenantId $(Get-AzContext).Tenant.Id -TenantIntermediateRoot $TenantIntermediateRoot -LogFilePath $logFilePath } catch { Write-Error $Error[0].Exception.Message } if ($tenantIntermediateRootTestResult.id) { $interviewQuestionSets.optionalCreatePrimaryIntermediateRoot = $false Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "Create Intermediate Root Test: `"$TenantIntermediateRoot`" exists , no deployment is needed..." -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Green } else { $interviewQuestionSets.optionalCreatePrimaryIntermediateRoot = $true Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "Create Intermediate Root: `"$TenantIntermediateRoot`" does not exist, adding discussion items..." -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Yellow } $interviewQuestionSets.optionalCreatePrimaryIntermediateRoot = $true $interviewQuestionSets.optionalCreateMainCaf3Hierarchy = $true $interviewQuestionSets.optionalRequireCaf3HierarchyRename = $true # TODO: The logic below needs some work before it can be used effectively, need to think through when we still want to offer the option as this is too limiting. # switch ($gatherData.currentTenantPacSelector.caf3Status) { # "PassedCaf3Exists" { # $interviewQuestionSets.optionalCreatePrimaryIntermediateRoot = $false # $interviewQuestionSets.optionalCreateMainCaf3Hierarchy = $true # May still want to create an alternate # $interviewQuestionSets.optionalRequireCaf3HierarchyRename = $false # May still want to create an alternate # Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "Default CAF3 Hierarchy Test: CAF3 hierarchy is in place under the management group `"$TenantIntermediateRoot`" , no additional Management Group deployment items will be recommended." -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Green # } # "PassedRunCaf3" { # Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "Caf3 Hierarchy: CAF3 Hierarchy is a viable option under `"$TenantIntermediateRoot`", adding discussion items..." -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Yellow # $interviewQuestionSets.optionalCreateMainCaf3Hierarchy = $true # $interviewQuestionSets.optionalRequireCaf3HierarchyRename = $false # } # "FailedNameCollision" { # $interviewQuestionSets.optionalCreateMainCaf3Hierarchy = $true # $interviewQuestionSets.optionalRequireCaf3HierarchyRename = $true # Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "Create CAF3 Hierarchy: A default CAF3 hierarchy does not exist in an expected state, which is to say that default names are used in a non-standard hierarchy. ` # `n -This can be addressed by creating a new hierarchy (if desired) using a prefix/suffix to the standard default strings, adding discussion items..." -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Yellow # } # "Failed" { # Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "Test Failed, please review error message, access levels, and connection to Azure before testing again." -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Red # $endAfterDataGather = $true # } # Default { # $message = "This should not happen, `$gatherData.currentTenantPacSelector.caf3Status is set to an invalid value, '$($gatherData.currentTenantPacSelector.caf3Status)'" # Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData $message -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Red # return # } # } Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "Data Gathered: $($gatherData | Convertto-Json -depth 100 -compress)" -LogFilePath $logFilePath -UseUtc:$UseUtc -Silent Write-Host "`n" $endSummary.gatherData = $summary New-HydrationContinuePrompt -Interactive:$Interactive -SleepTime:$sleepTime } if ($AnswerFile) { Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "Answer File Provided: $AnswerFilePath" -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Yellow try { $allInterviewAnswers = Get-Content $AnswerFilePath | ConvertFrom-Json -Depth 10 -AsHashtable } catch { Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData $Error[0].Exception.Message -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Red Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "Failed to import the answer file, please review the error message and rerun the process." -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Red } } else { ################################################################################ ################################################################################ # Gather Deployment Decisions for/from Answer File # Run Interview Process Clear-Host $stageBlock = $stageBlocks.generateAnswerFile $allInterviewAnswers = [ordered]@{} New-HydrationSeparatorBlock @stageBlock Write-HydrationLogFile -EntryType newStage -EntryData $stageBlock.DisplayText -LogFilePath $logFilePath -UseUtc:$UseUtc -Silent ################################################################################ # Main Tenant Loops ############ # Confirm Tenant $loopId = "initial" Write-HydrationLogFile -EntryType newStage -EntryData "Processing QuestionSet: $loopId" -LogFilePath $logFilePath -UseUtc:$UseUtc -Silent if ($interviewQuestionSets.$loopId) { $loopNotes = @( "Current Tenant ID: $((Get-AzContext).tenant.Id)" ) try { $interview = New-HydrationAnswerSet -LoopId $loopId -QuestionsFilePath $questionsFilePath -Notes $loopNotes -UseUtc:$UseUtc -LogFilePath $logFilePath -TerminalWidth:$TerminalWidth -ErrorAction Stop } catch { Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData $error[0].Exception.Message -LogFilePath $logFilePath -UseUtc:$UseUtc -Silent Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "Failed to process QuestionSet $loopId" -LogFilePath $logFilePath -UseUtc:$UseUtc -Silent Write-Error "Error in the interview process for $loopId, please review the error message and rerun the process." return } Write-HydrationLogFile -EntryType answerSetProvided -EntryData "$($interview | Convertto-Json -depth 100 -compress)" -LogFilePath $logFilePath -UseUtc:$UseUtc -Silent Remove-Variable loopId -ErrorAction SilentlyContinue Remove-Variable loopNotes -ErrorAction SilentlyContinue Write-Host "`nQuestion Set is complete, responses outlined below..." -ForegroundColor Yellow foreach ($intKey in $interview.keys) { $allInterviewAnswers.Add($intKey, $interview.$intKey) $testSummarySeparator = "-" $hashString = $( -join ($intKey, " ", ($testSummarySeparator * ($TerminalWidth - ($intKey.Length + $($interview.$intKey).Length + 2))), " ", $($interview.$intKey))) Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "$hashString" -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Green } # Process Responses if ($interview.initialTenantId -eq "Yes") { $allInterviewAnswers.initialTenantId = (Get-AzContext).tenant.id Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "`nNo Tenant ID was provided, using the current Tenant ID...`n" -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Yellow } else { Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "`nDiffering Tenant ID provided, exiting to use the provided Tenant ID...`n" -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Yellow Write-Error "Reconnect to the appropriate Tenant ID using `'Connect-AzAccount -TenantId [YourTenantId]`', and then rerun the process." return } if ($interview.pacOwnerId -eq "" -or $null -eq $interview.pacOwnerId) { Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "`nNo Owner ID was provided, generating a new GUID for the Owner ID...`n" -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Yellow $interview.pacOwnerId = $allInterviewAnswers.pacOwnerId = (New-Guid).Guid $testSummarySeparator = "-" $ikey = "PacOwnerId Generated" $hashString = $( -join ($ikey, " ", ($testSummarySeparator * ($TerminalWidth - ($ikey.Length + $($interview.pacOwnerId).Length + 2))), " ", $($interview.pacOwnerId))) Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "$hashString" -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Green } New-HydrationContinuePrompt -Interactive:$Interactive -SleepTime:$sleepTime $allInterviewAnswers.initialTenantCloud = (Get-AzContext).environment.name $allInterviewAnswers.initialTenantIntermediateRoot = $TenantIntermediateRoot } $loopId = "optionalCreatePrimaryIntermediateRoot" Write-HydrationLogFile -EntryType newStage -EntryData "Processing QuestionSet: $loopId" -LogFilePath $logFilePath -UseUtc:$UseUtc -Silent if ($environmentEntry.intermediateRootStatus -eq "Available" -and $interviewQuestionSets.$loopId) { $loopNotes = @( "$TenantIntermediateRoot Name Status: $($environmentEntry.intermediateRootStatus)" ) try { $interview = New-HydrationAnswerSet -LoopId $loopId -QuestionsFilePath $questionsFilePath -Notes $loopNotes -UseUtc:$UseUtc -LogFilePath $logFilePath -TerminalWidth:$TerminalWidth -ErrorAction Stop } catch { Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData $error[0].Exception.Message -LogFilePath $logFilePath -UseUtc:$UseUtc -Silent Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "Failed to process QuestionSet $loopId" -LogFilePath $logFilePath -UseUtc:$UseUtc -Silent Write-Error "Error in the interview process for $loopId, please review the error message and rerun the process." return } Write-HydrationLogFile -EntryType answerSetProvided -EntryData "$($interview | Convertto-Json -depth 100 -compress)" -LogFilePath $logFilePath -UseUtc:$UseUtc -Silent Remove-Variable loopId -ErrorAction SilentlyContinue Remove-Variable loopNotes -ErrorAction SilentlyContinue Write-Host "`nQuestion Set is complete, responses outlined below..." -ForegroundColor Yellow foreach ($intKey in $interview.keys) { $allInterviewAnswers.Add($intKey, $interview.$intKey) $testSummarySeparator = "-" $hashString = $( -join ($intKey, " ", ($testSummarySeparator * ($TerminalWidth - ($intKey.Length + $($interview.$intKey).Length + 2))), " ", $($interview.$intKey))) Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "$hashString" -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Green } New-HydrationContinuePrompt -Interactive:$Interactive -SleepTime:$sleepTime } # ############ # # Main Tenant Caf3 Loop $loopId = "optionalCreateMainCaf3Hierarchy" Write-HydrationLogFile -EntryType newStage -EntryData "Processing QuestionSet: $loopId" -LogFilePath $logFilePath -UseUtc:$UseUtc -Silent if ($interviewQuestionSets.$loopId -and $allInterviewAnswers.createMainCaf3Hierarchy -eq "Yes") { if ($environmentEntry.caf3Status -eq "FailedNameCollision") { $loopNotes = @( "CAF3 Hierarchy will require a prefix and/or suffix in order to be deployed properly.") } try { $interview = New-HydrationAnswerSet -LoopId $loopId -QuestionsFilePath $questionsFilePath -Notes $loopNotes -UseUtc:$UseUtc -LogFilePath $logFilePath -TerminalWidth:$TerminalWidth -ErrorAction Stop } catch { Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData $error[0].Exception.Message -LogFilePath $logFilePath -UseUtc:$UseUtc -Silent Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "Failed to process QuestionSet $loopId" -LogFilePath $logFilePath -UseUtc:$UseUtc -Silent Write-Error "Error in the interview process for $loopId, please review the error message and rerun the process." return } Write-HydrationLogFile -EntryType answerSetProvided -EntryData "$($interview | Convertto-Json -depth 100 -compress)" -LogFilePath $logFilePath -UseUtc:$UseUtc -Silent Write-Host "`nQuestion Set is complete, responses outlined below..." -ForegroundColor Yellow foreach ($intKey in $interview.keys) { $allInterviewAnswers.Add($intKey, $interview.$intKey) $testSummarySeparator = "-" $hashString = $( -join ($intKey, " ", ($testSummarySeparator * ($TerminalWidth - ($intKey.Length + $($interview.$intKey).Length + 2))), " ", $($interview.$intKey))) Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "$hashString" -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Green } New-HydrationContinuePrompt -Interactive:$Interactive -SleepTime:$sleepTime Remove-Variable loopId -ErrorAction SilentlyContinue Remove-Variable loopNotes -ErrorAction SilentlyContinue } elseif ($environmentEntry.intermediateRootStatus -eq "PassedCaf3Exists" ) { Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "CAF3 Hierarchy is in place under the management group `"$TenantIntermediateRoot`", skipping Caf3 Deployment questions." -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Green } ############ # Main Tenant Caf3 Naming Loop # $loopId = "optionalCreateMainCaf3HierarchyNaming" # Write-HydrationLogFile -EntryType newStage -EntryData "Processing QuestionSet: $loopId" -LogFilePath $logFilePath -UseUtc:$UseUtc -Silent # if ($allInterviewAnswers.createMainCaf3Hierarchy -eq "Yes") { # if ($environmentEntry.caf3Status -eq "FailedNameCollision") { # $loopNotes = @( "CAF3 Hierarchy will require a prefix and/or suffix in order to be deployed properly.") # } # try { # $interview = New-HydrationAnswerSet -LoopId $loopId -QuestionsFilePath $questionsFilePath -Notes $loopNotes -UseUtc:$UseUtc -LogFilePath $logFilePath -TerminalWidth:$TerminalWidth -ErrorAction Stop # } # catch { # Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData $error[0].Exception.Message -LogFilePath $logFilePath -UseUtc:$UseUtc -Silent # Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "Failed to process QuestionSet $loopId" -LogFilePath $logFilePath -UseUtc:$UseUtc -Silent # Write-Error "Error in the interview process for $loopId, please review the error message and rerun the process." # return # } # Write-HydrationLogFile -EntryType answerSetProvided -EntryData "$($interview | Convertto-Json -depth 100 -compress)" -LogFilePath $logFilePath -UseUtc:$UseUtc -Silent # Write-Host "`nQuestion Set is complete, responses outlined below..." -ForegroundColor Yellow # foreach ($intKey in $interview.keys) { # $allInterviewAnswers.Add($intKey, $interview.$intKey) # $testSummarySeparator = "-" # $hashString = $( -join ($intKey, " ", ($testSummarySeparator * ($TerminalWidth - ($intKey.Length + $($interview.$intKey).Length + 2))), " ", $($interview.$intKey))) # Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "$hashString" -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Green # } # New-HydrationContinuePrompt -Interactive:$Interactive -SleepTime:$sleepTime # Remove-Variable loopId -ErrorAction SilentlyContinue # Remove-Variable loopNotes -ErrorAction SilentlyContinue # } # else { # Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "CAF3 Hierarchy name mutation not requested." -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Green # } ############ # corePacSelectors Loop $loopId = "corePacSelectors" Write-HydrationLogFile -EntryType newStage -EntryData "Processing QuestionSet: $loopId" -LogFilePath $logFilePath -UseUtc:$UseUtc -Silent if ($summary.RbacAuthorization -like "Passed*") { $loopNotes = @("Location List: $($endSummary.gatherData.locationList)") } else{ $loopNotes = @("Location List: Rbac Test Failed, no location list gathered.") } try { $interview = New-HydrationAnswerSet -LoopId $loopId -QuestionsFilePath $questionsFilePath -Notes $loopNotes -UseUtc:$UseUtc -LogFilePath $logFilePath -TerminalWidth:$TerminalWidth -ErrorAction Stop } catch { Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData $error[0].Exception.Message -LogFilePath $logFilePath -UseUtc:$UseUtc -Silent Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "Failed to process QuestionSet $loopId" -LogFilePath $logFilePath -UseUtc:$UseUtc -Silent Write-Error "Error in the interview process for $loopId, please review the error message and rerun the process." return } Write-HydrationLogFile -EntryType answerSetProvided -EntryData "$($interview | Convertto-Json -depth 100 -compress)" -LogFilePath $logFilePath -UseUtc:$UseUtc -Silent Write-Host "`nQuestion Set is complete, responses outlined below..." -ForegroundColor Yellow foreach ($intKey in $interview.keys) { $allInterviewAnswers.Add($intKey, $interview.$intKey) $testSummarySeparator = "-" $hashString = $( -join ($intKey, " ", ($testSummarySeparator * ($TerminalWidth - ($intKey.Length + $($interview.$intKey).Length + 2))), " ", $($interview.$intKey))) Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "$hashString" -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Green } Remove-Variable loopId -ErrorAction SilentlyContinue Remove-Variable loopNotes -ErrorAction SilentlyContinue if ($interview.mainTenantMainPacSelectorName -eq "" -or $null -eq $interview.mainTenantMainPacSelectorName -or -not $interview.mainTenantMainPacSelectorName) { Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "No pacSelectorName was provided, using 'tenant01'..." -LogFilePath $logFilePath -UseUtc:$UseUtc -Silent Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "`nNo pacSelectorName was provided, setting default value...`n" -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Yellow $interview.mainTenantMainPacSelectorName = $allInterviewAnswers.mainTenantMainPacSelectorName = "tenant01" $testSummarySeparator = "-" $ikey = "pacSelectorName" $hashString = $( -join ($ikey, " ", ($testSummarySeparator * ($TerminalWidth - ($ikey.Length + $($interview.mainTenantMainPacSelectorName).Length + 2))), " ", $($interview.mainTenantMainPacSelectorName))) Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "$hashString" -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Green } if ($interview.epacParent -eq "" -or $null -eq $interview.epacParent -or -not $interview.epacParent) { Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "No Parent for EPAC was provided, using Tenant Root for the Parent..." -LogFilePath $logFilePath -UseUtc:$UseUtc -Silent Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "`nNo epacParent was provided, setting default value...`n" -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Yellow $interview.epacParent = $allInterviewAnswers.epacParent = $gatherData.currentTenantPacSelector.tenantId $testSummarySeparator = "-" $ikey = "epacParent" $hashString = $( -join ($ikey, " ", ($testSummarySeparator * ($TerminalWidth - ($ikey.Length + $($interview.epacParent).Length + 2))), " ", $($interview.epacParent))) Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "$hashString" -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Green } New-HydrationContinuePrompt -Interactive:$Interactive -SleepTime:$sleepTime ############ # epacModifier Loop $loopId = "epacModifier" Write-HydrationLogFile -EntryType newStage -EntryData "Processing QuestionSet: $loopId" -LogFilePath $logFilePath -UseUtc:$UseUtc -Silent try { $interview = New-HydrationAnswerSet -LoopId $loopId -QuestionsFilePath $questionsFilePath -UseUtc:$UseUtc -LogFilePath $logFilePath -TerminalWidth:$TerminalWidth -ErrorAction Stop } catch { Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData $error[0].Exception.Message -LogFilePath $logFilePath -UseUtc:$UseUtc -Silent Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "Failed to process QuestionSet $loopId" -LogFilePath $logFilePath -UseUtc:$UseUtc -Silent Write-Error "Error in the interview process for $loopId, please review the error message and rerun the process." return } Write-HydrationLogFile -EntryType answerSetProvided -EntryData "$($interview | Convertto-Json -depth 100 -compress)" -LogFilePath $logFilePath -UseUtc:$UseUtc -Silent Write-Host "`nQuestion Set is complete, responses outlined below..." -ForegroundColor Yellow foreach ($intKey in $interview.keys) { $allInterviewAnswers.Add($intKey, $interview.$intKey) $testSummarySeparator = "-" $hashString = $( -join ($intKey, " ", ($testSummarySeparator * ($TerminalWidth - ($intKey.Length + $($interview.$intKey).Length + 2))), " ", $($interview.$intKey))) Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "$hashString" -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Green } $epacDevRoot = $( -join ($interview.epacPrefix, $TenantIntermediateRoot, $interview.epacSuffix)) if ($epacDevRoot -eq $TenantIntermediateRoot) { Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "`nEPAC Development Root $epacDevRoot is equal to the specified Tenant Intermediate Root group, $TenantIntermediateRootGroup. This will result in a name collision. Please choose a prefix and/or suffx when you run this process again." -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Red New-HydrationContinuePrompt -Interactive:$Interactive -SleepTime:$sleepTime return } else { $testSummarySeparator = "-" $ival = $( -join ($interview.epacPrefix, $TenantIntermediateRoot, $interview.epacSuffix)) $ikey = 'EpacDevelopmentRoot' $hashString = $( -join ($ikey, " ", ($testSummarySeparator * ($TerminalWidth - ($ikey.Length + $ival.Length + 2))), " ", $ival)) Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "$hashString" -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Green $allInterviewAnswers.Add($ikey, $ival) # Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "`nEPAC Development Root: " -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Green } Remove-Variable ikey -ErrorAction SilentlyContinue Remove-Variable ival -ErrorAction SilentlyContinue Remove-Variable loopId -ErrorAction SilentlyContinue Remove-Variable loopNotes -ErrorAction SilentlyContinue New-HydrationContinuePrompt -Interactive:$Interactive -SleepTime:$sleepTime ############ # policyDecisions Loop $loopId = "policyDecisions" Write-HydrationLogFile -EntryType newStage -EntryData "Processing QuestionSet: $loopId" -LogFilePath $logFilePath -UseUtc:$UseUtc -Silent try { $interview = New-HydrationAnswerSet -LoopId $loopId -QuestionsFilePath $questionsFilePath -UseUtc:$UseUtc -LogFilePath $logFilePath -TerminalWidth:$TerminalWidth -ErrorAction Stop } catch { Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData $error[0].Exception.Message -LogFilePath $logFilePath -UseUtc:$UseUtc -Silent Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "Failed to process QuestionSet $loopId" -LogFilePath $logFilePath -UseUtc:$UseUtc -Silent Write-Error "Error in the interview process for $loopId, please review the error message and rerun the process." return } Write-HydrationLogFile -EntryType answerSetProvided -EntryData "$($interview | Convertto-Json -depth 100 -compress)" -LogFilePath $logFilePath -UseUtc:$UseUtc -Silent Write-Host "`nQuestion Set is complete, responses outlined below..." -ForegroundColor Yellow foreach ($intKey in $interview.keys) { $allInterviewAnswers.Add($intKey, $interview.$intKey) $testSummarySeparator = "-" $hashString = $( -join ($intKey, " ", ($testSummarySeparator * ($TerminalWidth - ($intKey.Length + $($interview.$intKey).Length + 2))), " ", $($interview.$intKey))) Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "$hashString" -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Green } Remove-Variable loopId -ErrorAction SilentlyContinue Remove-Variable loopNotes -ErrorAction SilentlyContinue New-HydrationContinuePrompt -Interactive:$Interactive -SleepTime:$sleepTime ############ # pipelineDecisions Loop $loopId = "pipelineDecisions" Write-HydrationLogFile -EntryType newStage -EntryData "Processing QuestionSet: $loopId" -LogFilePath $logFilePath -UseUtc:$UseUtc -Silent try { $interview = New-HydrationAnswerSet -LoopId $loopId -QuestionsFilePath $questionsFilePath -UseUtc:$UseUtc -LogFilePath $logFilePath -TerminalWidth:$TerminalWidth -ErrorAction Stop } catch { Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData $error[0].Exception.Message -LogFilePath $logFilePath -UseUtc:$UseUtc -Silent Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "Failed to process QuestionSet $loopId" -LogFilePath $logFilePath -UseUtc:$UseUtc -Silent Write-Error "Error in the interview process for $loopId, please review the error message and rerun the process." return } Write-HydrationLogFile -EntryType answerSetProvided -EntryData "$($interview | Convertto-Json -depth 100 -compress)" -LogFilePath $logFilePath -UseUtc:$UseUtc -Silent Write-Host "`nQuestion Set is complete, responses outlined below..." -ForegroundColor Yellow foreach ($intKey in $interview.keys) { $allInterviewAnswers.Add($intKey, $interview.$intKey) $testSummarySeparator = "-" $hashString = $( -join ($intKey, " ", ($testSummarySeparator * ($TerminalWidth - ($intKey.Length + $($interview.$intKey).Length + 2))), " ", $($interview.$intKey))) Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "$hashString" -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Green } Remove-Variable loopId -ErrorAction SilentlyContinue Remove-Variable loopNotes -ErrorAction SilentlyContinue New-HydrationContinuePrompt -Interactive:$Interactive -SleepTime:$sleepTime ############ # otherPipeline Loop if ($AllInterviewAnswers.pipelinePlatform -eq "Other") { $loopId = "otherPipeline" Write-HydrationLogFile -EntryType newStage -EntryData "Processing QuestionSet: $loopId" -LogFilePath $logFilePath -UseUtc:$UseUtc -Silent try { $interview = New-HydrationAnswerSet -LoopId $loopId -QuestionsFilePath $questionsFilePath -UseUtc:$UseUtc -LogFilePath $logFilePath -TerminalWidth:$TerminalWidth -ErrorAction Stop } catch { Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData $error[0].Exception.Message -LogFilePath $logFilePath -UseUtc:$UseUtc -Silent Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "Failed to process QuestionSet $loopId" -LogFilePath $logFilePath -UseUtc:$UseUtc -Silent Write-Error "Error in the interview process for $loopId, please review the error message and rerun the process." return } Write-HydrationLogFile -EntryType answerSetProvided -EntryData "$($interview | Convertto-Json -depth 100 -compress)" -LogFilePath $logFilePath -UseUtc:$UseUtc -Silent Write-Host "`nQuestion Set is complete, responses outlined below..." -ForegroundColor Yellow foreach ($intKey in $interview.keys) { $allInterviewAnswers.Add($intKey, $interview.$intKey) $testSummarySeparator = "-" $hashString = $( -join ($intKey, " ", ($testSummarySeparator * ($TerminalWidth - ($intKey.Length + $($interview.$intKey).Length + 2))), " ", $($interview.$intKey))) Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "$hashString" -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Green } Remove-Variable loopId -ErrorAction SilentlyContinue Remove-Variable loopNotes -ErrorAction SilentlyContinue New-HydrationContinuePrompt -Interactive:$Interactive -SleepTime:$sleepTime } else { $allInterviewAnswers.Add("pipelineCustomPath", "NotApplicable") } # Output Answer File $writeAnswerFile = $stageBlocks.writeAnswerFile New-HydrationSeparatorBlock @writeAnswerFile Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "File Location: $AnswerFilePath"-LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Yellow try { $allInterviewAnswers | ConvertTo-Json -Depth 100 | Set-Content -Path $AnswerFilePath -Force } catch { Write-Error "Unable to write the answer file to $AnswerFilePath. Please ensure that you have write access to the location and try again. This was tested during the preliminary checks, so this is an odd situation. It is possible that a write lock, or some other lock, has occurred." return } # Summary separator block $displayAnswerData = $stageBlocks.displayAnswerData New-HydrationSeparatorBlock @displayAnswerData Write-HydrationLogFile -EntryType newStage -EntryData $displayAnswerData.DisplayText -LogFilePath $logFilePath -UseUtc:$UseUtc -Silent } # Display Summary $testSummarySeparator = "-" Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "Summary:" -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Yellow foreach ($hashkey in $allInterviewAnswers.keys) { $hashString = $( -join ($hashKey, " ", ($testSummarySeparator * ($TerminalWidth - ($hashKey.Length + $($allInterviewAnswers.$hashKey).Length + 2))), " ", $($allInterviewAnswers.$hashKey))) Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "$hashString" -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Green } New-HydrationContinuePrompt -Interactive:$Interactive -SleepTime:$sleepTime ################################################################################ # Execute hydration process Clear-Host $blockData = $stageBlocks.beginHydrationProcess New-HydrationSeparatorBlock @blockData Write-HydrationLogFile -EntryType newStage -EntryData "Hydrating EPAC based on the answers provided below:" -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Yellow foreach ($hashkey in $allInterviewAnswers.keys) { $hashString = $( -join ($hashKey, " ", ($testSummarySeparator * ($TerminalWidth - ($hashKey.Length + $($allInterviewAnswers.$hashKey).Length + 2))), " ", $($allInterviewAnswers.$hashKey))) Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "$hashString" -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Green } ######################### # Create Definitions Folder Structure Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "Creating Definitions folder structure..." -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Yellow try { New-HydrationDefinitionsFolder -DefinitionsRootFolder $DefinitionsRootFolder -ErrorAction Stop } catch { Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData $Error[0].Exception.Message -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Red Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "Unable to create Definitions folder. Please ensure that you have write access to $(Get-Location) and try again." -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Red Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "Use answer file at $AnswerFilePath to rerun the script without prompting for questions to retry this process once the problem is resolved." -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Yellow return } Write-Host " Definitions folder structure has been validated..." -ForegroundColor Green ######################### # Create Global Settings File # Generate Environment List $allInterviewAnswers.initialTenantId = (Get-AzContext).tenant.id $allInterviewAnswers.initialTenantCloud = (Get-AzContext).environment.name $allInterviewAnswers.initialTenantIntermediateRoot = $TenantIntermediateRoot $globalSettingsInputs = [ordered]@{ PacOwnerId = $allInterviewAnswers.pacOwnerId DefinitionsRootFolder = $DefinitionsRootFolder ManagedIdentityLocation = $allInterviewAnswers.managedIdAssignmentLocation MainPacSelector = $allInterviewAnswers.mainTenantMainPacSelectorName EpacPacSelector = "epac-dev" # TODO: Improvement, add a question to the mainpacselector loop for the epacPacSelector Cloud = (Get-AzContext).environment.name # This never need not be prompted, choice of initialTenantId will choose this implicitly TenantId = $allInterviewAnswers.initialTenantId MainDeploymentRoot = $allInterviewAnswers.initialTenantIntermediateRoot EpacDevelopmentRoot = $allInterviewAnswers.EpacDevelopmentRoot Strategy = 'ownedOnly' # This is kept at ownedOnly on purpose. People should be comfortable enough with the deployment to update this before changing to full to help prevent accidents # RepoRoot = $(Resolve-Path $DefinitionsRootFolder | Split-Path -Parent) LogFilePath = $logFilePath UseUtc = $UseUtc KeepDfcSecurityAssignments = $false # TODO: Improvement, add a question to the mainpacselector loop for the dfcSecurityAssignments } # TODO: Improvement, add a question to main loop that enables a second loop that can be run multiple times to generate additional environments to add here in a loop # Use a list of hashtable input (AdditionalPacSelectors) to add additional environments to the global settings file # $additionalEnvironments = @{$i=@{same block as above},$i+n=@{same block as above}} Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "Creating Global Settings file..." -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Yellow try { $null = New-HydrationGlobalSettingsFile @globalSettingsInputs -ErrorAction Stop } catch { Write-Error "Unable to create global-settings file. This is likely a flaw in the choices made above that should have been caught in earlier tests. Please retain your answer file and report this to the EPAC team, and attemp this process again." return } Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "Creating Pipeline..." -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Yellow Remove-Variable usePipelineCustomPath -ErrorAction SilentlyContinue if ($allInterviewAnswers.pipelineCustomPath -eq "NotApplicable") { try { New-PipelinesFromStarterKit -StarterKitFolder $StarterKit ` -PipelineType $allInterviewAnswers.pipelinePlatform ` -BranchingFlow $allInterviewAnswers.pipelineFlow ` -ScriptType $allInterviewAnswers.codeExecutionType ` -ErrorAction Stop } catch { Write-Error "Unable to create pipeline. This is likely a flaw in the choices made above that should have been caught in earlier tests. Please retain your answer file and report this to the EPAC team, and attemp this process again." return } } else { try { New-PipelinesFromStarterKit -StarterKitFolder $StarterKit ` -PipelinesFolder:$usePipelineCustomPath ` -PipelineType $allInterviewAnswers.pipelinePlatform ` -BranchingFlow $allInterviewAnswers.pipelineFlow ` -ScriptType $allInterviewAnswers.codeExecutionType ` -ErrorAction Stop } catch { Write-Error "Unable to create pipeline. This is likely a flaw in the choices made above that should have been caught in earlier tests. Please retain your answer file and report this to the EPAC team, and attemp this process again." return } } ## Build MG Structure if ($allInterviewAnswers.createMainIntermediateRoot -eq "Yes") { Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "Creating Main Intermediate Root..." -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Yellow do{ try{ New-AzManagementGroup -GroupName $allInterviewAnswers.initialTenantIntermediateRoot -ErrorAction Stop } catch{ $AzMgRetry = $true } if(Get-AzManagementGroupRestMethod -GroupId $allInterviewAnswers.initialTenantIntermediateRoot){ $AzMgRetry = $false } }until($AzMgRetry -eq $false) } if ($allInterviewAnswers.createMainCaf3Hierarchy -eq "Yes") { Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "Creating Main CAF3 Management Groups..." -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Yellow New-HydrationCaf3Hierarchy -DestinationRootName $allInterviewAnswers.initialTenantIntermediateRoot -Prefix $allInterviewAnswers.mainCaf3Prefix -Suffix $allInterviewAnswers.maincaf3Suffix -ErrorAction Stop } ## Build EPAC MG Structure if (!($skipEpacMgDeploy)) { Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "Creating Epac Management Groups in $(-join($allInterviewAnswers.epacPrefix,$allInterviewAnswers.initialTenantIntermediateRoot,$allInterviewAnswers.epacSuffix)), a child of $($allInterviewAnswers.epacParent)..." -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Yellow Copy-HydrationManagementGroupHierarchy -SourceGroupName $allInterviewAnswers.initialTenantIntermediateRoot -DestinationParentGroupName $allInterviewAnswers.epacParent -Prefix:$allInterviewAnswers.epacPrefix -Suffix:$allInterviewAnswers.epacSuffix | Out-Null } ## Import Existing Policy Assignments (if applicable) if ($allInterviewAnswers.importExistingPolicies -eq "Yes") { Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "Importing Existing Policy Assignments..." -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Yellow Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData " Exporting existing content for PacSelector `'$($allInterviewAnswers.mainTenantMainPacSelectorName)`', for which the root is defined as $($allInterviewAnswers.initialTenantIntermediateRoot)" -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Yellow Export-AzPolicyResources -DefinitionsRootFolder $DefinitionsRootFolder -ExemptionFiles 'csv' -FileExtension 'jsonc' -IncludeAutoAssigned -IncludeChildScopes -InputPacSelector $allInterviewAnswers.mainTenantMainPacSelectorName -Mode 'export' -OutputFolder $Output -ErrorAction Stop $fpath = Join-Path $Output "Export" "Definitions" if (!(Test-Path $fpath)) { Write-Error "Unable to find the folder $fpath. You should go to https://portal.azure.com and confirm whether or not assignments exist that are assigned within the referenced scope $($answerFile.initialTenantIntermediateRoot) and its children." $noExport = Read-Host "Type 'Confirmed' and press enter to continue, otherwise simply press enter to quit..." if (!($noExport -eq "Confirmed")) { { Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "You have confirmed that the export does not contain your contents. Confirm access, and run the script again, choosing to use the answer file at $answerFile." -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Red } } } elseif (Test-Path $fpath) { $nonAssignmentExportFolders = Get-ChildItem $fpath -Directory -Exclude policyAssignments Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData " Copying $($nonAssignmentExportFolders.count) non-assignment content to definitions folder..." -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Yellow if ($nonAssignmentExportFolders.count -gt 0) { foreach ($sourceDir in $nonAssignmentExportFolders) { $updatedFiles = Get-ChildItem -Path $sourceDir -Recurse -Include "*.json", "*.jsonc", '*.csv' Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData " Copying content from $sourceDir to $DefinitionsRootFolder" -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Yellow foreach ($uFile in $updatedFiles) { # Calculate the relative path of the item $relativePath = $uFile.FullName.Substring($(Split-Path $sourceDir.FullName).Length + 1) # Create the full destination path $destinationPath = Join-Path -Path $DefinitionsRootFolder -ChildPath $relativePath # Create the necessary destination directories $destinationDirPath = Split-Path -Path $destinationPath if (!(Test-Path -Path $destinationDirPath)) { $null = New-Item -Path $destinationDirPath -ItemType Directory Write-HydrationLogFile -EntryType logEntryDataAsPresented ` -EntryData " Created $destinationDirPath" ` -LogFilePath $logFilePath ` -UseUtc:$UseUtc ` -Silent } # Copy the file or directory Copy-Item -Path $uFile.FullName -Destination $destinationPath -Force Write-HydrationLogFile -EntryType logEntryDataAsPresented ` -EntryData " Copied $uFile to $destinationPath" ` -LogFilePath $logFilePath ` -UseUtc:$UseUtc ` -Silent if(!(Test-Path -Path $destinationPath)) { Write-HydrationLogFile -EntryType logEntryDataAsPresented ` -EntryData " Failed to copy $uFile to $destinationPath, this should be copied manually or the task should be run again." ` -LogFilePath $logFilePath ` -UseUtc:$UseUtc ` -ForegroundColor Red } } } } } Remove-Variable noExport -ErrorAction SilentlyContinue } # Add Audit standards at root of hierarchies if ($allInterviewAnswers.importPciDssPolicies -eq "Yes" -or $allInterviewAnswers.importMcsbPolicies -eq "Yes" -or $allInterviewAnswers.importNist80053Policies -eq "Yes") { Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "Adding Specified Compliance Standards" -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Yellow $auditStandardList = @() if ($allInterviewAnswers.importPciDssPolicies -eq "Yes") { $auditStandardList += "/providers/Microsoft.Authorization/policySetDefinitions/c676748e-3af9-4e22-bc28-50feed564afb" } if ($allInterviewAnswers.importMcsbPolicies -eq "Yes") { $auditStandardList += "/providers/Microsoft.Authorization/policySetDefinitions/1f3afdf9-d0c9-4c3d-847f-89da613e70a8" } if ($allInterviewAnswers.importNist80053Policies -eq "Yes") { $auditStandardList += "/providers/Microsoft.Authorization/policySetDefinitions/179d1daa-458f-4e47-8086-2a68d0d6c38f" } foreach ($polSet in $auditStandardList) { Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData " Exporting $polSet" -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Yellow Export-PolicyToEPAC -PolicySetDefinitionId $polSet -OutputFolder $(Join-Path $Output "NewExportedAssignments") -AutoCreateParameters $TRUE -UseBuiltIn $TRUE -Scope $allInterviewAnswers.initialTenantIntermediateRoot -PacSelector $allInterviewAnswers.mainTenantMainPacSelectorName Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData " Copying to $(Join-Path $Output "NewExportedAssignments" "Export" "policyAssignments")" -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Yellow Copy-Item -Path $(Join-Path $Output "NewExportedAssignments" "Export" "policyAssignments") ` -Destination $(Join-Path $Output "export" "definitions") ` -Recurse ` -Force } } # Copy the updated assignments to the definitions folder Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "Updating exported and newly created assignments with epac-dev pacSelector information based on assignments in PacSelector `'$($allInterviewAnswers.mainTenantMainPacSelectorName)...`'" -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Yellow if($(Get-ChildItem $(Join-Path $Output 'export' 'definitions' 'policyAssignments') -ErrorAction SilentlyContinue).count -lt 1) { Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData " No exported assignments found, skipping assignment update..." -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Yellow }else{ Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData " Adding EPAC pacSelector 'epac-dev' to assignments based on current scope..." -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Yellow New-HydrationAssignmentPacSelector -SourcePacSelector $allInterviewAnswers.mainTenantMainPacSelectorName -NewPacSelector 'epac-dev' -MGHierarchyPrefix:$allInterviewAnswers.epacPrefix -MGHierarchySuffix:$allInterviewAnswers.epacSuffix -Definitions $(Join-Path $Output 'export' 'Definitions') -Output $Output -ErrorAction Stop } $updatedAssignmentList = Get-ChildItem -Path $(Join-Path $Output "UpdatedAssignments") -Recurse -Include "*.json", "*.jsonc" -ErrorAction SilentlyContinue if ($updatedAssignmentList.count -gt 0) { Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData " Copying updated $($UpdatedAssignmentList.count) assignments to definitions folder..." -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Yellow Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "Copying new content to $DefinitionsRootFolder" -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Yellow $destinationPath = Join-Path -Path $DefinitionsRootFolder -ChildPath "policyAssignments" if (!(Test-Path -Path $destinationPath)) { $null = New-Item -Path $destinationPath -ItemType Directory } foreach ($assignment in $updatedAssignmentList) { Copy-Item -Path $assignment.FullName -Destination $destinationPath if(Test-Path $(Join-Path $destinationPath $assignment.Name)){ Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData " Copied $assignment to $destinationPath" -LogFilePath $logFilePath -UseUtc:$UseUtc -Silent } else{ Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData " Failed to copy $($assignment.fullname) to $(Join-Path $destinationPath $assignment.Name)" -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Red } } } else{ Write-HydrationLogFile -EntryType logEntryDataAsPresented ` -EntryData " No updated assignments found, skipping assignment update, but it is recommended that that you consider running this again with choices that include generation of an assignment so you have a template to review..." ` -LogFilePath $logFilePath ` -UseUtc:$UseUtc ` -ForegroundColor Yellow } ################################################################################ # Test in EPAC-Dev New-HydrationSeparatorBlock -DisplayText "End Hydration Process" -Location Bottom Write-HydrationLogFile -EntryType logEntryDataAsPresented -EntryData "Hydration process complete" -LogFilePath $logFilePath -UseUtc:$UseUtc -ForegroundColor Magenta ################################################################################ # Test in EPAC-Dev New-HydrationSeparatorBlock -DisplayText "Deploy to epac-dev" -Location Top Write-Host "These stepes require that the Az and EnterprisePolicyAsCode modules be available" -ForegroundColor Yellow Write-Host "When you are ready to deploy the changes to epac-dev, please perform the following tasks to complete deployment to the 'epac-dev' pacSelector:" -ForegroundColor Yellow Write-Host " Build-DeploymentPlans -PacEnvironmentSelector 'epac-dev' -OutputFolder $Output -DefinitionsRootFolder $DefinitionsRootFolder" Write-Host "`nOnce this is complete, begin updating your pipeline environment to test deployment to the epac-dev environment via pipeline..." -ForegroundColor Yellow Write-Host " General Guidance: https://azure.github.io/enterprise-azure-policy-as-code/ci-cd-overview/" Write-Host " Azure DevOps: https://azure.github.io/enterprise-azure-policy-as-code/ci-cd-ado-pipelines/" Write-Host " GitHub Actions: https://azure.github.io/enterprise-azure-policy-as-code/ci-cd-github-actions/" } |