Private/Kinds/OfficeAddins.ps1

# The OfficeAddins Kind: what else is loaded inside Word, Excel or Outlook.
#
# An add-in is code from a third party running inside the application, and it is the
# faulting module in most Office crashes a Technician will ever see. The count is the
# Finding; the list beneath it is what names the one to disable.

# LoadBehavior, as Office records it. 3 is "loaded, and load at startup", which is the only
# value that means the add-in is actually in the process. 2 and 0 are installed and idle;
# 9 and 8 are load-on-demand. Numeric because it is a registry value, not a display string.
$script:OfficeAddinLoadBehaviorActive = 3

# The add-ins Office and Teams install themselves, by ProgId: OneNote, the Outlook Social
# Connector, Skype for Business, Exchange Unified Messaging, the Teams meeting add-in,
# VBA, the Access data collection, the SharePoint colleague import and Business
# Connectivity Services. They are on nearly every machine and nobody put them there, so
# they do not count towards "many add-ins". The default of OfficeOwnAddinPattern.
$script:OfficeOwnAddinPattern = '^(OneNote\.OutlookAddin|OscAddin\.Connect|UCAddin\.LyncAddin\.\d+|UmOutlookAddin\.FormRegionAddin|TeamsAddin\.FastConnect|Microsoft\.VbaAddinForOutlook\.\d+|AccessAddin\.DC|ColleagueImport\.ColleagueImportAddin|BCSAddin\.Connect)$'

function Get-OfficeAddinsData {
    [CmdletBinding()]
    [OutputType([psobject])]
    param([hashtable]$Parameters = @{})

    $application = Get-Parameter $Parameters 'OfficeApp' ''
    if (-not $application) {
        return [pscustomobject]@{
            PSTypeName    = 'Gutcheck.Data.OfficeAddins'
            OfficeApp     = ''
            Addins        = @()
            DisabledItems = $null
        }
    }

    # Click-to-Run keeps a virtualised copy of the machine hive, so an add-in registered
    # by an installer shows up there and nowhere else.
    $roots = @(
        "HKCU:\Software\Microsoft\Office\$application\Addins"
        "HKLM:\Software\Microsoft\Office\$application\Addins"
        "HKLM:\Software\WOW6432Node\Microsoft\Office\$application\Addins"
        "HKLM:\SOFTWARE\Microsoft\Office\ClickToRun\REGISTRY\MACHINE\Software\Microsoft\Office\$application\Addins"
        "HKLM:\SOFTWARE\Microsoft\Office\ClickToRun\REGISTRY\MACHINE\Software\WOW6432Node\Microsoft\Office\$application\Addins"
    )

    $addins = @(foreach ($root in $roots) {
        if (-not (Test-Path $root)) { continue }
        Get-ChildItem $root -ErrorAction SilentlyContinue | ForEach-Object {
            $properties = Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue
            [pscustomobject]@{
                ProgId       = $_.PSChildName
                Name         = $properties.FriendlyName
                LoadBehavior = $properties.LoadBehavior
                Key          = $root
            }
        }
    })

    # Office disables what crashed it and never mentions it again, so a Customer whose
    # add-in stopped working has a machine that will not say why.
    $disabled = $null
    $resiliency = Get-Item "HKCU:\Software\Microsoft\Office\16.0\$application\Resiliency\DisabledItems" `
        -ErrorAction SilentlyContinue
    if ($resiliency) { $disabled = $resiliency.ValueCount }

    [pscustomobject]@{
        PSTypeName    = 'Gutcheck.Data.OfficeAddins'
        OfficeApp     = $application
        Addins        = $addins
        DisabledItems = $disabled
    }
}

function ConvertTo-OfficeAddinsFinding {
    [CmdletBinding()]
    [OutputType([psobject])]
    param(
        [AllowNull()]$Data,
        [hashtable]$Parameters = @{}
    )

    $warnAbove = Get-Parameter $Parameters 'LoadedAddinWarnAbove' 6
    $failAbove = Get-Parameter $Parameters 'LoadedAddinFailAbove' ([double]::MaxValue)

    $application = Get-DataProperty $Data 'OfficeApp'
    if (-not $application) {
        return New-Finding -Category Apps -Check (Get-Text 'Check.OfficeAddins.OfficeAddIns') -Severity INFO `
            -Value (Get-Text 'Value.OfficeAddins.NoOfficeApp') `
            -Hint (Get-Text 'Hint.OfficeAddins.ThisCheckDefinitionNamesNo')
    }

    $loaded = @((Get-DataCollection $Data 'Addins') |
        Where-Object { (ConvertTo-Number $_.LoadBehavior) -eq $script:OfficeAddinLoadBehaviorActive } |
        Sort-Object ProgId -Unique)

    # Judged on what somebody installed. Office's own are named after them: they load
    # too, and an add-in that is switched off to test is one of them as often as not.
    $ownPattern = "$(Get-Parameter $Parameters 'OfficeOwnAddinPattern' $script:OfficeOwnAddinPattern)"
    $own    = @($loaded | Where-Object { $ownPattern -and "$($_.ProgId)" -match $ownPattern })
    $others = @($loaded | Where-Object { $own -notcontains $_ })

    $value = ('{0}: {1}' -f $others.Count, (($others | ForEach-Object { $_.ProgId }) -join ', ')).TrimEnd(': ')
    if ($own.Count) {
        $value = '{0} | {1}' -f $value, ((Get-Text 'Value.OfficeAddins.OfficeOwn') -f $own.Count, (($own | ForEach-Object { $_.ProgId }) -join ', '))
    }

    New-Finding -Category Apps -Check ((Get-Text 'Check.OfficeAddins.ComAddinsSetToLoad') -f $application) `
        -Severity (Get-Severity $others.Count $warnAbove $failAbove) `
        -Value $value `
        -Hint ((Get-Text 'Hint.OfficeAddins.AddinsSlowDown') -f $application)

    $disabled = ConvertTo-Number (Get-DataProperty $Data 'DisabledItems')
    if ($null -ne $disabled -and $disabled -gt 0) {
        New-Finding -Category Apps -Check ((Get-Text 'Check.OfficeAddins.DisabledItems') -f $application) `
            -Severity WARN -Value $disabled `
            -Hint ((Get-Text 'Hint.OfficeAddins.HasDisabledItems') -f $application)
    }
}

function ConvertTo-OfficeAddinsSection {
    [CmdletBinding()]
    [OutputType([psobject])]
    param([AllowNull()]$Data)

    $application = Get-DataProperty $Data 'OfficeApp'
    if (-not $application) { return }

    # LoadBehavior is a number in the registry and is shown with what it means before it:
    # a code is never shown bare.
    New-Section -Title ((Get-Text 'Title.OfficeAddins.ComAddins') -f $application) -Row @(
        (Get-DataCollection $Data 'Addins') | ForEach-Object {
            $row = [ordered]@{}
            $row[(Get-Text 'Column.OfficeAddins.ProgId')]       = $_.ProgId
            $row[(Get-Text 'Column.OfficeAddins.Name')]         = $_.Name
            $row[(Get-Text 'Column.OfficeAddins.LoadBehavior')] = Get-OfficeAddinLoadBehaviorText -LoadBehavior $_.LoadBehavior
            $row[(Get-Text 'Column.OfficeAddins.Key')]          = $_.Key
            [pscustomobject]$row
        }
    )
}

function Get-OfficeAddinLoadBehaviorText {
    <#
    .SYNOPSIS
        "laedt beim Start (3)": what a LoadBehavior says, with the number behind it. The
        number alone for one Office does not document. Pure.
    .DESCRIPTION
        The values Microsoft documents for the LoadBehavior of an Office add-in: whether
        it is loaded now (the low bit) and when Office loads it - never by itself, at
        startup, on demand, or once at the next start.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param([AllowNull()]$LoadBehavior)

    $number = ConvertTo-Number $LoadBehavior
    if ($null -eq $number) { return "$LoadBehavior" }
    $meaning = switch ([double]$number) {
        0       { Get-Text 'Value.OfficeAddins.LoadBehavior.0' }
        1       { Get-Text 'Value.OfficeAddins.LoadBehavior.1' }
        2       { Get-Text 'Value.OfficeAddins.LoadBehavior.2' }
        3       { Get-Text 'Value.OfficeAddins.LoadBehavior.3' }
        8       { Get-Text 'Value.OfficeAddins.LoadBehavior.8' }
        9       { Get-Text 'Value.OfficeAddins.LoadBehavior.9' }
        16      { Get-Text 'Value.OfficeAddins.LoadBehavior.16' }
        default { '' }
    }
    if (-not $meaning) { return "$number" }
    (Get-Text 'Value.OfficeAddins.LoadBehavior') -f $meaning, $number
}