[{"data":1,"prerenderedAt":2571},["ShallowReactive",2],{"post:\u002F2026\u002F09\u002F02\u002Fwindows-management-building-a-discovery-inventory-nobody-has-to-maintain-by-hand\u002F":3},{"post":4,"newer":2520,"older":2529,"related":2539,"series":2569},{"id":5,"title":6,"body":7,"canonical":2500,"categories":2501,"date":2504,"description":2505,"extension":2506,"featured":2507,"hero":2508,"image":2508,"meta":2509,"navigation":356,"path":2510,"readingTime":183,"seo":2511,"series":2508,"seriesOrder":2508,"sites":2512,"source":2508,"stem":2513,"tags":2514,"updated":2508,"url":2518,"__hash__":2519},"blog\u002Fblog\u002F2026\u002F09\u002F02\u002Fwindows-management-building-a-discovery-inventory-nobody-has-to-maintain-by-hand.md","Windows Management: A Discovery Inventory Nobody Maintains by Hand",{"type":8,"value":9,"toc":2491},"minimark",[10,14,17,22,25,29,32,104,122,138,141,1712,1715,1723,1730,1734,1737,1740,2171,2177,2187,2191,2194,2274,2288,2292,2295,2421,2425,2432,2436,2487],[11,12,13],"p",{},"Every Windows shop I have worked in has had at least one asset spreadsheet that was accurate for about a month after someone built it and has been slowly lying ever since. The problem was never a lack of effort, it was that the inventory depended on a human remembering to update it every time a machine was imaged, retired, or moved to a different subnet. The inventories that have actually stayed useful for me are the ones nobody has to remember to touch.",[11,15,16],{},"This post is the design and the scripts I use to get there: a scheduled sweep that asks Active Directory what should exist, asks each machine over CIM what it actually is, merges the answers into a file that remembers when each machine was last seen, and reports on the ones that have gone quiet.",[18,19,21],"h2",{"id":20},"the-spreadsheet-fails-the-moment-it-requires-a-human-step","The spreadsheet fails the moment it requires a human step",[11,23,24],{},"A manually maintained inventory has exactly one job and one failure mode: someone forgets to add a row, or forgets to remove one, and from that moment the document is actively wrong rather than merely incomplete. Wrong is worse than incomplete, because an incomplete inventory gets treated with appropriate suspicion while a wrong one gets trusted right up until it causes a bad decision, like decommissioning a server the spreadsheet says is unused. Any inventory design that depends on someone remembering a manual step will eventually fail exactly that way, no matter how well-intentioned the team maintaining it is.",[18,26,28],{"id":27},"query-the-machines-instead-of-asking-people-to-report-them","Query the machines instead of asking people to report them",[11,30,31],{},"The fix is to make discovery something the machines answer, not something a person types. There are two sources, and they answer different questions:",[33,34,35,72],"ul",{},[36,37,38,42,43,47,48,52,53,56,57,60,61,60,64,67,68,71],"li",{},[39,40,41],"strong",{},"Active Directory"," says what ",[44,45,46],"em",{},"should"," exist. ",[49,50,51],"code",{},"Get-ADComputer"," returns every enabled computer account, and with ",[49,54,55],{},"-Properties"," it adds ",[49,58,59],{},"OperatingSystem",", ",[49,62,63],{},"OperatingSystemVersion",[49,65,66],{},"LastLogonDate"," and ",[49,69,70],{},"IPv4Address"," (resolved from DNS) without touching the machine.",[36,73,74,77,78,81,82,85,86,60,89,67,92,95,96,99,100,103],{},[39,75,76],{},"CIM"," says what the machine actually ",[44,79,80],{},"is",". ",[49,83,84],{},"Win32_OperatingSystem"," has ",[49,87,88],{},"Caption",[49,90,91],{},"BuildNumber",[49,93,94],{},"LastBootUpTime","; ",[49,97,98],{},"Win32_ComputerSystem"," has manufacturer, model and memory; ",[49,101,102],{},"Win32_BIOS"," has the serial number your hardware vendor and your warranty lookups want.",[11,105,106,109,110,113,114,117,118,121],{},[49,107,108],{},"New-CimSession"," uses WSMan by default when you give it a computer name, so the same WinRM configuration you already need for PowerShell remoting covers it. For the handful of old servers where WinRM isn't available, ",[49,111,112],{},"New-CimSessionOption -Protocol Dcom"," gets you a DCOM session instead. Pass an array of sessions to ",[49,115,116],{},"Get-CimInstance"," and it queries them all at once, tagging each result with ",[49,119,120],{},"PSComputerName",".",[11,123,124,125,81,128,130,131,134,135,121],{},"One thing AD can't tell you is whether a machine is alive ",[44,126,127],{},"right now",[49,129,66],{}," is derived from ",[49,132,133],{},"lastLogonTimestamp",", and Microsoft documents that by default that attribute is only updated when its current value is 9 to 14 days older than the logon, to keep replication load down. It's fine for \"hasn't been seen in 90 days\" and useless for \"is it on this week\". That's why the sweep keeps its own ",[49,136,137],{},"LastSeen",[11,139,140],{},"Here is the sweep. It needs the Active Directory module (RSAT), WinRM reachable on the targets, and an account with admin rights on them (the default WinRM endpoint only admits Administrators and, if you configure it, Remote Management Users):",[142,143,148],"pre",{"className":144,"code":145,"language":146,"meta":147,"style":147},"language-powershell shiki shiki-themes github-dark","\u003C#\n.SYNOPSIS\n    Discovers Windows computers from Active Directory, collects OS and hardware facts\n    over CIM, and merges them into an inventory CSV that tracks when each was last seen.\n.DESCRIPTION\n    Reads enabled computer accounts from AD, opens CIM sessions (WSMan) in bulk, queries\n    Win32_OperatingSystem, Win32_ComputerSystem and Win32_BIOS, and merges the results\n    into the existing inventory. Reachable machines get fresh facts and a new LastSeen;\n    unreachable ones keep their last known facts and their old LastSeen, so they age\n    into the staleness report instead of disappearing.\n.PARAMETER SearchBase\n    Distinguished name of the OU to discover. Defaults to the whole domain.\n.PARAMETER InventoryPath\n    Inventory CSV to read and rewrite.\n.PARAMETER TimeoutSec\n    CIM operation timeout per computer, in seconds.\n.EXAMPLE\n    .\\Invoke-DiscoverySweep.ps1 -SearchBase \"OU=Workstations,DC=corp,DC=example,DC=com\" -InventoryPath D:\\Inventory\\inventory.csv\n.NOTES\n    Author  : Thomas Lasswell (https:\u002F\u002Fwww.techcolumnist.com)\n    Version : 1.0 (2026-09-02)\n    Requires: ActiveDirectory module (RSAT); WinRM on targets; admin rights on targets\n#>\n[CmdletBinding()]\nparam (\n    [string]$SearchBase,\n\n    [string]$InventoryPath = \".\\inventory.csv\",\n\n    [int]$TimeoutSec = 30\n)\n\nImport-Module -Name ActiveDirectory -ErrorAction Stop\n\n$now = Get-Date\n$adParams = @{\n    Filter     = 'Enabled -eq $true'\n    Properties = \"OperatingSystem\", \"OperatingSystemVersion\", \"LastLogonDate\", \"IPv4Address\"\n}\nif ($SearchBase) {\n    $adParams.SearchBase = $SearchBase\n}\n\n$adComputers = @(Get-ADComputer @adParams)\nWrite-Host \"AD returned $($adComputers.Count) enabled computer account(s).\"\n\n# Load the previous inventory so unreachable machines keep their last known facts.\n$inventory = @{}\nif (Test-Path -Path $InventoryPath) {\n    foreach ($row in Import-Csv -Path $InventoryPath) {\n        $inventory[$row.Name] = $row\n    }\n}\n\n# Open CIM sessions in bulk; failures land in $sessionErrors instead of stopping the run.\n$sessionOption = New-CimSessionOption -Protocol Wsman\n$targets = @($adComputers | Where-Object { $_.DNSHostName } | Select-Object -ExpandProperty DNSHostName)\n$sessions = @(New-CimSession -ComputerName $targets -SessionOption $sessionOption -OperationTimeoutSec $TimeoutSec -ErrorAction SilentlyContinue -ErrorVariable sessionErrors)\nWrite-Host \"Opened $($sessions.Count) CIM session(s); $(@($sessionErrors).Count) computer(s) unreachable.\"\n\n$facts = @{}\nif ($sessions.Count -gt 0) {\n    $cimParams = @{ CimSession = $sessions; OperationTimeoutSec = $TimeoutSec; ErrorAction = \"SilentlyContinue\" }\n\n    foreach ($os in Get-CimInstance @cimParams -ClassName Win32_OperatingSystem -Property Caption, BuildNumber, LastBootUpTime) {\n        $facts[$os.PSComputerName] = @{ OS = $os }\n    }\n    foreach ($cs in Get-CimInstance @cimParams -ClassName Win32_ComputerSystem -Property Manufacturer, Model, TotalPhysicalMemory) {\n        if ($facts.ContainsKey($cs.PSComputerName)) { $facts[$cs.PSComputerName].CS = $cs }\n    }\n    foreach ($bios in Get-CimInstance @cimParams -ClassName Win32_BIOS -Property SerialNumber) {\n        if ($facts.ContainsKey($bios.PSComputerName)) { $facts[$bios.PSComputerName].BIOS = $bios }\n    }\n\n    $sessions | Remove-CimSession\n}\n\nforeach ($computer in $adComputers) {\n    $previous = $inventory[$computer.Name]\n    $live = if ($computer.DNSHostName) { $facts[$computer.DNSHostName] } else { $null }\n\n    if ($live) {\n        $inventory[$computer.Name] = [pscustomobject]@{\n            Name           = $computer.Name\n            DNSHostName    = $computer.DNSHostName\n            IPv4Address    = $computer.IPv4Address\n            OSCaption      = $live.OS.Caption\n            OSBuild        = $live.OS.BuildNumber\n            LastBoot       = $live.OS.LastBootUpTime.ToString(\"s\")\n            Manufacturer   = $live.CS.Manufacturer\n            Model          = $live.CS.Model\n            MemoryGB       = [math]::Round($live.CS.TotalPhysicalMemory \u002F 1GB, 1)\n            SerialNumber   = $live.BIOS.SerialNumber\n            ADLastLogon    = if ($computer.LastLogonDate) { $computer.LastLogonDate.ToString(\"s\") } else { \"\" }\n            LastSeen       = $now.ToString(\"s\")\n            InAD           = $true\n        }\n    } elseif ($previous) {\n        # Unreachable this run: keep the old facts and the old LastSeen, refresh the AD fields.\n        $previous.IPv4Address = $computer.IPv4Address\n        $previous.ADLastLogon = if ($computer.LastLogonDate) { $computer.LastLogonDate.ToString(\"s\") } else { \"\" }\n        $previous.InAD = $true\n    } else {\n        # Known to AD but never reached: record it so it shows up as never seen.\n        $inventory[$computer.Name] = [pscustomobject]@{\n            Name           = $computer.Name\n            DNSHostName    = $computer.DNSHostName\n            IPv4Address    = $computer.IPv4Address\n            OSCaption      = $computer.OperatingSystem\n            OSBuild        = $computer.OperatingSystemVersion\n            LastBoot       = \"\"\n            Manufacturer   = \"\"\n            Model          = \"\"\n            MemoryGB       = \"\"\n            SerialNumber   = \"\"\n            ADLastLogon    = if ($computer.LastLogonDate) { $computer.LastLogonDate.ToString(\"s\") } else { \"\" }\n            LastSeen       = \"\"\n            InAD           = $true\n        }\n    }\n}\n\n# Rows whose computer account is gone or disabled stay in the file, flagged.\n$adNames = @($adComputers.Name)\nforeach ($name in @($inventory.Keys)) {\n    if ($adNames -notcontains $name) {\n        $inventory[$name].InAD = $false\n    }\n}\n\n$inventory.Values | Sort-Object -Property Name | Export-Csv -Path $InventoryPath -NoTypeInformation\nWrite-Host \"Inventory written to $InventoryPath ($($inventory.Count) rows, $($facts.Count) refreshed this run).\"\n","powershell","",[49,149,150,159,169,175,181,189,195,201,207,213,219,230,236,246,252,262,268,276,282,290,296,302,308,314,327,336,351,358,377,382,398,404,409,427,432,443,457,468,495,501,510,521,526,531,549,572,577,583,596,612,631,642,648,653,658,664,680,718,757,795,800,812,829,860,865,901,919,924,958,972,977,1001,1014,1019,1024,1035,1040,1045,1059,1070,1094,1099,1108,1129,1140,1151,1162,1173,1184,1200,1211,1222,1251,1262,1289,1304,1315,1321,1333,1339,1349,1373,1383,1393,1399,1416,1425,1434,1443,1453,1463,1473,1482,1491,1500,1509,1532,1541,1550,1555,1560,1565,1570,1576,1589,1604,1618,1629,1634,1639,1644,1675],{"__ignoreMap":147},[151,152,155],"span",{"class":153,"line":154},"line",1,[151,156,158],{"class":157},"sAwPA","\u003C#\n",[151,160,162,165],{"class":153,"line":161},2,[151,163,121],{"class":164},"sDLfK",[151,166,168],{"class":167},"snl16","SYNOPSIS\n",[151,170,172],{"class":153,"line":171},3,[151,173,174],{"class":157},"    Discovers Windows computers from Active Directory, collects OS and hardware facts\n",[151,176,178],{"class":153,"line":177},4,[151,179,180],{"class":157},"    over CIM, and merges them into an inventory CSV that tracks when each was last seen.\n",[151,182,184,186],{"class":153,"line":183},5,[151,185,121],{"class":164},[151,187,188],{"class":167},"DESCRIPTION\n",[151,190,192],{"class":153,"line":191},6,[151,193,194],{"class":157},"    Reads enabled computer accounts from AD, opens CIM sessions (WSMan) in bulk, queries\n",[151,196,198],{"class":153,"line":197},7,[151,199,200],{"class":157},"    Win32_OperatingSystem, Win32_ComputerSystem and Win32_BIOS, and merges the results\n",[151,202,204],{"class":153,"line":203},8,[151,205,206],{"class":157},"    into the existing inventory. Reachable machines get fresh facts and a new LastSeen;\n",[151,208,210],{"class":153,"line":209},9,[151,211,212],{"class":157},"    unreachable ones keep their last known facts and their old LastSeen, so they age\n",[151,214,216],{"class":153,"line":215},10,[151,217,218],{"class":157},"    into the staleness report instead of disappearing.\n",[151,220,222,224,227],{"class":153,"line":221},11,[151,223,121],{"class":164},[151,225,226],{"class":167},"PARAMETER",[151,228,229],{"class":167}," SearchBase\n",[151,231,233],{"class":153,"line":232},12,[151,234,235],{"class":157},"    Distinguished name of the OU to discover. Defaults to the whole domain.\n",[151,237,239,241,243],{"class":153,"line":238},13,[151,240,121],{"class":164},[151,242,226],{"class":167},[151,244,245],{"class":167}," InventoryPath\n",[151,247,249],{"class":153,"line":248},14,[151,250,251],{"class":157},"    Inventory CSV to read and rewrite.\n",[151,253,255,257,259],{"class":153,"line":254},15,[151,256,121],{"class":164},[151,258,226],{"class":167},[151,260,261],{"class":167}," TimeoutSec\n",[151,263,265],{"class":153,"line":264},16,[151,266,267],{"class":157},"    CIM operation timeout per computer, in seconds.\n",[151,269,271,273],{"class":153,"line":270},17,[151,272,121],{"class":164},[151,274,275],{"class":167},"EXAMPLE\n",[151,277,279],{"class":153,"line":278},18,[151,280,281],{"class":157},"    .\\Invoke-DiscoverySweep.ps1 -SearchBase \"OU=Workstations,DC=corp,DC=example,DC=com\" -InventoryPath D:\\Inventory\\inventory.csv\n",[151,283,285,287],{"class":153,"line":284},19,[151,286,121],{"class":164},[151,288,289],{"class":167},"NOTES\n",[151,291,293],{"class":153,"line":292},20,[151,294,295],{"class":157},"    Author  : Thomas Lasswell (https:\u002F\u002Fwww.techcolumnist.com)\n",[151,297,299],{"class":153,"line":298},21,[151,300,301],{"class":157},"    Version : 1.0 (2026-09-02)\n",[151,303,305],{"class":153,"line":304},22,[151,306,307],{"class":157},"    Requires: ActiveDirectory module (RSAT); WinRM on targets; admin rights on targets\n",[151,309,311],{"class":153,"line":310},23,[151,312,313],{"class":157},"#>\n",[151,315,317,321,324],{"class":153,"line":316},24,[151,318,320],{"class":319},"s95oV","[",[151,322,323],{"class":164},"CmdletBinding",[151,325,326],{"class":319},"()]\n",[151,328,330,333],{"class":153,"line":329},25,[151,331,332],{"class":167},"param",[151,334,335],{"class":319}," (\n",[151,337,339,342,345,348],{"class":153,"line":338},26,[151,340,341],{"class":319},"    [",[151,343,344],{"class":167},"string",[151,346,347],{"class":319},"]$SearchBase",[151,349,350],{"class":167},",\n",[151,352,354],{"class":153,"line":353},27,[151,355,357],{"emptyLinePlaceholder":356},true,"\n",[151,359,361,363,365,368,371,375],{"class":153,"line":360},28,[151,362,341],{"class":319},[151,364,344],{"class":167},[151,366,367],{"class":319},"]$InventoryPath ",[151,369,370],{"class":167},"=",[151,372,374],{"class":373},"sU2Wk"," \".\\inventory.csv\"",[151,376,350],{"class":167},[151,378,380],{"class":153,"line":379},29,[151,381,357],{"emptyLinePlaceholder":356},[151,383,385,387,390,393,395],{"class":153,"line":384},30,[151,386,341],{"class":319},[151,388,389],{"class":167},"int",[151,391,392],{"class":319},"]$TimeoutSec ",[151,394,370],{"class":167},[151,396,397],{"class":164}," 30\n",[151,399,401],{"class":153,"line":400},31,[151,402,403],{"class":319},")\n",[151,405,407],{"class":153,"line":406},32,[151,408,357],{"emptyLinePlaceholder":356},[151,410,412,415,418,421,424],{"class":153,"line":411},33,[151,413,414],{"class":164},"Import-Module",[151,416,417],{"class":167}," -",[151,419,420],{"class":319},"Name ActiveDirectory ",[151,422,423],{"class":167},"-",[151,425,426],{"class":319},"ErrorAction Stop\n",[151,428,430],{"class":153,"line":429},34,[151,431,357],{"emptyLinePlaceholder":356},[151,433,435,438,440],{"class":153,"line":434},35,[151,436,437],{"class":319},"$now ",[151,439,370],{"class":167},[151,441,442],{"class":164}," Get-Date\n",[151,444,446,449,451,454],{"class":153,"line":445},36,[151,447,448],{"class":319},"$adParams ",[151,450,370],{"class":167},[151,452,453],{"class":167}," @",[151,455,456],{"class":319},"{\n",[151,458,460,463,465],{"class":153,"line":459},37,[151,461,462],{"class":319},"    Filter     ",[151,464,370],{"class":167},[151,466,467],{"class":373}," 'Enabled -eq $true'\n",[151,469,471,474,476,479,482,485,487,490,492],{"class":153,"line":470},38,[151,472,473],{"class":319},"    Properties ",[151,475,370],{"class":167},[151,477,478],{"class":373}," \"OperatingSystem\"",[151,480,481],{"class":167},",",[151,483,484],{"class":373}," \"OperatingSystemVersion\"",[151,486,481],{"class":167},[151,488,489],{"class":373}," \"LastLogonDate\"",[151,491,481],{"class":167},[151,493,494],{"class":373}," \"IPv4Address\"\n",[151,496,498],{"class":153,"line":497},39,[151,499,500],{"class":319},"}\n",[151,502,504,507],{"class":153,"line":503},40,[151,505,506],{"class":167},"if",[151,508,509],{"class":319}," ($SearchBase) {\n",[151,511,513,516,518],{"class":153,"line":512},41,[151,514,515],{"class":319},"    $adParams.SearchBase ",[151,517,370],{"class":167},[151,519,520],{"class":319}," $SearchBase\n",[151,522,524],{"class":153,"line":523},42,[151,525,500],{"class":319},[151,527,529],{"class":153,"line":528},43,[151,530,357],{"emptyLinePlaceholder":356},[151,532,534,537,539,541,544,546],{"class":153,"line":533},44,[151,535,536],{"class":319},"$adComputers ",[151,538,370],{"class":167},[151,540,453],{"class":167},[151,542,543],{"class":319},"(",[151,545,51],{"class":164},[151,547,548],{"class":319}," @adParams)\n",[151,550,552,555,558,561,563,566,569],{"class":153,"line":551},45,[151,553,554],{"class":164},"Write-Host",[151,556,557],{"class":373}," \"AD returned ",[151,559,560],{"class":167},"$",[151,562,543],{"class":373},[151,564,565],{"class":319},"$adComputers.Count",[151,567,568],{"class":373},")",[151,570,571],{"class":373}," enabled computer account(s).\"\n",[151,573,575],{"class":153,"line":574},46,[151,576,357],{"emptyLinePlaceholder":356},[151,578,580],{"class":153,"line":579},47,[151,581,582],{"class":157},"# Load the previous inventory so unreachable machines keep their last known facts.\n",[151,584,586,589,591,593],{"class":153,"line":585},48,[151,587,588],{"class":319},"$inventory ",[151,590,370],{"class":167},[151,592,453],{"class":167},[151,594,595],{"class":319},"{}\n",[151,597,599,601,604,607,609],{"class":153,"line":598},49,[151,600,506],{"class":167},[151,602,603],{"class":319}," (",[151,605,606],{"class":164},"Test-Path",[151,608,417],{"class":167},[151,610,611],{"class":319},"Path $InventoryPath) {\n",[151,613,615,618,621,624,627,629],{"class":153,"line":614},50,[151,616,617],{"class":167},"    foreach",[151,619,620],{"class":319}," ($row ",[151,622,623],{"class":167},"in",[151,625,626],{"class":164}," Import-Csv",[151,628,417],{"class":167},[151,630,611],{"class":319},[151,632,634,637,639],{"class":153,"line":633},51,[151,635,636],{"class":319},"        $inventory[$row.Name] ",[151,638,370],{"class":167},[151,640,641],{"class":319}," $row\n",[151,643,645],{"class":153,"line":644},52,[151,646,647],{"class":319},"    }\n",[151,649,651],{"class":153,"line":650},53,[151,652,500],{"class":319},[151,654,656],{"class":153,"line":655},54,[151,657,357],{"emptyLinePlaceholder":356},[151,659,661],{"class":153,"line":660},55,[151,662,663],{"class":157},"# Open CIM sessions in bulk; failures land in $sessionErrors instead of stopping the run.\n",[151,665,667,670,672,675,677],{"class":153,"line":666},56,[151,668,669],{"class":319},"$sessionOption ",[151,671,370],{"class":167},[151,673,674],{"class":164}," New-CimSessionOption",[151,676,417],{"class":167},[151,678,679],{"class":319},"Protocol Wsman\n",[151,681,683,686,688,690,693,696,699,702,705,708,710,713,715],{"class":153,"line":682},57,[151,684,685],{"class":319},"$targets ",[151,687,370],{"class":167},[151,689,453],{"class":167},[151,691,692],{"class":319},"($adComputers ",[151,694,695],{"class":167},"|",[151,697,698],{"class":164}," Where-Object",[151,700,701],{"class":319}," { ",[151,703,704],{"class":164},"$_",[151,706,707],{"class":319},".DNSHostName } ",[151,709,695],{"class":167},[151,711,712],{"class":164}," Select-Object",[151,714,417],{"class":167},[151,716,717],{"class":319},"ExpandProperty DNSHostName)\n",[151,719,721,724,726,728,730,732,734,737,739,742,744,747,749,752,754],{"class":153,"line":720},58,[151,722,723],{"class":319},"$sessions ",[151,725,370],{"class":167},[151,727,453],{"class":167},[151,729,543],{"class":319},[151,731,108],{"class":164},[151,733,417],{"class":167},[151,735,736],{"class":319},"ComputerName $targets ",[151,738,423],{"class":167},[151,740,741],{"class":319},"SessionOption $sessionOption ",[151,743,423],{"class":167},[151,745,746],{"class":319},"OperationTimeoutSec $TimeoutSec ",[151,748,423],{"class":167},[151,750,751],{"class":319},"ErrorAction SilentlyContinue ",[151,753,423],{"class":167},[151,755,756],{"class":319},"ErrorVariable sessionErrors)\n",[151,758,760,762,765,767,769,772,774,777,779,781,784,786,789,792],{"class":153,"line":759},59,[151,761,554],{"class":164},[151,763,764],{"class":373}," \"Opened ",[151,766,560],{"class":167},[151,768,543],{"class":373},[151,770,771],{"class":319},"$sessions.Count",[151,773,568],{"class":373},[151,775,776],{"class":373}," CIM session(s); ",[151,778,560],{"class":167},[151,780,543],{"class":373},[151,782,783],{"class":167},"@",[151,785,543],{"class":373},[151,787,788],{"class":319},"$sessionErrors",[151,790,791],{"class":373},").Count)",[151,793,794],{"class":373}," computer(s) unreachable.\"\n",[151,796,798],{"class":153,"line":797},60,[151,799,357],{"emptyLinePlaceholder":356},[151,801,803,806,808,810],{"class":153,"line":802},61,[151,804,805],{"class":319},"$facts ",[151,807,370],{"class":167},[151,809,453],{"class":167},[151,811,595],{"class":319},[151,813,815,817,820,823,826],{"class":153,"line":814},62,[151,816,506],{"class":167},[151,818,819],{"class":319}," ($sessions.Count ",[151,821,822],{"class":167},"-gt",[151,824,825],{"class":164}," 0",[151,827,828],{"class":319},") {\n",[151,830,832,835,837,839,842,844,847,849,852,854,857],{"class":153,"line":831},63,[151,833,834],{"class":319},"    $cimParams ",[151,836,370],{"class":167},[151,838,453],{"class":167},[151,840,841],{"class":319},"{ CimSession ",[151,843,370],{"class":167},[151,845,846],{"class":319}," $sessions; OperationTimeoutSec ",[151,848,370],{"class":167},[151,850,851],{"class":319}," $TimeoutSec; ErrorAction ",[151,853,370],{"class":167},[151,855,856],{"class":373}," \"SilentlyContinue\"",[151,858,859],{"class":319}," }\n",[151,861,863],{"class":153,"line":862},64,[151,864,357],{"emptyLinePlaceholder":356},[151,866,868,870,873,875,878,881,883,886,888,891,893,896,898],{"class":153,"line":867},65,[151,869,617],{"class":167},[151,871,872],{"class":319}," ($os ",[151,874,623],{"class":167},[151,876,877],{"class":164}," Get-CimInstance",[151,879,880],{"class":319}," @cimParams ",[151,882,423],{"class":167},[151,884,885],{"class":319},"ClassName Win32_OperatingSystem ",[151,887,423],{"class":167},[151,889,890],{"class":319},"Property Caption",[151,892,481],{"class":167},[151,894,895],{"class":319}," BuildNumber",[151,897,481],{"class":167},[151,899,900],{"class":319}," LastBootUpTime) {\n",[151,902,904,907,909,911,914,916],{"class":153,"line":903},66,[151,905,906],{"class":319},"        $facts[$os.PSComputerName] ",[151,908,370],{"class":167},[151,910,453],{"class":167},[151,912,913],{"class":319},"{ OS ",[151,915,370],{"class":167},[151,917,918],{"class":319}," $os }\n",[151,920,922],{"class":153,"line":921},67,[151,923,647],{"class":319},[151,925,927,929,932,934,936,938,940,943,945,948,950,953,955],{"class":153,"line":926},68,[151,928,617],{"class":167},[151,930,931],{"class":319}," ($cs ",[151,933,623],{"class":167},[151,935,877],{"class":164},[151,937,880],{"class":319},[151,939,423],{"class":167},[151,941,942],{"class":319},"ClassName Win32_ComputerSystem ",[151,944,423],{"class":167},[151,946,947],{"class":319},"Property Manufacturer",[151,949,481],{"class":167},[151,951,952],{"class":319}," Model",[151,954,481],{"class":167},[151,956,957],{"class":319}," TotalPhysicalMemory) {\n",[151,959,961,964,967,969],{"class":153,"line":960},69,[151,962,963],{"class":167},"        if",[151,965,966],{"class":319}," ($facts.ContainsKey($cs.PSComputerName)) { $facts[$cs.PSComputerName].CS ",[151,968,370],{"class":167},[151,970,971],{"class":319}," $cs }\n",[151,973,975],{"class":153,"line":974},70,[151,976,647],{"class":319},[151,978,980,982,985,987,989,991,993,996,998],{"class":153,"line":979},71,[151,981,617],{"class":167},[151,983,984],{"class":319}," ($bios ",[151,986,623],{"class":167},[151,988,877],{"class":164},[151,990,880],{"class":319},[151,992,423],{"class":167},[151,994,995],{"class":319},"ClassName Win32_BIOS ",[151,997,423],{"class":167},[151,999,1000],{"class":319},"Property SerialNumber) {\n",[151,1002,1004,1006,1009,1011],{"class":153,"line":1003},72,[151,1005,963],{"class":167},[151,1007,1008],{"class":319}," ($facts.ContainsKey($bios.PSComputerName)) { $facts[$bios.PSComputerName].BIOS ",[151,1010,370],{"class":167},[151,1012,1013],{"class":319}," $bios }\n",[151,1015,1017],{"class":153,"line":1016},73,[151,1018,647],{"class":319},[151,1020,1022],{"class":153,"line":1021},74,[151,1023,357],{"emptyLinePlaceholder":356},[151,1025,1027,1030,1032],{"class":153,"line":1026},75,[151,1028,1029],{"class":319},"    $sessions ",[151,1031,695],{"class":167},[151,1033,1034],{"class":164}," Remove-CimSession\n",[151,1036,1038],{"class":153,"line":1037},76,[151,1039,500],{"class":319},[151,1041,1043],{"class":153,"line":1042},77,[151,1044,357],{"emptyLinePlaceholder":356},[151,1046,1048,1051,1054,1056],{"class":153,"line":1047},78,[151,1049,1050],{"class":167},"foreach",[151,1052,1053],{"class":319}," ($computer ",[151,1055,623],{"class":167},[151,1057,1058],{"class":319}," $adComputers) {\n",[151,1060,1062,1065,1067],{"class":153,"line":1061},79,[151,1063,1064],{"class":319},"    $previous ",[151,1066,370],{"class":167},[151,1068,1069],{"class":319}," $inventory[$computer.Name]\n",[151,1071,1073,1076,1078,1081,1084,1087,1089,1092],{"class":153,"line":1072},80,[151,1074,1075],{"class":319},"    $live ",[151,1077,370],{"class":167},[151,1079,1080],{"class":167}," if",[151,1082,1083],{"class":319}," ($computer.DNSHostName) { $facts[$computer.DNSHostName] } ",[151,1085,1086],{"class":167},"else",[151,1088,701],{"class":319},[151,1090,1091],{"class":164},"$null",[151,1093,859],{"class":319},[151,1095,1097],{"class":153,"line":1096},81,[151,1098,357],{"emptyLinePlaceholder":356},[151,1100,1102,1105],{"class":153,"line":1101},82,[151,1103,1104],{"class":167},"    if",[151,1106,1107],{"class":319}," ($live) {\n",[151,1109,1111,1114,1116,1119,1122,1125,1127],{"class":153,"line":1110},83,[151,1112,1113],{"class":319},"        $inventory[$computer.Name] ",[151,1115,370],{"class":167},[151,1117,1118],{"class":319}," [",[151,1120,1121],{"class":167},"pscustomobject",[151,1123,1124],{"class":319},"]",[151,1126,783],{"class":167},[151,1128,456],{"class":319},[151,1130,1132,1135,1137],{"class":153,"line":1131},84,[151,1133,1134],{"class":319},"            Name           ",[151,1136,370],{"class":167},[151,1138,1139],{"class":319}," $computer.Name\n",[151,1141,1143,1146,1148],{"class":153,"line":1142},85,[151,1144,1145],{"class":319},"            DNSHostName    ",[151,1147,370],{"class":167},[151,1149,1150],{"class":319}," $computer.DNSHostName\n",[151,1152,1154,1157,1159],{"class":153,"line":1153},86,[151,1155,1156],{"class":319},"            IPv4Address    ",[151,1158,370],{"class":167},[151,1160,1161],{"class":319}," $computer.IPv4Address\n",[151,1163,1165,1168,1170],{"class":153,"line":1164},87,[151,1166,1167],{"class":319},"            OSCaption      ",[151,1169,370],{"class":167},[151,1171,1172],{"class":319}," $live.OS.Caption\n",[151,1174,1176,1179,1181],{"class":153,"line":1175},88,[151,1177,1178],{"class":319},"            OSBuild        ",[151,1180,370],{"class":167},[151,1182,1183],{"class":319}," $live.OS.BuildNumber\n",[151,1185,1187,1190,1192,1195,1198],{"class":153,"line":1186},89,[151,1188,1189],{"class":319},"            LastBoot       ",[151,1191,370],{"class":167},[151,1193,1194],{"class":319}," $live.OS.LastBootUpTime.ToString(",[151,1196,1197],{"class":373},"\"s\"",[151,1199,403],{"class":319},[151,1201,1203,1206,1208],{"class":153,"line":1202},90,[151,1204,1205],{"class":319},"            Manufacturer   ",[151,1207,370],{"class":167},[151,1209,1210],{"class":319}," $live.CS.Manufacturer\n",[151,1212,1214,1217,1219],{"class":153,"line":1213},91,[151,1215,1216],{"class":319},"            Model          ",[151,1218,370],{"class":167},[151,1220,1221],{"class":319}," $live.CS.Model\n",[151,1223,1225,1228,1230,1232,1235,1238,1241,1244,1247,1249],{"class":153,"line":1224},92,[151,1226,1227],{"class":319},"            MemoryGB       ",[151,1229,370],{"class":167},[151,1231,1118],{"class":319},[151,1233,1234],{"class":167},"math",[151,1236,1237],{"class":319},"]::Round($live.CS.TotalPhysicalMemory ",[151,1239,1240],{"class":167},"\u002F",[151,1242,1243],{"class":164}," 1",[151,1245,1246],{"class":167},"GB,",[151,1248,1243],{"class":164},[151,1250,403],{"class":319},[151,1252,1254,1257,1259],{"class":153,"line":1253},93,[151,1255,1256],{"class":319},"            SerialNumber   ",[151,1258,370],{"class":167},[151,1260,1261],{"class":319}," $live.BIOS.SerialNumber\n",[151,1263,1265,1268,1270,1272,1275,1277,1280,1282,1284,1287],{"class":153,"line":1264},94,[151,1266,1267],{"class":319},"            ADLastLogon    ",[151,1269,370],{"class":167},[151,1271,1080],{"class":167},[151,1273,1274],{"class":319}," ($computer.LastLogonDate) { $computer.LastLogonDate.ToString(",[151,1276,1197],{"class":373},[151,1278,1279],{"class":319},") } ",[151,1281,1086],{"class":167},[151,1283,701],{"class":319},[151,1285,1286],{"class":373},"\"\"",[151,1288,859],{"class":319},[151,1290,1292,1295,1297,1300,1302],{"class":153,"line":1291},95,[151,1293,1294],{"class":319},"            LastSeen       ",[151,1296,370],{"class":167},[151,1298,1299],{"class":319}," $now.ToString(",[151,1301,1197],{"class":373},[151,1303,403],{"class":319},[151,1305,1307,1310,1312],{"class":153,"line":1306},96,[151,1308,1309],{"class":319},"            InAD           ",[151,1311,370],{"class":167},[151,1313,1314],{"class":164}," $true\n",[151,1316,1318],{"class":153,"line":1317},97,[151,1319,1320],{"class":319},"        }\n",[151,1322,1324,1327,1330],{"class":153,"line":1323},98,[151,1325,1326],{"class":319},"    } ",[151,1328,1329],{"class":167},"elseif",[151,1331,1332],{"class":319}," ($previous) {\n",[151,1334,1336],{"class":153,"line":1335},99,[151,1337,1338],{"class":157},"        # Unreachable this run: keep the old facts and the old LastSeen, refresh the AD fields.\n",[151,1340,1342,1345,1347],{"class":153,"line":1341},100,[151,1343,1344],{"class":319},"        $previous.IPv4Address ",[151,1346,370],{"class":167},[151,1348,1161],{"class":319},[151,1350,1352,1355,1357,1359,1361,1363,1365,1367,1369,1371],{"class":153,"line":1351},101,[151,1353,1354],{"class":319},"        $previous.ADLastLogon ",[151,1356,370],{"class":167},[151,1358,1080],{"class":167},[151,1360,1274],{"class":319},[151,1362,1197],{"class":373},[151,1364,1279],{"class":319},[151,1366,1086],{"class":167},[151,1368,701],{"class":319},[151,1370,1286],{"class":373},[151,1372,859],{"class":319},[151,1374,1376,1379,1381],{"class":153,"line":1375},102,[151,1377,1378],{"class":319},"        $previous.InAD ",[151,1380,370],{"class":167},[151,1382,1314],{"class":164},[151,1384,1386,1388,1390],{"class":153,"line":1385},103,[151,1387,1326],{"class":319},[151,1389,1086],{"class":167},[151,1391,1392],{"class":319}," {\n",[151,1394,1396],{"class":153,"line":1395},104,[151,1397,1398],{"class":157},"        # Known to AD but never reached: record it so it shows up as never seen.\n",[151,1400,1402,1404,1406,1408,1410,1412,1414],{"class":153,"line":1401},105,[151,1403,1113],{"class":319},[151,1405,370],{"class":167},[151,1407,1118],{"class":319},[151,1409,1121],{"class":167},[151,1411,1124],{"class":319},[151,1413,783],{"class":167},[151,1415,456],{"class":319},[151,1417,1419,1421,1423],{"class":153,"line":1418},106,[151,1420,1134],{"class":319},[151,1422,370],{"class":167},[151,1424,1139],{"class":319},[151,1426,1428,1430,1432],{"class":153,"line":1427},107,[151,1429,1145],{"class":319},[151,1431,370],{"class":167},[151,1433,1150],{"class":319},[151,1435,1437,1439,1441],{"class":153,"line":1436},108,[151,1438,1156],{"class":319},[151,1440,370],{"class":167},[151,1442,1161],{"class":319},[151,1444,1446,1448,1450],{"class":153,"line":1445},109,[151,1447,1167],{"class":319},[151,1449,370],{"class":167},[151,1451,1452],{"class":319}," $computer.OperatingSystem\n",[151,1454,1456,1458,1460],{"class":153,"line":1455},110,[151,1457,1178],{"class":319},[151,1459,370],{"class":167},[151,1461,1462],{"class":319}," $computer.OperatingSystemVersion\n",[151,1464,1466,1468,1470],{"class":153,"line":1465},111,[151,1467,1189],{"class":319},[151,1469,370],{"class":167},[151,1471,1472],{"class":373}," \"\"\n",[151,1474,1476,1478,1480],{"class":153,"line":1475},112,[151,1477,1205],{"class":319},[151,1479,370],{"class":167},[151,1481,1472],{"class":373},[151,1483,1485,1487,1489],{"class":153,"line":1484},113,[151,1486,1216],{"class":319},[151,1488,370],{"class":167},[151,1490,1472],{"class":373},[151,1492,1494,1496,1498],{"class":153,"line":1493},114,[151,1495,1227],{"class":319},[151,1497,370],{"class":167},[151,1499,1472],{"class":373},[151,1501,1503,1505,1507],{"class":153,"line":1502},115,[151,1504,1256],{"class":319},[151,1506,370],{"class":167},[151,1508,1472],{"class":373},[151,1510,1512,1514,1516,1518,1520,1522,1524,1526,1528,1530],{"class":153,"line":1511},116,[151,1513,1267],{"class":319},[151,1515,370],{"class":167},[151,1517,1080],{"class":167},[151,1519,1274],{"class":319},[151,1521,1197],{"class":373},[151,1523,1279],{"class":319},[151,1525,1086],{"class":167},[151,1527,701],{"class":319},[151,1529,1286],{"class":373},[151,1531,859],{"class":319},[151,1533,1535,1537,1539],{"class":153,"line":1534},117,[151,1536,1294],{"class":319},[151,1538,370],{"class":167},[151,1540,1472],{"class":373},[151,1542,1544,1546,1548],{"class":153,"line":1543},118,[151,1545,1309],{"class":319},[151,1547,370],{"class":167},[151,1549,1314],{"class":164},[151,1551,1553],{"class":153,"line":1552},119,[151,1554,1320],{"class":319},[151,1556,1558],{"class":153,"line":1557},120,[151,1559,647],{"class":319},[151,1561,1563],{"class":153,"line":1562},121,[151,1564,500],{"class":319},[151,1566,1568],{"class":153,"line":1567},122,[151,1569,357],{"emptyLinePlaceholder":356},[151,1571,1573],{"class":153,"line":1572},123,[151,1574,1575],{"class":157},"# Rows whose computer account is gone or disabled stay in the file, flagged.\n",[151,1577,1579,1582,1584,1586],{"class":153,"line":1578},124,[151,1580,1581],{"class":319},"$adNames ",[151,1583,370],{"class":167},[151,1585,453],{"class":167},[151,1587,1588],{"class":319},"($adComputers.Name)\n",[151,1590,1592,1594,1597,1599,1601],{"class":153,"line":1591},125,[151,1593,1050],{"class":167},[151,1595,1596],{"class":319}," ($name ",[151,1598,623],{"class":167},[151,1600,453],{"class":167},[151,1602,1603],{"class":319},"($inventory.Keys)) {\n",[151,1605,1607,1609,1612,1615],{"class":153,"line":1606},126,[151,1608,1104],{"class":167},[151,1610,1611],{"class":319}," ($adNames ",[151,1613,1614],{"class":167},"-notcontains",[151,1616,1617],{"class":319}," $name) {\n",[151,1619,1621,1624,1626],{"class":153,"line":1620},127,[151,1622,1623],{"class":319},"        $inventory[$name].InAD ",[151,1625,370],{"class":167},[151,1627,1628],{"class":164}," $false\n",[151,1630,1632],{"class":153,"line":1631},128,[151,1633,647],{"class":319},[151,1635,1637],{"class":153,"line":1636},129,[151,1638,500],{"class":319},[151,1640,1642],{"class":153,"line":1641},130,[151,1643,357],{"emptyLinePlaceholder":356},[151,1645,1647,1650,1652,1655,1657,1660,1662,1665,1667,1670,1672],{"class":153,"line":1646},131,[151,1648,1649],{"class":319},"$inventory.Values ",[151,1651,695],{"class":167},[151,1653,1654],{"class":164}," Sort-Object",[151,1656,417],{"class":167},[151,1658,1659],{"class":319},"Property Name ",[151,1661,695],{"class":167},[151,1663,1664],{"class":164}," Export-Csv",[151,1666,417],{"class":167},[151,1668,1669],{"class":319},"Path $InventoryPath ",[151,1671,423],{"class":167},[151,1673,1674],{"class":319},"NoTypeInformation\n",[151,1676,1678,1680,1683,1686,1688,1690,1692,1695,1697,1700,1702,1704,1707,1709],{"class":153,"line":1677},132,[151,1679,554],{"class":164},[151,1681,1682],{"class":373}," \"Inventory written to ",[151,1684,1685],{"class":319},"$InventoryPath",[151,1687,603],{"class":373},[151,1689,560],{"class":167},[151,1691,543],{"class":373},[151,1693,1694],{"class":319},"$inventory.Count",[151,1696,568],{"class":373},[151,1698,1699],{"class":373}," rows, ",[151,1701,560],{"class":167},[151,1703,543],{"class":373},[151,1705,1706],{"class":319},"$facts.Count",[151,1708,568],{"class":373},[151,1710,1711],{"class":373}," refreshed this run).\"\n",[11,1713,1714],{},"Sample console output from a run against a workstation OU:",[142,1716,1721],{"className":1717,"code":1719,"language":1720,"meta":147},[1718],"language-text","AD returned 412 enabled computer account(s).\nOpened 377 CIM session(s); 35 computer(s) unreachable.\nInventory written to D:\\Inventory\\inventory.csv (419 rows, 377 refreshed this run).\n","text",[49,1722,1719],{"__ignoreMap":147},[11,1724,1725,1726,1729],{},"Notice the row count is higher than the AD count. The extra rows are computers whose accounts were disabled or deleted since the last sweep. They stay in the file with ",[49,1727,1728],{},"InAD = False"," until someone confirms the hardware is really gone, which is exactly the conversation a spreadsheet never forces.",[18,1731,1733],{"id":1732},"staleness-is-a-feature-not-a-bug-if-you-surface-it","Staleness is a feature, not a bug, if you surface it",[11,1735,1736],{},"An automated inventory should record when each entry was last refreshed, and that timestamp is more valuable than most of the other fields combined. A machine that has not reported in three weeks is telling you something: it is off the network, decommissioned without the paperwork catching up, or broken in a way worth investigating. I treat \"last seen\" as the primary health signal of the whole inventory system, and the stale-entries report has turned up orphaned assets that no human review of a spreadsheet ever caught.",[11,1738,1739],{},"The report is deliberately small. It reads the inventory and sorts machines into the buckets that need different follow-ups:",[142,1741,1743],{"className":144,"code":1742,"language":146,"meta":147,"style":147},"\u003C#\n.SYNOPSIS\n    Reports inventory rows that have not been seen recently.\n.PARAMETER InventoryPath\n    Inventory CSV written by Invoke-DiscoverySweep.ps1.\n.PARAMETER StaleDays\n    Days since LastSeen after which a machine counts as stale.\n.EXAMPLE\n    .\\Get-StaleInventory.ps1 -InventoryPath D:\\Inventory\\inventory.csv -StaleDays 21\n.NOTES\n    Author  : Thomas Lasswell (https:\u002F\u002Fwww.techcolumnist.com)\n    Version : 1.0 (2026-09-02)\n#>\n[CmdletBinding()]\nparam (\n    [string]$InventoryPath = \".\\inventory.csv\",\n\n    [int]$StaleDays = 21\n)\n\n$cutoff = (Get-Date).AddDays(-$StaleDays)\n\nImport-Csv -Path $InventoryPath | ForEach-Object {\n    $lastSeen = if ($_.LastSeen) { [datetime]$_.LastSeen } else { $null }\n\n    $bucket = if ($_.InAD -eq \"False\") {\n        \"Removed from AD: confirm disposal\"\n    } elseif (-not $lastSeen) {\n        \"Never reached: check WinRM, firewall or whether it exists\"\n    } elseif ($lastSeen -lt $cutoff) {\n        \"Stale: not seen in $StaleDays+ days\"\n    } else {\n        $null\n    }\n\n    if ($bucket) {\n        [pscustomobject]@{\n            Name        = $_.Name\n            Bucket      = $bucket\n            LastSeen    = $_.LastSeen\n            ADLastLogon = $_.ADLastLogon\n            Model       = $_.Model\n            Serial      = $_.SerialNumber\n        }\n    }\n} | Sort-Object -Property Bucket, LastSeen | Format-Table -AutoSize\n",[49,1744,1745,1749,1755,1760,1768,1773,1782,1787,1793,1798,1804,1808,1812,1816,1824,1830,1844,1848,1862,1866,1870,1890,1894,1910,1944,1948,1972,1977,1991,1996,2011,2022,2030,2035,2039,2043,2050,2063,2076,2086,2098,2110,2122,2134,2138,2142],{"__ignoreMap":147},[151,1746,1747],{"class":153,"line":154},[151,1748,158],{"class":157},[151,1750,1751,1753],{"class":153,"line":161},[151,1752,121],{"class":164},[151,1754,168],{"class":167},[151,1756,1757],{"class":153,"line":171},[151,1758,1759],{"class":157},"    Reports inventory rows that have not been seen recently.\n",[151,1761,1762,1764,1766],{"class":153,"line":177},[151,1763,121],{"class":164},[151,1765,226],{"class":167},[151,1767,245],{"class":167},[151,1769,1770],{"class":153,"line":183},[151,1771,1772],{"class":157},"    Inventory CSV written by Invoke-DiscoverySweep.ps1.\n",[151,1774,1775,1777,1779],{"class":153,"line":191},[151,1776,121],{"class":164},[151,1778,226],{"class":167},[151,1780,1781],{"class":167}," StaleDays\n",[151,1783,1784],{"class":153,"line":197},[151,1785,1786],{"class":157},"    Days since LastSeen after which a machine counts as stale.\n",[151,1788,1789,1791],{"class":153,"line":203},[151,1790,121],{"class":164},[151,1792,275],{"class":167},[151,1794,1795],{"class":153,"line":209},[151,1796,1797],{"class":157},"    .\\Get-StaleInventory.ps1 -InventoryPath D:\\Inventory\\inventory.csv -StaleDays 21\n",[151,1799,1800,1802],{"class":153,"line":215},[151,1801,121],{"class":164},[151,1803,289],{"class":167},[151,1805,1806],{"class":153,"line":221},[151,1807,295],{"class":157},[151,1809,1810],{"class":153,"line":232},[151,1811,301],{"class":157},[151,1813,1814],{"class":153,"line":238},[151,1815,313],{"class":157},[151,1817,1818,1820,1822],{"class":153,"line":248},[151,1819,320],{"class":319},[151,1821,323],{"class":164},[151,1823,326],{"class":319},[151,1825,1826,1828],{"class":153,"line":254},[151,1827,332],{"class":167},[151,1829,335],{"class":319},[151,1831,1832,1834,1836,1838,1840,1842],{"class":153,"line":264},[151,1833,341],{"class":319},[151,1835,344],{"class":167},[151,1837,367],{"class":319},[151,1839,370],{"class":167},[151,1841,374],{"class":373},[151,1843,350],{"class":167},[151,1845,1846],{"class":153,"line":270},[151,1847,357],{"emptyLinePlaceholder":356},[151,1849,1850,1852,1854,1857,1859],{"class":153,"line":278},[151,1851,341],{"class":319},[151,1853,389],{"class":167},[151,1855,1856],{"class":319},"]$StaleDays ",[151,1858,370],{"class":167},[151,1860,1861],{"class":164}," 21\n",[151,1863,1864],{"class":153,"line":284},[151,1865,403],{"class":319},[151,1867,1868],{"class":153,"line":292},[151,1869,357],{"emptyLinePlaceholder":356},[151,1871,1872,1875,1877,1879,1882,1885,1887],{"class":153,"line":298},[151,1873,1874],{"class":319},"$cutoff ",[151,1876,370],{"class":167},[151,1878,603],{"class":319},[151,1880,1881],{"class":164},"Get-Date",[151,1883,1884],{"class":319},").AddDays(",[151,1886,423],{"class":167},[151,1888,1889],{"class":319},"$StaleDays)\n",[151,1891,1892],{"class":153,"line":304},[151,1893,357],{"emptyLinePlaceholder":356},[151,1895,1896,1899,1901,1903,1905,1908],{"class":153,"line":310},[151,1897,1898],{"class":164},"Import-Csv",[151,1900,417],{"class":167},[151,1902,1669],{"class":319},[151,1904,695],{"class":167},[151,1906,1907],{"class":164}," ForEach-Object",[151,1909,1392],{"class":319},[151,1911,1912,1915,1917,1919,1921,1923,1926,1929,1931,1933,1936,1938,1940,1942],{"class":153,"line":316},[151,1913,1914],{"class":319},"    $lastSeen ",[151,1916,370],{"class":167},[151,1918,1080],{"class":167},[151,1920,603],{"class":319},[151,1922,704],{"class":164},[151,1924,1925],{"class":319},".LastSeen) { [",[151,1927,1928],{"class":167},"datetime",[151,1930,1124],{"class":319},[151,1932,704],{"class":164},[151,1934,1935],{"class":319},".LastSeen } ",[151,1937,1086],{"class":167},[151,1939,701],{"class":319},[151,1941,1091],{"class":164},[151,1943,859],{"class":319},[151,1945,1946],{"class":153,"line":329},[151,1947,357],{"emptyLinePlaceholder":356},[151,1949,1950,1953,1955,1957,1959,1961,1964,1967,1970],{"class":153,"line":338},[151,1951,1952],{"class":319},"    $bucket ",[151,1954,370],{"class":167},[151,1956,1080],{"class":167},[151,1958,603],{"class":319},[151,1960,704],{"class":164},[151,1962,1963],{"class":319},".InAD ",[151,1965,1966],{"class":167},"-eq",[151,1968,1969],{"class":373}," \"False\"",[151,1971,828],{"class":319},[151,1973,1974],{"class":153,"line":353},[151,1975,1976],{"class":373},"        \"Removed from AD: confirm disposal\"\n",[151,1978,1979,1981,1983,1985,1988],{"class":153,"line":360},[151,1980,1326],{"class":319},[151,1982,1329],{"class":167},[151,1984,603],{"class":319},[151,1986,1987],{"class":167},"-not",[151,1989,1990],{"class":319}," $lastSeen) {\n",[151,1992,1993],{"class":153,"line":379},[151,1994,1995],{"class":373},"        \"Never reached: check WinRM, firewall or whether it exists\"\n",[151,1997,1998,2000,2002,2005,2008],{"class":153,"line":384},[151,1999,1326],{"class":319},[151,2001,1329],{"class":167},[151,2003,2004],{"class":319}," ($lastSeen ",[151,2006,2007],{"class":167},"-lt",[151,2009,2010],{"class":319}," $cutoff) {\n",[151,2012,2013,2016,2019],{"class":153,"line":400},[151,2014,2015],{"class":373},"        \"Stale: not seen in ",[151,2017,2018],{"class":319},"$StaleDays",[151,2020,2021],{"class":373},"+ days\"\n",[151,2023,2024,2026,2028],{"class":153,"line":406},[151,2025,1326],{"class":319},[151,2027,1086],{"class":167},[151,2029,1392],{"class":319},[151,2031,2032],{"class":153,"line":411},[151,2033,2034],{"class":164},"        $null\n",[151,2036,2037],{"class":153,"line":429},[151,2038,647],{"class":319},[151,2040,2041],{"class":153,"line":434},[151,2042,357],{"emptyLinePlaceholder":356},[151,2044,2045,2047],{"class":153,"line":445},[151,2046,1104],{"class":167},[151,2048,2049],{"class":319}," ($bucket) {\n",[151,2051,2052,2055,2057,2059,2061],{"class":153,"line":459},[151,2053,2054],{"class":319},"        [",[151,2056,1121],{"class":167},[151,2058,1124],{"class":319},[151,2060,783],{"class":167},[151,2062,456],{"class":319},[151,2064,2065,2068,2070,2073],{"class":153,"line":470},[151,2066,2067],{"class":319},"            Name        ",[151,2069,370],{"class":167},[151,2071,2072],{"class":164}," $_",[151,2074,2075],{"class":319},".Name\n",[151,2077,2078,2081,2083],{"class":153,"line":497},[151,2079,2080],{"class":319},"            Bucket      ",[151,2082,370],{"class":167},[151,2084,2085],{"class":319}," $bucket\n",[151,2087,2088,2091,2093,2095],{"class":153,"line":503},[151,2089,2090],{"class":319},"            LastSeen    ",[151,2092,370],{"class":167},[151,2094,2072],{"class":164},[151,2096,2097],{"class":319},".LastSeen\n",[151,2099,2100,2103,2105,2107],{"class":153,"line":512},[151,2101,2102],{"class":319},"            ADLastLogon ",[151,2104,370],{"class":167},[151,2106,2072],{"class":164},[151,2108,2109],{"class":319},".ADLastLogon\n",[151,2111,2112,2115,2117,2119],{"class":153,"line":523},[151,2113,2114],{"class":319},"            Model       ",[151,2116,370],{"class":167},[151,2118,2072],{"class":164},[151,2120,2121],{"class":319},".Model\n",[151,2123,2124,2127,2129,2131],{"class":153,"line":528},[151,2125,2126],{"class":319},"            Serial      ",[151,2128,370],{"class":167},[151,2130,2072],{"class":164},[151,2132,2133],{"class":319},".SerialNumber\n",[151,2135,2136],{"class":153,"line":533},[151,2137,1320],{"class":319},[151,2139,2140],{"class":153,"line":551},[151,2141,647],{"class":319},[151,2143,2144,2147,2149,2151,2153,2156,2158,2161,2163,2166,2168],{"class":153,"line":574},[151,2145,2146],{"class":319},"} ",[151,2148,695],{"class":167},[151,2150,1654],{"class":164},[151,2152,417],{"class":167},[151,2154,2155],{"class":319},"Property Bucket",[151,2157,481],{"class":167},[151,2159,2160],{"class":319}," LastSeen ",[151,2162,695],{"class":167},[151,2164,2165],{"class":164}," Format-Table",[151,2167,417],{"class":167},[151,2169,2170],{"class":319},"AutoSize\n",[142,2172,2175],{"className":2173,"code":2174,"language":1720,"meta":147},[1718],"Name      Bucket                                                   LastSeen            ADLastLogon         Model        Serial\n----      ------                                                   --------            -----------         -----        ------\nWKS-0091  Never reached: check WinRM, firewall or whether it exists                    2026-05-02T08:14:51\nWKS-0310  Removed from AD: confirm disposal                        2026-07-28T02:00:12 2026-07-21T09:33:02 Latitude 5440 \u003Cserial>\nWKS-0142  Stale: not seen in 21+ days                              2026-08-03T02:00:09 2026-08-01T16:20:45 OptiPlex 7010 \u003Cserial>\n",[49,2176,2174],{"__ignoreMap":147},[11,2178,2179,2180,2183,2184,2186],{},"Reading ",[49,2181,2182],{},"ADLastLogon"," next to ",[49,2185,137],{}," is where the insight is. Stale in the sweep but recently logged on in AD usually means the machine is alive and the sweep can't reach it (a firewall rule, a VPN-only laptop). Stale in both is a machine that's genuinely gone somewhere.",[18,2188,2190],{"id":2189},"store-it-somewhere-that-answers-questions-not-just-holds-rows","Store it somewhere that answers questions, not just holds rows",[11,2192,2193],{},"A spreadsheet is a poor home for this kind of data past a few hundred rows, because nobody can ask it a real question without exporting it somewhere else first. Landing the discovery data somewhere Power BI (or an equivalent) can query directly, even a CSV on a file share to start with, turns the inventory into something that answers \"which machines are still running an unsupported OS build\" in seconds instead of a manual filter-and-scroll exercise:",[142,2195,2197],{"className":144,"code":2196,"language":146,"meta":147,"style":147},"Import-Csv -Path D:\\Inventory\\inventory.csv |\n    Where-Object { $_.OSCaption -like \"*Windows 11*\" } |\n    Group-Object -Property OSBuild |\n    Sort-Object -Property Name |\n    Format-Table Name, Count -AutoSize\n",[49,2198,2199,2211,2234,2246,2257],{"__ignoreMap":147},[151,2200,2201,2203,2205,2208],{"class":153,"line":154},[151,2202,1898],{"class":164},[151,2204,417],{"class":167},[151,2206,2207],{"class":319},"Path D:\\Inventory\\inventory.csv ",[151,2209,2210],{"class":167},"|\n",[151,2212,2213,2216,2218,2220,2223,2226,2229,2232],{"class":153,"line":161},[151,2214,2215],{"class":164},"    Where-Object",[151,2217,701],{"class":319},[151,2219,704],{"class":164},[151,2221,2222],{"class":319},".OSCaption ",[151,2224,2225],{"class":167},"-like",[151,2227,2228],{"class":373}," \"*Windows 11*\"",[151,2230,2231],{"class":319}," } ",[151,2233,2210],{"class":167},[151,2235,2236,2239,2241,2244],{"class":153,"line":171},[151,2237,2238],{"class":164},"    Group-Object",[151,2240,417],{"class":167},[151,2242,2243],{"class":319},"Property OSBuild ",[151,2245,2210],{"class":167},[151,2247,2248,2251,2253,2255],{"class":153,"line":177},[151,2249,2250],{"class":164},"    Sort-Object",[151,2252,417],{"class":167},[151,2254,1659],{"class":319},[151,2256,2210],{"class":167},[151,2258,2259,2262,2265,2267,2270,2272],{"class":153,"line":183},[151,2260,2261],{"class":164},"    Format-Table",[151,2263,2264],{"class":319}," Name",[151,2266,481],{"class":167},[151,2268,2269],{"class":319}," Count ",[151,2271,423],{"class":167},[151,2273,2170],{"class":319},[11,2275,2276,2277,2282,2283,2287],{},"The format matters less than the principle: the inventory should be queryable by the people who need answers, not just readable by the person who happens to have it open. The same file joins cleanly with the per-machine CSVs from the ",[2278,2279,2281],"a",{"href":2280},"\u002F2025\u002F08\u002F27\u002Fpowershell-windows-inventory-installed-software-across-a-domain\u002F","software inventory script"," and the ",[2278,2284,2286],{"href":2285},"\u002F2025\u002F10\u002F08\u002Fpowershell-windows-discover-local-admins-across-every-workstation\u002F","local admin audit",", because all three are keyed by computer name.",[18,2289,2291],{"id":2290},"schedule-it-or-its-just-another-spreadsheet","Schedule it, or it's just another spreadsheet",[11,2293,2294],{},"The last step is the one that makes it maintenance-free: run the sweep on a schedule from a management server, as a service account that has admin rights on the targets, so nobody has to remember to run it either.",[142,2296,2298],{"className":144,"code":2297,"language":146,"meta":147,"style":147},"$credential = Get-Credential -Message \"Discovery service account (for example CORP\\svc-discovery)\"\n\n$action = New-ScheduledTaskAction -Execute \"powershell.exe\" -Argument '-NoProfile -ExecutionPolicy Bypass -File \"D:\\Inventory\\Invoke-DiscoverySweep.ps1\" -InventoryPath \"D:\\Inventory\\inventory.csv\"'\n$trigger = New-ScheduledTaskTrigger -Daily -At \"02:00\"\n\nRegister-ScheduledTask -TaskName \"Discovery Sweep\" -TaskPath \"\\Inventory\\\" -Action $action -Trigger $trigger -User $credential.UserName -Password $credential.GetNetworkCredential().Password -RunLevel Highest\n",[49,2299,2300,2318,2322,2348,2371,2375],{"__ignoreMap":147},[151,2301,2302,2305,2307,2310,2312,2315],{"class":153,"line":154},[151,2303,2304],{"class":319},"$credential ",[151,2306,370],{"class":167},[151,2308,2309],{"class":164}," Get-Credential",[151,2311,417],{"class":167},[151,2313,2314],{"class":319},"Message ",[151,2316,2317],{"class":373},"\"Discovery service account (for example CORP\\svc-discovery)\"\n",[151,2319,2320],{"class":153,"line":161},[151,2321,357],{"emptyLinePlaceholder":356},[151,2323,2324,2327,2329,2332,2334,2337,2340,2342,2345],{"class":153,"line":171},[151,2325,2326],{"class":319},"$action ",[151,2328,370],{"class":167},[151,2330,2331],{"class":164}," New-ScheduledTaskAction",[151,2333,417],{"class":167},[151,2335,2336],{"class":319},"Execute ",[151,2338,2339],{"class":373},"\"powershell.exe\"",[151,2341,417],{"class":167},[151,2343,2344],{"class":319},"Argument ",[151,2346,2347],{"class":373},"'-NoProfile -ExecutionPolicy Bypass -File \"D:\\Inventory\\Invoke-DiscoverySweep.ps1\" -InventoryPath \"D:\\Inventory\\inventory.csv\"'\n",[151,2349,2350,2353,2355,2358,2360,2363,2365,2368],{"class":153,"line":177},[151,2351,2352],{"class":319},"$trigger ",[151,2354,370],{"class":167},[151,2356,2357],{"class":164}," New-ScheduledTaskTrigger",[151,2359,417],{"class":167},[151,2361,2362],{"class":319},"Daily ",[151,2364,423],{"class":167},[151,2366,2367],{"class":319},"At ",[151,2369,2370],{"class":373},"\"02:00\"\n",[151,2372,2373],{"class":153,"line":183},[151,2374,357],{"emptyLinePlaceholder":356},[151,2376,2377,2380,2382,2385,2388,2390,2393,2396,2398,2401,2403,2406,2408,2411,2413,2416,2418],{"class":153,"line":191},[151,2378,2379],{"class":164},"Register-ScheduledTask",[151,2381,417],{"class":167},[151,2383,2384],{"class":319},"TaskName ",[151,2386,2387],{"class":373},"\"Discovery Sweep\"",[151,2389,417],{"class":167},[151,2391,2392],{"class":319},"TaskPath ",[151,2394,2395],{"class":373},"\"\\Inventory\\\"",[151,2397,417],{"class":167},[151,2399,2400],{"class":319},"Action $action ",[151,2402,423],{"class":167},[151,2404,2405],{"class":319},"Trigger $trigger ",[151,2407,423],{"class":167},[151,2409,2410],{"class":319},"User $credential.UserName ",[151,2412,423],{"class":167},[151,2414,2415],{"class":319},"Password $credential.GetNetworkCredential().Password ",[151,2417,423],{"class":167},[151,2419,2420],{"class":319},"RunLevel Highest\n",[18,2422,2424],{"id":2423},"keep-the-collection-script-simpler-than-the-reporting-layer","Keep the collection script simpler than the reporting layer",[11,2426,2427,2428,2431],{},"The temptation once this is running is to keep adding fields to the collection step until it is fetching everything CIM can possibly return. I have found the opposite discipline pays off better: keep what each sweep collects narrow and fast so the collection never becomes fragile or slow enough that people start disabling it, and push the complexity into the reporting queries instead, where it can change without touching a thousand endpoints. The sweep above asks three CIM classes for eight properties, and uses ",[49,2429,2430],{},"-Property"," so the remote side only returns those. An inventory that nobody has to maintain by hand only stays that way if the thing collecting it stays simple enough that it never needs hand maintenance either.",[18,2433,2435],{"id":2434},"references","References",[33,2437,2438,2445,2455,2462,2469,2476],{},[36,2439,2440],{},[2278,2441,51],{"href":2442,"rel":2443},"https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fpowershell\u002Fmodule\u002Factivedirectory\u002Fget-adcomputer",[2444],"nofollow",[36,2446,2447,67,2451],{},[2278,2448,108],{"href":2449,"rel":2450},"https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fpowershell\u002Fmodule\u002Fcimcmdlets\u002Fnew-cimsession",[2444],[2278,2452,116],{"href":2453,"rel":2454},"https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fpowershell\u002Fmodule\u002Fcimcmdlets\u002Fget-ciminstance",[2444],[36,2456,2457],{},[2278,2458,2461],{"href":2459,"rel":2460},"https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fwindows\u002Fwin32\u002Fcimwin32prov\u002Fwin32-operatingsystem",[2444],"Win32_OperatingSystem class",[36,2463,2464],{},[2278,2465,2468],{"href":2466,"rel":2467},"https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Ftroubleshoot\u002Fmem\u002Fconfigmgr\u002Fdiscovery\u002Flastlogontimestamp-not-accurate",[2444],"lastLogonTimestamp is not accurate (9 to 14 day update behavior)",[36,2470,2471],{},[2278,2472,2475],{"href":2473,"rel":2474},"https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fpowershell\u002Fmodule\u002Fmicrosoft.powershell.core\u002Fabout\u002Fabout_remote_requirements",[2444],"about_Remote_Requirements",[36,2477,2478,67,2483],{},[2278,2479,2482],{"href":2480,"rel":2481},"https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fpowershell\u002Fmodule\u002Fscheduledtasks\u002Fnew-scheduledtasktrigger",[2444],"New-ScheduledTaskTrigger",[2278,2484,2379],{"href":2485,"rel":2486},"https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fpowershell\u002Fmodule\u002Fscheduledtasks\u002Fregister-scheduledtask",[2444],[2488,2489,2490],"style",{},"html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":147,"searchDepth":161,"depth":161,"links":2492},[2493,2494,2495,2496,2497,2498,2499],{"id":20,"depth":161,"text":21},{"id":27,"depth":161,"text":28},{"id":1732,"depth":161,"text":1733},{"id":2189,"depth":161,"text":2190},{"id":2290,"depth":161,"text":2291},{"id":2423,"depth":161,"text":2424},{"id":2434,"depth":161,"text":2435},"techcolumnist",[2502,2503],"engineering","strategy","2026-09-02T14:00:00Z","Why a self-refreshing Windows inventory built from AD and CIM discovery outlasts a spreadsheet, with the sweep script, staleness report and schedule.","md",false,null,{},"\u002Fblog\u002F2026\u002F09\u002F02\u002Fwindows-management-building-a-discovery-inventory-nobody-has-to-maintain-by-hand",{"title":6,"description":2505},[2500],"blog\u002F2026\u002F09\u002F02\u002Fwindows-management-building-a-discovery-inventory-nobody-has-to-maintain-by-hand",[2515,2516,2517],"discovery","windows","active-directory","\u002F2026\u002F09\u002F02\u002Fwindows-management-building-a-discovery-inventory-nobody-has-to-maintain-by-hand\u002F","2SysQ1C1k0kIO7lGPFsesTl_A6yuunQGu7BbUy1Tnk8",{"title":2521,"description":2522,"date":2523,"url":2524,"categories":2525,"tags":2526,"image":2508,"readingTime":191,"canonical":2500,"sites":2528,"series":2508,"seriesOrder":2508},"Google Cloud: IAM Least Privilege in Practice, Not Just in Theory","Running least privilege in a real Google Cloud org: finding basic roles, working the IAM recommender, custom roles, temporary access, and service account risk.","2026-09-09T14:00:00Z","\u002F2026\u002F09\u002F09\u002Fgoogle-cloud-iam-least-privilege-in-practice-not-just-in-theory\u002F",[2502],[2527],"gcloud",[2500],{"title":2530,"description":2531,"date":2532,"url":2533,"categories":2534,"tags":2535,"image":2508,"readingTime":197,"canonical":2500,"sites":2538,"series":2508,"seriesOrder":2508},"Talos Linux: Upgrading a Cluster Without a Maintenance Window","Rolling Talos and Kubernetes upgrades through a cluster one node at a time: preflight checks, etcd snapshots, Image Factory installers, and scripts.","2026-08-26T14:00:00Z","\u002F2026\u002F08\u002F26\u002Ftalos-linux-upgrading-a-cluster-without-a-maintenance-window\u002F",[2502],[2536,2537],"talos","kubernetes",[2500],[2540,2550,2560],{"title":2541,"description":2542,"date":2543,"url":2544,"categories":2545,"tags":2546,"image":2508,"readingTime":197,"canonical":2500,"sites":2549,"series":2508,"seriesOrder":2508},"Intune: Migrating From Group Policy to Cloud-Native Management","Moving a Windows estate from Group Policy to Intune: GPO inventory script, Group Policy analytics, Settings catalog migration, MDMWinsOverGP and conflict checks.","2026-04-01T14:00:00Z","\u002F2026\u002F04\u002F01\u002Fintune-migrating-from-group-policy-to-cloud-native-endpoint-management\u002F",[2502,2503],[2547,2548,2516,2517],"intune","gpo",[2500],{"title":2551,"description":2552,"date":2553,"url":2554,"categories":2555,"tags":2556,"image":2508,"readingTime":203,"canonical":2500,"sites":2559,"series":2508,"seriesOrder":2508},"Infrastructure: Documenting an Undocumented Network, Discovery First","Document an undocumented network from what it already knows: DHCP leases, AD, ARP and switch MAC tables, then a scoped nmap pass. Scripts included.","2026-07-15T14:00:00Z","\u002F2026\u002F07\u002F15\u002Finfrastructure-a-discovery-first-approach-to-documenting-an-undocumented-network\u002F",[2502,2503],[2515,2557,2558],"network","python",[2500],{"title":2561,"description":2562,"date":2563,"url":2564,"categories":2565,"tags":2567,"image":2508,"readingTime":171,"canonical":2500,"sites":2568,"series":2508,"seriesOrder":2508},"Python: Discovery – Crawling DNS Zones for Orphaned Records","A dnspython script that transfers a zone with AXFR, probes every A\u002FAAAA record for signs of life, flags dangling CNAMEs, and reports orphan cleanup candidates.","2026-01-28T14:00:00Z","\u002F2026\u002F01\u002F28\u002Fpython-discovery-crawling-dns-zones-for-orphaned-records\u002F",[2566,2502],"scripts",[2558,2515,2557,2516],[2500],{"doc":2508,"posts":2570},[],1790052514236]