Framework/Core/SVT/AADResourceResolver.ps1

Set-StrictMode -Version Latest

class AADResourceResolver: Resolver
{
    [SVTResource[]] $SVTResources = @();
    [string] $ResourcePath;
    [string] $tenantId;
    [int] $SVTResourcesFoundCount=0;
    [bool] $scanTenant;
    [int] $MaxObjectsToScan;
    [int] $BatchThreshold;
    [bool] $ShouldBatchScan;
    [string[]] $ObjectTypesToScan;

    hidden static [string[]] $AllTypes = @("AppRegistration", "EnterpriseApplication");

    hidden [int] $hardStopLimit;
    hidden [bool] $isTenantScanned = $false;


    AADResourceResolver([string]$tenantId, [bool] $bScanTenant): Base($tenantId)
    {
        if ([string]::IsNullOrEmpty($tenantId))
        {
            $this.tenantId = ([AccountHelper]::GetCurrentMgContext()).TenantId
        }
        else 
        {
            $this.tenantId = $tenantId
        }
        $this.scanTenant = $bScanTenant
        #TODO: See if we can read this from some settings file.
        $this.BatchThreshold = 999;
        $this.hardStopLimit = 15000; 
    }

    [void] SetScanParameters([string[]] $objTypesToScan, $maxObj)
    { 
        if ($objTypesToScan.Contains("All"))
        {
            if ($objTypesToScan.Count -ne 1)
            {
                throw ([SuppressedException]::new("The objectType 'All' cannot be used in combination with other types.", [SuppressedExceptionType]::InvalidOperation))
            }
            $this.ObjectTypesToScan = [AADResourceResolver]::AllTypes
        }
        elseif ($objTypesToScan.Contains("None"))
        {
            if ($objTypesToScan.Count -ne 1)
            {
                throw ([SuppressedException]::new("The objectType 'None' cannot be used in combination with other types.", [SuppressedExceptionType]::InvalidOperation))
            }
            $this.ObjectTypesToScan = $objTypesToScan
        }
        else
        {
            $this.ObjectTypesToScan = $objTypesToScan
        }

        $this.ShouldBatchScan = ($maxObj -le 0 -or $maxObj -gt $this.BatchThreshold);
        if ($maxObj -le 0 -or $maxObj -gt $this.hardStopLimit)
        {
            $this.MaxObjectsToScan = $this.hardStopLimit
        }
        else
        {
            $this.MaxObjectsToScan = $maxObj
        }
    }

    [bool] NeedToScanType([string] $objType)
    {
        return $this.ObjectTypesToScan -contains $objType
    }

    [void] ClearResources()
    {
        $this.SVTResources = @();
    }

    [string] ExtractDisplayNameFromResource([PsCustomObject] $resource)
    {
        if($this.scanTenant)
        {
            return $resource.DisplayName;
        }
        else
        {
           return $resource.AdditionalProperties["displayName"];
        }
    }

    [void] LoadResourcesForScan()
    {
        $tenantInfoMsg = [AccountHelper]::GetCurrentTenantInfo();
        #Write-Host -ForegroundColor Green $tenantInfoMsg #TODO: Need to do with PublishCustomMessage...just before #-of-resources...etc.?
        $this.PublishCustomMessage([Constants]::DoubleDashLine + "`r`n$tenantInfoMsg`r`n" + [Constants]::DoubleDashLine, [MessageType]::Update )

        #TODO: TBD - for use later...
        $bAdmin = [AccountHelper]::IsUserInAPermanentAdminRole();

        #scanTenant is used to determine is the scan is tenant wide or just within the scope of the current (logged-in) user.
        # if ($this.scanTenant -and !$this.isTenantScanned)
        # {
        # $svtResource = [SVTResource]::new();
        # $svtResource.ResourceName = $this.tenantContext.TenantName;
        # $svtResource.ResourceType = "AAD.Tenant";
        # $svtResource.ResourceId = $this.tenantId
        # $svtResource.ResourceTypeMapping = ([SVTMapping]::AADResourceMapping |
        # Where-Object { $_.ResourceType -eq $svtResource.ResourceType } |
        # Select-Object -First 1)
        # $this.SVTResources +=$svtResource
        # $this.isTenantScanned = $true;
        # }

        $currUser = [AccountHelper]::GetCurrentSessionUserObjectId();

        $userOwnedObjects = @()

        try {  #BUGBUG: Investigate why this crashes in the Live tenant (even if user-created-objects exist...which should show up as 'user-owned' by default!)
            if ($this.ShouldBatchScan)
            {
                $userOwnedObjects = @(Get-MgUserOwnedObject -UserId $currUser -PageSize $this.BatchThreshold -All -Limit $this.MaxObjectsToScan);
            }
            else
            {
                $userOwnedObjects = @(Get-MgUserOwnedObject -UserId $currUser -Top $this.MaxObjectsToScan);   
            }
        }
        catch { #As a workaround, we take user-created objects, which seems to work (strange!)
            if ($this.ShouldBatchScan)
            {
                $userCreatedObjects = @(Get-MgUserCreatedObject -UserId $currUser -PageSize $this.BatchThreshold -All -Limit $this.MaxObjectsToScan);
                $this.BatchCounters.UserOwnedObjects += $userCreatedObjects.Count;
            }
            else
            {
                $userCreatedObjects =  @(Get-MgUserCreatedObject -UserId $currUser -Top $this.MaxObjectsToScan);   
            }
            $userOwnedObjects = $userCreatedObjects
        }
        #TODO Explore delta between 'user-created' v. 'user-owned' for Apps/SPNs

        if ($this.NeedToScanType("AppRegistration"))
        {
            $appObjects = @()
            if ($this.scanTenant)
            {
                if($this.ShouldBatchScan)
                {
                    Write-Host "You have requested for a full tenant scan. Loading resources will take some time. Since this is a preview version, we have added a hard stop to scan first 15K resources." -ForegroundColor "Yello"
                }
                if ($this.ShouldBatchScan)
                {
                    $appObjects =  @(Get-MgApplication -PageSize $this.BatchThreshold -All -Limit $this.MaxObjectsToScan -Property Id, DisplayName);
                }
                else
                {
                    $appObjects =  @(Get-MgApplication -Top $this.MaxObjectsToScan -Property Id, DisplayName);
                }
            }
            else {
                $appObjects = @($userOwnedObjects | Where-Object {$_.AdditionalProperties."@odata.type" -eq '#microsoft.graph.application'})
            }

            $appTypeMapping = ([SVTMapping]::AADResourceMapping |
                Where-Object { $_.ResourceType -eq 'AAD.AppRegistration' } |
                Select-Object -First 1)

            $nObj = $this.MaxObjectsToScan
            foreach ($obj in $appObjects) {
                $svtResource = [SVTResource]::new();
                $svtResource.ResourceName = $this.ExtractDisplayNameFromResource($obj);
                $svtResource.ResourceGroupName = ""  #If blank, the column gets skipped in CSV file.
                #TODO: If rgName == "" then all LOGs end up in root folder alongside CSV, README.txt. May need to have a reasonable 'mock' RGName.
                $svtResource.ResourceType = "AAD.AppRegistration";
                $svtResource.ResourceId = $obj.Id     
                $svtResource.ResourceTypeMapping = $appTypeMapping   
                $this.SVTResources +=$svtResource
                if (--$nObj -eq 0) { break;} 
            }
        }

        if ($this.NeedToScanType("EnterpriseApplication"))
        {
            $spnObjects = @()
            if ($this.scanTenant)
            {
                if ($this.ShouldBatchScan)
                {
                    $spnObjects = @(Get-MgServicePrincipal -PageSize $this.BatchThreshold -All -Limit $this.MaxObjectsToScan -Property Id, DisplayName);
                }
                else
                {
                    $spnObjects = @(Get-MgServicePrincipal -Top $this.MaxObjectsToScan -Property Id, DisplayName);
                }
            }
            else {
                $spnObjects = @($userOwnedObjects | Where-Object {$_.AdditionalProperties."@odata.type" -eq '#microsoft.graph.servicePrincipal'})
            }
            
            $spnTypeMapping = ([SVTMapping]::AADResourceMapping |
                Where-Object { $_.ResourceType -eq 'AAD.EnterpriseApplication' } |
                Select-Object -First 1)

            $nObj = $this.MaxObjectsToScan
            foreach ($obj in $spnObjects) {
                $svtResource = [SVTResource]::new();
                $svtResource.ResourceName = $this.ExtractDisplayNameFromResource($obj);
                $svtResource.ResourceGroupName = ""  #If blank, the column gets skipped in CSV file.
                $svtResource.ResourceType = "AAD.EnterpriseApplication";
                $svtResource.ResourceId = $obj.Id     
                $svtResource.ResourceTypeMapping = $spnTypeMapping   
                $this.SVTResources +=$svtResource
                if (--$nObj -eq 0) { break;} 
            }   #TODO odd that above query does not show user created 'Group' objects.
        }

        if ($this.NeedToScanType("Device"))
        {
            $deviceObjects = @()
            if ($this.scanTenant)
            {
                if ($this.ShouldBatchScan)
                {
                    $deviceObjects = @(Get-MgDevice -PageSize $this.BatchThreshold -All -Limit $this.MaxObjectsToScan -Property Id, DisplayName);
                }
                else
                {
                    $deviceObjects = @(Get-MgDevice -Top $this.MaxObjectsToScan -Property Id, DisplayName);
                }
            }
            else {
                if ($this.ShouldBatchScan)
                {
                    $DeviceObjects = @(Get-MgUserOwnedDevice -UserId $currUser -PageSize)
                }
                else
                {
                    $DeviceObjects = @(Get-MgUserOwnedDevice -UserId $currUser -Top $this.MaxObjectsToScan)
                }
            }
            
            $deviceTypeMapping = ([SVTMapping]::AADResourceMapping |
                Where-Object { $_.ResourceType -eq 'AAD.Device' } |
                Select-Object -First 1)

            $nObj = $this.MaxObjectsToScan
            foreach ($obj in $deviceObjects) {
                $svtResource = [SVTResource]::new();
                $svtResource.ResourceName = $this.ExtractDisplayNameFromResource($obj)
                $svtResource.ResourceGroupName = ""  #If blank, the column gets skipped in CSV file.
                $svtResource.ResourceType = "AAD.Device";
                $svtResource.ResourceId = $obj.Id     
                $svtResource.ResourceTypeMapping = $deviceTypeMapping   
                $this.SVTResources +=$svtResource
                if (--$nObj -eq 0) { break;} 
            }   #TODO odd that above query does not show user created 'Group' objects.
        }

    
        if ($this.NeedToScanType("User"))
        {
            $userObjects = @()
            if ($this.scanTenant)
            {
                if ($this.ShouldBatchScan)
                {
                    $userObjects = @(Get-MgUser -PageSize $this.BatchThreshold -All -Limit $this.MaxObjectsToScan -Property Id, DisplayName);
                }
                else
                {
                    $userObjects = @(Get-MgUser -Top $this.MaxObjectsToScan -Property Id, DisplayName)
                }
            }
            else {
                $userObjects = @(Get-MgUser -UserId $currUser -Property Id, DisplayName)
            }

            $userTypeMapping = ([SVTMapping]::AADResourceMapping |
                Where-Object { $_.ResourceType -eq 'AAD.User' } |
                Select-Object -First 1)

            $nObj = $this.MaxObjectsToScan
            foreach ($obj in $userObjects) {
                $svtResource = [SVTResource]::new();
                $svtResource.ResourceName = $obj.DisplayName
                $svtResource.ResourceGroupName = ""  #If blank, the column gets skipped in CSV file.
                $svtResource.ResourceType = "AAD.User";
                $svtResource.ResourceId = $obj.Id     
                $svtResource.ResourceTypeMapping = $userTypeMapping   
                $this.SVTResources +=$svtResource
                if (--$nObj -eq 0) { break;} 
            } 
        }


        if ($this.NeedToScanType("Group"))
        {
            $grpObjects = @()
            if ($this.scanTenant)
            {
                if ($this.ShouldBatchScan)
                {
                    $grpObjects = @(Get-MgGroup -PageSize $this.BatchThreshold -All -Limit $this.MaxObjectsToScan -Property Id, DisplayName);
                }
                else
                {
                    $grpObjects = @(Get-MgGroup -Top $this.MaxObjectsToScan -Property Id, DisplayName)
                }
            }
            else {
                $grpObjects = @($userOwnedObjects | Where-Object {$_.AdditionalProperties."@odata.type" -eq '#microsoft.graph.group'})
            }

            $grpTypeMapping = ([SVTMapping]::AADResourceMapping |
                Where-Object { $_.ResourceType -eq 'AAD.Group' } |
                Select-Object -First 1)

            $nObj = $this.MaxObjectsToScan;
            foreach ($obj in $grpObjects) {
                $svtResource = [SVTResource]::new();
                $svtResource.ResourceName = $this.ExtractDisplayNameFromResource($obj);;
                $svtResource.ResourceGroupName = ""  #If blank, the column gets skipped in CSV file.
                $svtResource.ResourceType = "AAD.Group";
                $svtResource.ResourceId = $obj.Id     
                $svtResource.ResourceTypeMapping = $grpTypeMapping   
                $this.SVTResources +=$svtResource
                if (--$nObj -eq 0) { break;} 
            }   #TODO Why does this not show user created 'Group' objects in live tenant?
        }

        $this.SVTResourcesFoundCount = $this.SVTResources.Count;
    }
}