Conditional Access Policy Exporter

I perform best practice audits of customers’ Conditional Access (CA) policies on a regular basis. If you have ever done this, you will quickly notice that it can be a very intensive exercise due to Azure AD’s portal design. When customers only have a handful of CA policies it can be very easy and quick. However, when reviewing many policies it can quickly become impossible and overwhelming. When having to expand out every policy and sub config, it’s easy to forget all the options and what each policy did.

So to help with this, I wrote a Powershell script to help export these into an easier-to-audit and human-readable format. You might say, “But Doug, there are plenty of other options for exporting!” While this may be true, most of the options I found relied on GraphAPI calls that have proven difficult for clients to set up. Additionally, many of the outputs for these scripts did not help make my analysis easier. So the script below relies only on the Azure AD PowerShell module and hopefully makes it easier to read the policy configs.

Hope this helps you, and if it does send me a note and let me know. @Dougsbaker

CA-Export/Export-CaPolicy.v1.ps1 at main · dougsbaker/CA-Export (github.com)

Script Features

  • Only Requires AzureAD PowerShell Module
  • Exports to HTML Grid View
  • Ability to highlight Control / Condition

Features I am considering adding

  • Moveable Columns / Filters to hide
  • Multiple output formats
  • Export/Restore option

<#
	.SYNOPSIS
		Conditional Access Export Utility
	.DESCRIPTION
       Exports CA Policy to HTML Format for auditing/historical purposes. 

	.NOTES
		Douglas Baker
		@dougsbaker
        
        
        
        ############################################################################
        This sample script is not supported under any standard support program or service. 
        This sample script is provided AS IS without warranty of any kind. 
        This work is licensed under a Creative Commons Attribution 4.0 International License
        https://creativecommons.org/licenses/by-nc-sa/4.0/
        ############################################################################    
	
#>

#ExportLocation
$ExportLocation = "C:\scripts\"
$FileName = "CAPolicy.html"


$HTMLExport = $true

#Connect-AzureAD

try {
    Get-AzureADMSConditionalAccessPolicy -ErrorAction Stop > $null
    Write-host "Connected: AzureAD"
  }
  catch {
    Write-host "Connecting: AzureAD"  
   Try {
    Connect-AzureAD
   }
   Catch
   {
       Write-host "Error: Please Install AzureAD Module"
       Write-Host "Run: Install-module AzureAD"
   }
}
  
#Collect CA Policy
Write-host "Exporting: CA Policy"
$CAPolicy = Get-AzureADMSConditionalAccessPolicy
$TenantData = Get-AzureADTenantDetail
$TenantName = $TenantData.DisplayName


 
$date = Get-Date

$CAExport = [PSCustomObject]@()

$AdUsers = @()
$Apps = @()
#Extract Values
Write-host "Extracting: CA Policy Data"
foreach( $Policy in $CAPolicy)
{

    $IncludeUG = $null
    $IncludeUG = $Policy.Conditions.Users.IncludeUsers
    $IncludeUG +=$Policy.Conditions.Users.IncludeGroups
    $IncludeUG +=$Policy.Conditions.Users.IncludeRoles


    $ExcludeUG = $null
    $ExcludeUG = $Policy.Conditions.Users.ExcludeUsers
    $ExcludeUG +=$Policy.Conditions.Users.ExcludeGroups
    $ExcludeUG +=$Policy.Conditions.Users.ExcludeRoles
    
    
    $Apps += $Policy.Conditions.Applications.IncludeApplications
    $Apps += $Policy.Conditions.Applications.ExcludeApplications

    
    $AdUsers +=$ExcludeUG
    $AdUsers +=$IncludeUG
    
    $InclLocation = $Null
    $ExclLocation = $Null 
    $InclLocation = $Policy.Conditions.Locations.includelocations
    $ExclLocation = $Policy.Conditions.Locations.Excludelocations

    $InclPlat = $Null
    $ExclPlat = $Null 
    $InclPlat = $Policy.Conditions.Platforms.IncludePlatforms
    $ExclPlat = $Policy.Conditions.Platforms.ExcludePlatforms
    $InclDev = $null
    $ExclDev = $null
    $InclDev = $Policy.Conditions.Devices.IncludeDevices
    $ExclDev = $Policy.Conditions.Devices.ExcludeDevices
    $devFilters = $null
    $devFilters = $Policy.Conditions.Devices.DeviceFilter

    $CAExport += New-Object PSObject -Property @{ 
        Name = $Policy.DisplayName;
        Status = $Policy.State;
        Users = "";
        UsersInclude = ($IncludeUG -join ", `r`n");
        UsersExclude = ($ExcludeUG -join ", `r`n");
        'Cloud apps or actions' ="";
        ApplicationsIncluded = ($Policy.Conditions.Applications.IncludeApplications -join ", `r`n");
        ApplicationsExcluded = ($Policy.Conditions.Applications.ExcludeApplications -join ", `r`n");
        userActions = ($Policy.Conditions.Applications.IncludeUserActions -join ", `r`n");
        AuthContext = ($Policy.Conditions.Applications.IncludeAuthenticationContextClassReferences -join ", `r`n");
        Conditions = "";
        UserRisk = ($Policy.Conditions.UserRiskLevels -join ", `r`n");
        SignInRisk = ($Policy.Conditions.SignInRiskLevels -join ", `r`n");
       # Platforms = $Policy.Conditions.Platforms;
        PlatformsInclude =  ($InclPlat -join ", `r`n");
        PlatformsExclude =  ($ExclPlat -join ", `r`n");
       # Locations = $Policy.Conditions.Locations;
        LocationsIncluded = ($InclLocation -join ", `r`n");
        LocationsExcluded = ($ExclLocation -join ", `r`n");
        ClientApps = ($Policy.Conditions.ClientAppTypes -join ", `r`n");
       # Devices = $Policy.Conditions.Devices;
        DevicesIncluded = ($InclDev -join ", `r`n");
        DevicesExcluded = ($ExclDev -join ", `r`n");
        DeviceFilters =($devFilters -join ", `r`n");
        'Access Controls' = "";
     #   Grant = ($Policy.GrantControls.BuiltInControls -join ", `r`n");
        Block = if ($Policy.GrantControls.BuiltInControls -contains "Block") { "True"} else { ""}
        'Require MFA' = if ($Policy.GrantControls.BuiltInControls -contains "Mfa") { "True"} else { ""}
        'CompliantDevice' = if ($Policy.GrantControls.BuiltInControls -contains "CompliantDevice") { "True"} else { ""}
        'DomainJoinedDevice'  = if ($Policy.GrantControls.BuiltInControls -contains "DomainJoinedDevice") { "True"} else { ""}
        'CompliantApplication' = if ($Policy.GrantControls.BuiltInControls -contains "CompliantApplication") { "True"} else { ""}
        'ApprovedApplication'  = if ($Policy.GrantControls.BuiltInControls -contains "ApprovedApplication") { "True"} else { ""}
        'PasswordChange' = if ($Policy.GrantControls.BuiltInControls -contains "PasswordChange") { "True"} else { ""}
        TermsOfUse = if ($Policy.GrantControls.TermsOfUse -ne $null) {"True"} else {""};
        CustomControls =  if ($Policy.GrantControls.CustomAuthenticationFactors -ne $null) {"True"} else {""};
        GrantOperator = $Policy.GrantControls._Operator
       # Session = $Policy.SessionControls
        ApplicationEnforcedRestrictions = $Policy.SessionControls.ApplicationEnforcedRestrictions.IsEnabled
        CloudAppSecurity                = $Policy.SessionControls.CloudAppSecurity.IsEnabled
        PersistentBrowser               = $Policy.SessionControls.PersistentBrowser.Mode
        SignInFrequency                 = "$($Policy.SessionControls.SignInFrequency.Value) $($conditionalAccessPolicy.SessionControls.SignInFrequency.Type)"
    }
  
    
}

#Swith user/group Guid to display names
    Write-host "Converting: AzureAD Guid"
    #Filter out Objects
    $ADsearch = $AdUsers | Where-Object {$_ -ne 'All' -and $_ -ne 'GuestsOrExternalUsers' -and $_ -ne 'None'}
    $cajson =  $CAExport | ConvertTo-Json -Depth 4
    $AdNames =@{}
    Get-AzureADObjectByObjectId -ObjectIds $ADsearch |ForEach-Object{ 
        $obj = $_.ObjectId
        $disp = $_.DisplayName
        $AdNames.$obj=$disp
        $cajson = $cajson -replace "$obj", "$disp"
    }
    $CAExport = $cajson |ConvertFrom-Json
#Switch Apps Guid with Display names
     $allApps =  Get-AzureADServicePrincipal -All $true 
   $allApps | Where-Object{ $_.AppId -in $Apps} | ForEach-Object{
       $obj = $_.AppId
       $disp =$_.DisplayName
       $cajson = $cajson -replace "$obj", "$disp"
   }
#switch named location Guid for Display Names
    Get-AzureADMSNamedLocationPolicy| ForEach-Object{
        $obj = $_.Id
        $disp =$_.DisplayName
        $cajson = $cajson -replace "$obj", "$disp"
    }
#Switch Roles Guid to Names
    Get-AzureADDirectoryRole| ForEach-Object{
        $obj = $_.RoleTemplateId
        $disp =$_.DisplayName
        $cajson = $cajson -replace "$obj", "$disp"
    }
   $CAExport = $cajson |ConvertFrom-Json

#Export Setup
    Write-host "Pivoting: CA to Export Format"
    $pivot = @()
    $rowItem = New-Object PSObject
    $rowitem | Add-Member -type NoteProperty -Name 'CA Item' -Value "row1"
    $Pcount = 1
    foreach($CA in $CAExport)
    {
        $rowitem | Add-Member -type NoteProperty -Name "Policy $pcount" -Value "row1"
                #$ca.Name
                $pcount += 1
    }
    $pivot += $rowItem
  #  $pivot | Out-GridView
#Add Data to Report
$Rows = $CAExport | Get-Member | Where-Object {$_.MemberType -eq "NoteProperty"}
$Rows| ForEach-Object{
    $rowItem = New-Object PSObject
    $rowname = $_.Name
    $rowitem | Add-Member -type NoteProperty -Name 'CA Item' -Value $_.Name
    $Pcount = 1
    foreach($CA in $CAExport)
    {
        $ca | Get-Member | Where-Object {$_.MemberType -eq "NoteProperty"} | ForEach-Object {
            $a = $_.name
            $b = $ca.$a
            if ($a -eq $rowname) {
              $rowitem | Add-Member -type NoteProperty -Name "Policy $pcount" -Value $b  
            }
            
        }
      # $ca.UsersInclude
      $pcount += 1
    }
    $pivot += $rowItem
}
#Set Row Order
$sort = "Name","Status","Users","UsersInclude","UsersExclude","Cloud apps or actions", "ApplicationsIncluded","ApplicationsExcluded",`
        "userActions","AuthContext","Conditions", "UserRisk","SignInRisk","PlatformsInclude","PlatformsExclude","ClientApps", "LocationsIncluded",`
        "LocationsExcluded","Devices","DevicesIncluded","DevicesExcluded","DeviceFilters", "Access Controls", "Block", "Require MFA", "CompliantDevice",`
        "DomainJoinedDevice","CompliantApplication", "ApprovedApplication","PasswordChange", "TermsOfUse", "CustomControls", "GrantOperator", `
        "Session","ApplicationEnforcedRestrictions", "CloudAppSecurity", "PersistentBrowser", "SignInFrequency"

       


if ($HTMLExport) {
    Write-host "Saving to File: HTML"
$jquery = '  <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
    <script>
    $(document).ready(function(){
        $("tr").click(function(){
        if(!$(this).hasClass("selected")){
            $(this).addClass("selected");
        } else {
            $(this).removeClass("selected");
        }

        });
        $("th").click(function(){
        if(!$(this).hasClass("colselected")){
            $(this).addClass("colselected");
        } else {
            $(this).removeClass("colselected");
        }

        });
    });
    </script>'
$html = "<html><head><base href='https://docs.microsoft.com/' target='_blank'>
                $jquery<style>
                .title{
                    display: block;
                    font-size: 2em;
                    margin-block-start: 0.67em;
                    margin-block-end: 0.67em;
                    margin-inline-start: 0px;
                    margin-inline-end: 0px;
                    font-weight: bold;
                    font-family: Segoe UI;
                    
                }
                table{
                    border-collapse: collapse;
                    margin: 25px 0;
                    font-size: 0.9em;
                    font-family: Segoe UI;
                    min-width: 400px;
                    box-shadow: 0 0 20px rgba(0, 0, 0, 0.15) ;
                    text-align: center;
               }
                thead tr {
                    background-color: #009879;
                    color: #ffffff;
                    text-align: left;
               }
                th, td {
                    min-width: 250px;
                    padding: 12px 15px;
                    border: 1px solid lightgray;
                    vertical-align: top;
               }
               
                td {
                    vertical-align: top;
               }
                tbody tr {
                    border-bottom: 1px solid #dddddd;
               }
                tbody tr:nth-of-type(even) {
                    background-color: #f3f3f3;
               }
               tbody tr:nth-of-type(4), tbody tr:nth-of-type(7), tbody tr:nth-of-type(12), tbody tr:nth-of-type(23){
                    background-color: #36c;
                    text-aling:left !important
                }
                tbody tr:last-of-type {
                    border-bottom: 2px solid #009879;
               }
               tr:hover{
                background-color: #ffea76!important;
            }
            
            .selected:not(th){
                background-color:#ffea76!important;
                
                }
                th{
                   background-color:white !important;
                }
                .colselected {
              
              background-color: rgb(93, 236, 213)!important;
              
              }
              table tr th:first-child,table tr td:first-child {
                    position: sticky;
                    inset-inline-start: 0; 
                    background-color: #36c!important;
                    Color: #fff;
                    font-weight: bolder;
                    text-align: center;
               }
                </style></head><body> <div class='Title'>CA Export: $Tenantname - $Date </div>"
                

    Write-host "Launching: Web Browser"           
    $Launch = $ExportLocation+$FileName
    $HTML += $pivot  | Where-Object {$_."CA Item" -ne 'row1' } | Sort-object { $sort.IndexOf($_."CA Item") }| convertto-html -Fragment
    $HTML | Out-File $Launch
        start-process $Launch
}

Defender for Identity Audit Deleted Objects

So recently I noticed in my new Server 2019 DFI lab I was not getting auditing when an object was deleted. This was curious to me as I have always in the past gotten this type of info from the product. Turns out there is one line I missed on pre-reqs that I have never run into it being an issue before.

Deleted Objects container Recommendation: User should have read-only permissions on the Deleted Objects container. Read-only permissions on this container allow Defender for Identity to detect user deletions from your Active Directory.

Microsoft Defender for Identity prerequisites | Microsoft Docs

I have always just blazed past this note because I go to the root level of the domain granted Read Acces to my service account. This works great everywhere except apparently the Deleted Objects Folder. Turns out that when you enable the Deleted Objects folder it does not by default inherit permissions. Well in this lab it the issue presented itself because I had actually gone in and enabled the AD Recycle Bin, but hadn’t done any customizations. Domain Admins had rights but no other accounts, including my Service Account. So Defender for Identity couldn’t see when an object was moved to that temp container.

You can identify if you have this issue by deleting a test AD account and wait for your portal to update. or by running the below command

#List permissions
dsacls "CN=Deleted Objects,DC=Attack1Lab,DC=local"

Well if you find you have this issue don’t be too alarmed fixing it is pretty easy.

#Give yourself Permissions to modify the container
dsacls "CN=Deleted Objects,DC=Attack1Lab,DC=local" /takeownership
#Give your DFI service account access to read items in the container
dsacls "CN=Deleted Objects,DC=Attack1Lab,DC=local" /g Attack1Lab\srv_ATP:LCRP
#if using GMSA make sure you single quote the command or powershell will convert it to a variable. 
dsacls "CN=Deleted Objects,DC=Attack1Lab,DC=local" /g 'Attack1Lab\gmsa-DFI$:LCRP'

Hope This helps!

Audit All Mailbox Activity

Note: Updated 11/12/2021 to include SearchQueryInitiated

Ever wanted to make sure you are auditing all available activities in Exchange Online? Me too! So I wrote a PowerShell to turn on logging for every possible item EXO can audit. Adjust to your liking and license level!

So why would you want this? Isn’t logging enabled by default in EXO? Well, sort of… According to MSFT documentation, not all available activities are enabled by default. Some of these may be inconsequential, like updating record tags, but some of these like moving an item to a folder or accessing a folder may paint an important picture of activities that happened in a mailbox. The other more important reason you would want to do this is I have noticed EXO does not always enable logging. A few times I have randomly found users with Audit logging disabled, or more commonly during license changes, E3 to E5 upgrades, not all of the Advanced Auditing turns on. Also just as a note to audit everything you will need some version of an E5, see KB articles above.

#Enable global audit logging
Get-Mailbox -ResultSize Unlimited -Filter `
 {RecipientTypeDetails -eq "UserMailbox" -or RecipientTypeDetails -eq "SharedMailbox" -or RecipientTypeDetails -eq "RoomMailbox" -or RecipientTypeDetails -eq "DiscoveryMailbox"} `
 | Select PrimarySmtpAddress `
 | ForEach {$_.PrimarySmtpAddress
    Set-Mailbox -Identity $_.PrimarySmtpAddress -AuditEnabled $true -AuditLogAgeLimit 180 `
    -AuditAdmin   @{add="ApplyRecord","Copy","Create", "FolderBind" , "HardDelete", "MailItemsAccessed",  "Move", "MoveToDeletedItems","RecordDelete", "Send", "SendAs", "SendOnBehalf", "SoftDelete", "Update", "UpdateCalendarDelegation", "UpdateComplianceTag", "UpdateFolderPermissions", "UpdateInboxRules"  } `
    -AuditDelegate @{add="ApplyRecord", "Create", "FolderBind" , "HardDelete", "MailItemsAccessed" , "Move", "MoveToDeletedItems","RecordDelete",  "SendAs", "SendOnBehalf", "SoftDelete", "Update",  "UpdateComplianceTag", "UpdateFolderPermissions", "UpdateInboxRules"  } `
    -AuditOwner  @{add="ApplyRecord", "Create", "HardDelete", "MailItemsAccessed", "MailboxLogin", "Move", "MoveToDeletedItems","RecordDelete", "Send",  "SoftDelete", "Update", "UpdateCalendarDelegation", "UpdateComplianceTag", "UpdateFolderPermissions", "UpdateInboxRules", "SearchQueryInitiated"  }
   }# #

#Double-Check It!
$FormatEnumerationLimit=-1
Get-Mailbox -ResultSize Unlimited | select Name, email, AuditEnabled, AuditLogAgeLimit, Auditowner, auditdelegate, AuditAdmin  | Out-Gridview

Deploy MDATP Tags with Intune

Do you feel its a little funny that Microsoft doesn’t have a built-in way to deploy MDATP tags Via Intune? Well, so do I! To get around this weakness I went and wrote a little Powershell script to help take care of it. Deploy it via intune script policy and you should be set/manage any regional tags you want in MDATP via intune.

Shout out to the Microsoft Scripting Guy Ed Wilson for the base code to update the values. https://devblogs.microsoft.com/scripting/update-or-add-registry-key-value-with-powershell/

$registryPath = "HKLM:SOFTWARE\Policies\Microsoft\Windows Advanced Threat Protection\DeviceTagging\"

$Name = "Group"
$value = "PowerShellTag"

IF(!(Test-Path $registryPath))

  {
    New-Item -Path $registryPath -Force | Out-Null
    New-ItemProperty -Path $registryPath -Name $name -Value $value -PropertyType String -Force | Out-Null}

 ELSE {
    New-ItemProperty -Path $registryPath -Name $name -Value $value -PropertyType string -Force | Out-Null}
Intune Settings

Let me know if there is an easier way to do this.

MDATP Portal

Export Azure backups in VHD format

Have you ever run into an issue where you need to export a backup of an Azure vm? No? Just me? Okay, well It can be a pain because there is no native way to just get the VHD of the backup file. If you want to restore a backup point, it’s no problem. If you want to clone a machine, no problem! Export a backup that you have already taken, well now that is a lot of work!

The trouble is due to how managed disks work in Azure. Since I am running machines with managed disks, when you restore the backup it retains that format and is only in a format that can be used by Azure. You may have noticed that if you ever pull up Azure file explorer you can’t see any of your vm’s disks, so to get around that you have to convert your managed disks to VHD and copy them to a new storage account.

Here is the hard thing, there is no way in the GUI to do this you have to use power shell if you want to get this to work. Lucky for you here is how you can do it without coming up with another option.

1st go into your Azure backups and restore a backup point. Here you want to create a new restore disk. Make sure to select the

2nd create a new blob you want to copy the VHD exports to. Once created you will need to determine the storage account Name, Container Name, and keys for the blob you plan on using.

You can find most of this info in your storage account.

3rd connect to your Azure environment via power shell.

At this point we need to identify the name or names of the restored disks we want to convert or export.

Use the following script to identify there name based on their creation date.

Connect-AzureRmAccount
get-azurermdisk | sort-object -property timecreated | ft name, TimeCreated

Finally we just need to put it all together and we can export these to our blob and then use storage explorer to download the files.

$disks = 'Disk1','Disk2'
$resourcegroup = 'enter the managed disk resource group name'

foreach ( $diskname in $disks){
$diskname
Get-AzureRmDisk -DiskName $diskname -ResourceGroupName $resourcegroup
$SAS = Grant-AzureRmDiskAccess -DiskName $diskname -ResourceGroupName $resourcegroup -DurationInSecond 58600 -Access Read
# Get the destination details
$storageAccountName = "##theNameof yourBlob##"
$storageContainerName = "##vhd##"
$destinationVHDFileName = $diskname
$storageAccountKey = "##yourkey##"
$destinationContext = New-AzureStorageContext –StorageAccountName $storageAccountName -StorageAccountKey $storageAccountKey
$sas.AccessSAS
# copy the vhd
Start-AzureStorageBlobCopy -AbsoluteUri $sas.AccessSAS -DestContainer $storageContainerName -DestContext $destinationContext -DestBlob $destinationVHDFileName
}

If you are in a time crunch you can monitor the status of the export by using the following code.

foreach ( $diskname in $disks){
$status = Get-AzureStorageBlobCopyState -Blob $destinationVHDFileName -Container $storageContainerName -Context $destinationContext
$percentage = $status.BytesCopied/$status.TotalBytes*100
$percentage = "{0:N2}" -f $percentage
Write-Host -ForegroundColor Yellow "$percentage completed!"
}

Powershell admin tool launcher

I Often find my self needing to open tools such as AD or DNS as a different user account. This is because as a security best practices I usually recommend organizations run dual account security. Where an IT team member uses a separate account for admin activity vs their day to day account that has email. This is a great way to increase security and limit your exposure. However, it often can prove to be a productivity killer for IT teams. As each tool such as AD or DHCP needs to be run as a different user, or you need to have a jump box to do administration.

To avoid this issue I went and wrote a powershell script to help launch your admin utilities as another account. 

I customized this one to allow any one to customize the app launcher to run any tools or commands you need it to. When it launches you will have simple way to start all your admin tools as another account. The Caveat, is you have to launch powershell once as a different user. This works for me since I almost always have powershell open. 

To get started with this script you will need to create a shortcut with the run as command and that launches powershell. And then simply add the following scripts to your profile and then you will have a customizable Launcher.

function Start-AdminTools { #=========================================================================== # Modify Tile names and programs you want run #=========================================================================== $buttonTitle1 = "AD Center" $buttonTitle2 = "Group Policy" $buttonTitle3 = "AD User - Comp" $buttonTitle4 = "Server Manager" $buttonTitle5 = "DHCP" $buttonTitle6 = "DNS" $launchapp1 = "C:\WINDOWS\system32\dsac.exe" $launchapp2 = "gpmc.msc" $launchapp3 = "dsa.msc" $launchapp4 = "servermanager" $launchapp5 = "dhcpmgmt.msc" $launchapp6 = "dnsmgmt.msc" #=========================================================================== # Xaml Script for GUI Interface #=========================================================================== $inputXML = @"