Public/Configuration.ps1

<#
.SYNOPSIS
    Gets the current desired state configuration for the tenant.
.DESCRIPTION
    This function reads and displays the current desired state configuration from a JSON file.
.PARAMETER Path
    The path to the configuration file. Defaults to '~/.O365-Toolkit/configs/default.json'.
.EXAMPLE
    Get-O365Configuration
.EXAMPLE
    Get-O365Configuration -Path 'C:\MyConfigs\custom.json'
#>

function Get-O365Configuration {
    [CmdletBinding()]
    param(
        [string]$Path = (Join-Path -Path $HOME -ChildPath '.O365-Toolkit\configs\default.json')
    )

    if (Test-Path -Path $Path) {
        return (Get-Content -Path $Path | ConvertFrom-Json)
    }
    else {
        Write-Warning "Configuration file not found at: $Path"
        return $null
    }
}

<#
.SYNOPSIS
    Generates a template for the desired state configuration.
.DESCRIPTION
    This function generates an empty or example configuration file that can be used as a template for defining the desired state of the tenant.
.PARAMETER Path
    The path to save the template file to. Defaults to '~/.O365-Toolkit/configs/template.json'.
.EXAMPLE
    New-O365ConfigurationTemplate
.EXAMPLE
    New-O365ConfigurationTemplate -Path 'C:\MyConfigs\new_template.json'
#>

function New-O365ConfigurationTemplate {
    [CmdletBinding()]
    param(
        [string]$Path = (Join-Path -Path $HOME -ChildPath '.O365-Toolkit\configs\template.json')
    )

    $ConfigDir = Split-Path -Path $Path -Parent
    if (-not (Test-Path -Path $ConfigDir)) {
        New-Item -ItemType Directory -Path $ConfigDir | Out-Null
    }

    $TemplateConfig = @{
        "GlobalTenantSettings" = @{
            "ExternalSharingEnabled" = $true
            "GuestAccessAllowed" = $true
        };
        "UserMFA" = @{
            "AllUsersMFAEnabled" = $true
            "ExceptionUsers" = @()
        };
        "MailboxForwarding" = @{
            "ExternalForwardingAllowed" = $false
            "InternalOnlyForwarding" = $true
        };
        "TeamsGovernance" = @{
            "AllowGuestCreateUpdateChannels" = $false
        }
    }

    $TemplateConfig | ConvertTo-Json -Depth 10 | Out-File -FilePath $Path
    Write-Verbose "Configuration template saved to: $Path"
}

<#
.SYNOPSIS
    Compares the active tenant configuration against a desired state configuration file.
.DESCRIPTION
    This function reads a desired state configuration file and compares it against the current configuration of the tenant.
    It reports on any discrepancies (drift).
.PARAMETER Path
    The path to the desired state configuration file. Defaults to '~/.O365-Toolkit/configs/default.json'.
.EXAMPLE
    Test-O365Configuration
.EXAMPLE
    Test-O365Configuration -Path 'C:\MyConfigs\custom.json'
#>

function Test-O365Configuration {
    [CmdletBinding()]
    param(
        [string]$Path = (Join-Path -Path $HOME -ChildPath '.O365-Toolkit\configs\default.json')
    )

    $DesiredConfig = Get-O365Configuration -Path $Path
    if (-not $DesiredConfig) {
        return
    }

    $Report = [System.Collections.Generic.List[Object]]::new()

    # Global Tenant Settings
    if ($DesiredConfig.GlobalTenantSettings) {
        $CurrentSettings = Get-O365GlobalTenantSettings
        if ($DesiredConfig.GlobalTenantSettings.ExternalSharingEnabled -ne $CurrentSettings.ExternalSharingEnabled) {
            $Report.Add([PSCustomObject]@{
                Setting = 'GlobalTenantSettings.ExternalSharingEnabled'
                Desired = $DesiredConfig.GlobalTenantSettings.ExternalSharingEnabled
                Current = $CurrentSettings.ExternalSharingEnabled
                Drift = $true
            })
        }
        if ($DesiredConfig.GlobalTenantSettings.GuestAccessAllowed -ne $CurrentSettings.GuestAccessAllowed) {
            $Report.Add([PSCustomObject]@{
                Setting = 'GlobalTenantSettings.GuestAccessAllowed'
                Desired = $DesiredConfig.GlobalTenantSettings.GuestAccessAllowed
                Current = $CurrentSettings.GuestAccessAllowed
                Drift = $true
            })
        }
    }

    # User MFA
    if ($DesiredConfig.UserMFA) {
        $CurrentSettings = Get-O365UserMFAStatus
        if ($DesiredConfig.UserMFA.AllUsersMFAEnabled -ne $CurrentSettings.AllUsersMFAEnabled) {
            $Report.Add([PSCustomObject]@{
                Setting = 'UserMFA.AllUsersMFAEnabled'
                Desired = $DesiredConfig.UserMFA.AllUsersMFAEnabled
                Current = $CurrentSettings.AllUsersMFAEnabled
                Drift = $true
            })
        }
    }

    # Mailbox Forwarding
    if ($DesiredConfig.MailboxForwarding) {
        $CurrentSettings = Get-O365MailboxForwardingSettings
        if ($DesiredConfig.MailboxForwarding.ExternalForwardingAllowed -ne $CurrentSettings.ExternalForwardingAllowed) {
            $Report.Add([PSCustomObject]@{
                Setting = 'MailboxForwarding.ExternalForwardingAllowed'
                Desired = $DesiredConfig.MailboxForwarding.ExternalForwardingAllowed
                Current = $CurrentSettings.ExternalForwardingAllowed
                Drift = $true
            })
        }
        if ($DesiredConfig.MailboxForwarding.InternalOnlyForwarding -ne $CurrentSettings.InternalOnlyForwarding) {
            $Report.Add([PSCustomObject]@{
                Setting = 'MailboxForwarding.InternalOnlyForwarding'
                Desired = $DesiredConfig.MailboxForwarding.InternalOnlyForwarding
                Current = $CurrentSettings.InternalOnlyForwarding
                Drift = $true
            })
        }
    }

    # Teams Governance
    if ($DesiredConfig.TeamsGovernance) {
        $CurrentSettings = Get-O365TeamsGovernanceSettings
        if ($DesiredConfig.TeamsGovernance.AllowGuestCreateUpdateChannels -ne $CurrentSettings.AllowGuestCreateUpdateChannels) {
            $Report.Add([PSCustomObject]@{
                Setting = 'TeamsGovernance.AllowGuestCreateUpdateChannels'
                Desired = $DesiredConfig.TeamsGovernance.AllowGuestCreateUpdateChannels
                Current = $CurrentSettings.AllowGuestCreateUpdateChannels
                Drift = $true
            })
        }
    }
    
    return $Report
}

<#
.SYNOPSIS
    Applies the settings from a desired state configuration file to the tenant.
.DESCRIPTION
    This function reads a desired state configuration file and applies its settings to the tenant.
.PARAMETER Path
    The path to the desired state configuration file. Defaults to '~/.O365-Toolkit/configs/default.json'.
.PARAMETER Confirm
    Prompts for confirmation before making changes.
.EXAMPLE
    Set-O365Configuration -Confirm
.EXAMPLE
    Set-O365Configuration -Path 'C:\MyConfigs\custom.json' -Confirm:$false
#>

function Set-O365Configuration {
    [CmdletBinding(SupportsShouldProcess=$true)]
    param(
        [string]$Path = (Join-Path -Path $HOME -ChildPath '.O365-Toolkit\configs\default.json'),
        [switch]$Confirm
    )

    $DesiredConfig = Get-O365Configuration -Path $Path
    if (-not $DesiredConfig) {
        return
    }

    if ($PSCmdlet.ShouldProcess("applying configuration from $Path")) {
        # Global Tenant Settings
        if ($DesiredConfig.GlobalTenantSettings) {
            Set-O365GlobalTenantSettings -ExternalSharingEnabled $DesiredConfig.GlobalTenantSettings.ExternalSharingEnabled -GuestAccessAllowed $DesiredConfig.GlobalTenantSettings.GuestAccessAllowed
        }

        # User MFA
        if ($DesiredConfig.UserMFA) {
            Set-O365UserMFAStatus -AllUsersMFAEnabled $DesiredConfig.UserMFA.AllUsersMFAEnabled -ExceptionUsers $DesiredConfig.UserMFA.ExceptionUsers
        }

        # Mailbox Forwarding
        if ($DesiredConfig.MailboxForwarding) {
            Set-O365MailboxForwardingSettings -ExternalForwardingAllowed $DesiredConfig.MailboxForwarding.ExternalForwardingAllowed -InternalOnlyForwarding $DesiredConfig.MailboxForwarding.InternalOnlyForwarding
        }

        # Teams Governance
        if ($DesiredConfig.TeamsGovernance) {
            Set-O365TeamsGovernanceSettings -AllowGuestCreateUpdateChannels $DesiredConfig.TeamsGovernance.AllowGuestCreateUpdateChannels
        }
        Write-Verbose "Configuration applied successfully."
    }
}

<#
.SYNOPSIS
    Creates a DSC snapshot of the current tenant state for onboarding workflows.
.DESCRIPTION
    Captures the current state of all tenant configuration settings and exports them to a JSON snapshot file.
    This snapshot serves as a baseline for onboarding validation and can be used to compare against desired configurations.
.PARAMETER OutputPath
    The path to save the snapshot file. Defaults to '~/.O365-Toolkit/snapshots/onboarding-baseline-<timestamp>.json'.
.PARAMETER IncludeConditionalAccess
    When set, includes Conditional Access policy snapshot in the baseline.
.PARAMETER IncludeAccessPackages
    When set, includes Access Package snapshot in the baseline.
.PARAMETER IncludeDirectoryRoles
    When set, includes directory role assignments snapshot in the baseline.
.EXAMPLE
    New-O365DscSnapshot -OutputPath './onboarding-baseline.json'
.EXAMPLE
    New-O365DscSnapshot -IncludeConditionalAccess -IncludeAccessPackages -IncludeDirectoryRoles
.NOTES
    Requires appropriate Graph scopes for each included component.
#>

function New-O365DscSnapshot {
    [CmdletBinding()]
    param(
        [string]$OutputPath,
        [switch]$IncludeConditionalAccess,
        [switch]$IncludeAccessPackages,
        [switch]$IncludeDirectoryRoles
    )

    if (-not $OutputPath) {
        $snapshotDir = Join-Path -Path $HOME -ChildPath '.O365-Toolkit\snapshots'
        if (-not (Test-Path -Path $snapshotDir)) {
            New-Item -ItemType Directory -Path $snapshotDir | Out-Null
        }
        $timestamp = (Get-Date).ToString('yyyyMMdd-HHmmss')
        $OutputPath = Join-Path -Path $snapshotDir -ChildPath "onboarding-baseline-$timestamp.json"
    }

    $tenant = Get-EntraTenantDetail | Select-Object -First 1
    $snapshot = @{
        snapshotDate = (Get-Date).ToString("o")
        tenantId     = $tenant.TenantId
        tenantName   = $tenant.DisplayName
    }

    # Capture Global Tenant Settings
    try {
        $snapshot['GlobalTenantSettings'] = Get-O365GlobalTenantSettings
    } catch {
        Write-Warning "Failed to capture GlobalTenantSettings: $_"
    }

    # Capture User MFA Status
    try {
        $snapshot['UserMFA'] = Get-O365UserMFAStatus
    } catch {
        Write-Warning "Failed to capture UserMFA: $_"
    }

    # Capture Mailbox Forwarding Settings
    try {
        $snapshot['MailboxForwarding'] = Get-O365MailboxForwardingSettings
    } catch {
        Write-Warning "Failed to capture MailboxForwarding: $_"
    }

    # Capture Teams Governance Settings
    try {
        $snapshot['TeamsGovernance'] = Get-O365TeamsGovernanceSettings
    } catch {
        Write-Warning "Failed to capture TeamsGovernance: $_"
    }

    # Capture Conditional Access Policies if requested
    if ($IncludeConditionalAccess) {
        try {
            $snapshot['ConditionalAccessPolicies'] = Get-O365ConditionalAccessPolicy | Select-Object -Property Id, DisplayName, State, Description
        } catch {
            Write-Warning "Failed to capture ConditionalAccessPolicies: $_"
        }
    }

    # Capture Access Packages if requested
    if ($IncludeAccessPackages) {
        try {
            $snapshot['AccessPackages'] = Get-O365AccessPackagePlan | Select-Object -Property Id, DisplayName, CatalogId, State
        } catch {
            Write-Warning "Failed to capture AccessPackages: $_"
        }
    }

    # Capture Directory Role Assignments if requested
    if ($IncludeDirectoryRoles) {
        try {
            $snapshot['DirectoryRoles'] = Get-EntraDirectoryRoleAssignment | Select-Object -Property PrincipalId, RoleDefinitionId, DirectoryScopeId
        } catch {
            Write-Warning "Failed to capture DirectoryRoles: $_"
        }
    }

    # Ensure output directory exists
    $outputDir = Split-Path -Path $OutputPath -Parent
    if ($outputDir -and -not (Test-Path -Path $outputDir)) {
        New-Item -ItemType Directory -Path $outputDir | Out-Null
    }

    # Export snapshot to JSON
    $json = $snapshot | ConvertTo-Json -Depth 10
    $json | Out-File -FilePath $OutputPath -Encoding utf8
    Write-Verbose "DSC snapshot saved to: $OutputPath"
    return $OutputPath
}

<#
.SYNOPSIS
    Compares a DSC snapshot against the current tenant state to identify configuration drift.
.DESCRIPTION
    Takes a previously captured snapshot and compares it against the current tenant state.
    Reports on any changes or drift detected since the snapshot was created.
.PARAMETER SnapshotPath
    The path to the DSC snapshot file to compare.
.PARAMETER ReportPath
    Optional path to save the drift report as JSON.
.EXAMPLE
    Test-O365DscSnapshot -SnapshotPath './onboarding-baseline.json'
.EXAMPLE
    Test-O365DscSnapshot -SnapshotPath './onboarding-baseline.json' -ReportPath './drift-report.json'
.NOTES
    Returns a list of configuration items with their previous and current states.
#>

function Test-O365DscSnapshot {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]$SnapshotPath,
        [string]$ReportPath
    )

    if (-not (Test-Path -Path $SnapshotPath)) {
        Write-Warning "Snapshot file not found: $SnapshotPath"
        return
    }

    $snapshot = Get-Content -Path $SnapshotPath -Raw | ConvertFrom-Json
    $report = [System.Collections.Generic.List[Object]]::new()

    # Compare Global Tenant Settings
    if ($snapshot.GlobalTenantSettings) {
        try {
            $current = Get-O365GlobalTenantSettings
            foreach ($setting in $snapshot.GlobalTenantSettings.PSObject.Properties) {
                $currentValue = $current.($setting.Name)
                if ($currentValue -ne $setting.Value) {
                    $report.Add([PSCustomObject]@{
                        Category     = 'GlobalTenantSettings'
                        Setting      = $setting.Name
                        SnapshotValue = $setting.Value
                        CurrentValue  = $currentValue
                        Drift         = $true
                    })
                }
            }
        } catch {
            Write-Warning "Failed to compare GlobalTenantSettings: $_"
        }
    }

    # Compare User MFA
    if ($snapshot.UserMFA) {
        try {
            $current = Get-O365UserMFAStatus
            foreach ($setting in $snapshot.UserMFA.PSObject.Properties) {
                $currentValue = $current.($setting.Name)
                if ($currentValue -ne $setting.Value) {
                    $report.Add([PSCustomObject]@{
                        Category      = 'UserMFA'
                        Setting       = $setting.Name
                        SnapshotValue = $setting.Value
                        CurrentValue  = $currentValue
                        Drift         = $true
                    })
                }
            }
        } catch {
            Write-Warning "Failed to compare UserMFA: $_"
        }
    }

    # Compare Mailbox Forwarding
    if ($snapshot.MailboxForwarding) {
        try {
            $current = Get-O365MailboxForwardingSettings
            foreach ($setting in $snapshot.MailboxForwarding.PSObject.Properties) {
                $currentValue = $current.($setting.Name)
                if ($currentValue -ne $setting.Value) {
                    $report.Add([PSCustomObject]@{
                        Category      = 'MailboxForwarding'
                        Setting       = $setting.Name
                        SnapshotValue = $setting.Value
                        CurrentValue  = $currentValue
                        Drift         = $true
                    })
                }
            }
        } catch {
            Write-Warning "Failed to compare MailboxForwarding: $_"
        }
    }

    # Compare Teams Governance
    if ($snapshot.TeamsGovernance) {
        try {
            $current = Get-O365TeamsGovernanceSettings
            foreach ($setting in $snapshot.TeamsGovernance.PSObject.Properties) {
                $currentValue = $current.($setting.Name)
                if ($currentValue -ne $setting.Value) {
                    $report.Add([PSCustomObject]@{
                        Category      = 'TeamsGovernance'
                        Setting       = $setting.Name
                        SnapshotValue = $setting.Value
                        CurrentValue  = $currentValue
                        Drift         = $true
                    })
                }
            }
        } catch {
            Write-Warning "Failed to compare TeamsGovernance: $_"
        }
    }

    # Save report if path provided
    if ($ReportPath) {
        $reportDir = Split-Path -Path $ReportPath -Parent
        if ($reportDir -and -not (Test-Path -Path $reportDir)) {
            New-Item -ItemType Directory -Path $reportDir | Out-Null
        }
        $report | ConvertTo-Json -Depth 10 | Out-File -FilePath $ReportPath -Encoding utf8
        Write-Verbose "Drift report saved to: $ReportPath"
    }

    return $report
}