Private/Signal.ps1

# The Signals: what a Finding observed, by a name that stays when its wording changes.
#
# A Finding's Check and Value are worded for a Technician and reworded whenever that helps
# one. What reads several Findings together - a Reference from one to another, a Lead -
# cannot go by that wording, so the Judge that produces a Finding says what it observed
# with a name from this list. See ADR-0005.
#
# One list, because two Kinds that named the same observation differently would never
# meet, and nothing would fail. A Signal is declared when the thing was observed: a
# Finding that says "no disk errors" carries none.

# Each with the Kinds that declare it: a Check that was skipped, or was refused the rights
# it needed, can then say what it would have looked for. "No Lead" and "not looked" must
# not read the same. A Kind that measures Servers through the Server Kind's Judge - Odbc,
# AppConnection - declares what that Judge declares.
$script:SignalValues = [ordered]@{
    'disk-errors'          = @{ Kind = @('Stability');  Description = 'Windows logged errors of a disk, its controller or its file system' }
    'drive-health-warning' = @{ Kind = @('DiskHealth'); Description = 'A drive reports wear, uncorrected errors or a temperature beyond its limits' }
    'stop-code-disk'       = @{ Kind = @('Stability');  Description = 'A blue screen whose stop code says a drive or its file system could not be read' }
    'disk-write-slow'      = @{ Kind = @('DiskTest');   Description = 'The disk write test was slower than its thresholds allow' }
    'system-drive-low'     = @{ Kind = @('Storage');    Description = 'The system drive has less free space than the Storage Check allows' }
    'program-not-paged-in' = @{ Kind = @('Stability');  Description = 'A program installed on a local drive crashed because a page of it could not be read' }
    'stop-code-memory'         = @{ Kind = @('Stability'); Description = 'A blue screen whose stop code says memory or its management was corrupted' }
    'hardware-errors'          = @{ Kind = @('Stability'); Description = 'Windows logged hardware errors (WHEA): processor, memory or a bus' }

    'memory-errors'            = @{ Kind = @('Stability'); Description = 'Windows logged hardware errors (WHEA) of the memory' }
    'access-violations-spread' = @{ Kind = @('Stability'); Description = 'Several unrelated programs crashed with an access violation' }
    'stop-code-graphics'       = @{ Kind = @('Stability'); Description = 'A blue screen whose stop code says the display driver could not be reset' }
    'live-kernel-graphics'     = @{ Kind = @('Stability'); Description = 'Windows recovered from a hang or fault of the graphics driver' }
    'graphics-driver-old'      = @{ Kind = @('System');    Description = 'The graphics driver is older than the age the Check notes' }
    'link-dropped'                  = @{ Kind = @('NetworkDisruption'); Description = 'The network was disconnected at the device, or its adapter was reset' }
    'servers-lost'                  = @{ Kind = @('NetworkDisruption'); Description = 'Existing connections to several Servers were lost' }
    'cable-wifi-switching'          = @{ Kind = @('NetworkDisruption'); Description = 'The network went away and a Wi-Fi disconnect followed shortly, repeatedly: the cable connection fails over to Wi-Fi and back' }
    'server-lost'                   = @{ Kind = @('NetworkDisruption'); Description = 'Existing connections to one Server only were lost; its subject is that Server' }
    'loss-on-own-network'           = @{ Kind = @('Server', 'Odbc', 'AppConnection'); Description = 'Pings to a Server in the machine''s own network were lost' }
    'usb-network-adapter'           = @{ Kind = @('NetworkDependency'); Description = 'Programs on a share are reached through a USB network adapter' }

    'adapter-power-saving'          = @{ Kind = @('Network'); Description = 'A network adapter that is up has power saving on: Windows may switch it off' }
    'crash-with-network-disruption' = @{ Kind = @('Stability');         Description = 'A program''s crashes coincide with network disruptions' }
    'server-slow'                   = @{ Kind = @('Server', 'NetworkDependency', 'Odbc', 'AppConnection'); Description = 'A Server answers slowly, with loss or only some of the time; its subject is that Server' }
    'server-unreachable'            = @{ Kind = @('Server', 'NetworkDependency', 'Odbc', 'AppConnection'); Description = 'A Server does not answer; its subject is that Server' }
    # Of the Server itself, not of a Service of it: what tells the Server not answering from
    # each of its Services refused alike, which are one Signal however many (ADR-0015).
    'ping-unanswered'               = @{ Kind = @('Server', 'Odbc', 'AppConnection'); Description = 'A Server answered no ping, and none of its ports that were tried answered either; its subject is that Server' }
    'printer-offline'               = @{ Kind = @('Printing'); Description = 'Windows reports a printer other than the default as offline' }
    'print-job-stuck'               = @{ Kind = @('Printing'); Description = 'A print job has been lying in a queue for longer than the Check allows' }
    'dependency-on-server'          = @{ Kind = @('NetworkDependency'); Description = 'A program runs from a share of a Server; its subject is that Server' }
    'drive-mapping-slow'            = @{ Kind = @('GroupPolicy');       Description = 'The part of Group Policy that maps the drives took long in some of its runs' }
    'share-not-found'               = @{ Kind = @('NetworkDisruption'); Description = 'A Server answered that it does not have the share it was asked for' }
    'sign-in-intermittent'          = @{ Kind = @('Logon');             Description = 'Some sign-ins of the User are slow and others are not' }
    'sign-in-by-controller'         = @{ Kind = @('Logon');             Description = 'The slow sign-ins went to another domain controller than the quick ones' }
    'controller-differs'            = @{ Kind = @('Domain');            Description = 'One domain controller does not answer, or does not have or is slow to give what the others have' }
    'namespace-target-gone'         = @{ Kind = @('Domain');            Description = 'The DFS namespace behind the domain''s name names a Server that is not there' }
    'namespace-unreachable'         = @{ Kind = @('Domain');            Description = 'A folder of the DFS namespace behind the domain''s name has no Server it hands out that answers; the Namespace is one of its parts' }
    'policies-differ'               = @{ Kind = @('Domain');            Description = 'The domain controllers hold different versions of a policy in SYSVOL' }
    # Of NetworkProtocol, which reads the sign-ins Windows logs however it is set up. AppLog and
    # Nextcloud declare it too, where what they read names an HTTP status - a Definition whose
    # pattern reads one, a Nextcloud client up to 3.16 - and are not listed: a skipped one would
    # say it looked for what it may never have seen.
    'service-denied'                = @{ Kind = @('NetworkProtocol'); Description = 'A Service answered and refused the identity it was given: HTTP 401 or 403, a Kerberos ticket refused, a sign-in that failed' }
    # Causes of a refusal, which a Lead joins to it (ADR-0016). Their subject is the account's
    # Service.
    'credential-missing'            = @{ Kind = @('Nextcloud'); Description = 'No credential is saved for an account of an App, by the target names the user''s credential store lists: the App cannot sign in without asking' }
    'credential-refused'            = @{ Kind = @('Nextcloud'); Description = 'An account''s Service refused it, and let it in on no attempt since: its saved credential is no longer accepted' }
    'service-maintenance'           = @{ Kind = @('Nextcloud'); Description = 'A Service says, asked without credentials, that it is in maintenance or waits for an upgrade to finish: it refuses every sign-in while that lasts' }
    # Of a Service that offers Windows sign-in: one cause of a refusal, which a Lead joins to it.
    'spn-missing'                   = @{ Kind = @('Kerberos'); Description = 'An HTTPS Service offers Windows sign-in (Negotiate) and the directory has no SPN HTTP/<host> or HOST/<host> for it: the client falls back to NTLM, or fails; its subject is that Service' }
    'spn-duplicate'                 = @{ Kind = @('Kerberos'); Description = 'The directory has the SPN of an HTTPS Service (HTTP/<host>, or HOST/<host> where there is no HTTP one) on two or more accounts: the domain controller cannot tell which account the ticket is for, and Windows sign-in to the Service fails or falls back to NTLM; its subject is that Service' }
    # Of the User's own account in the domain: why the domain refuses every sign-in of the
    # User, whatever the Service. Of the account, and of no Service.
    'password-expired'              = @{ Kind = @('DomainAccount'); Description = 'The password of the User''s domain account has expired: the domain refuses every new sign-in until it is changed' }
    'password-expiring'             = @{ Kind = @('DomainAccount'); Description = 'The password of the User''s domain account expires within the days the Check warns ahead' }
    'password-must-change'          = @{ Kind = @('DomainAccount'); Description = 'The User''s domain account must change its password at the next sign-in: Servers refuse it until then' }
    'account-locked'                = @{ Kind = @('DomainAccount'); Description = 'The User''s domain account is locked out after too many wrong passwords: the domain refuses every sign-in, the right password too, until the lockout ends or somebody unlocks it' }
    'account-disabled'              = @{ Kind = @('DomainAccount'); Description = 'The User''s domain account is disabled or has expired: the domain refuses every sign-in until whoever manages accounts changes it' }
    'bad-passwords'                 = @{ Kind = @('DomainAccount'); Description = 'Wrong passwords for the User''s domain account came so recently and so many, as the PDC emulator counts them, that a few more lock it out' }
    'password-held'                 = @{ Kind = @('DomainAccount'); Description = 'Something on the Target Machine keeps the User''s own domain account name with a password of its own - a drive mapped under it, a saved credential, a scheduled task or a Windows service that runs as it - and sends the old password after a change; its Subject is the holder' }
    # Of the machine's clock and of the tickets refused to it: a Lead joins the two, the clock
    # being the cause. Of the machine, and of no Service.
    'clock-off'                     = @{ Kind = @('NetworkProtocol'); Description = 'This machine''s clock is further from its time source than the Check allows: Kerberos refuses a ticket from a clock more than five minutes off' }
    'kerberos-refused'              = @{ Kind = @('NetworkProtocol'); Description = 'Windows logged that a Server refused a Kerberos ticket of this machine: Kerberos events 4 and 5 (a ticket refused, the clock)' }
    # Said of the time of events, not of what they were: two Findings at 14:01 every day are
    # what a later Lead connects.
    'events-daily'                  = @{ Kind = @('NetworkDisruption', 'NetworkProtocol', 'RemoteDesktop', 'Printing', 'GroupPolicy'); Description = 'Events a Finding counts come back at the same time of day on many days' }
    'events-burst'                  = @{ Kind = @('NetworkDisruption', 'NetworkProtocol', 'RemoteDesktop', 'Printing', 'GroupPolicy'); Description = 'Most of the events a Finding counts fell in one short window' }
}

function Get-SignalList {
    <#
    .SYNOPSIS
        Every Signal a Judge may declare, with what it marks and which Kinds declare it.
    #>

    [CmdletBinding()]
    [OutputType([psobject])]
    param()

    foreach ($name in $script:SignalValues.Keys) {
        [pscustomobject]@{ Name = $name; Description = $script:SignalValues[$name].Description; Kind = @($script:SignalValues[$name].Kind) }
    }
}

function Get-SignalOfKind {
    <#
    .SYNOPSIS
        The Signals a Kind can declare: what a Check of that Kind would have looked for.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param([AllowNull()][AllowEmptyCollection()][string[]]$Kind)

    $kinds = @($Kind | Where-Object { $_ })
    foreach ($name in $script:SignalValues.Keys) {
        if (@($script:SignalValues[$name].Kind | Where-Object { $_ -in $kinds }).Count) { $name }
    }
}
function Assert-Signal {
    <#
    .SYNOPSIS
        Throws for a Signal that is not on the list, naming it and the list.
    #>

    [CmdletBinding()]
    param([AllowNull()][AllowEmptyCollection()][string[]]$Signal)

    foreach ($name in @($Signal | Where-Object { $_ })) {
        if (-not $script:SignalValues.Contains($name)) {
            throw ("'{0}' is not a Signal. Add it to Private/Signal.ps1, or use one of: {1}." -f $name, (@($script:SignalValues.Keys) -join ', '))
        }
    }
}