Private/Insight.ps1
|
#Requires -Version 5.1 # Where each Delivery Optimization setting came from, and which endpoints served the bytes. # # Pure functions: the registry, CIM, log and TCP reads are wrappers in Platform.ps1, so every rule here is tested on # fixtures shaped like what Windows returned on a real device (see tests/Insight.Tests.ps1 for the provenance of each). Set-StrictMode -Version Latest # ---- configuration sources --------------------------------------------------------------------------------------- # Values PolicyManager keeps beside each policy value (verified on Windows 11 26H2, PolicyManager\current\device\<Area>: # "<Name>", "<Name>_ProviderSet" (DWORD 1), "<Name>_WinningProvider" (REG_SZ enrolment GUID, no braces); under # PolicyManager\providers\<GUID>\default\Device\<Area>: "<Name>" and "<Name>_LastWrite"). $script:DDPolicyManagerSuffix = '(?i)_(ProviderSet|WinningProvider|LastWrite|ADMXInstanceData)$' function ConvertFrom-DDRegistryData { # A registry dump value ({ name; type; data } from Get-DDRegistryValues) as a setting value: numbers stay numbers. [CmdletBinding()] param([Parameter(Mandatory)] $Value) $d = [string]$Value['data'] if ($Value['type'] -in 'REG_DWORD', 'REG_QWORD') { $n = [long]0 if ([long]::TryParse($d, [System.Globalization.NumberStyles]::Integer, [System.Globalization.CultureInfo]::InvariantCulture, [ref]$n)) { return $n } } $d } function ConvertFrom-DDPolicyStore { <# .SYNOPSIS One registry store as name -> @{ Value; WinningProvider } (names lower-cased for lookup, original kept in Name). PolicyManager bookkeeping values (_ProviderSet, _WinningProvider, _LastWrite) are folded into their setting. #> [CmdletBinding()] param($Dump) $out = @{} if ($null -eq $Dump -or -not $Dump['Present']) { return $out } $winners = @{} foreach ($v in @($Dump['Values'])) { if ([string]$v['name'] -match '(?i)^(?<n>.+)_WinningProvider$') { $winners[$Matches['n'].ToLowerInvariant()] = ([string]$v['data']).Trim('{', '}', ' ') } } foreach ($v in @($Dump['Values'])) { $name = [string]$v['name'] if ($name -match $script:DDPolicyManagerSuffix -or $name -eq '(Default)') { continue } $k = $name.ToLowerInvariant() $out[$k] = @{ Name = $name; Value = (ConvertFrom-DDRegistryData -Value $v); WinningProvider = $(if ($winners.ContainsKey($k)) { $winners[$k] } else { $null }) } } $out } function Resolve-DDMdmProvider { # Enrolment GUID -> @{ provider = intune|other; providerId }. "MS DM Server" is the ProviderID of an Intune enrolment # (HKLM\SOFTWARE\Microsoft\Enrollments\<GUID>, verified on an Intune-managed device; ConfigMgr co-management shows # as "WMI_Bridge_SCCM_Server"). [CmdletBinding()] param([AllowNull()][string] $EnrollmentId, [AllowEmptyCollection()] $Enrollments = @()) if (-not $EnrollmentId) { return $null } $rec = @($Enrollments | Where-Object { $_ -and ([string]$_.EnrollmentId).Trim('{', '}') -ieq $EnrollmentId.Trim('{', '}') }) | Select-Object -First 1 $provId = $null if ($rec -and $rec.ProviderId) { $provId = ([string]$rec.ProviderId) -replace '[^\x20-\x7E]', '' } if ($provId -and $provId.Length -gt 64) { $provId = $provId.Substring(0, 64) } [ordered]@{ provider = $(if ($provId -eq 'MS DM Server') { 'intune' } else { 'other' }); providerId = $(if ($provId) { $provId } else { $null }) } } function Resolve-DDSettingSources { <# .SYNOPSIS Attribute every Delivery Optimization setting to the channel that set it, and add policies that Get-DOConfig does not report (DOGroupId, DOCacheHost, DOMaxCacheAge, ...) from the policy stores. .DESCRIPTION Evidence, strongest first: 1. Get-DOConfig's own provider per setting (DO's statement of the value in force): basis do-service. 2. Both MDM (PolicyManager\current\device\DeliveryOptimization) and Group Policy (HKLM\SOFTWARE\Policies\Microsoft\Windows\DeliveryOptimization) hold a value: Policy CSP ControlPolicyConflict/MDMWinsOverGP = 1 means "the MDM policy is used and the GP policy is blocked", so MDM wins (basis mdm-wins-over-gp). Without it Microsoft says there is "a race condition and no guarantee which one wins", so the winner is left unknown (basis unresolved) and both are listed. 3. One store only: that store (basis registry). The Settings app store (HKU\S-1-5-20\...\DeliveryOptimization\Settings) only annotates settings already known; its value names are not CSP names and are matched without the DO prefix. MDM attribution follows <Name>_WinningProvider to the enrolment's ProviderID. .OUTPUTS Ordered dictionary CSP name -> @{ value; provider; source = @{ kind; mdm; basis; setIn; alt } }. #> [CmdletBinding()] param( [AllowNull()] $Effective, [hashtable] $Kinds = @{}, $MdmStore, $PolicyStore, $LocalStore, [AllowEmptyCollection()] $Enrollments = @(), [AllowNull()] $MdmWinsOverGp ) $mdm = ConvertFrom-DDPolicyStore -Dump $MdmStore $gp = ConvertFrom-DDPolicyStore -Dump $PolicyStore $local = ConvertFrom-DDPolicyStore -Dump $LocalStore $names = New-Object 'System.Collections.Generic.List[string]' $seen = @{} if ($null -ne $Effective) { foreach ($k in $Effective.Keys) { $names.Add([string]$k); $seen[([string]$k).ToLowerInvariant()] = $true } } foreach ($store in $mdm, $gp) { foreach ($e in $store.Values) { if ($e.Name -cmatch '^DO[A-Z]' -and -not $seen.ContainsKey($e.Name.ToLowerInvariant())) { $names.Add($e.Name); $seen[$e.Name.ToLowerInvariant()] = $true } } } $out = [ordered]@{} foreach ($name in $names) { $k = $name.ToLowerInvariant() $inMdm = $mdm.ContainsKey($k) $inGp = $gp.ContainsKey($k) $localKey = $(if ($local.ContainsKey($k)) { $k } elseif ($local.ContainsKey(($k -replace '^do', ''))) { $k -replace '^do', '' } else { $null }) $setIn = New-Object 'System.Collections.Generic.List[string]' if ($inMdm) { $setIn.Add('mdm') } if ($inGp) { $setIn.Add('gp') } if ($localKey) { $setIn.Add('local') } $doKind = $null if ($Kinds.ContainsKey($name)) { $doKind = $Kinds[$name] } $kind = 'unknown'; $basis = 'unresolved' if ($doKind -and $doKind -ne 'unknown') { $kind = $doKind; $basis = 'do-service' } elseif ($inMdm -and $inGp) { if ($MdmWinsOverGp -eq $true) { $kind = 'mdm'; $basis = 'mdm-wins-over-gp' } } elseif ($inMdm) { $kind = 'mdm'; $basis = 'registry' } elseif ($inGp) { $kind = 'gp'; $basis = 'registry' } elseif ($localKey) { $kind = 'local'; $basis = 'registry' } $mdmInfo = $null if ($kind -eq 'mdm' -and $inMdm) { $mdmInfo = Resolve-DDMdmProvider -EnrollmentId $mdm[$k].WinningProvider -Enrollments $Enrollments } $value = $null if ($null -ne $Effective -and $Effective.Contains($name)) { $value = $Effective[$name]['value'] } elseif ($kind -eq 'gp' -and $inGp) { $value = $gp[$k].Value } elseif ($inMdm) { $value = $mdm[$k].Value } elseif ($inGp) { $value = $gp[$k].Value } $alt = New-Object 'System.Collections.Generic.List[object]' if ($setIn.Count -gt 1 -or ($setIn.Count -eq 1 -and $setIn[0] -ne $kind)) { if ($inMdm -and $kind -ne 'mdm') { $alt.Add([ordered]@{ store = 'mdm'; value = $mdm[$k].Value }) } if ($inGp -and $kind -ne 'gp') { $alt.Add([ordered]@{ store = 'gp'; value = $gp[$k].Value }) } if ($localKey -and $kind -ne 'local') { $alt.Add([ordered]@{ store = 'local'; value = $local[$localKey].Value }) } } $out[$name] = [ordered]@{ value = $value provider = (ConvertTo-DDProviderName -Text $kind) source = [ordered]@{ kind = $kind; mdm = $mdmInfo; basis = $basis; setIn = $setIn.ToArray(); alt = $alt.ToArray() } } } $out } # ---- endpoints --------------------------------------------------------------------------------------------------- $script:DDEndpointRoles = 'cache', 'cdn', 'lan', 'group', 'internet', 'peer' $script:DDMaxEndpointRows = 256 function ConvertTo-DDNetworkPrefix { # '10.20.4.71' -> '10.20.4.0/24'; '2001:db8:1:2::5' -> '2001:db8:1::/48'. $null for anything that is not an address. [CmdletBinding()] param([AllowNull()][string] $Address) $ip = $null if (-not $Address -or -not [System.Net.IPAddress]::TryParse($Address, [ref]$ip)) { return $null } $b = $ip.GetAddressBytes() if ($ip.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetwork) { return ('{0}.{1}.{2}.0/24' -f $b[0], $b[1], $b[2]) } for ($i = 6; $i -lt 16; $i++) { $b[$i] = 0 } ([System.Net.IPAddress]::new($b)).ToString() + '/48' } function Get-DDEndpointAddress { <# .SYNOPSIS The address of an endpoint as it may leave the device (ADR-0031). .DESCRIPTION Connected Cache servers and CDN nodes are infrastructure: sent in clear. Internet peers are other people's devices: always reduced to a /24 (IPv4) or /48 (IPv6) prefix. LAN and group peers are the organisation's own devices: in clear unless -Anonymise, then reduced to the prefix. Never a hash beside a prefix: a hash of an address inside a known /24 is undone by trying 256 values. #> [CmdletBinding()] param([Parameter(Mandatory)][string] $Role, [AllowNull()][string] $Address, [switch] $Anonymise) if (-not $Address) { return $null } if ($Role -in 'cache', 'cdn') { return (ConvertTo-DDCacheHostName -Value $Address) } $ip = $null if (-not [System.Net.IPAddress]::TryParse($Address, [ref]$ip)) { return $null } if ([System.Net.IPAddress]::IsLoopback($ip) -or $ip.Equals([System.Net.IPAddress]::Any) -or $ip.Equals([System.Net.IPAddress]::IPv6Any)) { return $null } if ($Role -eq 'internet' -or $Anonymise) { return (ConvertTo-DDNetworkPrefix -Address $ip.ToString()) } $ip.ToString().ToLowerInvariant() } function New-DDEndpointObservation { [CmdletBinding()] param( [Parameter(Mandatory)][string] $Role, [Parameter(Mandatory)][string] $Address, [Parameter(Mandatory)][string] $Via, $Rx = $null, $Tx = $null, [int] $Attempts = 0, [int] $Ok = 0, [int] $Fails = 0, $At = $null, [string] $Origin = $null ) [pscustomobject]@{ Role = $Role; Address = $Address; Via = $Via; Rx = $Rx; Tx = $Tx; Attempts = $Attempts; Ok = $Ok; Fails = $Fails; At = $At; Origin = $Origin } } function ConvertTo-DDCacheOrigin { # The prefix DO writes before a cache host in its download telemetry ("cacheHost = Services:198.51.100.202"): Services # is the DO cloud service's assignment (the GEO response's CacheHost). Other prefixes are not observed (to-verify). [CmdletBinding()] param([AllowNull()][string] $Text) switch -Regex ([string]$Text) { '(?i)^services?$' { 'service' } '(?i)dhcp' { 'dhcp' } '(?i)polic|mdm|gp|admin|registry' { 'policy' } default { $null } } } function ConvertFrom-DDDoLogEndpoints { <# .SYNOPSIS Endpoint observations from Delivery Optimization log entries (Get-DeliveryOptimizationLog: TimeCreated, Function, Message). .DESCRIPTION Message formats, all read from a real DO log (Windows 11, DO client 10.1.0.15): CTelemetryLogger::TraceDownloadCompletedImpl "... cdnIp = [150.171.109.244]:443;, cacheHost = Services:198.51.100.202, bytes: [File: n, CDN: n, DOINC: n, ...]" (CDN includes DOINC, as BytesFromHttp includes BytesFromCacheServer) CHttpPeerConn::Initiate "Swarm: <id>, initiating HTTP connection, IP: 198.51.100.202" CHttpPeerConn::_OnComplete "Swarm: <id>, HTTP connection: [198.51.100.202]:80, rLEDBAT? 0" CSwarmConn::Initiate "Swarm: <id>, initiating peer connection: [10.100.0.74]:7680" CTelemetryLogger::TraceErrorPeerComm "FailurePeerConnect, hr: 8007274c, ..., peerIp: [10.100.0.74]:7680, peerId: , peerType: 1" CHttpDownloader::_DoSendRequest "Unresponsive cache host 198.51.100.202" CConnMan::_StartNewHttpConns "Connecting to banned cache host 198.51.100.202. Time until fallback: ..." CServiceConfigProvider::_CallService GEO response JSON with "CacheHost":"198.51.100.202" (the same JSON holds the device's public address, which is never read here) No line with bytes per peer was found; peer bytes come from the peer list (CIM) instead. #> [CmdletBinding()] param( [Parameter(Mandatory)][AllowEmptyCollection()] $Entries, [AllowEmptyCollection()][string[]] $CacheHosts = @(), [string] $LocalAddress, [int] $PrefixLength = 24, # The device's IPv6 prefixes (Get-DDNetworkFacts LocalNetworks): an IPv6 peer inside one is on this network. [AllowEmptyCollection()][string[]] $LocalNetworks = @() ) $obs = New-Object 'System.Collections.Generic.List[object]' $cache = @{} $origin = @{} $named = @{} # cache hosts the log itself names (the ones passed in are only used to classify connections) foreach ($h in $CacheHosts) { $n = ConvertTo-DDCacheHostName $h; if ($n) { $cache[$n] = $true } } $ipRx = '(?<ip>[0-9A-Fa-f:.]+)' # first pass: every cache host the log names, so an HTTP connection to one is not mistaken for the CDN foreach ($e in @($Entries)) { $m = [string](Get-DDProp $e 'Message') if (-not $m) { continue } foreach ($x in [regex]::Matches($m, 'cacheHost = (?:(?<src>[A-Za-z]+):)?(?<h>[^,\s]+)')) { $n = ConvertTo-DDCacheHostName $x.Groups['h'].Value if ($n) { $cache[$n] = $true; $named[$n] = $true; $o = ConvertTo-DDCacheOrigin $x.Groups['src'].Value; if ($o) { $origin[$n] = $o } } } $g = [regex]::Match($m, '"CacheHost"\s*:\s*"(?<h>[^"]+)"') if ($g.Success) { $n = ConvertTo-DDCacheHostName $g.Groups['h'].Value; if ($n) { $cache[$n] = $true; $named[$n] = $true; if (-not $origin.ContainsKey($n)) { $origin[$n] = 'service' } } } $u = [regex]::Match($m, '(?:Unresponsive|banned) cache host (?<h>[^\s.,]+(?:\.[^\s.,]+)*)') if ($u.Success) { $n = ConvertTo-DDCacheHostName $u.Groups['h'].Value; if ($n) { $cache[$n] = $true; $named[$n] = $true } } } $peerRole = { param($ip, $typeNum) if ($null -ne $typeNum) { switch (ConvertTo-DDPeerType -Value $typeNum -AllowCdn) { 'Lan' { return 'lan' } 'Group' { return 'group' } 'Internet' { return 'internet' } 'CacheServer' { return 'cache' } 'Cdn' { return 'cdn' } } } # On this device's subnet, a link-local address, or inside one of its IPv6 prefixes. if (Test-DDSameSubnet -Address $ip -LocalAddress ([string]$LocalAddress) -PrefixLength $PrefixLength -LocalNetworks $LocalNetworks) { return 'lan' } 'peer' } $httpRole = { param($a) $n = ConvertTo-DDCacheHostName $a; if ($n -and $cache.ContainsKey($n)) { 'cache' } else { 'cdn' } } foreach ($e in @($Entries)) { $m = [string](Get-DDProp $e 'Message') if (-not $m) { continue } $t = Get-DDProp $e 'TimeCreated', 'Time', 'Timestamp' if ($t -isnot [datetime]) { $t = $null } $done = [regex]::Match($m, 'cdnIp = \[?' + $ipRx + '\]?(?::(?<port>\d+))?;') if ($done.Success -and $m -match 'bytes: \[') { $cdnBytes = [System.Numerics.BigInteger]::Zero; $doinc = [System.Numerics.BigInteger]::Zero $b = [regex]::Match($m, 'bytes: \[[^\]]*?\bCDN: (?<cdn>\d+)'); if ($b.Success) { $cdnBytes = [System.Numerics.BigInteger]::Parse($b.Groups['cdn'].Value) } $b = [regex]::Match($m, 'bytes: \[[^\]]*?\bDOINC: (?<d>\d+)'); if ($b.Success) { $doinc = [System.Numerics.BigInteger]::Parse($b.Groups['d'].Value) } $ch = $null $c = [regex]::Match($m, 'cacheHost = (?:[A-Za-z]+:)?(?<h>[^,\s]+)'); if ($c.Success) { $ch = ConvertTo-DDCacheHostName $c.Groups['h'].Value } $ip = $done.Groups['ip'].Value if ((& $httpRole $ip) -eq 'cache') { $obs.Add((New-DDEndpointObservation -Role cache -Address $ip -Via log -Rx $doinc -At $t)) } else { $http = $(if ($cdnBytes -ge $doinc) { $cdnBytes - $doinc } else { $cdnBytes }) $obs.Add((New-DDEndpointObservation -Role cdn -Address $ip -Via log -Rx $http -At $t)) if ($ch -and $doinc -gt 0) { $obs.Add((New-DDEndpointObservation -Role cache -Address $ch -Via log -Rx $doinc -At $t)) } } continue } $x = [regex]::Match($m, 'initiating HTTP connection, IP: ' + $ipRx) if ($x.Success) { $ip = $x.Groups['ip'].Value; $obs.Add((New-DDEndpointObservation -Role (& $httpRole $ip) -Address $ip -Via log -Attempts 1 -At $t)); continue } $x = [regex]::Match($m, 'HTTP connection: \[' + $ipRx + '\]') if ($x.Success) { $ip = $x.Groups['ip'].Value; $obs.Add((New-DDEndpointObservation -Role (& $httpRole $ip) -Address $ip -Via log -Ok 1 -At $t)); continue } $x = [regex]::Match($m, 'initiating peer connection: \[' + $ipRx + '\]') if ($x.Success) { $ip = $x.Groups['ip'].Value; $obs.Add((New-DDEndpointObservation -Role (& $peerRole $ip $null) -Address $ip -Via log -Attempts 1 -At $t)); continue } $x = [regex]::Match($m, 'peerIp: \[' + $ipRx + '\](?::\d+)?(?:,.*?peerType: (?<pt>\d+))?') if ($x.Success) { $ip = $x.Groups['ip'].Value $pt = $(if ($x.Groups['pt'].Success) { $x.Groups['pt'].Value } else { $null }) $obs.Add((New-DDEndpointObservation -Role (& $peerRole $ip $pt) -Address $ip -Via log -Fails 1 -At $t)); continue } $x = [regex]::Match($m, '(?:Unresponsive|banned) cache host (?<h>[^\s,]+?)\.?(?:\s|$)') if ($x.Success) { $obs.Add((New-DDEndpointObservation -Role cache -Address $x.Groups['h'].Value -Via log -Fails 1 -At $t)); continue } } foreach ($h in $named.Keys) { $o = $(if ($origin.ContainsKey($h)) { $origin[$h] } else { $null }) $obs.Add((New-DDEndpointObservation -Role cache -Address $h -Via log -Origin $o)) } , $obs.ToArray() } function ConvertFrom-DDStatusEndpoints { # Connected Cache per file (Get-DeliveryOptimizationStatus CacheHost, documented "IP address for the cache server"; # also set on files the cache served nothing for, so `rx` decides "used"). [CmdletBinding()] param([Parameter(Mandatory)][AllowEmptyCollection()] $Items, $At = $null) $obs = New-Object 'System.Collections.Generic.List[object]' foreach ($i in @($Items)) { $h = ConvertTo-DDCacheHostName (Get-DDProp $i 'CacheHost') if (-not $h) { continue } $conn = Get-DDProp $i 'CacheServerConnectionCount' $obs.Add((New-DDEndpointObservation -Role cache -Address $h -Via status -Rx (ConvertTo-DDBigInt (Get-DDProp $i 'BytesFromCacheServer')) -Attempts $(if ($null -ne $conn) { [int]$conn } else { 0 }) -At $At)) } , $obs.ToArray() } function ConvertFrom-DDPeerEndpoints { # The swarm peer list (MSFT_DeliveryOptimizationFilePeerInfo: IPs, PeerTypes, BytesReceived, BytesSent, # ConnectionEstablished; flattened to one object per peer by Get-DDDOPeerInfoRaw). [CmdletBinding()] param([Parameter(Mandatory)][AllowEmptyCollection()] $Items, $At = $null) $obs = New-Object 'System.Collections.Generic.List[object]' foreach ($it in @($Items)) { $inner = Get-DDProp $it 'Peers', 'PeersInfo', 'PeerInfo' $set = $(if ($null -ne $inner -and $inner -isnot [string]) { @($inner) } elseif ($it -isnot [string]) { @($it) } else { @() }) foreach ($p in $set) { $ip = [string](Get-DDProp $p 'IP', 'IPAddress', 'PeerIP') if (-not $ip) { continue } $role = switch (ConvertTo-DDPeerType -Value (Get-DDProp $p 'PeerType') -AllowCdn) { 'Lan' { 'lan' } 'Group' { 'group' } 'Internet' { 'internet' } 'CacheServer' { 'cache' } 'Cdn' { 'cdn' } default { 'peer' } } $est = Get-DDProp $p 'ConnectionEstablished' $obs.Add((New-DDEndpointObservation -Role $role -Address $ip -Via peers -Rx (ConvertTo-DDBigInt (Get-DDProp $p 'BytesReceived')) -Tx (ConvertTo-DDBigInt (Get-DDProp $p 'BytesSent')) -Attempts 1 -Ok $(if ($est -eq $true) { 1 } else { 0 }) -At $At)) } } , $obs.ToArray() } function ConvertFrom-DDTcpEndpoints { # DoSvc's TCP connections at the moment of collection (Get-NetTCPConnection -OwningProcess <DoSvc PID>; DoSvc runs in # its own svchost). Port 7680 either way is a peer; a connection to a known cache host is the cache. Other HTTPS is the # DO cloud service, not content, and is not reported. [CmdletBinding()] param([Parameter(Mandatory)][AllowEmptyCollection()] $Items, [AllowEmptyCollection()][string[]] $CacheHosts = @(), [string] $LocalAddress, [int] $PrefixLength = 24, $At = $null, [AllowEmptyCollection()][string[]] $LocalNetworks = @()) $obs = New-Object 'System.Collections.Generic.List[object]' $cache = @{} foreach ($h in $CacheHosts) { $n = ConvertTo-DDCacheHostName $h; if ($n) { $cache[$n] = $true } } foreach ($c in @($Items)) { $remote = [string](Get-DDProp $c 'RemoteAddress') $state = [string](Get-DDProp $c 'State') if (-not $remote -or $state -match '(?i)^(Listen|Bound)$') { continue } $rp = [int](Get-DDProp $c 'RemotePort'); $lp = [int](Get-DDProp $c 'LocalPort') $when = Get-DDProp $c 'CreationTime'; if ($when -isnot [datetime]) { $when = $At } $ok = $(if ($state -match '(?i)^Established$') { 1 } else { 0 }) $n = ConvertTo-DDCacheHostName $remote if ($n -and $cache.ContainsKey($n)) { $obs.Add((New-DDEndpointObservation -Role cache -Address $remote -Via tcp -Attempts 1 -Ok $ok -At $when)); continue } if ($rp -eq 7680 -or $lp -eq 7680) { $role = $(if (Test-DDSameSubnet -Address $remote -LocalAddress ([string]$LocalAddress) -PrefixLength $PrefixLength -LocalNetworks $LocalNetworks) { 'lan' } else { 'peer' }) $obs.Add((New-DDEndpointObservation -Role $role -Address $remote -Via tcp -Attempts 1 -Ok $ok -At $when)) } } , $obs.ToArray() } function Merge-DDEndpoints { <# .SYNOPSIS Fold endpoint observations into the bundle's `endpoints` rows: privacy applied, one row per role and address. .DESCRIPTION An address seen as an untyped peer takes the type another source gave it. Bytes are the largest any one source reported (the job list and the log overlap; adding them would count the same bytes twice). Rows are ordered cache first, then by bytes, then by connections, and capped. #> [CmdletBinding()] param([Parameter(Mandatory)][AllowEmptyCollection()] $Observations, [Parameter(Mandatory)] $Context, [int] $Max = $script:DDMaxEndpointRows) $typed = @{} foreach ($o in @($Observations)) { if ($o.Role -in 'lan', 'group', 'internet') { $typed[[string]$o.Address] = $o.Role } } $rows = [ordered]@{} foreach ($o in @($Observations)) { $role = [string]$o.Role if ($role -eq 'peer' -and $typed.ContainsKey([string]$o.Address)) { $role = $typed[[string]$o.Address] } if ($role -notin $script:DDEndpointRoles) { continue } $addr = Get-DDEndpointAddress -Role $role -Address ([string]$o.Address) -Anonymise:$Context.Anonymise if (-not $addr) { continue } $key = "$role|$addr" if (-not $rows.Contains($key)) { $rows[$key] = @{ role = $role; addr = $addr; raw = (New-Object 'System.Collections.Generic.HashSet[string]'); rx = @{}; tx = @{}; conns = 0; ok = 0; fails = 0; first = $null; last = $null; via = (New-Object 'System.Collections.Generic.List[string]'); origin = $null } } $r = $rows[$key] [void]$r.raw.Add(([string]$o.Address).ToLowerInvariant()) if ($null -ne $o.Rx) { if (-not $r.rx.ContainsKey($o.Via)) { $r.rx[$o.Via] = [System.Numerics.BigInteger]::Zero }; $r.rx[$o.Via] += (ConvertTo-DDBigInt $o.Rx) } if ($null -ne $o.Tx) { if (-not $r.tx.ContainsKey($o.Via)) { $r.tx[$o.Via] = [System.Numerics.BigInteger]::Zero }; $r.tx[$o.Via] += (ConvertTo-DDBigInt $o.Tx) } $r.conns += [int]$o.Attempts; $r.ok += [int]$o.Ok; $r.fails += [int]$o.Fails if ($o.At -is [datetime]) { $u = $o.At.ToUniversalTime() if ($null -eq $r.first -or $u -lt $r.first) { $r.first = $u } if ($null -eq $r.last -or $u -gt $r.last) { $r.last = $u } } if (-not $r.via.Contains([string]$o.Via)) { $r.via.Add([string]$o.Via) } if ($o.Origin -and -not $r.origin) { $r.origin = [string]$o.Origin } } $best = { param($h) $m = $null; foreach ($v in $h.Values) { if ($null -eq $m -or $v -gt $m) { $m = $v } }; $m } $list = New-Object 'System.Collections.Generic.List[object]' foreach ($r in $rows.Values) { $rx = & $best $r.rx; $tx = & $best $r.tx $list.Add([ordered]@{ role = $r.role addr = $r.addr n = [math]::Max(1, $r.raw.Count) rx = $(if ($null -ne $rx) { $rx.ToString() } else { $null }) tx = $(if ($null -ne $tx) { $tx.ToString() } else { $null }) conns = $r.conns ok = $r.ok fails = $r.fails first = $(if ($r.first) { Format-DDIso -Value $r.first } else { $null }) last = $(if ($r.last) { Format-DDIso -Value $r.last } else { $null }) via = @($r.via | Sort-Object) origin = $r.origin }) } Select-DDEndpointRows -Rows $list.ToArray() -Max $Max } function Select-DDEndpointRows { # Order and cap endpoint rows: cache first, then bytes (rx + tx) descending, then connections, insertion order last. # The same rule as selectEndpoints in packages/wire/src/compact.ts. [CmdletBinding()] param([Parameter(Mandatory)][AllowEmptyCollection()] $Rows, [int] $Max) $wrapped = New-Object 'System.Collections.Generic.List[object]' $i = 0 foreach ($r in @($Rows)) { $b = (ConvertTo-DDBigInt $r['rx']) + (ConvertTo-DDBigInt $r['tx']) $wrapped.Add([pscustomobject]@{ I = $i; C = $(if ($r['role'] -eq 'cache') { 0 } else { 1 }); B = $b; N = [int]$r['conns']; V = $r }); $i++ } $cmp = [System.Comparison[object]] { param($a, $b) if ($a.C -ne $b.C) { return ($a.C - $b.C) } $c = [System.Numerics.BigInteger]::Compare($b.B, $a.B) if ($c -ne 0) { return $c } if ($a.N -ne $b.N) { return ($b.N - $a.N) } return ($a.I - $b.I) } $wrapped.Sort($cmp) $out = New-Object 'System.Collections.Generic.List[object]' foreach ($w in $wrapped) { if ($out.Count -ge $Max) { break }; $out.Add($w.V) } , $out.ToArray() } |