Modules/M365DSCAgent.psm1

<#
.Description
This function tests the configuration of the agent
 
.Example
Test-M365DSCAgent
 
.Functionality
Public
#>

function Test-M365DSCAgent
{
    [CmdletBinding()]
    param(

    )
    #Ensure the proper dependencies are installed in the current environment.
    Confirm-M365DSCDependencies

    #region Telemetry
    $data = [System.Collections.Generic.Dictionary[[String], [String]]]::new()
    $data.Add("Event", "TestAgent")
    Add-M365DSCTelemetryEvent -Data $data
    #endregion

    [array]$Recommendations = @()
    [array]$Issues = @()
    $TotalSteps = 3

    #region PowerShell Version
    Write-Progress -Activity "Scanning PowerShell Version..." -PercentComplete (1 / $TotalSteps * 100)
    $CurrentPSVersion = [version]$PSVersionTable.PSVersion

    if ($CurrentPSVersion -lt [version]5.1)
    {
        $Recommendations += @{
            ID      = 'R1'
            Message = "We recommend installing PowerShell 5.1. You can download and install it from: " + `
                "https://www.microsoft.com/en-us/download/details.aspx?id=54616"
        }
    }
    elseif ($CurrentPSVersion -ge [version]"6.0")
    {
        $Issues += @{
            ID      = 'I1'
            Message = "Microsoft365DSC is not supported with PowerShell Version $CurrentPSVersion. Please install version 5.1 from: " + `
                "https://www.microsoft.com/en-us/download/details.aspx?id=54616"
        }
    }
    #endregion

    # We need to do a quick configuration of WinRM in order to be able to obtain configuration information;
    Write-Progress -Activity "Scanning WinRM MaxEnvelopeSize..." -PercentComplete (2 / $TotalSteps * 100)
    WinRM QuickConfig -Force | Out-Null

    #region MaxEnvelopeSize
    $CurrentMaxEnvelopeSize = (Get-Item -Path WSMan:\localhost\MaxEnvelopeSizekb).Value

    if ($CurrentMaxEnvelopeSize -le 1024)
    {
        $Recommendations += @{
            ID      = 'R1'
            Message = "We recommend increasing the MaxEnvelopeSize of the agent's WinRM to a minimum of 10 Mb." + `
                " To make the change, run: Set-Item -Path WSMan:\localhost\MaxEnvelopeSizekb -Value 10240"
        }
    }
    #endregion

    #region Modules Dependencies
    Write-Progress -Activity "Scanning Dependencies..." -PercentComplete (3 / $TotalSteps * 100)
    $M365DSC = Get-Module Microsoft365DSC
    $ManifestPath = Join-Path -Path $M365DSC.ModuleBase -ChildPath 'Microsoft365DSC.psd1'
    $manifest = Import-PowerShellDataFile $ManifestPath
    $dependencies = $manifest.RequiredModules
    foreach ($dependency in $dependencies)
    {
        $module = Get-Module $dependency.ModuleName -ListAvailable | `
            Where-Object -FilterScript { $_.Version -eq $dependency.RequiredVersion }
        if ($null -eq $module)
        {
            $Issues += @{
                ID      = 'I2'
                Message = "M365DSC has a dependency on module $($dependency.ModuleName) which was not found. You need to install " + `
                    "this module by running: Install-Module $($dependency.ModuleName) -RequiredVersion $($dependency.RequiredVersion) -Force"
            }
        }
    }
    #endregion

    Write-Progress -Completed -Activity "Completed Analysis"
    if ($Issues.Count -gt 0)
    {
        Write-Host "The following issues were detected with the current agent's configuration. Please take " + `
            "proper action to remediate."
        $i = 1
        foreach ($issue in $Issues)
        {
            Write-Error -Message " [$i/$($Issues.Count)] $($issue.Message)"
        }
    }

    if ($Recommendations.Count -gt 0)
    {
        Write-Host "The following recommendations were issued. We strongly recommend adressing those: "
        $i = 1
        foreach ($recommendation in $Recommendations)
        {
            Write-Warning " [$i/$($Recommendations.Count)] $($recommendation.Message)"
        }
    }

    if ($Recommendations.Count -eq 0 -and $Issues.Count -eq 0)
    {
        Write-Host "The agent is properly configured."
    }
}

<#
.Description
This function configures the LCM with a self signed encryption certificate
 
.Parameter KeepCertificate
Specifies that the temporarily created CER file should not be deleted.
 
.Parameter ForceRenew
Specifies that a new certificate should be forcefully created.
 
.Parameter GeneratePFX
Specifies that a PFX export should be created for the generated certificate.
 
.Parameter Password
Specifies the password for the PFX file.
 
.Example
Set-M365DSCAgentCertificateConfiguration -KeepCertificate
 
.Example
Set-M365DSCAgentCertificateConfiguration -GeneratePFX -Password 'P@ssword123!'
 
.Functionality
Public
#>

function Set-M365DSCAgentCertificateConfiguration
{
    [CmdletBinding()]
    [OutputType([System.String])]
    param(
        [Parameter()]
        [Switch]
        $KeepCertificate,

        [Parameter()]
        [Switch]
        $ForceRenew,

        [Parameter()]
        [Switch]
        $GeneratePFX,

        [Parameter()]
        [System.String]
        $Password = "Temp!P@ss123"
    )

    $existingCertificate = Get-ChildItem -Path Cert:\LocalMachine\My | `
        Where-Object { $_.Subject -match "M365DSCEncryptionCert" }

    if ($ForceRenew)
    {
        foreach ($cert in $existingCertificate)
        {
            Remove-Item $cert.PSPath | Out-Null
        }
        $existingCertificate = $null
    }
    if ($null -eq $existingCertificate)
    {
        Write-Verbose -Message "No existing M365DSC certificate found. Creating one."
        $certificateFilePath = "$env:Temp\M365DSC.cer"
        $cert = New-SelfSignedCertificate -Type DocumentEncryptionCertLegacyCsp `
            -DnsName 'Microsoft365DSC' `
            -Subject 'M365DSCEncryptionCert' `
            -HashAlgorithm SHA256 `
            -NotAfter (Get-Date).AddYears(10)
        $cert | Export-Certificate -FilePath $certificateFilePath -Force | Out-Null
        Import-Certificate -FilePath $certificateFilePath `
            -CertStoreLocation 'Cert:\LocalMachine\My' -Confirm:$false | Out-Null
        $existingCertificate = Get-ChildItem -Path Cert:\LocalMachine\My | `
            Where-Object { $_.Subject -match "M365DSCEncryptionCert" }
    }
    else
    {
        Write-Verbose -Message "An existing M365DSc certificate was found. Re-using it."
    }
    $thumbprint = $existingCertificate.Thumbprint
    Write-Verbose -Message "Using M365DSCEncryptionCert with thumbprint {$thumbprint}"

    $configOutputFile = $env:Temp + "\M365DSCAgentLCMConfig.ps1"
    $LCMConfigContent = @"
    [DSCLocalConfigurationManager()]
    Configuration M365AgentConfig
    {
        Node Localhost
        {
            Settings
            {
                CertificateID = '$thumbprint'
            }
        }
    }
    M365AgentConfig | Out-Null
    Set-DSCLocalConfigurationManager M365AgentConfig -Force
"@

    $LCMConfigContent | Out-File $configOutputFile
    & $configOutputFile

    if ($KeepCertificate)
    {
        Write-Host "Certificate {$thumbprint} was stored under {$($env:Temp)} with name M365DSC.cer and M365DSC.pfx"
    }
    else
    {
        try
        {
            Remove-Item -Path $configOutputFile -Confirm:$false -ErrorAction SilentlyContinue
            Remove-Item -Path "./M365AgentConfig" -Recurse -Confirm:$false -ErrorAction SilentlyContinue
        }
        catch
        {
            Write-Error $_
        }
    }

    if ($GeneratePFX)
    {
        if ($Password -eq $null)
        {
            Throw "When the GeneratePFX switch is used, you also need to provide a password."
        }
        $securePassword = ConvertTo-SecureString -String $password -Force -AsPlainText
        Export-PfxCertificate -Cert $existingCertificate.PSPath `
            -FilePath $certificateFilePath.Replace('.cer', '.pfx') `
            -Password $securePassword | Out-Null
        Write-Host "Private Key stored at {$($certificateFilePath.Replace('.cer','.pfx'))}"
    }
    return $thumbprint
}

Export-ModuleMember -Function @(
    'Set-M365DSCAgentCertificateConfiguration',
    'Test-M365DSCAgent'
)