public/get-changedPermissions.ps1

Function Get-ChangedPermissions{
    <#
        Author = "Jos Lieben (jos@lieben.nu)"
        CompanyName = "Lieben Consultancy"
        Copyright = "https://www.lieben.nu/liebensraum/commercial-use/"
         
        Parameters:
        oldPermissionsReportFolder: the path to the previous report folder you want to compare against, auto-detected if not specified
        newPermissionsReportFolder: the path to the new report folder you want to compare against, auto-detected if not specified
        resources: the resources to compare, default is all resources
    #>
        
    Param(
        [Parameter(Mandatory = $false)][String]$oldPermissionsReportFolder,
        [Parameter(Mandatory = $false)][String]$newPermissionsReportFolder,
        [String[]]$resources = @("SharePoint","Onedrive","Teams","O365Group","PowerBI","GroupsAndMembers","Entra","ExoRecipients","ExoRoles","Azure","PowerPlatform")
    )

    $excludeProps = @{
        All = @()
        "Entra" = @("principalName","startDateTime","endDateTime")
        "ExoRecipients" = @("PrincipalName")
        "ExoRoles" = @("PrincipalName")
        "GroupsAndMembers" = @("MemberName","GroupName")
        "O365Group" = @("Name")
        "OneDrive" = @("Name")
        "Teams" = @("Name")
        "SharePoint" = @("Name")
        "PowerBI" = @("createdDateTime","modifiedDateTime")
        "Azure" = @("createdDateTime","modifiedDateTime")
        "PowerPlatform" = @("createdDateTime","modifiedDateTime")
    }

    #register version specific exclusions here to avoid generating large numbers of falsely detected changed permissions.
    #if comparing different versions, the NEW version's one time exclusions will be applied when comparing differences
    $versionChangeTransitionalExclusions = @{
        "1.1.5" = @{"All" = @("ObjectId")}
    }

    if(!$oldPermissionsReportFolder -and !$newPermissionsReportFolder){
        if($global:octo.connection -eq "Connected"){
            Write-LogMessage -Level 4 -message "No report folders specified, auto-detecting changes since last run based on session identifier $($global:octo.userConfig.sessionIdentifier)"
        }else{
            Throw "Please run connect-M365 before using this function, OR specify the report folders manually using -oldPermissionsReportFolder and -newPermissionsReportFolder"
        }
        $newPermissionsReportFolder = $global:octo.userConfig.outputFolder
        $newReportFiles = Get-ChildItem -Path $newPermissionsReportFolder -Filter "*.json" | Where-Object { $_.Name -notlike "*delta*" }
        $allSessions = Get-ChildItem -Path (Split-Path -Path $newPermissionsReportFolder -Parent) -Filter "$($global:octo.tenantName)_*" | Sort-Object -Property Name -Descending
        if($allSessions.Count -lt 2){
            Throw "Less than 2 sessions found for `"$($global:octo.tenantName)`" in $(Split-Path -Path $global:octo.userConfig.outputFolder -Parent), please run a second scan first or specify the report folders manually using -oldPermissionsReportFolder and -newPermissionsReportFolder"
        }
        $oldPermissionsReportFolder = $allSessions[1].FullName
        $oldReportFiles = Get-ChildItem -Path $oldPermissionsReportFolder -Filter "*.json" | Where-Object { $_.Name -notlike "*delta*" }
    }elseif($newPermissionsReportFolder -and $oldPermissionsReportFolder){
        $newReportFiles = Get-ChildItem -Path $newPermissionsReportFolder -Filter "*.json" | Where-Object { $_.Name -notlike "*delta*" }
        $oldReportFiles = Get-ChildItem -Path $oldPermissionsReportFolder -Filter "*.json" | Where-Object { $_.Name -notlike "*delta*" }
    }

    if($newReportFiles.Count -lt 1){
        Throw "Less than 1 JSON reports found in $($newPermissionsReportFolder). Please run a scan first or specify the report folder using -newPermissionsReportFolder"
    }

    if($oldReportFiles.Count -lt 1){
        Throw "Less than 1 JSON reports found in $($oldPermissionsReportFolder). Please run a scan first or specify the report folder using -oldPermissionsReportFolder"
    }

    $currentRunVersion = $null
    Select-String -Path ($newReportFiles | where{$_.Name -like '*statistics.json'} | select -first 1).FullName -Pattern '"Module version"\s*:\s*"([^"]+)"' | Select-Object -First 1 | 
    ForEach-Object { $currentRunVersion = $_.Matches.Groups[1].Value }

    $previousRunVersion = $Null
    Select-String -Path ($oldReportFiles | where{$_.Name -like '*statistics.json'} | select -first 1).FullName -Pattern '"Module version"\s*:\s*"([^"]+)"' | Select-Object -First 1 | 
    ForEach-Object { $previousRunVersion = $_.Matches.Groups[1].Value }

    if($currentRunVersion -and $previousRunVersion){
        if($currentRunVersion -ne $previousRunVersion){
            Write-LogMessage -Level 4 -message "Detected version change from $($previousRunVersion) to $($currentRunVersion), applying transitional exclusions"
            $versionChangeExclusions = $versionChangeTransitionalExclusions.$currentRunVersion
            if($versionChangeExclusions){
                foreach($key in $versionChangeExclusions.Keys){
                    $excludeProps.$key += $versionChangeExclusions.$key
                }
            }
        }
    }

    Write-LogMessage -Level 3 -message "Comparing $($newPermissionsReportFolder) with $($oldPermissionsReportFolder)"

    $count = 0
    foreach($newReportFile in $newReportFiles){
        $count++
        $diffResults = @()
        $targetFileName = $newReportFile.FullName.Replace(".json","_delta.json")
        $oldReportFile = $oldReportFiles | Where-Object { $_.Name -eq $newReportFile.Name }        
        try{$percentComplete = (($count  / ($newReportFiles.Count)) * 100)}catch{$percentComplete = 0}
        $resource = $newReportFile.Name.Split(".")[0].Split("_")[1]
        if($resources -notcontains $resource){
            continue
        }

        $applicableExclusions = $excludeProps.All + $excludeProps.$resource
        #create the actual regex with the final list of excluded properties
        $pattern = '"(' + ($applicableExclusions -join '|') + ')"'

        Write-Progress -Id 1 -Activity "Comparing reports" -Status "$count / $($newReportFiles.Count) $resource Loading previous permissions..." -PercentComplete $percentComplete        
        $oldTab = $Null; 
        if($oldReportFile){
            $oldTab = ConvertFrom-JsonToHash -path $oldReportFile.FullName -exclusionPattern $pattern
        }

        Write-Progress -Id 1 -Activity "Comparing reports" -Status "$count / $($newReportFiles.Count) $resource Loading current permissions..." -PercentComplete $percentComplete      
        $newTab = $Null; 
        if($newReportFile){
            $newTab = ConvertFrom-JsonToHash -path $newReportFile.FullName -exclusionPattern $pattern
        }  
        
        if(!$oldTab -or $oldTab.Count -eq 0){
            $oldTab = @()
            Write-LogMessage -Level 4 -message "No previous permissions found for $resource"
        }
        if(!$newTab -or $newTab.Count -eq 0){
            $newTab = @()
            Write-LogMessage -Level 4 -message "No current permissions found in $($newReportFile.Name)"
        }     

        Write-Progress -Id 1 -Activity "Comparing reports" -Status "$count / $($newReportFiles.Count) $resource Processing removals..." -PercentComplete $percentComplete

        #current workload found, check for removals
        $i = 0
        foreach($oldObject in $oldTab.Keys){
            try{$percentComplete = ((($i+1)  / ($oldTab.Keys.Count+1)) * 100)}catch{$percentComplete = 0}
            Write-Progress -Id 2 -Activity "Processing removals for $resource" -Status "$($i+1) / $($oldTab.Keys.Count))" -PercentComplete $percentComplete
            $existed = $newTab.ContainsKey($oldObject)
            if (!$existed) {
                if($oldTab[$oldObject] -eq $True){
                    [PSCustomObject]$diffItem = $oldObject | ConvertFrom-Json -Depth 10
                }else{
                    [PSCustomObject]$diffItem = $oldTab[$oldObject] | ConvertFrom-Json -Depth 10
                }                
                $diffItem | Add-Member -MemberType NoteProperty -Name Action -Value "Removed"
                $diffResults += $diffItem
            }
            $i++
        }

        $removedCount = $diffResults.count
        Write-LogMessage -message "Found $($removedCount) removed permissions for $resource"
        Write-Progress -Id 2 -Activity "Processing removals for $resource" -Completed  
        
        Write-Progress -Id 1 -Activity "Comparing reports" -Status "$count / $($newReportFiles.Count) $resource Processing additions / updates..." -PercentComplete $percentComplete

        #current workload found, check for additions
        $i = 0
        foreach($newObject in $newTab.Keys){
            try{$percentComplete = ((($i+1)  / ($newTab.Keys.Count+1)) * 100)}catch{$percentComplete = 0}
            Write-Progress -Id 2 -Activity "Processing additions for $resource" -Status "$($i+1) / $($newTab.Keys.Count))" -PercentComplete $percentComplete
            $existed = $oldTab.ContainsKey($newObject)
            if (!$existed) {
                if($newTab[$newObject] -eq $True){
                    [PSCustomObject]$diffItem = $newObject | ConvertFrom-Json -Depth 10
                }else{
                    [PSCustomObject]$diffItem = $newTab[$newObject] | ConvertFrom-Json -Depth 10
                }
                $diffItem | Add-Member -MemberType NoteProperty -Name Action -Value "New or Updated"
                $diffResults += $diffItem
            }
            $i++
        }
        
        Write-LogMessage -message "Found $($diffResults.count - $removedCount) new or updated permissions for $resource"
        Write-Progress -Id 2 -Activity "Processing additions for $resource" -Completed          

        Write-Progress -Id 1 -Activity "Comparing reports" -Status "$count / $($newReportFiles.Count) $resource storing delta file..." -PercentComplete $percentComplete
        Write-LogMessage -message ""
        if($diffResults.count -eq 0){
            continue
        }
        
        $diffResults | ConvertTo-Json -Depth 100 | Out-File -Path $targetFileName -Force
    }

    Write-Progress -Id 1 -Completed
    Remove-Variable -Name diffResults -Force -Confirm:$False
    [System.GC]::GetTotalMemory($true) | out-null
}