Private/DoData.ps1
|
#Requires -Version 5.1 # Mapping of Delivery Optimization cmdlet output to the BundleV1 row shapes (packages/schema/src/telemetry.ts). # # Property names of the DeliveryOptimization module's output objects (DOSwarmStats, DOFilePeerInfo, DOPeerInfo, # DOPerfSnap, DOLogEntry) are only partly documented on Microsoft Learn (the cmdlet pages are stubs), so each # mapper looks a field up under the candidate names seen in the monitoring article and reports what it could not # find. All of that is `to-verify` until a Windows lab pass; the mappers are pure and tested on synthetic objects. Set-StrictMode -Version Latest $script:DDModeNames = @{ cdnonly = 0; httponly = 0; lan = 1; group = 2; internet = 3; simple = 99; bypass = 100 } function ConvertTo-DDDownloadMode { [CmdletBinding()] param($Value) if ($null -eq $Value) { return -1 } $n = 0 if ([int]::TryParse([string]$Value, [ref]$n)) { return $n } $k = ([string]$Value).ToLowerInvariant() if ($script:DDModeNames.ContainsKey($k)) { return [int]$script:DDModeNames[$k] } -1 } function ConvertTo-DDPriority { [CmdletBinding()] param($Value) $s = [string]$Value if ($s -match '(?i)^fore') { return 'Foreground' } if ($s -match '(?i)^back') { return 'Background' } 'Unknown' } # PeerType as DeliveryOptimizationStatus.psm1 declares it (enum PeerType, read on Windows 11 26H2): 0 none, 1 LAN, 2 CDN, # 3 Internet, 4 Group, 5 DOINC (Connected Cache), 6 LocalCache, 7 LinkLocal. The cmdlet emits the enum name # ("PeerType_LAN"); the CIM class MSFT_DeliveryOptimizationFilePeerInfo emits the number. LocalCache is taken as a cache # server (inferred from the name; not documented). $script:DDPeerTypeNumbers = @{ 1 = 'Lan'; 2 = 'Cdn'; 3 = 'Internet'; 4 = 'Group'; 5 = 'CacheServer'; 6 = 'CacheServer'; 7 = 'Lan' } function ConvertTo-DDPeerType { [CmdletBinding()] param($Value, [switch] $AllowCdn) $t = 'Unknown' $n = 0 if ($null -ne $Value -and [int]::TryParse([string]$Value, [ref]$n)) { if ($script:DDPeerTypeNumbers.ContainsKey($n)) { $t = $script:DDPeerTypeNumbers[$n] } } else { $s = ([string]$Value) -replace '(?i)^PeerType_', '' switch -Regex ($s) { '(?i)^lan$|linklocal' { $t = 'Lan'; break } '(?i)^group' { $t = 'Group'; break } '(?i)^internet|^inet$' { $t = 'Internet'; break } '(?i)cache|^doinc$' { $t = 'CacheServer'; break } '(?i)^cdn$|^http$' { $t = 'Cdn'; break } } } if ($t -eq 'Cdn' -and -not $AllowCdn) { return 'Unknown' } $t } function ConvertTo-DDCacheHostName { # A Connected Cache host as DO reports it ([uri] on DOSwarmStats, text in the log and in policy), reduced to the bare # host or address: never a scheme, path, credentials or port. Anything that does not look like a host is dropped. [CmdletBinding()] param([AllowNull()] $Value) if ($null -eq $Value) { return $null } $s = ([string]$Value).Trim() if ($s -eq '') { return $null } $s = $s -replace '^(?i)[a-z][a-z0-9+.-]*://', '' $s = ($s -split '[/?#]')[0] if ($s.Contains('@')) { $s = $s.Substring($s.LastIndexOf('@') + 1) } if ($s -match '^\[(?<v6>[0-9A-Fa-f:.]+)\](:\d+)?$') { $s = $Matches['v6'] } elseif ($s -match '^(?<h>[^:]+):\d+$') { $s = $Matches['h'] } $s = $s.TrimEnd('.').ToLowerInvariant() if ($s.Length -gt 253 -or $s -notmatch '^[a-z0-9.:-]+$') { return $null } $s } function Get-DDUrlPathHint { [CmdletBinding()] param([AllowNull()][string] $Url) if ([string]::IsNullOrWhiteSpace($Url)) { return $null } $u = $null if (-not [System.Uri]::TryCreate($Url, [System.UriKind]::Absolute, [ref]$u)) { return $null } # Microsoft Update catalog layout, /<shard>/msdownload/update/<software|driver>/<category>/...: the first two # segments are a shard letter and a constant, so the classifying pair is the kind and the category # (e.g. /software/defu). Observed on au.download.windowsupdate.com; meaning of each category is to-verify. if ($u.AbsolutePath -match '(?i)^/[a-z0-9]{1,2}/msdownload/update/(software|driver)/([a-z0-9]{2,8})(/|$)') { return '/{0}/{1}' -f $Matches[1].ToLowerInvariant(), $Matches[2].ToLowerInvariant() } $segs = @($u.AbsolutePath.Split('/') | Where-Object { $_ -ne '' -and $_ -notmatch '^[0-9a-fA-F-]{8,}$' } | Select-Object -First 2) if ($segs.Count -eq 0) { return $null } '/' + ($segs -join '/') } function ConvertTo-DDDownloadMs { <# .SYNOPSIS DownloadDuration in whole milliseconds, or $null. Microsoft documents it as "Total download time in seconds" (Monitor Delivery Optimization); the cmdlet returns a [TimeSpan] (verified on Windows 11 26H2), which is what this reads. A number, or text that is only a number ("172"), is taken as seconds, as documented, and text in TimeSpan form ("00:02:52.0970000") is parsed. Anything else is $null. #> [CmdletBinding()] param([AllowNull()] $Value) if ($null -eq $Value) { return $null } $ms = $null if ($Value -is [timespan]) { $ms = $Value.TotalMilliseconds } elseif ($Value -is [string]) { # A plain number is seconds, as documented. TimeSpan.TryParse would read '172' as 172 days. $d = 0.0 $ts = [timespan]::Zero if ($Value.Trim() -match '^\d+(\.\d+)?$' -and [double]::TryParse($Value.Trim(), [System.Globalization.NumberStyles]::Float, [System.Globalization.CultureInfo]::InvariantCulture, [ref]$d)) { $ms = $d * 1000 } elseif ([timespan]::TryParse($Value, [System.Globalization.CultureInfo]::InvariantCulture, [ref]$ts)) { $ms = $ts.TotalMilliseconds } } elseif ($Value -is [ValueType]) { $d = 0.0 if ([double]::TryParse([string]$Value, [System.Globalization.NumberStyles]::Float, [System.Globalization.CultureInfo]::InvariantCulture, [ref]$d)) { $ms = $d * 1000 } } if ($null -eq $ms -or [double]::IsNaN($ms) -or $ms -lt 0 -or $ms -gt 9e15) { return $null } [long][math]::Round($ms, [System.MidpointRounding]::AwayFromZero) } function ConvertTo-DDStatusRow { [CmdletBinding()] param([Parameter(Mandatory)] $Item, [Parameter(Mandatory)] $Context) $url = Get-DDProp $Item 'SourceURL', 'SourceUrl' # ADR-0031: the Connected Cache server is infrastructure and is sent in clear, under -Anonymise as well. $cache = ConvertTo-DDCacheHostName (Get-DDProp $Item 'CacheHost') # Under -Anonymise a source host that is not a Microsoft content host (a customer's WSUS, file server or internal web # server) is pseudonymised like other internal topology, with the tenant's salt so the same server compares equal across the # tenant's devices. A Microsoft CDN host says nothing about the organisation. $urlHost = Get-DDUrlHost -Url ([string]$url) if ($Context.Anonymise -and $urlHost -and -not (Test-DDMicrosoftContentHost -HostName $urlHost)) { $urlHost = Protect-DDTopologyValue -Value $urlHost -Salt $Context.TenantSalt } # Documented: BytesFromHTTP "represents all HTTP sources, which includes BytesFromCacheServer". The contract's five # sources are disjoint (buildDeviceFlow sums them), so the cache bytes are taken out of the HTTP figure here. $httpAll = ConvertTo-DDBigInt (Get-DDProp $Item 'BytesFromHttp', 'BytesFromHTTP') $cacheBytes = ConvertTo-DDBigInt (Get-DDProp $Item 'BytesFromCacheServer') # A cache figure above the HTTP one (two counters read a moment apart) means the HTTP bytes all came from the cache: none # are left over, rather than counting the cache bytes twice. $httpOnly = $(if ($httpAll -ge $cacheBytes) { $httpAll - $cacheBytes } else { [System.Numerics.BigInteger]::Zero }) $expire = Get-DDProp $Item 'ExpireOn' $expireIso = $null if ($expire -is [datetime]) { $expireIso = Format-DDIso -Value $expire } $caller = Get-DDProp $Item 'PredefinedCallerApplication' $numPeers = Get-DDProp $Item 'NumPeers' # A mode name the lookup does not know is sent as -1 ("unknown"), and named once in notCollected so it is not silent. $modeRaw = Get-DDProp $Item 'DownloadMode' $mode = ConvertTo-DDDownloadMode $modeRaw if ($mode -eq -1 -and $null -ne $modeRaw) { Add-DDNotCollected -Context $Context -Field 'status.downloadMode' -Reason 'error' -Detail ("a job's DownloadMode '{0}' is not a known mode; sent as -1" -f ([string]$modeRaw).Substring(0, [math]::Min(40, ([string]$modeRaw).Length))) } [ordered]@{ fileId = [string](Get-DDProp $Item 'FileId', 'File ID') fileSize = (ConvertTo-DDBigInt (Get-DDProp $Item 'FileSize')).ToString() totalBytesDownloaded = (ConvertTo-DDBigInt (Get-DDProp $Item 'TotalBytesDownloaded')).ToString() bytesFromHttp = $httpOnly.ToString() bytesFromLanPeers = (ConvertTo-DDBigInt (Get-DDProp $Item 'BytesFromLanPeers')).ToString() bytesFromGroupPeers = (ConvertTo-DDBigInt (Get-DDProp $Item 'BytesFromGroupPeers')).ToString() bytesFromInternetPeers = (ConvertTo-DDBigInt (Get-DDProp $Item 'BytesFromInternetPeers')).ToString() bytesFromCacheServer = (ConvertTo-DDBigInt (Get-DDProp $Item 'BytesFromCacheServer')).ToString() downloadMode = $mode status = [string](Get-DDProp $Item 'Status') priority = (ConvertTo-DDPriority (Get-DDProp $Item 'Priority')) numPeers = $(if ($null -ne $numPeers) { [math]::Max(0, [int]$numPeers) } else { 0 }) predefinedCallerApplication = $(if ($null -ne $caller) { [string]$caller } else { $null }) sourceUrlHost = $urlHost sourceUrlPathHint = (Get-DDUrlPathHint -Url ([string]$url)) expireOn = $expireIso cacheHost = $cache # The job's whole DownloadDuration, waits included: what TelemetryV1.sources times a source by (docs/08). downloadMs = (ConvertTo-DDDownloadMs (Get-DDProp $Item 'DownloadDuration')) # What the job served to peers, by where they were found (documented: BytesToLanPeers, BytesToGroupPeers, # BytesToInternetPeers), running totals like the download counters: telemetry sends what they rose by (docs/08). bytesToLanPeers = (ConvertTo-DDBigInt (Get-DDProp $Item 'BytesToLanPeers')).ToString() bytesToGroupPeers = (ConvertTo-DDBigInt (Get-DDProp $Item 'BytesToGroupPeers')).ToString() bytesToInternetPeers = (ConvertTo-DDBigInt (Get-DDProp $Item 'BytesToInternetPeers')).ToString() } } function ConvertTo-DDPeerRows { <# .SYNOPSIS Flatten `Get-DeliveryOptimizationStatus -PeerInfo` output. Accepts per-file objects that carry a collection of peers, or flat peer rows. Peer addresses are hashed here and never leave this function. #> [CmdletBinding()] param( [Parameter(Mandatory)][AllowEmptyCollection()] $Items, [Parameter(Mandatory)] $Context, [string] $LocalAddress, [int] $PrefixLength = 24, # The device's IPv6 prefixes (Get-DDNetworkFacts LocalNetworks), so an IPv6 peer on this network counts as same-subnet (A-14). [AllowNull()][AllowEmptyCollection()][string[]] $LocalNetworks = @() ) $rows = New-Object 'System.Collections.Generic.List[object]' $emit = { param($peer, $fileId) $ip = [string](Get-DDProp $peer 'IP', 'IPAddress', 'PeerIP') if (-not $ip) { return } $type = ConvertTo-DDPeerType (Get-DDProp $peer 'PeerType') -AllowCdn if ($type -eq 'Cdn') { return } # an HTTP source listed among the swarm's connections is not a peer $same = $false if ($LocalAddress -or @($LocalNetworks).Count -gt 0) { $same = Test-DDSameSubnet -Address $ip -LocalAddress ([string]$LocalAddress) -PrefixLength $PrefixLength -LocalNetworks $LocalNetworks } if ($type -eq 'Lan' -and -not $LocalAddress) { $same = $true } $rows.Add([ordered]@{ fileId = $(if ($fileId) { [string]$fileId } else { 'unknown' }) peerHash = (Get-DDSaltedHash -Value $ip -Salt $Context.TenantSalt -Domain 'peer') peerType = $type sameSubnet = [bool]$same bytesSent = (ConvertTo-DDBigInt (Get-DDProp $peer 'BytesSent')).ToString() bytesReceived = (ConvertTo-DDBigInt (Get-DDProp $peer 'BytesReceived')).ToString() }) } foreach ($it in @($Items)) { # DOFilePeerInfo carries its peers in PeersInfo (DeliveryOptimizationStatus.psm1); Peers is this module's own shape. $inner = Get-DDProp $it 'Peers', 'PeersInfo', 'PeerInfo' $fid = Get-DDProp $it 'FileId', 'File ID' if ($null -ne $inner -and $inner -isnot [string]) { foreach ($p in @($inner)) { & $emit $p $fid } } elseif ($it -isnot [string]) { & $emit $it $fid } } , $rows.ToArray() } function ConvertTo-DDPerfSnap { <# .SYNOPSIS PerfSnapV1: the readings of `Get-DeliveryOptimizationPerfSnap` (documented: NumberOfPeers, CacheHostConnections, DownlinkUsageBps) and the byte counters of the jobs in the cache. .DESCRIPTION Microsoft documents no per-source byte totals for the service, so the byte counters are the running totals of the current Delivery Optimization jobs (Get-DeliveryOptimizationStatus): bytes by source, uploads by peer type (BytesTo*Peers) and bytes by priority. Telemetry never sends these sums: it sends each job's own increase since the last report (Compact.ps1, docs/08), so a job leaving the cache is not mistaken for a service restart. Collectors before 0.6.0 read the byte counters from the undocumented -Verbose text when it parsed. #> [CmdletBinding()] param( [AllowNull()] $Item, [AllowEmptyCollection()] $StatusRows = @() ) if ($null -eq $Item) { return $null } $zero = [System.Numerics.BigInteger]::Zero $sum = { param($name) $t = $zero foreach ($r in @($StatusRows)) { $t += (ConvertTo-DDBigInt $r[$name]) } $t } $sumPriority = { param($pri) $t = $zero foreach ($r in @($StatusRows)) { if ($r['priority'] -eq $pri) { $t += (ConvertTo-DDBigInt $r['totalBytesDownloaded']) } } $t } $out = [ordered]@{ downloadHttpBytes = (& $sum 'bytesFromHttp').ToString() downloadLanBytes = (& $sum 'bytesFromLanPeers').ToString() downloadGroupBytes = (& $sum 'bytesFromGroupPeers').ToString() downloadInternetBytes = (& $sum 'bytesFromInternetPeers').ToString() downloadCacheHostBytes = (& $sum 'bytesFromCacheServer').ToString() uploadLanBytes = (& $sum 'bytesToLanPeers').ToString() uploadGroupBytes = (& $sum 'bytesToGroupPeers').ToString() uploadInternetBytes = (& $sum 'bytesToInternetPeers').ToString() foregroundDownloadBytes = (& $sumPriority 'Foreground').ToString() backgroundDownloadBytes = (& $sumPriority 'Background').ToString() } $peers = Get-DDProp $Item 'NumberOfPeers', 'NumPeers' $cacheConn = Get-DDProp $Item 'CacheHostConnections' $down = Get-DDProp $Item 'DownlinkUsageBps' $out['numberOfPeers'] = $(if ($null -ne $peers) { [math]::Max(0, [int]$peers) } else { 0 }) $out['cacheHostConnections'] = $(if ($null -ne $cacheConn) { [math]::Max(0, [int]$cacheConn) } else { $null }) # DownlinkUsageBps is "average bandwidth currently used by Delivery Optimization for downloads"; the unit behind # "Bps" is not stated (to-verify). Treated as bytes per second, reported in KB/s. $out['averageDownloadKBps'] = $(if ($null -ne $down) { [math]::Round([math]::Max(0, [double]$down) / 1024, 1) } else { $null }) $out } function ConvertTo-DDMonthTotals { <# .SYNOPSIS DoMonthTotals (BundleV1.doMonth): Delivery Optimization's own byte counters for the current calendar month, from `Get-DeliveryOptimizationPerfSnapThisMonth` (product owner, 19 Oct 2026: the "Delivery Optimization statistics" that Get-AutopilotDiagnostics prints from the same cmdlet). .DESCRIPTION Microsoft Learn (Monitor Delivery Optimization) says the cmdlet "returns data similar to data from Get-DeliveryOptimizationPerfSnap but limited to the current calendar month" and lists no fields. The names read here are the cmdlet's own properties on Windows 11 26H2 (19 Oct 2026): MonthStartDate, DownloadHttpBytes, DownloadCacheHostBytes, DownloadLanBytes, DownloadInternetBytes, UploadLanBytes, UploadInternetBytes. That build has no DownloadGroupBytes; it is read when a build has it and is otherwise null, never 0. DownloadHttpBytes includes the Connected Cache bytes, as Learn documents for the job-level BytesFromHTTP ("all HTTP sources, which includes BytesFromCacheServer"). $null when there is no snapshot or it has no DownloadHttpBytes (a build whose cmdlet returns another shape): the caller records `doMonth` as not collected. Byte counters are decimal strings, as the bundle's other counters are. #> [CmdletBinding()] param([AllowNull()] $Item) if ($null -eq $Item -or $null -eq (Get-DDProp $Item 'DownloadHttpBytes')) { return $null } $count = { param($name) $v = ConvertTo-DDBigInt (Get-DDProp $Item $name) if ($v.Sign -lt 0) { $v = [System.Numerics.BigInteger]::Zero } $v.ToString() } $start = Get-DDProp $Item 'MonthStartDate' $group = Get-DDProp $Item 'DownloadGroupBytes' [ordered]@{ # The device's own calendar date of the month's first day (the cmdlet returns local midnight). monthStart = $(if ($start -is [datetime]) { $start.ToString('yyyy-MM-dd', [System.Globalization.CultureInfo]::InvariantCulture) } else { $null }) httpBytes = (& $count 'DownloadHttpBytes') cacheBytes = (& $count 'DownloadCacheHostBytes') lanBytes = (& $count 'DownloadLanBytes') internetBytes = (& $count 'DownloadInternetBytes') groupBytes = $(if ($null -ne $group) { & $count 'DownloadGroupBytes' } else { $null }) uploadLanBytes = (& $count 'UploadLanBytes') uploadInternetBytes = (& $count 'UploadInternetBytes') } } # An HRESULT in a DO log message, in the three shapes the log uses: 0x80D02002 anywhere, "hr: 8007274c" (the peer-connect # failure line, bare hex) and "errorCode = 80D02002" / "errorCode = 0x80D02002". Only failure codes (0x8...) are taken. $script:DDHresultRx = '(?i)\b0x(?<h>8[0-9a-f]{7})\b|\b(?:hr|hresult|error ?code)\s*[:=]\s*(?:0x)?(?<h>8[0-9a-f]{7})\b' function ConvertTo-DDErrorRows { <# .SYNOPSIS Extract HRESULTs from Delivery Optimization log entries (DOLogEntry: TimeCreated, Function, Message), normalised to 0x and eight upper-case hex digits whichever shape the line used ($script:DDHresultRx). #> [CmdletBinding()] param( [Parameter(Mandatory)][AllowEmptyCollection()] $Entries, [Parameter(Mandatory)] $Context, [datetime] $Since = [datetime]::MinValue, [string[]] $KnownFileIds = @(), [int] $Max = 2000 ) $rows = New-Object 'System.Collections.Generic.List[object]' foreach ($e in @($Entries)) { if ($rows.Count -ge $Max) { break } $msg = [string](Get-DDProp $e 'Message') if (-not $msg) { continue } $m = [regex]::Match($msg, $script:DDHresultRx) if (-not $m.Success) { continue } $t = Get-DDProp $e 'TimeCreated', 'Time', 'Timestamp' if ($t -isnot [datetime]) { continue } if ($t.ToUniversalTime() -lt $Since) { continue } $fn = Get-DDProp $e 'Function', 'Component' $fileId = $null foreach ($f in $KnownFileIds) { if ($f -and $msg.Contains($f)) { $fileId = $f; break } } $scrub = Protect-DDLogText -Text $msg -Anonymise:$Context.Anonymise -Salt $Context.Salt -ComputerName $Context.ComputerName $raw = ('{0} {1}: {2}' -f (Format-DDIso $t), $fn, $scrub) $rows.Add([ordered]@{ at = (Format-DDIso $t) hresult = ('0x' + $m.Groups['h'].Value.ToUpperInvariant()) component = $(if ($fn) { [string]$fn } else { $null }) fileId = $fileId message = $scrub.Substring(0, [math]::Min(200, $scrub.Length)) raw = $raw.Substring(0, [math]::Min(2048, $raw.Length)) }) } , $rows.ToArray() } function ConvertTo-DDConnectionRows { [CmdletBinding()] param([Parameter(Mandatory)][AllowEmptyCollection()] $Items, [Parameter(Mandatory)] $Context) $rows = New-Object 'System.Collections.Generic.List[object]' $unrecognised = 0 foreach ($c in @($Items)) { $dest = [string](Get-DDProp $c 'DestinationIP', 'DestinationIp', 'Destination', 'IP', 'PeerIP') if (-not $dest) { $unrecognised++; continue } $status = Get-DDProp $c 'StatusCode', 'Status', 'Result', 'ErrorCode' $conn = Get-DDProp $c 'Connected', 'ConnectionEstablished', 'IsConnected' $rows.Add([ordered]@{ fileId = $(if (Get-DDProp $c 'FileId') { [string](Get-DDProp $c 'FileId') } else { 'unknown' }) destinationHash = (Get-DDSaltedHash -Value $dest -Salt $Context.TenantSalt -Domain 'peer') peerType = (ConvertTo-DDPeerType (Get-DDProp $c 'PeerType', 'Type')) statusCode = $(if ($null -ne $status) { [string]$status } else { '' }) connected = $(if ($null -ne $conn) { [bool]$conn } else { ([string]$status -match '^(200|0|Success)$') }) bytesSent = (ConvertTo-DDBigInt (Get-DDProp $c 'BytesSent')).ToString() bytesReceived = (ConvertTo-DDBigInt (Get-DDProp $c 'BytesReceived')).ToString() }) } if ($unrecognised -gt 0) { Add-DDNotCollected -Context $Context -Field 'connections' -Reason 'error' -Detail "$unrecognised connection row(s) had no destination the collector recognises (this Windows build may name it differently); left out" } , $rows.ToArray() } # Get-DOConfig property -> CSP setting name. Source (documented): "Monitor Delivery Optimization", section # "Get-DOConfig -Verbose" https://learn.microsoft.com/windows/deployment/do/waas-delivery-optimization-monitor # The two *Bps limits are reported in the cmdlet's unit, which is not stated (to-verify), so they are NOT folded # into the KB/s policies DOMaxBackgroundDownloadBandwidth / DOMaxForegroundDownloadBandwidth: they keep their own names. $script:DDDoConfigNames = @{ batterypcttoseed = 'DOMinBatteryPercentageAllowedToUpload' mintotaldisksize = 'DOMinDiskSizeAllowedToPeer' mintotalram = 'DOMinRAMAllowedToPeer' vpnpeercachingallowed = 'DOAllowVPNPeerCaching' sethourstolimitdownloadbackground = 'DOSetHoursToLimitBackgroundDownloadBandwidth' sethourstolimitdownloadforeground = 'DOSetHoursToLimitForegroundDownloadBandwidth' downloadmode = 'DODownloadMode' downbacklimitpct = 'DOPercentageMaxBackgroundBandwidth' downloadforegroundlimitpct = 'DOPercentageMaxForegroundBandwidth' maxuploadratepct = 'DOMaxUploadBandwidth' uploadlimitmonthlygb = 'DOMonthlyUploadDataCap' downbacklimitbps = 'DownBackLimitBps' downloadforegroundlimitbps = 'DownloadForegroundLimitBps' } $script:DDDoConfigNotSettings = 'workingdirectory', 'vpnkeywords' # Get-DOConfig's provider property names that do not follow "<setting>Provider" (DOConfig: MaxUploadRatePct is paired # with MaxUploadRateProvider). $script:DDDoConfigProviderAlias = @{ maxuploadrate = 'maxuploadratepct' } function ConvertTo-DDProviderKind { <# .SYNOPSIS Who set a value, from Get-DOConfig's provider. Returns mdm, gp, local, admin, default or unknown. .DESCRIPTION Verified on Windows 11 26H2: the provider is a property of the output object typed as the module's enum ConfigProvider { RegistryProvider = 5; MdmProvider = 7; SettingsProvider = 8 ("Settings (UX) provider"); AdminProvider = 9 ("Admin provider (powershell, WMI, etc.)"); DefaultProvider = 99 }, and the DO log's own downloadModeSrc field carries the same numbers. RegistryProvider is read as Group Policy: it is the provider for the registry-backed policy key (inferred from the name; the registry cross-check in Resolve-DDSettingSources confirms it per setting). The documented text form "Mdm Provider" / "Default Provider" is accepted too. #> [CmdletBinding()] param([AllowNull()] $Value) if ($null -eq $Value) { return 'unknown' } $n = 0 if ($Value -isnot [System.Enum] -and [int]::TryParse([string]$Value, [ref]$n)) { switch ($n) { 5 { return 'gp' } 7 { return 'mdm' } 8 { return 'local' } 9 { return 'admin' } 99 { return 'default' } default { return 'unknown' } } } $t = [string]$Value if ($t -match '(?i)^mdm') { return 'mdm' } if ($t -match '(?i)^registry|group|gpo') { return 'gp' } if ($t -match '(?i)^settings') { return 'local' } if ($t -match '(?i)^admin') { return 'admin' } if ($t -match '(?i)^default') { return 'default' } 'unknown' } function ConvertTo-DDProviderName { # The contract's `provider` field (Mdm, GroupPolicy, Default, Unknown). The richer answer travels in `source`. [CmdletBinding()] param([AllowNull()] $Text) $k = $(if ([string]$Text -cin 'mdm', 'gp', 'local', 'admin', 'default', 'unknown') { [string]$Text } else { ConvertTo-DDProviderKind -Value $Text }) switch ($k) { 'mdm' { 'Mdm' } 'gp' { 'GroupPolicy' } 'default' { 'Default' } default { 'Unknown' } } } function ConvertFrom-DDDoConfig { <# .SYNOPSIS Build doConfig.effective from `Get-DOConfig -Verbose`. .DESCRIPTION Values come from the output object, whose property names are documented but are NOT the CSP names (MinTotalRAM is DOMinRAMAllowedToPeer): they are mapped through $script:DDDoConfigNames. Providers: on current Windows (verified on 26H2) each setting's provider is a property of the same object, "<Name>Provider", typed as the enum ConfigProvider (MdmProvider, DefaultProvider, ...). The first build of this collector read providers only from the verbose text and treated every "<Name>Provider" property as a setting of its own, which is why a managed device reported 28 settings, all "Unknown source". The documented verbose text ("DownloadMode:Simple DownloadModeProvider:Mdm Provider") is still accepted. A setting whose provider is not stated anywhere is reported as Unknown, never guessed. Each setting also carries `_kind` (mdm, gp, local, admin, default, unknown) for Resolve-DDSettingSources. WorkingDirectory and VpnKeywords are not policies: they are returned separately. #> [CmdletBinding()] param($Config, [AllowEmptyCollection()][string[]] $VerboseLines = @()) $providers = @{} $verboseValues = @{} if ($null -ne $Config) { foreach ($prop in $Config.PSObject.Properties) { if ($prop.MemberType -notin 'NoteProperty', 'Property' -or $prop.Name -notmatch '(?i)^(?<n>.+)Provider$') { continue } $base = $Matches['n'].ToLowerInvariant() if ($script:DDDoConfigProviderAlias.ContainsKey($base)) { $base = $script:DDDoConfigProviderAlias[$base] } $providers[$base] = ConvertTo-DDProviderKind -Value $prop.Value } } foreach ($line in $VerboseLines) { foreach ($m in [regex]::Matches($line, '(?<n>[A-Za-z]+?)Provider\s*:\s*(?<p>[A-Za-z]+)')) { $base = $m.Groups['n'].Value.ToLowerInvariant() if ($script:DDDoConfigProviderAlias.ContainsKey($base)) { $base = $script:DDDoConfigProviderAlias[$base] } if (-not $providers.ContainsKey($base) -or $providers[$base] -eq 'unknown') { $providers[$base] = ConvertTo-DDProviderKind -Value $m.Groups['p'].Value } } foreach ($m in [regex]::Matches($line, '(?<n>[A-Za-z]+):(?<v>[^\s]+)')) { $n = $m.Groups['n'].Value if ($n -notmatch 'Provider$') { $verboseValues[$n.ToLowerInvariant()] = @{ Name = $n; Value = $m.Groups['v'].Value } } } } $eff = [ordered]@{} $aux = @{ wd = $null; vk = $null } $add = { param($rawName, $v) $key = $rawName.ToLowerInvariant() if ($key -in $script:DDDoConfigNotSettings) { if ($key -eq 'workingdirectory') { $aux.wd = $v } else { $aux.vk = $v } return } $name = $null if ($script:DDDoConfigNames.ContainsKey($key)) { $name = $script:DDDoConfigNames[$key] } else { $name = 'DO' + ($rawName -creplace '^DO', '') } $val = $null if ($null -ne $v) { $n = 0.0 if ($v -is [string] -and [double]::TryParse($v, [System.Globalization.NumberStyles]::Float, [System.Globalization.CultureInfo]::InvariantCulture, [ref]$n)) { $val = $n } elseif ($v -is [string]) { $val = $v } elseif ($v -is [bool]) { $val = $(if ($v) { 1 } else { 0 }) } elseif ($v -is [System.Enum]) { $val = [int]$v } elseif ($v -is [ValueType]) { $val = $v } else { $val = [string]$v } } if ($name -eq 'DODownloadMode' -and $val -is [string]) { $dm = ConvertTo-DDDownloadMode $val; if ($dm -ge 0) { $val = $dm } } if ($val -is [double] -and $val -eq [math]::Truncate($val) -and [math]::Abs($val) -lt 1e15) { $val = [long]$val } $kind = 'unknown' if ($providers.ContainsKey($key)) { $kind = $providers[$key] } $kinds[$name] = $kind $eff[$name] = [ordered]@{ value = $val; provider = (ConvertTo-DDProviderName -Text $kind) } } $kinds = @{} if ($null -ne $Config) { foreach ($prop in $Config.PSObject.Properties) { if ($prop.MemberType -notin 'NoteProperty', 'Property' -or $prop.Name -match '(?i)Provider$') { continue } & $add $prop.Name $prop.Value } } else { foreach ($k in $verboseValues.Keys) { if ($providers.ContainsKey($k)) { & $add $verboseValues[$k].Name $verboseValues[$k].Value } } } [pscustomobject]@{ Effective = $eff; Kinds = $kinds; WorkingDirectory = $aux.wd; VpnKeywords = $aux.vk } } |