From 8c87d8bba4f6809fdec05d56bcd3333e036faae4 Mon Sep 17 00:00:00 2001 From: alalvi00 Date: Mon, 18 Dec 2023 02:56:58 -0500 Subject: [PATCH] Created GA control --- setup/modules.json | 28 ++++ .../GR-ComplianceChecks-Msgs.psd1 | 9 + src/Guardrails-Common/GR-Common.psm1 | 156 ++++++++++++++++++ 3 files changed, 193 insertions(+) diff --git a/setup/modules.json b/setup/modules.json index 0a1fcf34..47566e3e 100644 --- a/setup/modules.json +++ b/setup/modules.json @@ -27,6 +27,34 @@ } ] }, + { + "ModuleName": "Check-GlobalAdministratorAccntsMFA", + "Control": "Guardrails1", + "ModuleType": "Builtin", + "Status": "Enabled", + "Required": "True", + "Script": "Check-GAAuthenticationMethods -StorageAccountName $vars.storageaccountname -ContainerName $vars.containerName -ResourceGroupName $ResourceGroupName -SubscriptionID $SubID -DocumentName $vars.DocumentName -ControlName $msgTable.CtrName1 -ItemName $msgTable.gaAccntsMFACheck -MsgTable $msgTable -ReportTime $ReportTime -itsgcode $vars.itsgcode", + "variables": [ + { + "Name": "storageAccountName", + "Value": "StorageAccountName" + }, + { + "Name": "containerName", + "Value": "ContainerName" + } + ], + "localVariables": [ + { + "Name": "DocumentName", + "Value": "GlobalAdministratorsUPN.txt" + }, + { + "Name": "itsgcode", + "Value": "IA2(1)" + } + ] + }, { "ModuleName": "Check-DocumentExistsInStorage", "Control": "Guardrails1", diff --git a/src/GuardRails-Localization/GR-ComplianceChecks-Msgs.psd1 b/src/GuardRails-Localization/GR-ComplianceChecks-Msgs.psd1 index ee640d66..5181760d 100644 --- a/src/GuardRails-Localization/GR-ComplianceChecks-Msgs.psd1 +++ b/src/GuardRails-Localization/GR-ComplianceChecks-Msgs.psd1 @@ -35,6 +35,7 @@ bgValidLicenseAssigned = has a valid Microsoft Entra ID P2 assigned bgAccountHasManager = BG Account {0} has a Manager bgAccountNoManager = BG Account {0} doesn't have a Manager bgBothHaveManager = Both BreakGlass accounts have manager +gaAccntsMFACheck = Global Administrators Accounts MFA check # GuardRail #2 MSEntIDLicenseTypeFound = Found correct license type @@ -183,5 +184,13 @@ enableMktPlace = Enable Azure Private MarketPlace as per: https://docs.microsoft # GR-Common procedureFileFound = File {0} found in Container. procedureFileNotFound = Could not find document for {0}, please create and upload a file with the name '{1}' in Container '{2}' on Storage Account '{3}' to confirm you have completed the Item in the control. +procedureFileDataInvalid = The global administrator file(s) contain(s) invalid User Principal Names (UPNs). Ensure that UPNs start with a hyphen, and type each of them on a new line. +globalAdminFileFound = File {0} found in Container. +globalAdminFileNotFound = Could not find document for {0}, please create and upload a file with the name '{1}' in Container '{2}' on Storage Account '{3}' to confirm you have completed the Item in the control. +globalAdminFileEmpty = Empty file {0} found in Container. +globalAdminNotExist = Global Administrator accounts not found or declared in file {0}. +globalAdminMFAPassAndMin2Accnts = Two or more global administrator accounts have been identified, and multi-factor authentication (MFA) is enabled for all of them. +globalAdminMinAccnts = There must be at least two global administrator accounts. +globalAdminAccntsMFADisabled = Not all the global administrator accounts have multi-factor authentication (MFA) enabled '@ diff --git a/src/Guardrails-Common/GR-Common.psm1 b/src/Guardrails-Common/GR-Common.psm1 index 08a40aa2..96c86928 100644 --- a/src/Guardrails-Common/GR-Common.psm1 +++ b/src/Guardrails-Common/GR-Common.psm1 @@ -292,6 +292,150 @@ function Add-LogAnalyticsResults { -logType $LogType ` -TimeStampField Get-Date } + +function Check-GAAuthenticationMethods { + param ( + [string] $StorageAccountName, + [string] $ContainerName, + [string] $ResourceGroupName, + [string] $SubscriptionID, + [string[]] $DocumentName, + [string] $ControlName, + [string]$ItemName, + [hashtable] $msgTable, + [string]$itsgcode, + [Parameter(Mandatory = $true)] + [string] + $ReportTime + ) + [PSCustomObject] $ErrorList = New-Object System.Collections.ArrayList + [bool] $IsCompliant = $false + [string] $Comments = $null + + try { + Select-AzSubscription -Subscription $SubscriptionID | out-null + } + catch{ + $ErrorList.Add("Failed to run 'Select-Azsubscription' with error: $_") + throw "Error: Failed to run 'Select-Azsubscription' with error: $_" + } + try { + $StorageAccount = Get-Azstorageaccount -ResourceGroupName $ResourceGroupName -Name $StorageAccountName -ErrorAction Stop + } + catch { + $ErrorList.Add("Could not find storage account '$storageAccountName' in resoruce group '$resourceGroupName' of ` + subscription '$subscriptionId'; verify that the storage account exists and that you have permissions to it. Error: $_") + + Write-Error "Could not find storage account '$storageAccountName' in resoruce group '$resourceGroupName' of ` + subscription '$subscriptionId'; verify that the storage account exists and that you have permissions to it. Error: $_" + } + + $docFileEmpty = $false + $docFileNotAvailable = $false + $mfaEnabled = $false + $globalAdminFound = $false + $globalAdminCount = 0 + $mfaCounter = 0 + $commentsArray = @() + $globalAdminUPNs = @() + + ForEach ($docName in $DocumentName) { + + $blobs = Get-AzStorageBlob -Container $ContainerName -Context $StorageAccount.Context -Blob $docName -ErrorAction SilentlyContinue + + If ($null -eq $blobs) { + # a blob with the name $DocumentName was located in the specified storage account + $commentsArray += $msgTable.procedureFileNotFound -f $ItemName, $docName, $ContainerName, $StorageAccountName + } + else { + # get blob content if blob exists + $blobContent = (Get-AzStorageBlobContent -Container $ContainerName -Blob $docName -Context $StorageAccount.Context -ErrorAction SilentlyContinue).ICloudBlob.DownloadText() + + if ($blobContent -eq '' -or $blobContent -eq ' ') { + docFileEmpty = $true + $commentsArray += $msgTable.globalAdminFileEmpty -f $docName + } + elseif ($blobContent -eq 'N/A' -or $blobContent -eq 'n/a') { + docFileNotAvailable = $true + $commentsArray += $msgTable.globalAdminNotExist -f $docName + } + else { + $globalAdminFound = $true + $globalAdminUPNs = $blobContent -split '-' | Where-Object { $_ -ne '' } + } + } + } + + if ($globalAdminFound) { + + #Clean up the data and remove any invalid email formats + $filteredUPNs = Clean-GAData -GAUPNs $globalAdminUPNs + + $globalAdminCount = $filteredUPNs.Count + + ForEach ($globalAdminAccount in $filteredUPNs) { + $urlPath = '/users/' + $globalAdminAccount + '/authentication/methods' + + try { + $response = Invoke-GraphQuery -urlPath $urlPath -ErrorAction Stop + } + catch { + $ErrorList.Add("Failed to call Microsoft Graph REST API at URL '$urlPath'; returned error message: $_" ) + Write-Error "Error: Failed to call Microsoft Graph REST API at URL '$urlPath'; returned error message: $_" + } + + $data = $response.Content + $authenticationmethods = $data.value + + # To check if MFA is setup for a user, we're looking for either : + # #microsoft.graph.microsoftAuthenticatorAuthenticationMethod or + # #microsoft.graph.phoneAuthenticationMethod + Write-Host $authenticationmethods + + foreach ($authmeth in $authenticationmethods) { + if (($($authmeth.'@odata.type') -eq "#microsoft.graph.phoneAuthenticationMethod") -or ` + ($($authmeth.'@odata.type') -eq "#microsoft.graph.microsoftAuthenticatorAuthenticationMethod")) { + $mfaCounter += 1 #Need to keep track of each GA mfa in counter and compare it to count + } + } + } + } + + if($mfaCounter -eq $globalAdminCount) { + $mfaEnabled = $true + } + else{ + $commentsArray += $msgTable.globalAdminAccntsMFADisabled + } + + if ($globalAdminCount -lt 2 -and ($docFileEmpty -and $docFileNotAvailable)) { + $commentsArray += $msgTable.globalAdminMinAccnts + } + + if ($globalAdminCount -ge 2 -and $mfaEnabled) { + $commentsArray += $msgTable.globalAdminMFAPassAndMin2Accnts + $IsCompliant = $mfaEnabled + } + $Comments = $commentsArray -join ";" + + $PsObject = [PSCustomObject]@{ + ComplianceStatus = $IsCompliant + ControlName = $ControlName + ItemName = $ItemName + DocumentName = $DocumentName + Comments = $Comments + ReportTime = $ReportTime + itsgcode = $itsgcode + } + $moduleOutput = [PSCustomObject]@{ + ComplianceResults = $PsObject + Errors = $ErrorList + AdditionalResults = $AdditionalResults + } + return $moduleOutput + +} + function Check-DocumentExistsInStorage { [Alias('Check-DocumentsExistInStorage')] param ( @@ -511,6 +655,18 @@ function Hide-Email { } } +function Clean-GAData { + param ( + [string[]] $GAUPNs + ) + + $FilteredUPNs = $GAUPNs | Where-Object { $_ -match '\S' -and $_ -like "*@*" } | ForEach-Object { $_ -replace '\s' } + + if ($FilteredUPNs) { + return $FilteredUPNs + } +} + function Invoke-GraphQuery { param( # URL path (ex: /users)