[{"data":1,"prerenderedAt":5931},["ShallowReactive",2],{"post:\u002F2026\u002F07\u002F15\u002Finfrastructure-a-discovery-first-approach-to-documenting-an-undocumented-network\u002F":3},{"post":4,"newer":5884,"older":5894,"related":5904,"series":5929},{"id":5,"title":6,"body":7,"canonical":5865,"categories":5866,"date":5869,"description":5870,"extension":5871,"featured":5872,"hero":5873,"image":5873,"meta":5874,"navigation":393,"path":5875,"readingTime":246,"seo":5876,"series":5873,"seriesOrder":5873,"sites":5877,"source":5873,"stem":5878,"tags":5879,"updated":5873,"url":5882,"__hash__":5883},"blog\u002Fblog\u002F2026\u002F07\u002F15\u002Finfrastructure-a-discovery-first-approach-to-documenting-an-undocumented-network.md","Infrastructure: Documenting an Undocumented Network, Discovery First",{"type":8,"value":9,"toc":5855},"minimark",[10,14,17,58,61,66,69,72,139,142,146,181,184,1003,1043,1047,1050,1124,1140,1173,2826,2897,2901,2904,2907,2928,5085,5088,5095,5110,5114,5120,5320,5370,5388,5392,5395,5408,5455,5459,5462,5469,5507,5732,5740,5762,5766,5851],[11,12,13],"p",{},"The environments I get called into are rarely undocumented by accident. They are undocumented because the person who knew the network left, the diagram was last touched three reorganizations ago, and every attempt to rebuild it starting from a blank Visio canvas has stalled the same way: nobody wants to be wrong in front of the team, so the document sits half-finished. The fix I keep coming back to is to stop trying to document from memory and interview, and start from discovery instead.",[11,15,16],{},"This post is the process, plus the scripts that do the collecting. There are four of them, run in this order:",[18,19,20,28,34,48],"ol",{},[21,22,23,27],"li",{},[24,25,26],"code",{},"Export-PassiveInventory.ps1",": DHCP leases and Active Directory computer accounts.",[21,29,30,33],{},[24,31,32],{},"snmp_collect.py",": ARP tables from routers and MAC address tables from switches, over read-only SNMP.",[21,35,36,39,40,43,44,47],{},[24,37,38],{},"merge_inventory.py",": joins everything on MAC address and writes ",[24,41,42],{},"inventory.csv"," plus ",[24,45,46],{},"gaps.txt",", the addresses nobody can explain.",[21,49,50,51,54,55,57],{},"A scoped ",[24,52,53],{},"nmap"," pass against ",[24,56,46],{}," only, then the merge again.",[11,59,60],{},"Only run any of this on networks you are authorized to assess, with the change window and the owner's sign-off in writing. Reading tables over SNMP is low-impact, but it is still access, and the active step is a port scan.",[62,63,65],"h2",{"id":64},"let-the-network-describe-itself-first","Let the Network Describe Itself First",[11,67,68],{},"Before I ask a single person what they think is connected to what, I pull what the infrastructure already knows about itself. Switch MAC address tables and ARP caches tell you what is actually plugged in and talking, not what someone remembers plugging in two years ago. DHCP lease tables tell you which device holds which address right now, and expired leases show what used to be there, including the things nobody put on a spreadsheet. Router and firewall configs list every VLAN, route, and rule that exists whether or not it was ever written down elsewhere. None of this requires a single meeting, and all of it is ground truth rather than institutional memory, which matters because institutional memory is exactly what is missing in an undocumented environment.",[11,70,71],{},"Each source answers a different question, and none is complete on its own:",[73,74,75,91],"table",{},[76,77,78],"thead",{},[79,80,81,85,88],"tr",{},[82,83,84],"th",{},"Source",[82,86,87],{},"Answers",[82,89,90],{},"Blind spot",[92,93,94,106,117,128],"tbody",{},[79,95,96,100,103],{},[97,98,99],"td",{},"DHCP leases",[97,101,102],{},"Which MAC holds which IP, and the hostname the client sent",[97,104,105],{},"Anything with a static IP",[79,107,108,111,114],{},[97,109,110],{},"AD computer accounts",[97,112,113],{},"Which machines are domain-joined, with OS",[97,115,116],{},"Printers, appliances, Linux, anything not joined",[79,118,119,122,125],{},[97,120,121],{},"Router ARP table",[97,123,124],{},"Every IP-to-MAC pair that talked through that router recently",[97,126,127],{},"Entries age out, so quiet devices can be missing",[79,129,130,133,136],{},[97,131,132],{},"Switch MAC table",[97,134,135],{},"Which physical port and VLAN each MAC was seen on",[97,137,138],{},"No IP address at all",[11,140,141],{},"The join key across all four is the MAC address. That's why the merge script is keyed on it rather than on IP or hostname.",[62,143,145],{"id":144},"step-1-dhcp-leases-and-ad-computers","Step 1: DHCP Leases and AD Computers",[11,147,148,149,152,153,156,157,160,161,164,165,168,169,172,173,176,177,180],{},"On Windows DHCP, ",[24,150,151],{},"Get-DhcpServerv4Lease"," returns only active leases by default; its ",[24,154,155],{},"-AllLeases"," switch adds offered, declined and expired leases, which is useful here because an expired lease is still evidence that a device existed. It is not evidence the device is there now, so the merge script only treats active leases as current and keeps the rest as history. The cmdlet takes scope objects from ",[24,158,159],{},"Get-DhcpServerv4Scope"," on the pipeline. ",[24,162,163],{},"Get-ADComputer"," needs ",[24,166,167],{},"-Properties"," for anything outside its default set, including ",[24,170,171],{},"IPv4Address",", ",[24,174,175],{},"OperatingSystem"," and ",[24,178,179],{},"LastLogonDate",".",[11,182,183],{},"Run it from a machine with the DhcpServer and ActiveDirectory modules (RSAT). It needs read access to each DHCP server and an ordinary domain account for the AD query.",[185,186,191],"pre",{"className":187,"code":188,"language":189,"meta":190,"style":190},"language-powershell shiki shiki-themes github-dark","\u003C#\n.SYNOPSIS\n    Exports DHCP leases and Active Directory computer accounts for discovery.\n.DESCRIPTION\n    Reads every IPv4 scope on each DHCP server, including offered, declined and\n    expired leases, and writes dhcp-leases.csv. Then exports every AD computer\n    account with its DNS name, IPv4 address, operating system and last logon\n    date to ad-computers.csv. Both files feed merge_inventory.py.\n.PARAMETER DhcpServer\n    One or more Windows DHCP servers to read.\n.PARAMETER OutputPath\n    Folder for the two CSV files. Created if missing.\n.PARAMETER SearchBase\n    Optional distinguished name to limit the AD computer export.\n.EXAMPLE\n    .\\Export-PassiveInventory.ps1 -DhcpServer dhcp01.corp.example -OutputPath C:\\Discovery\\2026-07-15\n.NOTES\n    Requires the DhcpServer and ActiveDirectory modules (RSAT).\n#>\n[CmdletBinding()]\nparam(\n    [Parameter(Mandatory)]\n    [string[]]$DhcpServer,\n\n    [Parameter(Mandatory)]\n    [string]$OutputPath,\n\n    [string]$SearchBase\n)\n\n$ErrorActionPreference = 'Stop'\nImport-Module DhcpServer, ActiveDirectory\nNew-Item -ItemType Directory -Path $OutputPath -Force | Out-Null\n\n$leases = foreach ($server in $DhcpServer) {\n    $scopes = Get-DhcpServerv4Scope -ComputerName $server\n    foreach ($scope in $scopes) {\n        try {\n            Get-DhcpServerv4Lease -ComputerName $server -ScopeId $scope.ScopeId -AllLeases |\n                Select-Object -Property IPAddress, ClientId, HostName, AddressState, LeaseExpiryTime,\n                    @{ Name = 'ScopeId'; Expression = { $scope.ScopeId } },\n                    @{ Name = 'Server'; Expression = { $server } }\n        }\n        catch {\n            Write-Warning \"Could not read scope $($scope.ScopeId) on ${server}: $($_.Exception.Message)\"\n        }\n    }\n}\n$leases | Export-Csv -Path (Join-Path -Path $OutputPath -ChildPath 'dhcp-leases.csv') -NoTypeInformation\n\n$adParams = @{\n    Filter     = '*'\n    Properties = 'DNSHostName', 'IPv4Address', 'OperatingSystem', 'LastLogonDate', 'Enabled'\n}\nif ($SearchBase) { $adParams.SearchBase = $SearchBase }\n\n$computers = Get-ADComputer @adParams |\n    Select-Object -Property Name, DNSHostName, IPv4Address, OperatingSystem, LastLogonDate, Enabled\n$computers | Export-Csv -Path (Join-Path -Path $OutputPath -ChildPath 'ad-computers.csv') -NoTypeInformation\n\nWrite-Output \"$(@($leases).Count) DHCP leases and $(@($computers).Count) AD computers written to $OutputPath\"\n","powershell","",[24,192,193,202,212,218,226,232,238,244,250,261,267,277,283,293,299,307,313,321,327,333,346,355,374,388,395,408,420,425,435,441,446,459,474,503,508,529,545,559,568,592,625,649,668,674,682,726,731,737,743,781,786,800,811,842,847,861,866,882,918,950,955],{"__ignoreMap":190},[194,195,198],"span",{"class":196,"line":197},"line",1,[194,199,201],{"class":200},"sAwPA","\u003C#\n",[194,203,205,208],{"class":196,"line":204},2,[194,206,180],{"class":207},"sDLfK",[194,209,211],{"class":210},"snl16","SYNOPSIS\n",[194,213,215],{"class":196,"line":214},3,[194,216,217],{"class":200},"    Exports DHCP leases and Active Directory computer accounts for discovery.\n",[194,219,221,223],{"class":196,"line":220},4,[194,222,180],{"class":207},[194,224,225],{"class":210},"DESCRIPTION\n",[194,227,229],{"class":196,"line":228},5,[194,230,231],{"class":200},"    Reads every IPv4 scope on each DHCP server, including offered, declined and\n",[194,233,235],{"class":196,"line":234},6,[194,236,237],{"class":200},"    expired leases, and writes dhcp-leases.csv. Then exports every AD computer\n",[194,239,241],{"class":196,"line":240},7,[194,242,243],{"class":200},"    account with its DNS name, IPv4 address, operating system and last logon\n",[194,245,247],{"class":196,"line":246},8,[194,248,249],{"class":200},"    date to ad-computers.csv. Both files feed merge_inventory.py.\n",[194,251,253,255,258],{"class":196,"line":252},9,[194,254,180],{"class":207},[194,256,257],{"class":210},"PARAMETER",[194,259,260],{"class":210}," DhcpServer\n",[194,262,264],{"class":196,"line":263},10,[194,265,266],{"class":200},"    One or more Windows DHCP servers to read.\n",[194,268,270,272,274],{"class":196,"line":269},11,[194,271,180],{"class":207},[194,273,257],{"class":210},[194,275,276],{"class":210}," OutputPath\n",[194,278,280],{"class":196,"line":279},12,[194,281,282],{"class":200},"    Folder for the two CSV files. Created if missing.\n",[194,284,286,288,290],{"class":196,"line":285},13,[194,287,180],{"class":207},[194,289,257],{"class":210},[194,291,292],{"class":210}," SearchBase\n",[194,294,296],{"class":196,"line":295},14,[194,297,298],{"class":200},"    Optional distinguished name to limit the AD computer export.\n",[194,300,302,304],{"class":196,"line":301},15,[194,303,180],{"class":207},[194,305,306],{"class":210},"EXAMPLE\n",[194,308,310],{"class":196,"line":309},16,[194,311,312],{"class":200},"    .\\Export-PassiveInventory.ps1 -DhcpServer dhcp01.corp.example -OutputPath C:\\Discovery\\2026-07-15\n",[194,314,316,318],{"class":196,"line":315},17,[194,317,180],{"class":207},[194,319,320],{"class":210},"NOTES\n",[194,322,324],{"class":196,"line":323},18,[194,325,326],{"class":200},"    Requires the DhcpServer and ActiveDirectory modules (RSAT).\n",[194,328,330],{"class":196,"line":329},19,[194,331,332],{"class":200},"#>\n",[194,334,336,340,343],{"class":196,"line":335},20,[194,337,339],{"class":338},"s95oV","[",[194,341,342],{"class":207},"CmdletBinding",[194,344,345],{"class":338},"()]\n",[194,347,349,352],{"class":196,"line":348},21,[194,350,351],{"class":210},"param",[194,353,354],{"class":338},"(\n",[194,356,358,361,364,367,371],{"class":196,"line":357},22,[194,359,360],{"class":338},"    [",[194,362,363],{"class":207},"Parameter",[194,365,366],{"class":338},"(",[194,368,370],{"class":369},"s9osk","Mandatory",[194,372,373],{"class":338},")]\n",[194,375,377,379,382,385],{"class":196,"line":376},23,[194,378,360],{"class":338},[194,380,381],{"class":210},"string",[194,383,384],{"class":338},"[]]$DhcpServer",[194,386,387],{"class":210},",\n",[194,389,391],{"class":196,"line":390},24,[194,392,394],{"emptyLinePlaceholder":393},true,"\n",[194,396,398,400,402,404,406],{"class":196,"line":397},25,[194,399,360],{"class":338},[194,401,363],{"class":207},[194,403,366],{"class":338},[194,405,370],{"class":369},[194,407,373],{"class":338},[194,409,411,413,415,418],{"class":196,"line":410},26,[194,412,360],{"class":338},[194,414,381],{"class":210},[194,416,417],{"class":338},"]$OutputPath",[194,419,387],{"class":210},[194,421,423],{"class":196,"line":422},27,[194,424,394],{"emptyLinePlaceholder":393},[194,426,428,430,432],{"class":196,"line":427},28,[194,429,360],{"class":338},[194,431,381],{"class":210},[194,433,434],{"class":338},"]$SearchBase\n",[194,436,438],{"class":196,"line":437},29,[194,439,440],{"class":338},")\n",[194,442,444],{"class":196,"line":443},30,[194,445,394],{"emptyLinePlaceholder":393},[194,447,449,452,455],{"class":196,"line":448},31,[194,450,451],{"class":207},"$ErrorActionPreference",[194,453,454],{"class":210}," =",[194,456,458],{"class":457},"sU2Wk"," 'Stop'\n",[194,460,462,465,468,471],{"class":196,"line":461},32,[194,463,464],{"class":207},"Import-Module",[194,466,467],{"class":338}," DhcpServer",[194,469,470],{"class":210},",",[194,472,473],{"class":338}," ActiveDirectory\n",[194,475,477,480,483,486,489,492,494,497,500],{"class":196,"line":476},33,[194,478,479],{"class":207},"New-Item",[194,481,482],{"class":210}," -",[194,484,485],{"class":338},"ItemType Directory ",[194,487,488],{"class":210},"-",[194,490,491],{"class":338},"Path $OutputPath ",[194,493,488],{"class":210},[194,495,496],{"class":338},"Force ",[194,498,499],{"class":210},"|",[194,501,502],{"class":207}," Out-Null\n",[194,504,506],{"class":196,"line":505},34,[194,507,394],{"emptyLinePlaceholder":393},[194,509,511,514,517,520,523,526],{"class":196,"line":510},35,[194,512,513],{"class":338},"$leases ",[194,515,516],{"class":210},"=",[194,518,519],{"class":210}," foreach",[194,521,522],{"class":338}," ($server ",[194,524,525],{"class":210},"in",[194,527,528],{"class":338}," $DhcpServer) {\n",[194,530,532,535,537,540,542],{"class":196,"line":531},36,[194,533,534],{"class":338},"    $scopes ",[194,536,516],{"class":210},[194,538,539],{"class":207}," Get-DhcpServerv4Scope",[194,541,482],{"class":210},[194,543,544],{"class":338},"ComputerName $server\n",[194,546,548,551,554,556],{"class":196,"line":547},37,[194,549,550],{"class":210},"    foreach",[194,552,553],{"class":338}," ($scope ",[194,555,525],{"class":210},[194,557,558],{"class":338}," $scopes) {\n",[194,560,562,565],{"class":196,"line":561},38,[194,563,564],{"class":210},"        try",[194,566,567],{"class":338}," {\n",[194,569,571,574,576,579,581,584,586,589],{"class":196,"line":570},39,[194,572,573],{"class":207},"            Get-DhcpServerv4Lease",[194,575,482],{"class":210},[194,577,578],{"class":338},"ComputerName $server ",[194,580,488],{"class":210},[194,582,583],{"class":338},"ScopeId $scope.ScopeId ",[194,585,488],{"class":210},[194,587,588],{"class":338},"AllLeases ",[194,590,591],{"class":210},"|\n",[194,593,595,598,600,603,605,608,610,613,615,618,620,623],{"class":196,"line":594},40,[194,596,597],{"class":207},"                Select-Object",[194,599,482],{"class":210},[194,601,602],{"class":338},"Property IPAddress",[194,604,470],{"class":210},[194,606,607],{"class":338}," ClientId",[194,609,470],{"class":210},[194,611,612],{"class":338}," HostName",[194,614,470],{"class":210},[194,616,617],{"class":338}," AddressState",[194,619,470],{"class":210},[194,621,622],{"class":338}," LeaseExpiryTime",[194,624,387],{"class":210},[194,626,628,631,634,636,639,642,644,647],{"class":196,"line":627},41,[194,629,630],{"class":210},"                    @",[194,632,633],{"class":338},"{ Name ",[194,635,516],{"class":210},[194,637,638],{"class":457}," 'ScopeId'",[194,640,641],{"class":338},"; Expression ",[194,643,516],{"class":210},[194,645,646],{"class":338}," { $scope.ScopeId } }",[194,648,387],{"class":210},[194,650,652,654,656,658,661,663,665],{"class":196,"line":651},42,[194,653,630],{"class":210},[194,655,633],{"class":338},[194,657,516],{"class":210},[194,659,660],{"class":457}," 'Server'",[194,662,641],{"class":338},[194,664,516],{"class":210},[194,666,667],{"class":338}," { $server } }\n",[194,669,671],{"class":196,"line":670},43,[194,672,673],{"class":338},"        }\n",[194,675,677,680],{"class":196,"line":676},44,[194,678,679],{"class":210},"        catch",[194,681,567],{"class":338},[194,683,685,688,691,694,696,699,702,705,708,711,713,715,718,721,723],{"class":196,"line":684},45,[194,686,687],{"class":207},"            Write-Warning",[194,689,690],{"class":457}," \"Could not read scope ",[194,692,693],{"class":210},"$",[194,695,366],{"class":457},[194,697,698],{"class":338},"$scope.ScopeId",[194,700,701],{"class":457},")",[194,703,704],{"class":457}," on ",[194,706,707],{"class":338},"${server}",[194,709,710],{"class":457},": ",[194,712,693],{"class":210},[194,714,366],{"class":457},[194,716,717],{"class":207},"$_",[194,719,720],{"class":338},".Exception.Message",[194,722,701],{"class":457},[194,724,725],{"class":457},"\"\n",[194,727,729],{"class":196,"line":728},46,[194,730,673],{"class":338},[194,732,734],{"class":196,"line":733},47,[194,735,736],{"class":338},"    }\n",[194,738,740],{"class":196,"line":739},48,[194,741,742],{"class":338},"}\n",[194,744,746,748,750,753,755,758,761,763,765,767,770,773,776,778],{"class":196,"line":745},49,[194,747,513],{"class":338},[194,749,499],{"class":210},[194,751,752],{"class":207}," Export-Csv",[194,754,482],{"class":210},[194,756,757],{"class":338},"Path (",[194,759,760],{"class":207},"Join-Path",[194,762,482],{"class":210},[194,764,491],{"class":338},[194,766,488],{"class":210},[194,768,769],{"class":338},"ChildPath ",[194,771,772],{"class":457},"'dhcp-leases.csv'",[194,774,775],{"class":338},") ",[194,777,488],{"class":210},[194,779,780],{"class":338},"NoTypeInformation\n",[194,782,784],{"class":196,"line":783},50,[194,785,394],{"emptyLinePlaceholder":393},[194,787,789,792,794,797],{"class":196,"line":788},51,[194,790,791],{"class":338},"$adParams ",[194,793,516],{"class":210},[194,795,796],{"class":210}," @",[194,798,799],{"class":338},"{\n",[194,801,803,806,808],{"class":196,"line":802},52,[194,804,805],{"class":338},"    Filter     ",[194,807,516],{"class":210},[194,809,810],{"class":457}," '*'\n",[194,812,814,817,819,822,824,827,829,832,834,837,839],{"class":196,"line":813},53,[194,815,816],{"class":338},"    Properties ",[194,818,516],{"class":210},[194,820,821],{"class":457}," 'DNSHostName'",[194,823,470],{"class":210},[194,825,826],{"class":457}," 'IPv4Address'",[194,828,470],{"class":210},[194,830,831],{"class":457}," 'OperatingSystem'",[194,833,470],{"class":210},[194,835,836],{"class":457}," 'LastLogonDate'",[194,838,470],{"class":210},[194,840,841],{"class":457}," 'Enabled'\n",[194,843,845],{"class":196,"line":844},54,[194,846,742],{"class":338},[194,848,850,853,856,858],{"class":196,"line":849},55,[194,851,852],{"class":210},"if",[194,854,855],{"class":338}," ($SearchBase) { $adParams.SearchBase ",[194,857,516],{"class":210},[194,859,860],{"class":338}," $SearchBase }\n",[194,862,864],{"class":196,"line":863},56,[194,865,394],{"emptyLinePlaceholder":393},[194,867,869,872,874,877,880],{"class":196,"line":868},57,[194,870,871],{"class":338},"$computers ",[194,873,516],{"class":210},[194,875,876],{"class":207}," Get-ADComputer",[194,878,879],{"class":338}," @adParams ",[194,881,591],{"class":210},[194,883,885,888,890,893,895,898,900,903,905,908,910,913,915],{"class":196,"line":884},58,[194,886,887],{"class":207},"    Select-Object",[194,889,482],{"class":210},[194,891,892],{"class":338},"Property Name",[194,894,470],{"class":210},[194,896,897],{"class":338}," DNSHostName",[194,899,470],{"class":210},[194,901,902],{"class":338}," IPv4Address",[194,904,470],{"class":210},[194,906,907],{"class":338}," OperatingSystem",[194,909,470],{"class":210},[194,911,912],{"class":338}," LastLogonDate",[194,914,470],{"class":210},[194,916,917],{"class":338}," Enabled\n",[194,919,921,923,925,927,929,931,933,935,937,939,941,944,946,948],{"class":196,"line":920},59,[194,922,871],{"class":338},[194,924,499],{"class":210},[194,926,752],{"class":207},[194,928,482],{"class":210},[194,930,757],{"class":338},[194,932,760],{"class":207},[194,934,482],{"class":210},[194,936,491],{"class":338},[194,938,488],{"class":210},[194,940,769],{"class":338},[194,942,943],{"class":457},"'ad-computers.csv'",[194,945,775],{"class":338},[194,947,488],{"class":210},[194,949,780],{"class":338},[194,951,953],{"class":196,"line":952},60,[194,954,394],{"emptyLinePlaceholder":393},[194,956,958,961,964,966,968,971,973,976,979,982,984,986,988,990,993,995,998,1001],{"class":196,"line":957},61,[194,959,960],{"class":207},"Write-Output",[194,962,963],{"class":457}," \"",[194,965,693],{"class":210},[194,967,366],{"class":457},[194,969,970],{"class":210},"@",[194,972,366],{"class":457},[194,974,975],{"class":338},"$leases",[194,977,978],{"class":457},").Count)",[194,980,981],{"class":457}," DHCP leases and ",[194,983,693],{"class":210},[194,985,366],{"class":457},[194,987,970],{"class":210},[194,989,366],{"class":457},[194,991,992],{"class":338},"$computers",[194,994,978],{"class":457},[194,996,997],{"class":457}," AD computers written to ",[194,999,1000],{"class":338},"$OutputPath",[194,1002,725],{"class":457},[185,1004,1006],{"className":187,"code":1005,"language":189,"meta":190,"style":190},".\\Export-PassiveInventory.ps1 -DhcpServer dhcp01.corp.example, dhcp02.corp.example -OutputPath C:\\Discovery\\2026-07-15\n",[24,1007,1008],{"__ignoreMap":190},[194,1009,1010,1012,1015,1017,1020,1022,1025,1027,1030,1033,1035,1038,1040],{"class":196,"line":197},[194,1011,180],{"class":338},[194,1013,1014],{"class":207},"\\Export-PassiveInventory.ps1",[194,1016,482],{"class":210},[194,1018,1019],{"class":338},"DhcpServer dhcp01.corp.example",[194,1021,470],{"class":210},[194,1023,1024],{"class":338}," dhcp02.corp.example ",[194,1026,488],{"class":210},[194,1028,1029],{"class":338},"OutputPath C:\\Discovery\\",[194,1031,1032],{"class":207},"2026",[194,1034,488],{"class":210},[194,1036,1037],{"class":207},"07",[194,1039,488],{"class":210},[194,1041,1042],{"class":207},"15\n",[62,1044,1046],{"id":1045},"step-2-arp-and-mac-address-tables-over-snmp","Step 2: ARP and MAC Address Tables over SNMP",[11,1048,1049],{},"Switches and routers publish their tables in standard MIBs, so one collector works across vendors:",[1051,1052,1053,1071,1097],"ul",{},[21,1054,1055,710,1059,1062,1063,1066,1067,1070],{},[1056,1057,1058],"strong",{},"ARP",[24,1060,1061],{},"ipNetToMediaPhysAddress"," (",[24,1064,1065],{},"1.3.6.1.2.1.4.22.1.2",") in the IP-MIB. The table is indexed by interface and IP address, so the IP comes out of the row index and the MAC is the value. RFC 4293 superseded this table with ",[24,1068,1069],{},"ipNetToPhysicalTable",", which adds IPv6, but the older IPv4 table is the one most gear still answers. If yours doesn't, that's the table to switch to.",[21,1072,1073,710,1076,1062,1079,1082,1083,1086,1087,172,1090,172,1093,1096],{},[1056,1074,1075],{},"MAC table",[24,1077,1078],{},"dot1dTpFdbPort",[24,1080,1081],{},"1.3.6.1.2.1.17.4.3.1.2",") in the BRIDGE-MIB (RFC 4188). The MAC is encoded in the row index as six decimal numbers and the value is a bridge port number. ",[24,1084,1085],{},"dot1dTpFdbStatus"," marks each entry as ",[24,1088,1089],{},"learned(3)",[24,1091,1092],{},"self(4)",[24,1094,1095],{},"mgmt(5)"," and so on.",[21,1098,1099,1102,1103,1062,1106,1109,1110,1113,1114,1062,1117,1120,1121,180],{},[1056,1100,1101],{},"Port names",": bridge port numbers aren't interface numbers. ",[24,1104,1105],{},"dot1dBasePortIfIndex",[24,1107,1108],{},"1.3.6.1.2.1.17.1.4.1.2",") maps bridge port to ",[24,1111,1112],{},"ifIndex",", and ",[24,1115,1116],{},"ifName",[24,1118,1119],{},"1.3.6.1.2.1.31.1.1.1.1",") turns that into ",[24,1122,1123],{},"Gi1\u002F0\u002F12",[11,1125,1126,1127,1131,1132,1135,1136,1139],{},"One vendor quirk matters a lot. Cisco Catalyst switches keep a separate BRIDGE-MIB instance per VLAN, reached with ",[1128,1129,1130],"em",{},"community string indexing",": query with ",[24,1133,1134],{},"community@20"," to get VLAN 20's MAC table. Without it you only see VLAN 1. The collector takes a ",[24,1137,1138],{},"--vlans"," list for that.",[11,1141,1142,1143,1146,1147,1150,1151,1154,1155,1158,1159,1162,1163,1166,1167,1146,1169,1172],{},"The script shells out to Net-SNMP's ",[24,1144,1145],{},"snmpbulkwalk"," with ",[24,1148,1149],{},"-On"," (numeric OIDs), ",[24,1152,1153],{},"-Oq"," (no type labels), ",[24,1156,1157],{},"-Oe"," (numeric enums), ",[24,1160,1161],{},"-Ob"," (numeric indexes) and ",[24,1164,1165],{},"-Ox"," (hex strings, for MAC values). It reads the community string from an environment variable, which keeps it out of scripts and shell history. It is still passed to ",[24,1168,1145],{},[24,1170,1171],{},"-c",", so anyone who can list processes on the collector can see it while a walk runs: run the collector on a host only you can log in to. SNMPv2c sends that string in clear text, so use a read-only community restricted by ACL to your collector, or move to SNMPv3 if the gear supports it.",[185,1174,1178],{"className":1175,"code":1176,"language":1177,"meta":190,"style":190},"language-python shiki shiki-themes github-dark","#!\u002Fusr\u002Fbin\u002Fenv python3\n\"\"\"Read ARP and MAC address tables from routers and switches over SNMP.\n\nWrites arp.csv (IP -> MAC per router) and fdb.csv (MAC -> switch port per switch).\nNeeds Net-SNMP's snmpbulkwalk on PATH and read-only SNMP access to each device.\nThe community string comes from the SNMP_COMMUNITY environment variable so it\nstays out of scripts and shell history. It is still visible in the process list\nwhile snmpbulkwalk runs, so run this on a host only you can log in to.\n\"\"\"\nimport argparse\nimport csv\nimport os\nimport subprocess\nimport sys\n\nARP_PHYS = \".1.3.6.1.2.1.4.22.1.2\"        # IP-MIB ipNetToMediaPhysAddress\nFDB_PORT = \".1.3.6.1.2.1.17.4.3.1.2\"      # BRIDGE-MIB dot1dTpFdbPort\nFDB_STATUS = \".1.3.6.1.2.1.17.4.3.1.3\"    # BRIDGE-MIB dot1dTpFdbStatus\nBASE_PORT_IFINDEX = \".1.3.6.1.2.1.17.1.4.1.2\"  # BRIDGE-MIB dot1dBasePortIfIndex\nIF_NAME = \".1.3.6.1.2.1.31.1.1.1.1\"       # IF-MIB ifName\nFDB_STATUS_NAMES = {\"1\": \"other\", \"2\": \"invalid\", \"3\": \"learned\", \"4\": \"self\", \"5\": \"mgmt\"}\n\n\ndef walk(host, community, oid, hex_strings=False):\n    \"\"\"Return [(index_suffix, value)] for every row under oid.\"\"\"\n    fmt = \"-Onqeb\" + (\"x\" if hex_strings else \"\")\n    cmd = [\"snmpbulkwalk\", \"-v2c\", \"-c\", community, \"-t\", \"2\", \"-r\", \"1\", fmt, host, oid]\n    result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)\n    if result.returncode != 0:\n        print(f\"warning: {host} {oid}: {result.stderr.strip()}\", file=sys.stderr)\n        return []\n    rows = []\n    for line in result.stdout.splitlines():\n        if not line.startswith(oid + \".\"):\n            continue  # \"No Such Object\" and similar end-of-table lines\n        name, _, value = line.partition(\" \")\n        rows.append((name[len(oid) + 1:], value.strip().strip('\"')))\n    return rows\n\n\ndef hex_to_mac(value):\n    octets = value.replace(\":\", \" \").split()\n    if len(octets) != 6:\n        return None\n    return \":\".join(o.lower().zfill(2) for o in octets)\n\n\ndef collect_arp(host, community):\n    rows = []\n    for index, value in walk(host, community, ARP_PHYS, hex_strings=True):\n        parts = index.split(\".\")\n        if len(parts) != 5:\n            continue\n        mac = hex_to_mac(value)\n        if mac:\n            rows.append({\"router\": host, \"ifindex\": parts[0], \"ip\": \".\".join(parts[1:]), \"mac\": mac})\n    return rows\n\n\ndef collect_fdb(host, community, vlans):\n    if_names = dict(walk(host, community, IF_NAME))\n    rows = []\n    # Cisco Catalyst keeps one BRIDGE-MIB instance per VLAN, reached with community@vlan.\n    contexts = [(community + \"@\" + v, v) for v in vlans] if vlans else [(community, \"\")]\n    for ctx_community, vlan in contexts:\n        port_ifindex = dict(walk(host, ctx_community, BASE_PORT_IFINDEX))\n        status = dict(walk(host, ctx_community, FDB_STATUS))\n        for index, port in walk(host, ctx_community, FDB_PORT):\n            octets = index.split(\".\")\n            if len(octets) != 6 or port == \"0\":\n                continue\n            ifindex = port_ifindex.get(port, \"\")\n            rows.append({\n                \"switch\": host,\n                \"vlan\": vlan,\n                \"mac\": \":\".join(f\"{int(o):02x}\" for o in octets),\n                \"bridge_port\": port,\n                \"port_name\": if_names.get(ifindex, \"\"),\n                \"status\": FDB_STATUS_NAMES.get(status.get(index, \"\"), \"\"),\n            })\n    return rows\n\n\ndef write_csv(path, rows, fields):\n    with open(path, \"w\", newline=\"\") as handle:\n        writer = csv.DictWriter(handle, fieldnames=fields)\n        writer.writeheader()\n        writer.writerows(rows)\n\n\ndef main():\n    parser = argparse.ArgumentParser(description=\"Read ARP and MAC address tables from routers and switches over SNMP.\")\n    parser.add_argument(\"--routers\", nargs=\"*\", default=[], help=\"Layer 3 devices to read ARP tables from\")\n    parser.add_argument(\"--switches\", nargs=\"*\", default=[], help=\"Switches to read MAC address tables from\")\n    parser.add_argument(\"--vlans\", nargs=\"*\", default=[], help=\"VLAN IDs for Cisco community@vlan indexing\")\n    parser.add_argument(\"--out\", default=\".\", help=\"Output directory\")\n    args = parser.parse_args()\n\n    community = os.environ.get(\"SNMP_COMMUNITY\")\n    if not community:\n        sys.exit(\"Set SNMP_COMMUNITY to a read-only community string.\")\n\n    os.makedirs(args.out, exist_ok=True)\n    arp = [row for host in args.routers for row in collect_arp(host, community)]\n    fdb = [row for host in args.switches for row in collect_fdb(host, community, args.vlans)]\n    write_csv(os.path.join(args.out, \"arp.csv\"), arp, [\"router\", \"ifindex\", \"ip\", \"mac\"])\n    write_csv(os.path.join(args.out, \"fdb.csv\"), fdb, [\"switch\", \"vlan\", \"mac\", \"bridge_port\", \"port_name\", \"status\"])\n    print(f\"{len(arp)} ARP entries, {len(fdb)} MAC table entries\")\n\n\nif __name__ == \"__main__\":\n    main()\n","python",[24,1179,1180,1185,1190,1194,1199,1204,1209,1214,1219,1224,1232,1239,1246,1253,1260,1264,1277,1290,1303,1316,1329,1389,1393,1397,1417,1422,1454,1499,1538,1555,1606,1614,1623,1636,1655,1663,1678,1703,1711,1715,1719,1729,1749,1766,1773,1799,1803,1807,1817,1825,1850,1865,1881,1886,1896,1903,1948,1954,1958,1962,1972,1990,1999,2005,2051,2064,2081,2097,2115,2129,2157,2163,2178,2184,2193,2202,2242,2251,2265,2287,2293,2300,2305,2310,2321,2353,2372,2378,2384,2389,2394,2405,2426,2465,2498,2531,2558,2569,2574,2590,2600,2611,2616,2631,2662,2690,2719,2759,2794,2799,2804,2820],{"__ignoreMap":190},[194,1181,1182],{"class":196,"line":197},[194,1183,1184],{"class":200},"#!\u002Fusr\u002Fbin\u002Fenv python3\n",[194,1186,1187],{"class":196,"line":204},[194,1188,1189],{"class":457},"\"\"\"Read ARP and MAC address tables from routers and switches over SNMP.\n",[194,1191,1192],{"class":196,"line":214},[194,1193,394],{"emptyLinePlaceholder":393},[194,1195,1196],{"class":196,"line":220},[194,1197,1198],{"class":457},"Writes arp.csv (IP -> MAC per router) and fdb.csv (MAC -> switch port per switch).\n",[194,1200,1201],{"class":196,"line":228},[194,1202,1203],{"class":457},"Needs Net-SNMP's snmpbulkwalk on PATH and read-only SNMP access to each device.\n",[194,1205,1206],{"class":196,"line":234},[194,1207,1208],{"class":457},"The community string comes from the SNMP_COMMUNITY environment variable so it\n",[194,1210,1211],{"class":196,"line":240},[194,1212,1213],{"class":457},"stays out of scripts and shell history. It is still visible in the process list\n",[194,1215,1216],{"class":196,"line":246},[194,1217,1218],{"class":457},"while snmpbulkwalk runs, so run this on a host only you can log in to.\n",[194,1220,1221],{"class":196,"line":252},[194,1222,1223],{"class":457},"\"\"\"\n",[194,1225,1226,1229],{"class":196,"line":263},[194,1227,1228],{"class":210},"import",[194,1230,1231],{"class":338}," argparse\n",[194,1233,1234,1236],{"class":196,"line":269},[194,1235,1228],{"class":210},[194,1237,1238],{"class":338}," csv\n",[194,1240,1241,1243],{"class":196,"line":279},[194,1242,1228],{"class":210},[194,1244,1245],{"class":338}," os\n",[194,1247,1248,1250],{"class":196,"line":285},[194,1249,1228],{"class":210},[194,1251,1252],{"class":338}," subprocess\n",[194,1254,1255,1257],{"class":196,"line":295},[194,1256,1228],{"class":210},[194,1258,1259],{"class":338}," sys\n",[194,1261,1262],{"class":196,"line":301},[194,1263,394],{"emptyLinePlaceholder":393},[194,1265,1266,1269,1271,1274],{"class":196,"line":309},[194,1267,1268],{"class":207},"ARP_PHYS",[194,1270,454],{"class":210},[194,1272,1273],{"class":457}," \".1.3.6.1.2.1.4.22.1.2\"",[194,1275,1276],{"class":200},"        # IP-MIB ipNetToMediaPhysAddress\n",[194,1278,1279,1282,1284,1287],{"class":196,"line":315},[194,1280,1281],{"class":207},"FDB_PORT",[194,1283,454],{"class":210},[194,1285,1286],{"class":457}," \".1.3.6.1.2.1.17.4.3.1.2\"",[194,1288,1289],{"class":200},"      # BRIDGE-MIB dot1dTpFdbPort\n",[194,1291,1292,1295,1297,1300],{"class":196,"line":323},[194,1293,1294],{"class":207},"FDB_STATUS",[194,1296,454],{"class":210},[194,1298,1299],{"class":457}," \".1.3.6.1.2.1.17.4.3.1.3\"",[194,1301,1302],{"class":200},"    # BRIDGE-MIB dot1dTpFdbStatus\n",[194,1304,1305,1308,1310,1313],{"class":196,"line":329},[194,1306,1307],{"class":207},"BASE_PORT_IFINDEX",[194,1309,454],{"class":210},[194,1311,1312],{"class":457}," \".1.3.6.1.2.1.17.1.4.1.2\"",[194,1314,1315],{"class":200},"  # BRIDGE-MIB dot1dBasePortIfIndex\n",[194,1317,1318,1321,1323,1326],{"class":196,"line":335},[194,1319,1320],{"class":207},"IF_NAME",[194,1322,454],{"class":210},[194,1324,1325],{"class":457}," \".1.3.6.1.2.1.31.1.1.1.1\"",[194,1327,1328],{"class":200},"       # IF-MIB ifName\n",[194,1330,1331,1334,1336,1339,1342,1344,1347,1349,1352,1354,1357,1359,1362,1364,1367,1369,1372,1374,1377,1379,1382,1384,1387],{"class":196,"line":348},[194,1332,1333],{"class":207},"FDB_STATUS_NAMES",[194,1335,454],{"class":210},[194,1337,1338],{"class":338}," {",[194,1340,1341],{"class":457},"\"1\"",[194,1343,710],{"class":338},[194,1345,1346],{"class":457},"\"other\"",[194,1348,172],{"class":338},[194,1350,1351],{"class":457},"\"2\"",[194,1353,710],{"class":338},[194,1355,1356],{"class":457},"\"invalid\"",[194,1358,172],{"class":338},[194,1360,1361],{"class":457},"\"3\"",[194,1363,710],{"class":338},[194,1365,1366],{"class":457},"\"learned\"",[194,1368,172],{"class":338},[194,1370,1371],{"class":457},"\"4\"",[194,1373,710],{"class":338},[194,1375,1376],{"class":457},"\"self\"",[194,1378,172],{"class":338},[194,1380,1381],{"class":457},"\"5\"",[194,1383,710],{"class":338},[194,1385,1386],{"class":457},"\"mgmt\"",[194,1388,742],{"class":338},[194,1390,1391],{"class":196,"line":357},[194,1392,394],{"emptyLinePlaceholder":393},[194,1394,1395],{"class":196,"line":376},[194,1396,394],{"emptyLinePlaceholder":393},[194,1398,1399,1402,1406,1409,1411,1414],{"class":196,"line":390},[194,1400,1401],{"class":210},"def",[194,1403,1405],{"class":1404},"svObZ"," walk",[194,1407,1408],{"class":338},"(host, community, oid, hex_strings",[194,1410,516],{"class":210},[194,1412,1413],{"class":207},"False",[194,1415,1416],{"class":338},"):\n",[194,1418,1419],{"class":196,"line":397},[194,1420,1421],{"class":457},"    \"\"\"Return [(index_suffix, value)] for every row under oid.\"\"\"\n",[194,1423,1424,1427,1429,1432,1435,1437,1440,1443,1446,1449,1452],{"class":196,"line":410},[194,1425,1426],{"class":338},"    fmt ",[194,1428,516],{"class":210},[194,1430,1431],{"class":457}," \"-Onqeb\"",[194,1433,1434],{"class":210}," +",[194,1436,1062],{"class":338},[194,1438,1439],{"class":457},"\"x\"",[194,1441,1442],{"class":210}," if",[194,1444,1445],{"class":338}," hex_strings ",[194,1447,1448],{"class":210},"else",[194,1450,1451],{"class":457}," \"\"",[194,1453,440],{"class":338},[194,1455,1456,1459,1461,1464,1467,1469,1472,1474,1477,1480,1483,1485,1487,1489,1492,1494,1496],{"class":196,"line":422},[194,1457,1458],{"class":338},"    cmd ",[194,1460,516],{"class":210},[194,1462,1463],{"class":338}," [",[194,1465,1466],{"class":457},"\"snmpbulkwalk\"",[194,1468,172],{"class":338},[194,1470,1471],{"class":457},"\"-v2c\"",[194,1473,172],{"class":338},[194,1475,1476],{"class":457},"\"-c\"",[194,1478,1479],{"class":338},", community, ",[194,1481,1482],{"class":457},"\"-t\"",[194,1484,172],{"class":338},[194,1486,1351],{"class":457},[194,1488,172],{"class":338},[194,1490,1491],{"class":457},"\"-r\"",[194,1493,172],{"class":338},[194,1495,1341],{"class":457},[194,1497,1498],{"class":338},", fmt, host, oid]\n",[194,1500,1501,1504,1506,1509,1512,1514,1517,1519,1522,1524,1526,1528,1531,1533,1536],{"class":196,"line":427},[194,1502,1503],{"class":338},"    result ",[194,1505,516],{"class":210},[194,1507,1508],{"class":338}," subprocess.run(cmd, ",[194,1510,1511],{"class":369},"capture_output",[194,1513,516],{"class":210},[194,1515,1516],{"class":207},"True",[194,1518,172],{"class":338},[194,1520,1521],{"class":369},"text",[194,1523,516],{"class":210},[194,1525,1516],{"class":207},[194,1527,172],{"class":338},[194,1529,1530],{"class":369},"timeout",[194,1532,516],{"class":210},[194,1534,1535],{"class":207},"300",[194,1537,440],{"class":338},[194,1539,1540,1543,1546,1549,1552],{"class":196,"line":437},[194,1541,1542],{"class":210},"    if",[194,1544,1545],{"class":338}," result.returncode ",[194,1547,1548],{"class":210},"!=",[194,1550,1551],{"class":207}," 0",[194,1553,1554],{"class":338},":\n",[194,1556,1557,1560,1562,1565,1568,1571,1574,1577,1579,1582,1584,1586,1588,1591,1593,1596,1598,1601,1603],{"class":196,"line":443},[194,1558,1559],{"class":207},"        print",[194,1561,366],{"class":338},[194,1563,1564],{"class":210},"f",[194,1566,1567],{"class":457},"\"warning: ",[194,1569,1570],{"class":207},"{",[194,1572,1573],{"class":338},"host",[194,1575,1576],{"class":207},"}",[194,1578,1338],{"class":207},[194,1580,1581],{"class":338},"oid",[194,1583,1576],{"class":207},[194,1585,710],{"class":457},[194,1587,1570],{"class":207},[194,1589,1590],{"class":338},"result.stderr.strip()",[194,1592,1576],{"class":207},[194,1594,1595],{"class":457},"\"",[194,1597,172],{"class":338},[194,1599,1600],{"class":369},"file",[194,1602,516],{"class":210},[194,1604,1605],{"class":338},"sys.stderr)\n",[194,1607,1608,1611],{"class":196,"line":448},[194,1609,1610],{"class":210},"        return",[194,1612,1613],{"class":338}," []\n",[194,1615,1616,1619,1621],{"class":196,"line":461},[194,1617,1618],{"class":338},"    rows ",[194,1620,516],{"class":210},[194,1622,1613],{"class":338},[194,1624,1625,1628,1631,1633],{"class":196,"line":476},[194,1626,1627],{"class":210},"    for",[194,1629,1630],{"class":338}," line ",[194,1632,525],{"class":210},[194,1634,1635],{"class":338}," result.stdout.splitlines():\n",[194,1637,1638,1641,1644,1647,1650,1653],{"class":196,"line":505},[194,1639,1640],{"class":210},"        if",[194,1642,1643],{"class":210}," not",[194,1645,1646],{"class":338}," line.startswith(oid ",[194,1648,1649],{"class":210},"+",[194,1651,1652],{"class":457}," \".\"",[194,1654,1416],{"class":338},[194,1656,1657,1660],{"class":196,"line":510},[194,1658,1659],{"class":210},"            continue",[194,1661,1662],{"class":200},"  # \"No Such Object\" and similar end-of-table lines\n",[194,1664,1665,1668,1670,1673,1676],{"class":196,"line":531},[194,1666,1667],{"class":338},"        name, _, value ",[194,1669,516],{"class":210},[194,1671,1672],{"class":338}," line.partition(",[194,1674,1675],{"class":457},"\" \"",[194,1677,440],{"class":338},[194,1679,1680,1683,1686,1689,1691,1694,1697,1700],{"class":196,"line":547},[194,1681,1682],{"class":338},"        rows.append((name[",[194,1684,1685],{"class":207},"len",[194,1687,1688],{"class":338},"(oid) ",[194,1690,1649],{"class":210},[194,1692,1693],{"class":207}," 1",[194,1695,1696],{"class":338},":], value.strip().strip(",[194,1698,1699],{"class":457},"'\"'",[194,1701,1702],{"class":338},")))\n",[194,1704,1705,1708],{"class":196,"line":561},[194,1706,1707],{"class":210},"    return",[194,1709,1710],{"class":338}," rows\n",[194,1712,1713],{"class":196,"line":570},[194,1714,394],{"emptyLinePlaceholder":393},[194,1716,1717],{"class":196,"line":594},[194,1718,394],{"emptyLinePlaceholder":393},[194,1720,1721,1723,1726],{"class":196,"line":627},[194,1722,1401],{"class":210},[194,1724,1725],{"class":1404}," hex_to_mac",[194,1727,1728],{"class":338},"(value):\n",[194,1730,1731,1734,1736,1739,1742,1744,1746],{"class":196,"line":651},[194,1732,1733],{"class":338},"    octets ",[194,1735,516],{"class":210},[194,1737,1738],{"class":338}," value.replace(",[194,1740,1741],{"class":457},"\":\"",[194,1743,172],{"class":338},[194,1745,1675],{"class":457},[194,1747,1748],{"class":338},").split()\n",[194,1750,1751,1753,1756,1759,1761,1764],{"class":196,"line":670},[194,1752,1542],{"class":210},[194,1754,1755],{"class":207}," len",[194,1757,1758],{"class":338},"(octets) ",[194,1760,1548],{"class":210},[194,1762,1763],{"class":207}," 6",[194,1765,1554],{"class":338},[194,1767,1768,1770],{"class":196,"line":676},[194,1769,1610],{"class":210},[194,1771,1772],{"class":207}," None\n",[194,1774,1775,1777,1780,1783,1786,1788,1791,1794,1796],{"class":196,"line":684},[194,1776,1707],{"class":210},[194,1778,1779],{"class":457}," \":\"",[194,1781,1782],{"class":338},".join(o.lower().zfill(",[194,1784,1785],{"class":207},"2",[194,1787,775],{"class":338},[194,1789,1790],{"class":210},"for",[194,1792,1793],{"class":338}," o ",[194,1795,525],{"class":210},[194,1797,1798],{"class":338}," octets)\n",[194,1800,1801],{"class":196,"line":728},[194,1802,394],{"emptyLinePlaceholder":393},[194,1804,1805],{"class":196,"line":733},[194,1806,394],{"emptyLinePlaceholder":393},[194,1808,1809,1811,1814],{"class":196,"line":739},[194,1810,1401],{"class":210},[194,1812,1813],{"class":1404}," collect_arp",[194,1815,1816],{"class":338},"(host, community):\n",[194,1818,1819,1821,1823],{"class":196,"line":745},[194,1820,1618],{"class":338},[194,1822,516],{"class":210},[194,1824,1613],{"class":338},[194,1826,1827,1829,1832,1834,1837,1839,1841,1844,1846,1848],{"class":196,"line":783},[194,1828,1627],{"class":210},[194,1830,1831],{"class":338}," index, value ",[194,1833,525],{"class":210},[194,1835,1836],{"class":338}," walk(host, community, ",[194,1838,1268],{"class":207},[194,1840,172],{"class":338},[194,1842,1843],{"class":369},"hex_strings",[194,1845,516],{"class":210},[194,1847,1516],{"class":207},[194,1849,1416],{"class":338},[194,1851,1852,1855,1857,1860,1863],{"class":196,"line":788},[194,1853,1854],{"class":338},"        parts ",[194,1856,516],{"class":210},[194,1858,1859],{"class":338}," index.split(",[194,1861,1862],{"class":457},"\".\"",[194,1864,440],{"class":338},[194,1866,1867,1869,1871,1874,1876,1879],{"class":196,"line":802},[194,1868,1640],{"class":210},[194,1870,1755],{"class":207},[194,1872,1873],{"class":338},"(parts) ",[194,1875,1548],{"class":210},[194,1877,1878],{"class":207}," 5",[194,1880,1554],{"class":338},[194,1882,1883],{"class":196,"line":813},[194,1884,1885],{"class":210},"            continue\n",[194,1887,1888,1891,1893],{"class":196,"line":844},[194,1889,1890],{"class":338},"        mac ",[194,1892,516],{"class":210},[194,1894,1895],{"class":338}," hex_to_mac(value)\n",[194,1897,1898,1900],{"class":196,"line":849},[194,1899,1640],{"class":210},[194,1901,1902],{"class":338}," mac:\n",[194,1904,1905,1908,1911,1914,1917,1920,1923,1926,1929,1931,1933,1936,1939,1942,1945],{"class":196,"line":863},[194,1906,1907],{"class":338},"            rows.append({",[194,1909,1910],{"class":457},"\"router\"",[194,1912,1913],{"class":338},": host, ",[194,1915,1916],{"class":457},"\"ifindex\"",[194,1918,1919],{"class":338},": parts[",[194,1921,1922],{"class":207},"0",[194,1924,1925],{"class":338},"], ",[194,1927,1928],{"class":457},"\"ip\"",[194,1930,710],{"class":338},[194,1932,1862],{"class":457},[194,1934,1935],{"class":338},".join(parts[",[194,1937,1938],{"class":207},"1",[194,1940,1941],{"class":338},":]), ",[194,1943,1944],{"class":457},"\"mac\"",[194,1946,1947],{"class":338},": mac})\n",[194,1949,1950,1952],{"class":196,"line":868},[194,1951,1707],{"class":210},[194,1953,1710],{"class":338},[194,1955,1956],{"class":196,"line":884},[194,1957,394],{"emptyLinePlaceholder":393},[194,1959,1960],{"class":196,"line":920},[194,1961,394],{"emptyLinePlaceholder":393},[194,1963,1964,1966,1969],{"class":196,"line":952},[194,1965,1401],{"class":210},[194,1967,1968],{"class":1404}," collect_fdb",[194,1970,1971],{"class":338},"(host, community, vlans):\n",[194,1973,1974,1977,1979,1982,1985,1987],{"class":196,"line":957},[194,1975,1976],{"class":338},"    if_names ",[194,1978,516],{"class":210},[194,1980,1981],{"class":207}," dict",[194,1983,1984],{"class":338},"(walk(host, community, ",[194,1986,1320],{"class":207},[194,1988,1989],{"class":338},"))\n",[194,1991,1993,1995,1997],{"class":196,"line":1992},62,[194,1994,1618],{"class":338},[194,1996,516],{"class":210},[194,1998,1613],{"class":338},[194,2000,2002],{"class":196,"line":2001},63,[194,2003,2004],{"class":200},"    # Cisco Catalyst keeps one BRIDGE-MIB instance per VLAN, reached with community@vlan.\n",[194,2006,2008,2011,2013,2016,2018,2021,2023,2026,2028,2031,2033,2036,2038,2041,2043,2046,2049],{"class":196,"line":2007},64,[194,2009,2010],{"class":338},"    contexts ",[194,2012,516],{"class":210},[194,2014,2015],{"class":338}," [(community ",[194,2017,1649],{"class":210},[194,2019,2020],{"class":457}," \"@\"",[194,2022,1434],{"class":210},[194,2024,2025],{"class":338}," v, v) ",[194,2027,1790],{"class":210},[194,2029,2030],{"class":338}," v ",[194,2032,525],{"class":210},[194,2034,2035],{"class":338}," vlans] ",[194,2037,852],{"class":210},[194,2039,2040],{"class":338}," vlans ",[194,2042,1448],{"class":210},[194,2044,2045],{"class":338}," [(community, ",[194,2047,2048],{"class":457},"\"\"",[194,2050,373],{"class":338},[194,2052,2054,2056,2059,2061],{"class":196,"line":2053},65,[194,2055,1627],{"class":210},[194,2057,2058],{"class":338}," ctx_community, vlan ",[194,2060,525],{"class":210},[194,2062,2063],{"class":338}," contexts:\n",[194,2065,2067,2070,2072,2074,2077,2079],{"class":196,"line":2066},66,[194,2068,2069],{"class":338},"        port_ifindex ",[194,2071,516],{"class":210},[194,2073,1981],{"class":207},[194,2075,2076],{"class":338},"(walk(host, ctx_community, ",[194,2078,1307],{"class":207},[194,2080,1989],{"class":338},[194,2082,2084,2087,2089,2091,2093,2095],{"class":196,"line":2083},67,[194,2085,2086],{"class":338},"        status ",[194,2088,516],{"class":210},[194,2090,1981],{"class":207},[194,2092,2076],{"class":338},[194,2094,1294],{"class":207},[194,2096,1989],{"class":338},[194,2098,2100,2103,2106,2108,2111,2113],{"class":196,"line":2099},68,[194,2101,2102],{"class":210},"        for",[194,2104,2105],{"class":338}," index, port ",[194,2107,525],{"class":210},[194,2109,2110],{"class":338}," walk(host, ctx_community, ",[194,2112,1281],{"class":207},[194,2114,1416],{"class":338},[194,2116,2118,2121,2123,2125,2127],{"class":196,"line":2117},69,[194,2119,2120],{"class":338},"            octets ",[194,2122,516],{"class":210},[194,2124,1859],{"class":338},[194,2126,1862],{"class":457},[194,2128,440],{"class":338},[194,2130,2132,2135,2137,2139,2141,2143,2146,2149,2152,2155],{"class":196,"line":2131},70,[194,2133,2134],{"class":210},"            if",[194,2136,1755],{"class":207},[194,2138,1758],{"class":338},[194,2140,1548],{"class":210},[194,2142,1763],{"class":207},[194,2144,2145],{"class":210}," or",[194,2147,2148],{"class":338}," port ",[194,2150,2151],{"class":210},"==",[194,2153,2154],{"class":457}," \"0\"",[194,2156,1554],{"class":338},[194,2158,2160],{"class":196,"line":2159},71,[194,2161,2162],{"class":210},"                continue\n",[194,2164,2166,2169,2171,2174,2176],{"class":196,"line":2165},72,[194,2167,2168],{"class":338},"            ifindex ",[194,2170,516],{"class":210},[194,2172,2173],{"class":338}," port_ifindex.get(port, ",[194,2175,2048],{"class":457},[194,2177,440],{"class":338},[194,2179,2181],{"class":196,"line":2180},73,[194,2182,2183],{"class":338},"            rows.append({\n",[194,2185,2187,2190],{"class":196,"line":2186},74,[194,2188,2189],{"class":457},"                \"switch\"",[194,2191,2192],{"class":338},": host,\n",[194,2194,2196,2199],{"class":196,"line":2195},75,[194,2197,2198],{"class":457},"                \"vlan\"",[194,2200,2201],{"class":338},": vlan,\n",[194,2203,2205,2208,2210,2212,2215,2217,2219,2222,2225,2228,2230,2232,2235,2237,2239],{"class":196,"line":2204},76,[194,2206,2207],{"class":457},"                \"mac\"",[194,2209,710],{"class":338},[194,2211,1741],{"class":457},[194,2213,2214],{"class":338},".join(",[194,2216,1564],{"class":210},[194,2218,1595],{"class":457},[194,2220,2221],{"class":207},"{int",[194,2223,2224],{"class":338},"(o)",[194,2226,2227],{"class":210},":02x",[194,2229,1576],{"class":207},[194,2231,1595],{"class":457},[194,2233,2234],{"class":210}," for",[194,2236,1793],{"class":338},[194,2238,525],{"class":210},[194,2240,2241],{"class":338}," octets),\n",[194,2243,2245,2248],{"class":196,"line":2244},77,[194,2246,2247],{"class":457},"                \"bridge_port\"",[194,2249,2250],{"class":338},": port,\n",[194,2252,2254,2257,2260,2262],{"class":196,"line":2253},78,[194,2255,2256],{"class":457},"                \"port_name\"",[194,2258,2259],{"class":338},": if_names.get(ifindex, ",[194,2261,2048],{"class":457},[194,2263,2264],{"class":338},"),\n",[194,2266,2268,2271,2273,2275,2278,2280,2283,2285],{"class":196,"line":2267},79,[194,2269,2270],{"class":457},"                \"status\"",[194,2272,710],{"class":338},[194,2274,1333],{"class":207},[194,2276,2277],{"class":338},".get(status.get(index, ",[194,2279,2048],{"class":457},[194,2281,2282],{"class":338},"), ",[194,2284,2048],{"class":457},[194,2286,2264],{"class":338},[194,2288,2290],{"class":196,"line":2289},80,[194,2291,2292],{"class":338},"            })\n",[194,2294,2296,2298],{"class":196,"line":2295},81,[194,2297,1707],{"class":210},[194,2299,1710],{"class":338},[194,2301,2303],{"class":196,"line":2302},82,[194,2304,394],{"emptyLinePlaceholder":393},[194,2306,2308],{"class":196,"line":2307},83,[194,2309,394],{"emptyLinePlaceholder":393},[194,2311,2313,2315,2318],{"class":196,"line":2312},84,[194,2314,1401],{"class":210},[194,2316,2317],{"class":1404}," write_csv",[194,2319,2320],{"class":338},"(path, rows, fields):\n",[194,2322,2324,2327,2330,2333,2336,2338,2341,2343,2345,2347,2350],{"class":196,"line":2323},85,[194,2325,2326],{"class":210},"    with",[194,2328,2329],{"class":207}," open",[194,2331,2332],{"class":338},"(path, ",[194,2334,2335],{"class":457},"\"w\"",[194,2337,172],{"class":338},[194,2339,2340],{"class":369},"newline",[194,2342,516],{"class":210},[194,2344,2048],{"class":457},[194,2346,775],{"class":338},[194,2348,2349],{"class":210},"as",[194,2351,2352],{"class":338}," handle:\n",[194,2354,2356,2359,2361,2364,2367,2369],{"class":196,"line":2355},86,[194,2357,2358],{"class":338},"        writer ",[194,2360,516],{"class":210},[194,2362,2363],{"class":338}," csv.DictWriter(handle, ",[194,2365,2366],{"class":369},"fieldnames",[194,2368,516],{"class":210},[194,2370,2371],{"class":338},"fields)\n",[194,2373,2375],{"class":196,"line":2374},87,[194,2376,2377],{"class":338},"        writer.writeheader()\n",[194,2379,2381],{"class":196,"line":2380},88,[194,2382,2383],{"class":338},"        writer.writerows(rows)\n",[194,2385,2387],{"class":196,"line":2386},89,[194,2388,394],{"emptyLinePlaceholder":393},[194,2390,2392],{"class":196,"line":2391},90,[194,2393,394],{"emptyLinePlaceholder":393},[194,2395,2397,2399,2402],{"class":196,"line":2396},91,[194,2398,1401],{"class":210},[194,2400,2401],{"class":1404}," main",[194,2403,2404],{"class":338},"():\n",[194,2406,2408,2411,2413,2416,2419,2421,2424],{"class":196,"line":2407},92,[194,2409,2410],{"class":338},"    parser ",[194,2412,516],{"class":210},[194,2414,2415],{"class":338}," argparse.ArgumentParser(",[194,2417,2418],{"class":369},"description",[194,2420,516],{"class":210},[194,2422,2423],{"class":457},"\"Read ARP and MAC address tables from routers and switches over SNMP.\"",[194,2425,440],{"class":338},[194,2427,2429,2432,2435,2437,2440,2442,2445,2447,2450,2452,2455,2458,2460,2463],{"class":196,"line":2428},93,[194,2430,2431],{"class":338},"    parser.add_argument(",[194,2433,2434],{"class":457},"\"--routers\"",[194,2436,172],{"class":338},[194,2438,2439],{"class":369},"nargs",[194,2441,516],{"class":210},[194,2443,2444],{"class":457},"\"*\"",[194,2446,172],{"class":338},[194,2448,2449],{"class":369},"default",[194,2451,516],{"class":210},[194,2453,2454],{"class":338},"[], ",[194,2456,2457],{"class":369},"help",[194,2459,516],{"class":210},[194,2461,2462],{"class":457},"\"Layer 3 devices to read ARP tables from\"",[194,2464,440],{"class":338},[194,2466,2468,2470,2473,2475,2477,2479,2481,2483,2485,2487,2489,2491,2493,2496],{"class":196,"line":2467},94,[194,2469,2431],{"class":338},[194,2471,2472],{"class":457},"\"--switches\"",[194,2474,172],{"class":338},[194,2476,2439],{"class":369},[194,2478,516],{"class":210},[194,2480,2444],{"class":457},[194,2482,172],{"class":338},[194,2484,2449],{"class":369},[194,2486,516],{"class":210},[194,2488,2454],{"class":338},[194,2490,2457],{"class":369},[194,2492,516],{"class":210},[194,2494,2495],{"class":457},"\"Switches to read MAC address tables from\"",[194,2497,440],{"class":338},[194,2499,2501,2503,2506,2508,2510,2512,2514,2516,2518,2520,2522,2524,2526,2529],{"class":196,"line":2500},95,[194,2502,2431],{"class":338},[194,2504,2505],{"class":457},"\"--vlans\"",[194,2507,172],{"class":338},[194,2509,2439],{"class":369},[194,2511,516],{"class":210},[194,2513,2444],{"class":457},[194,2515,172],{"class":338},[194,2517,2449],{"class":369},[194,2519,516],{"class":210},[194,2521,2454],{"class":338},[194,2523,2457],{"class":369},[194,2525,516],{"class":210},[194,2527,2528],{"class":457},"\"VLAN IDs for Cisco community@vlan indexing\"",[194,2530,440],{"class":338},[194,2532,2534,2536,2539,2541,2543,2545,2547,2549,2551,2553,2556],{"class":196,"line":2533},96,[194,2535,2431],{"class":338},[194,2537,2538],{"class":457},"\"--out\"",[194,2540,172],{"class":338},[194,2542,2449],{"class":369},[194,2544,516],{"class":210},[194,2546,1862],{"class":457},[194,2548,172],{"class":338},[194,2550,2457],{"class":369},[194,2552,516],{"class":210},[194,2554,2555],{"class":457},"\"Output directory\"",[194,2557,440],{"class":338},[194,2559,2561,2564,2566],{"class":196,"line":2560},97,[194,2562,2563],{"class":338},"    args ",[194,2565,516],{"class":210},[194,2567,2568],{"class":338}," parser.parse_args()\n",[194,2570,2572],{"class":196,"line":2571},98,[194,2573,394],{"emptyLinePlaceholder":393},[194,2575,2577,2580,2582,2585,2588],{"class":196,"line":2576},99,[194,2578,2579],{"class":338},"    community ",[194,2581,516],{"class":210},[194,2583,2584],{"class":338}," os.environ.get(",[194,2586,2587],{"class":457},"\"SNMP_COMMUNITY\"",[194,2589,440],{"class":338},[194,2591,2593,2595,2597],{"class":196,"line":2592},100,[194,2594,1542],{"class":210},[194,2596,1643],{"class":210},[194,2598,2599],{"class":338}," community:\n",[194,2601,2603,2606,2609],{"class":196,"line":2602},101,[194,2604,2605],{"class":338},"        sys.exit(",[194,2607,2608],{"class":457},"\"Set SNMP_COMMUNITY to a read-only community string.\"",[194,2610,440],{"class":338},[194,2612,2614],{"class":196,"line":2613},102,[194,2615,394],{"emptyLinePlaceholder":393},[194,2617,2619,2622,2625,2627,2629],{"class":196,"line":2618},103,[194,2620,2621],{"class":338},"    os.makedirs(args.out, ",[194,2623,2624],{"class":369},"exist_ok",[194,2626,516],{"class":210},[194,2628,1516],{"class":207},[194,2630,440],{"class":338},[194,2632,2634,2637,2639,2642,2644,2647,2649,2652,2654,2657,2659],{"class":196,"line":2633},104,[194,2635,2636],{"class":338},"    arp ",[194,2638,516],{"class":210},[194,2640,2641],{"class":338}," [row ",[194,2643,1790],{"class":210},[194,2645,2646],{"class":338}," host ",[194,2648,525],{"class":210},[194,2650,2651],{"class":338}," args.routers ",[194,2653,1790],{"class":210},[194,2655,2656],{"class":338}," row ",[194,2658,525],{"class":210},[194,2660,2661],{"class":338}," collect_arp(host, community)]\n",[194,2663,2665,2668,2670,2672,2674,2676,2678,2681,2683,2685,2687],{"class":196,"line":2664},105,[194,2666,2667],{"class":338},"    fdb ",[194,2669,516],{"class":210},[194,2671,2641],{"class":338},[194,2673,1790],{"class":210},[194,2675,2646],{"class":338},[194,2677,525],{"class":210},[194,2679,2680],{"class":338}," args.switches ",[194,2682,1790],{"class":210},[194,2684,2656],{"class":338},[194,2686,525],{"class":210},[194,2688,2689],{"class":338}," collect_fdb(host, community, args.vlans)]\n",[194,2691,2693,2696,2699,2702,2704,2706,2708,2710,2712,2714,2716],{"class":196,"line":2692},106,[194,2694,2695],{"class":338},"    write_csv(os.path.join(args.out, ",[194,2697,2698],{"class":457},"\"arp.csv\"",[194,2700,2701],{"class":338},"), arp, [",[194,2703,1910],{"class":457},[194,2705,172],{"class":338},[194,2707,1916],{"class":457},[194,2709,172],{"class":338},[194,2711,1928],{"class":457},[194,2713,172],{"class":338},[194,2715,1944],{"class":457},[194,2717,2718],{"class":338},"])\n",[194,2720,2722,2724,2727,2730,2733,2735,2738,2740,2742,2744,2747,2749,2752,2754,2757],{"class":196,"line":2721},107,[194,2723,2695],{"class":338},[194,2725,2726],{"class":457},"\"fdb.csv\"",[194,2728,2729],{"class":338},"), fdb, [",[194,2731,2732],{"class":457},"\"switch\"",[194,2734,172],{"class":338},[194,2736,2737],{"class":457},"\"vlan\"",[194,2739,172],{"class":338},[194,2741,1944],{"class":457},[194,2743,172],{"class":338},[194,2745,2746],{"class":457},"\"bridge_port\"",[194,2748,172],{"class":338},[194,2750,2751],{"class":457},"\"port_name\"",[194,2753,172],{"class":338},[194,2755,2756],{"class":457},"\"status\"",[194,2758,2718],{"class":338},[194,2760,2762,2765,2767,2769,2771,2774,2777,2779,2782,2784,2787,2789,2792],{"class":196,"line":2761},108,[194,2763,2764],{"class":207},"    print",[194,2766,366],{"class":338},[194,2768,1564],{"class":210},[194,2770,1595],{"class":457},[194,2772,2773],{"class":207},"{len",[194,2775,2776],{"class":338},"(arp)",[194,2778,1576],{"class":207},[194,2780,2781],{"class":457}," ARP entries, ",[194,2783,2773],{"class":207},[194,2785,2786],{"class":338},"(fdb)",[194,2788,1576],{"class":207},[194,2790,2791],{"class":457}," MAC table entries\"",[194,2793,440],{"class":338},[194,2795,2797],{"class":196,"line":2796},109,[194,2798,394],{"emptyLinePlaceholder":393},[194,2800,2802],{"class":196,"line":2801},110,[194,2803,394],{"emptyLinePlaceholder":393},[194,2805,2807,2809,2812,2815,2818],{"class":196,"line":2806},111,[194,2808,852],{"class":210},[194,2810,2811],{"class":207}," __name__",[194,2813,2814],{"class":210}," ==",[194,2816,2817],{"class":457}," \"__main__\"",[194,2819,1554],{"class":338},[194,2821,2823],{"class":196,"line":2822},112,[194,2824,2825],{"class":338},"    main()\n",[185,2827,2831],{"className":2828,"code":2829,"language":2830,"meta":190,"style":190},"language-bash shiki shiki-themes github-dark","export SNMP_COMMUNITY='read-only-string'\npython3 snmp_collect.py --routers 10.0.0.1 10.20.0.1 \\\n    --switches 10.0.0.11 10.0.0.12 --vlans 1 10 20 30 --out \u002Fsrv\u002Fdiscovery\u002F2026-07-15\n","bash",[24,2832,2833,2846,2866],{"__ignoreMap":190},[194,2834,2835,2838,2841,2843],{"class":196,"line":197},[194,2836,2837],{"class":210},"export",[194,2839,2840],{"class":338}," SNMP_COMMUNITY",[194,2842,516],{"class":210},[194,2844,2845],{"class":457},"'read-only-string'\n",[194,2847,2848,2851,2854,2857,2860,2863],{"class":196,"line":204},[194,2849,2850],{"class":1404},"python3",[194,2852,2853],{"class":457}," snmp_collect.py",[194,2855,2856],{"class":207}," --routers",[194,2858,2859],{"class":207}," 10.0.0.1",[194,2861,2862],{"class":207}," 10.20.0.1",[194,2864,2865],{"class":207}," \\\n",[194,2867,2868,2871,2874,2877,2880,2882,2885,2888,2891,2894],{"class":196,"line":214},[194,2869,2870],{"class":207},"    --switches",[194,2872,2873],{"class":207}," 10.0.0.11",[194,2875,2876],{"class":207}," 10.0.0.12",[194,2878,2879],{"class":207}," --vlans",[194,2881,1693],{"class":207},[194,2883,2884],{"class":207}," 10",[194,2886,2887],{"class":207}," 20",[194,2889,2890],{"class":207}," 30",[194,2892,2893],{"class":207}," --out",[194,2895,2896],{"class":457}," \u002Fsrv\u002Fdiscovery\u002F2026-07-15\n",[62,2898,2900],{"id":2899},"build-the-inventory-before-the-diagram","Build the Inventory Before the Diagram",[11,2902,2903],{},"The instinct is to open a drawing tool immediately, because a diagram feels like real progress. I have learned to resist that until there is a structured inventory: every discovered device with its IP, MAC, switch port, VLAN, and whatever role can be inferred from its hostname or services. A diagram built directly from a spreadsheet like that is trustworthy in a way that a diagram built from someone's best recollection during a meeting is not, and updating a spreadsheet row is far less friction than redrawing a canvas every time discovery turns up something new.",[11,2905,2906],{},"The merge script builds that spreadsheet. Two details in it matter:",[1051,2908,2909,2919],{},[21,2910,2911,2914,2915,2918],{},[1056,2912,2913],{},"Uplinks."," A switch learns every downstream MAC on its uplink and trunk ports, so the same MAC appears on several switches. The script treats any port that learned more than ",[24,2916,2917],{},"--uplink-threshold"," MACs (default 5) as an uplink and keeps the port with the fewest MACs as the device's real access port. That's a heuristic: raise the threshold for ports that feed an unmanaged switch or a desk hub, and check the result against a couple of known devices.",[21,2920,2921,2924,2925,2927],{},[1056,2922,2923],{},"Gaps."," An IP that answered ARP but has no DHCP lease and no AD account is a statically addressed device nobody wrote down: a printer, a UPS card, a camera, an old appliance. Those go to ",[24,2926,46],{},", and they're the only thing the active scan in the next step touches.",[185,2929,2931],{"className":1175,"code":2930,"language":1177,"meta":190,"style":190},"#!\u002Fusr\u002Fbin\u002Fenv python3\n\"\"\"Merge passive discovery exports into one inventory keyed by MAC address.\n\nInputs (any may be missing): arp.csv and fdb.csv from snmp_collect.py,\ndhcp-leases.csv and ad-computers.csv from Export-PassiveInventory.ps1, and\noptionally an nmap XML file from the scoped active scan.\nOutputs inventory.csv, plus gaps.txt: IPs that answer ARP but have no DHCP\nlease and no AD computer account, which is the short list worth scanning.\n\"\"\"\nimport argparse\nimport csv\nimport ipaddress\nimport os\nimport xml.etree.ElementTree as ET\nfrom collections import Counter, defaultdict\n\n\ndef read_csv(path):\n    if not os.path.exists(path):\n        return []\n    with open(path, newline=\"\", encoding=\"utf-8-sig\") as handle:\n        return list(csv.DictReader(handle))\n\n\ndef norm_mac(value):\n    hexdigits = \"\".join(c for c in (value or \"\").lower() if c in \"0123456789abcdef\")\n    return \":\".join(hexdigits[i:i + 2] for i in range(0, 12, 2)) if len(hexdigits) == 12 else \"\"\n\n\ndef edge_ports(fdb, uplink_threshold):\n    \"\"\"Pick the access port for each MAC, ignoring ports that learn many MACs (uplinks, trunks).\"\"\"\n    per_port = Counter((r[\"switch\"], r[\"port_name\"] or r[\"bridge_port\"]) for r in fdb)\n    best = {}\n    for r in fdb:\n        if r.get(\"status\") not in (\"\", \"learned\"):\n            continue\n        port = (r[\"switch\"], r[\"port_name\"] or r[\"bridge_port\"])\n        count = per_port[port]\n        if count > uplink_threshold:\n            continue\n        mac = norm_mac(r[\"mac\"])\n        if mac and (mac not in best or count \u003C best[mac][1]):\n            best[mac] = ({\"switch\": port[0], \"port\": port[1], \"vlan\": r[\"vlan\"]}, count)\n    return {mac: info for mac, (info, _) in best.items()}\n\n\ndef read_nmap(path):\n    \"\"\"Return {ip: {\"mac\": ..., \"vendor\": ..., \"services\": \"22\u002Fssh OpenSSH; ...\"}}.\"\"\"\n    found = {}\n    if not path:\n        return found\n    for host in ET.parse(path).getroot().iter(\"host\"):\n        status = host.find(\"status\")\n        if status is None or status.get(\"state\") != \"up\":\n            continue\n        ip, mac, vendor = \"\", \"\", \"\"\n        for addr in host.findall(\"address\"):\n            if addr.get(\"addrtype\") == \"ipv4\":\n                ip = addr.get(\"addr\")\n            elif addr.get(\"addrtype\") == \"mac\":\n                mac, vendor = norm_mac(addr.get(\"addr\")), addr.get(\"vendor\", \"\")\n        services = []\n        for port in host.iter(\"port\"):\n            state = port.find(\"state\")\n            if state is None or state.get(\"state\") != \"open\":\n                continue\n            svc = port.find(\"service\")\n            label = f'{port.get(\"portid\")}\u002F{svc.get(\"name\", \"\") if svc is not None else \"\"}'\n            product = svc.get(\"product\", \"\") if svc is not None else \"\"\n            if product:\n                label += \" \" + product\n            services.append(label)\n        if ip:\n            found[ip] = {\"mac\": mac, \"vendor\": vendor, \"services\": \"; \".join(services)}\n    return found\n\n\ndef main():\n    parser = argparse.ArgumentParser(description=\"Merge passive discovery exports into one inventory keyed by MAC address.\")\n    parser.add_argument(\"--dir\", default=\".\", help=\"Folder holding the exports\")\n    parser.add_argument(\"--nmap\", help=\"nmap -oX file from the scoped scan\")\n    parser.add_argument(\"--uplink-threshold\", type=int, default=5,\n                        help=\"Ports that learned more MACs than this are treated as uplinks\")\n    args = parser.parse_args()\n    path = lambda name: os.path.join(args.dir, name)\n\n    inv = defaultdict(lambda: defaultdict(str))\n    sources = defaultdict(set)\n\n    for r in read_csv(path(\"arp.csv\")):\n        mac = norm_mac(r[\"mac\"])\n        if mac:\n            inv[mac][\"ip\"] = inv[mac][\"ip\"] or r[\"ip\"]\n            sources[mac].add(\"arp\")\n\n    for r in read_csv(path(\"dhcp-leases.csv\")):\n        mac = norm_mac(r[\"ClientId\"])\n        if not mac:\n            continue\n        inv[mac][\"hostname\"] = inv[mac][\"hostname\"] or r[\"HostName\"]\n        if r.get(\"AddressState\", \"\").startswith(\"Active\"):\n            inv[mac][\"ip\"] = inv[mac][\"ip\"] or r[\"IPAddress\"]\n            sources[mac].add(\"dhcp\")\n        else:\n            sources[mac].add(\"dhcp-history\")  # expired, declined or offered: seen once, not proof it is here now\n\n    for mac, port in edge_ports(read_csv(path(\"fdb.csv\")), args.uplink_threshold).items():\n        inv[mac].update(port)\n        sources[mac].add(\"switch\")\n\n    ip_to_mac = {row[\"ip\"]: mac for mac, row in inv.items() if row[\"ip\"]}\n    ad_ips = set()\n    for r in read_csv(path(\"ad-computers.csv\")):\n        ip = r.get(\"IPv4Address\", \"\")\n        if ip:\n            ad_ips.add(ip)\n        mac = ip_to_mac.get(ip)\n        if mac:\n            inv[mac][\"ad_name\"] = r[\"Name\"]\n            inv[mac][\"os\"] = r.get(\"OperatingSystem\", \"\")\n            sources[mac].add(\"ad\")\n\n    for ip, hit in read_nmap(args.nmap).items():\n        mac = hit[\"mac\"] or ip_to_mac.get(ip) or f\"ip:{ip}\"\n        inv[mac][\"ip\"] = inv[mac][\"ip\"] or ip\n        inv[mac][\"vendor\"] = hit[\"vendor\"]\n        inv[mac][\"services\"] = hit[\"services\"]\n        sources[mac].add(\"nmap\")\n\n    fields = [\"mac\", \"ip\", \"hostname\", \"ad_name\", \"os\", \"vendor\", \"switch\", \"port\", \"vlan\", \"services\", \"sources\"]\n    with open(path(\"inventory.csv\"), \"w\", newline=\"\") as handle:\n        writer = csv.DictWriter(handle, fieldnames=fields)\n        writer.writeheader()\n        for mac in sorted(inv, key=lambda m: ipaddress.ip_address(inv[m][\"ip\"] or \"0.0.0.0\")):\n            writer.writerow({\"mac\": mac, **{f: inv[mac][f] for f in fields[1:-1]}, \"sources\": \"+\".join(sorted(sources[mac]))})\n\n    gaps = sorted({row[\"ip\"] for mac, row in inv.items()\n                   if row[\"ip\"] and \"arp\" in sources[mac] and \"dhcp\" not in sources[mac] and row[\"ip\"] not in ad_ips}, key=ipaddress.ip_address)\n    with open(path(\"gaps.txt\"), \"w\") as handle:\n        handle.write(\"\\n\".join(gaps) + (\"\\n\" if gaps else \"\"))\n    print(f\"{len(inv)} devices in inventory.csv, {len(gaps)} unexplained IPs in gaps.txt\")\n\n\nif __name__ == \"__main__\":\n    main()\n",[24,2932,2933,2937,2942,2946,2951,2956,2961,2966,2971,2975,2981,2987,2994,3000,3012,3025,3029,3033,3043,3052,3058,3088,3098,3102,3106,3115,3156,3217,3221,3225,3235,3240,3279,3289,3300,3327,3331,3357,3367,3380,3384,3397,3432,3470,3487,3491,3495,3504,3509,3518,3527,3534,3553,3566,3596,3600,3618,3635,3654,3668,3686,3710,3719,3734,3748,3775,3779,3793,3855,3888,3895,3911,3916,3923,3953,3959,3963,3967,3975,3992,4018,4036,4064,4076,4084,4097,4101,4122,4136,4140,4156,4168,4174,4201,4211,4215,4230,4243,4251,4255,4282,4303,4328,4337,4344,4357,4361,4378,4383,4392,4396,4431,4444,4460,4479,4486,4492,4502,4509,4528,4551,4561,4566,4579,4614,4636,4653,4670,4680,4685,4740,4771,4786,4791,4826,4879,4884,4910,4965,4987,5024,5057,5062,5067,5080],{"__ignoreMap":190},[194,2934,2935],{"class":196,"line":197},[194,2936,1184],{"class":200},[194,2938,2939],{"class":196,"line":204},[194,2940,2941],{"class":457},"\"\"\"Merge passive discovery exports into one inventory keyed by MAC address.\n",[194,2943,2944],{"class":196,"line":214},[194,2945,394],{"emptyLinePlaceholder":393},[194,2947,2948],{"class":196,"line":220},[194,2949,2950],{"class":457},"Inputs (any may be missing): arp.csv and fdb.csv from snmp_collect.py,\n",[194,2952,2953],{"class":196,"line":228},[194,2954,2955],{"class":457},"dhcp-leases.csv and ad-computers.csv from Export-PassiveInventory.ps1, and\n",[194,2957,2958],{"class":196,"line":234},[194,2959,2960],{"class":457},"optionally an nmap XML file from the scoped active scan.\n",[194,2962,2963],{"class":196,"line":240},[194,2964,2965],{"class":457},"Outputs inventory.csv, plus gaps.txt: IPs that answer ARP but have no DHCP\n",[194,2967,2968],{"class":196,"line":246},[194,2969,2970],{"class":457},"lease and no AD computer account, which is the short list worth scanning.\n",[194,2972,2973],{"class":196,"line":252},[194,2974,1223],{"class":457},[194,2976,2977,2979],{"class":196,"line":263},[194,2978,1228],{"class":210},[194,2980,1231],{"class":338},[194,2982,2983,2985],{"class":196,"line":269},[194,2984,1228],{"class":210},[194,2986,1238],{"class":338},[194,2988,2989,2991],{"class":196,"line":279},[194,2990,1228],{"class":210},[194,2992,2993],{"class":338}," ipaddress\n",[194,2995,2996,2998],{"class":196,"line":285},[194,2997,1228],{"class":210},[194,2999,1245],{"class":338},[194,3001,3002,3004,3007,3009],{"class":196,"line":295},[194,3003,1228],{"class":210},[194,3005,3006],{"class":338}," xml.etree.ElementTree ",[194,3008,2349],{"class":210},[194,3010,3011],{"class":207}," ET\n",[194,3013,3014,3017,3020,3022],{"class":196,"line":301},[194,3015,3016],{"class":210},"from",[194,3018,3019],{"class":338}," collections ",[194,3021,1228],{"class":210},[194,3023,3024],{"class":338}," Counter, defaultdict\n",[194,3026,3027],{"class":196,"line":309},[194,3028,394],{"emptyLinePlaceholder":393},[194,3030,3031],{"class":196,"line":315},[194,3032,394],{"emptyLinePlaceholder":393},[194,3034,3035,3037,3040],{"class":196,"line":323},[194,3036,1401],{"class":210},[194,3038,3039],{"class":1404}," read_csv",[194,3041,3042],{"class":338},"(path):\n",[194,3044,3045,3047,3049],{"class":196,"line":329},[194,3046,1542],{"class":210},[194,3048,1643],{"class":210},[194,3050,3051],{"class":338}," os.path.exists(path):\n",[194,3053,3054,3056],{"class":196,"line":335},[194,3055,1610],{"class":210},[194,3057,1613],{"class":338},[194,3059,3060,3062,3064,3066,3068,3070,3072,3074,3077,3079,3082,3084,3086],{"class":196,"line":348},[194,3061,2326],{"class":210},[194,3063,2329],{"class":207},[194,3065,2332],{"class":338},[194,3067,2340],{"class":369},[194,3069,516],{"class":210},[194,3071,2048],{"class":457},[194,3073,172],{"class":338},[194,3075,3076],{"class":369},"encoding",[194,3078,516],{"class":210},[194,3080,3081],{"class":457},"\"utf-8-sig\"",[194,3083,775],{"class":338},[194,3085,2349],{"class":210},[194,3087,2352],{"class":338},[194,3089,3090,3092,3095],{"class":196,"line":357},[194,3091,1610],{"class":210},[194,3093,3094],{"class":207}," list",[194,3096,3097],{"class":338},"(csv.DictReader(handle))\n",[194,3099,3100],{"class":196,"line":376},[194,3101,394],{"emptyLinePlaceholder":393},[194,3103,3104],{"class":196,"line":390},[194,3105,394],{"emptyLinePlaceholder":393},[194,3107,3108,3110,3113],{"class":196,"line":397},[194,3109,1401],{"class":210},[194,3111,3112],{"class":1404}," norm_mac",[194,3114,1728],{"class":338},[194,3116,3117,3120,3122,3124,3127,3129,3132,3134,3137,3140,3142,3145,3147,3149,3151,3154],{"class":196,"line":410},[194,3118,3119],{"class":338},"    hexdigits ",[194,3121,516],{"class":210},[194,3123,1451],{"class":457},[194,3125,3126],{"class":338},".join(c ",[194,3128,1790],{"class":210},[194,3130,3131],{"class":338}," c ",[194,3133,525],{"class":210},[194,3135,3136],{"class":338}," (value ",[194,3138,3139],{"class":210},"or",[194,3141,1451],{"class":457},[194,3143,3144],{"class":338},").lower() ",[194,3146,852],{"class":210},[194,3148,3131],{"class":338},[194,3150,525],{"class":210},[194,3152,3153],{"class":457}," \"0123456789abcdef\"",[194,3155,440],{"class":338},[194,3157,3158,3160,3162,3165,3167,3170,3173,3175,3178,3180,3183,3185,3187,3189,3192,3194,3196,3199,3201,3203,3206,3208,3211,3214],{"class":196,"line":422},[194,3159,1707],{"class":210},[194,3161,1779],{"class":457},[194,3163,3164],{"class":338},".join(hexdigits[i:i ",[194,3166,1649],{"class":210},[194,3168,3169],{"class":207}," 2",[194,3171,3172],{"class":338},"] ",[194,3174,1790],{"class":210},[194,3176,3177],{"class":338}," i ",[194,3179,525],{"class":210},[194,3181,3182],{"class":207}," range",[194,3184,366],{"class":338},[194,3186,1922],{"class":207},[194,3188,172],{"class":338},[194,3190,3191],{"class":207},"12",[194,3193,172],{"class":338},[194,3195,1785],{"class":207},[194,3197,3198],{"class":338},")) ",[194,3200,852],{"class":210},[194,3202,1755],{"class":207},[194,3204,3205],{"class":338},"(hexdigits) ",[194,3207,2151],{"class":210},[194,3209,3210],{"class":207}," 12",[194,3212,3213],{"class":210}," else",[194,3215,3216],{"class":457}," \"\"\n",[194,3218,3219],{"class":196,"line":427},[194,3220,394],{"emptyLinePlaceholder":393},[194,3222,3223],{"class":196,"line":437},[194,3224,394],{"emptyLinePlaceholder":393},[194,3226,3227,3229,3232],{"class":196,"line":443},[194,3228,1401],{"class":210},[194,3230,3231],{"class":1404}," edge_ports",[194,3233,3234],{"class":338},"(fdb, uplink_threshold):\n",[194,3236,3237],{"class":196,"line":448},[194,3238,3239],{"class":457},"    \"\"\"Pick the access port for each MAC, ignoring ports that learn many MACs (uplinks, trunks).\"\"\"\n",[194,3241,3242,3245,3247,3250,3252,3255,3257,3259,3261,3264,3266,3269,3271,3274,3276],{"class":196,"line":461},[194,3243,3244],{"class":338},"    per_port ",[194,3246,516],{"class":210},[194,3248,3249],{"class":338}," Counter((r[",[194,3251,2732],{"class":457},[194,3253,3254],{"class":338},"], r[",[194,3256,2751],{"class":457},[194,3258,3172],{"class":338},[194,3260,3139],{"class":210},[194,3262,3263],{"class":338}," r[",[194,3265,2746],{"class":457},[194,3267,3268],{"class":338},"]) ",[194,3270,1790],{"class":210},[194,3272,3273],{"class":338}," r ",[194,3275,525],{"class":210},[194,3277,3278],{"class":338}," fdb)\n",[194,3280,3281,3284,3286],{"class":196,"line":476},[194,3282,3283],{"class":338},"    best ",[194,3285,516],{"class":210},[194,3287,3288],{"class":338}," {}\n",[194,3290,3291,3293,3295,3297],{"class":196,"line":505},[194,3292,1627],{"class":210},[194,3294,3273],{"class":338},[194,3296,525],{"class":210},[194,3298,3299],{"class":338}," fdb:\n",[194,3301,3302,3304,3307,3309,3311,3314,3317,3319,3321,3323,3325],{"class":196,"line":510},[194,3303,1640],{"class":210},[194,3305,3306],{"class":338}," r.get(",[194,3308,2756],{"class":457},[194,3310,775],{"class":338},[194,3312,3313],{"class":210},"not",[194,3315,3316],{"class":210}," in",[194,3318,1062],{"class":338},[194,3320,2048],{"class":457},[194,3322,172],{"class":338},[194,3324,1366],{"class":457},[194,3326,1416],{"class":338},[194,3328,3329],{"class":196,"line":531},[194,3330,1885],{"class":210},[194,3332,3333,3336,3338,3341,3343,3345,3347,3349,3351,3353,3355],{"class":196,"line":547},[194,3334,3335],{"class":338},"        port ",[194,3337,516],{"class":210},[194,3339,3340],{"class":338}," (r[",[194,3342,2732],{"class":457},[194,3344,3254],{"class":338},[194,3346,2751],{"class":457},[194,3348,3172],{"class":338},[194,3350,3139],{"class":210},[194,3352,3263],{"class":338},[194,3354,2746],{"class":457},[194,3356,2718],{"class":338},[194,3358,3359,3362,3364],{"class":196,"line":561},[194,3360,3361],{"class":338},"        count ",[194,3363,516],{"class":210},[194,3365,3366],{"class":338}," per_port[port]\n",[194,3368,3369,3371,3374,3377],{"class":196,"line":570},[194,3370,1640],{"class":210},[194,3372,3373],{"class":338}," count ",[194,3375,3376],{"class":210},">",[194,3378,3379],{"class":338}," uplink_threshold:\n",[194,3381,3382],{"class":196,"line":594},[194,3383,1885],{"class":210},[194,3385,3386,3388,3390,3393,3395],{"class":196,"line":627},[194,3387,1890],{"class":338},[194,3389,516],{"class":210},[194,3391,3392],{"class":338}," norm_mac(r[",[194,3394,1944],{"class":457},[194,3396,2718],{"class":338},[194,3398,3399,3401,3404,3407,3410,3412,3414,3417,3419,3421,3424,3427,3429],{"class":196,"line":651},[194,3400,1640],{"class":210},[194,3402,3403],{"class":338}," mac ",[194,3405,3406],{"class":210},"and",[194,3408,3409],{"class":338}," (mac ",[194,3411,3313],{"class":210},[194,3413,3316],{"class":210},[194,3415,3416],{"class":338}," best ",[194,3418,3139],{"class":210},[194,3420,3373],{"class":338},[194,3422,3423],{"class":210},"\u003C",[194,3425,3426],{"class":338}," best[mac][",[194,3428,1938],{"class":207},[194,3430,3431],{"class":338},"]):\n",[194,3433,3434,3437,3439,3442,3444,3447,3449,3451,3454,3456,3458,3460,3462,3465,3467],{"class":196,"line":670},[194,3435,3436],{"class":338},"            best[mac] ",[194,3438,516],{"class":210},[194,3440,3441],{"class":338}," ({",[194,3443,2732],{"class":457},[194,3445,3446],{"class":338},": port[",[194,3448,1922],{"class":207},[194,3450,1925],{"class":338},[194,3452,3453],{"class":457},"\"port\"",[194,3455,3446],{"class":338},[194,3457,1938],{"class":207},[194,3459,1925],{"class":338},[194,3461,2737],{"class":457},[194,3463,3464],{"class":338},": r[",[194,3466,2737],{"class":457},[194,3468,3469],{"class":338},"]}, count)\n",[194,3471,3472,3474,3477,3479,3482,3484],{"class":196,"line":676},[194,3473,1707],{"class":210},[194,3475,3476],{"class":338}," {mac: info ",[194,3478,1790],{"class":210},[194,3480,3481],{"class":338}," mac, (info, _) ",[194,3483,525],{"class":210},[194,3485,3486],{"class":338}," best.items()}\n",[194,3488,3489],{"class":196,"line":684},[194,3490,394],{"emptyLinePlaceholder":393},[194,3492,3493],{"class":196,"line":728},[194,3494,394],{"emptyLinePlaceholder":393},[194,3496,3497,3499,3502],{"class":196,"line":733},[194,3498,1401],{"class":210},[194,3500,3501],{"class":1404}," read_nmap",[194,3503,3042],{"class":338},[194,3505,3506],{"class":196,"line":739},[194,3507,3508],{"class":457},"    \"\"\"Return {ip: {\"mac\": ..., \"vendor\": ..., \"services\": \"22\u002Fssh OpenSSH; ...\"}}.\"\"\"\n",[194,3510,3511,3514,3516],{"class":196,"line":745},[194,3512,3513],{"class":338},"    found ",[194,3515,516],{"class":210},[194,3517,3288],{"class":338},[194,3519,3520,3522,3524],{"class":196,"line":783},[194,3521,1542],{"class":210},[194,3523,1643],{"class":210},[194,3525,3526],{"class":338}," path:\n",[194,3528,3529,3531],{"class":196,"line":788},[194,3530,1610],{"class":210},[194,3532,3533],{"class":338}," found\n",[194,3535,3536,3538,3540,3542,3545,3548,3551],{"class":196,"line":802},[194,3537,1627],{"class":210},[194,3539,2646],{"class":338},[194,3541,525],{"class":210},[194,3543,3544],{"class":207}," ET",[194,3546,3547],{"class":338},".parse(path).getroot().iter(",[194,3549,3550],{"class":457},"\"host\"",[194,3552,1416],{"class":338},[194,3554,3555,3557,3559,3562,3564],{"class":196,"line":813},[194,3556,2086],{"class":338},[194,3558,516],{"class":210},[194,3560,3561],{"class":338}," host.find(",[194,3563,2756],{"class":457},[194,3565,440],{"class":338},[194,3567,3568,3570,3573,3576,3579,3581,3584,3587,3589,3591,3594],{"class":196,"line":844},[194,3569,1640],{"class":210},[194,3571,3572],{"class":338}," status ",[194,3574,3575],{"class":210},"is",[194,3577,3578],{"class":207}," None",[194,3580,2145],{"class":210},[194,3582,3583],{"class":338}," status.get(",[194,3585,3586],{"class":457},"\"state\"",[194,3588,775],{"class":338},[194,3590,1548],{"class":210},[194,3592,3593],{"class":457}," \"up\"",[194,3595,1554],{"class":338},[194,3597,3598],{"class":196,"line":849},[194,3599,1885],{"class":210},[194,3601,3602,3605,3607,3609,3611,3613,3615],{"class":196,"line":863},[194,3603,3604],{"class":338},"        ip, mac, vendor ",[194,3606,516],{"class":210},[194,3608,1451],{"class":457},[194,3610,172],{"class":338},[194,3612,2048],{"class":457},[194,3614,172],{"class":338},[194,3616,3617],{"class":457},"\"\"\n",[194,3619,3620,3622,3625,3627,3630,3633],{"class":196,"line":868},[194,3621,2102],{"class":210},[194,3623,3624],{"class":338}," addr ",[194,3626,525],{"class":210},[194,3628,3629],{"class":338}," host.findall(",[194,3631,3632],{"class":457},"\"address\"",[194,3634,1416],{"class":338},[194,3636,3637,3639,3642,3645,3647,3649,3652],{"class":196,"line":884},[194,3638,2134],{"class":210},[194,3640,3641],{"class":338}," addr.get(",[194,3643,3644],{"class":457},"\"addrtype\"",[194,3646,775],{"class":338},[194,3648,2151],{"class":210},[194,3650,3651],{"class":457}," \"ipv4\"",[194,3653,1554],{"class":338},[194,3655,3656,3659,3661,3663,3666],{"class":196,"line":920},[194,3657,3658],{"class":338},"                ip ",[194,3660,516],{"class":210},[194,3662,3641],{"class":338},[194,3664,3665],{"class":457},"\"addr\"",[194,3667,440],{"class":338},[194,3669,3670,3673,3675,3677,3679,3681,3684],{"class":196,"line":952},[194,3671,3672],{"class":210},"            elif",[194,3674,3641],{"class":338},[194,3676,3644],{"class":457},[194,3678,775],{"class":338},[194,3680,2151],{"class":210},[194,3682,3683],{"class":457}," \"mac\"",[194,3685,1554],{"class":338},[194,3687,3688,3691,3693,3696,3698,3701,3704,3706,3708],{"class":196,"line":957},[194,3689,3690],{"class":338},"                mac, vendor ",[194,3692,516],{"class":210},[194,3694,3695],{"class":338}," norm_mac(addr.get(",[194,3697,3665],{"class":457},[194,3699,3700],{"class":338},")), addr.get(",[194,3702,3703],{"class":457},"\"vendor\"",[194,3705,172],{"class":338},[194,3707,2048],{"class":457},[194,3709,440],{"class":338},[194,3711,3712,3715,3717],{"class":196,"line":1992},[194,3713,3714],{"class":338},"        services ",[194,3716,516],{"class":210},[194,3718,1613],{"class":338},[194,3720,3721,3723,3725,3727,3730,3732],{"class":196,"line":2001},[194,3722,2102],{"class":210},[194,3724,2148],{"class":338},[194,3726,525],{"class":210},[194,3728,3729],{"class":338}," host.iter(",[194,3731,3453],{"class":457},[194,3733,1416],{"class":338},[194,3735,3736,3739,3741,3744,3746],{"class":196,"line":2007},[194,3737,3738],{"class":338},"            state ",[194,3740,516],{"class":210},[194,3742,3743],{"class":338}," port.find(",[194,3745,3586],{"class":457},[194,3747,440],{"class":338},[194,3749,3750,3752,3755,3757,3759,3761,3764,3766,3768,3770,3773],{"class":196,"line":2053},[194,3751,2134],{"class":210},[194,3753,3754],{"class":338}," state ",[194,3756,3575],{"class":210},[194,3758,3578],{"class":207},[194,3760,2145],{"class":210},[194,3762,3763],{"class":338}," state.get(",[194,3765,3586],{"class":457},[194,3767,775],{"class":338},[194,3769,1548],{"class":210},[194,3771,3772],{"class":457}," \"open\"",[194,3774,1554],{"class":338},[194,3776,3777],{"class":196,"line":2066},[194,3778,2162],{"class":210},[194,3780,3781,3784,3786,3788,3791],{"class":196,"line":2083},[194,3782,3783],{"class":338},"            svc ",[194,3785,516],{"class":210},[194,3787,3743],{"class":338},[194,3789,3790],{"class":457},"\"service\"",[194,3792,440],{"class":338},[194,3794,3795,3798,3800,3803,3806,3808,3811,3814,3816,3818,3821,3823,3826,3829,3831,3833,3835,3837,3840,3842,3844,3846,3848,3850,3852],{"class":196,"line":2099},[194,3796,3797],{"class":338},"            label ",[194,3799,516],{"class":210},[194,3801,3802],{"class":210}," f",[194,3804,3805],{"class":457},"'",[194,3807,1570],{"class":207},[194,3809,3810],{"class":338},"port.get(",[194,3812,3813],{"class":457},"\"portid\"",[194,3815,701],{"class":338},[194,3817,1576],{"class":207},[194,3819,3820],{"class":457},"\u002F",[194,3822,1570],{"class":207},[194,3824,3825],{"class":338},"svc.get(",[194,3827,3828],{"class":457},"\"name\"",[194,3830,172],{"class":338},[194,3832,2048],{"class":457},[194,3834,775],{"class":338},[194,3836,852],{"class":210},[194,3838,3839],{"class":338}," svc ",[194,3841,3575],{"class":210},[194,3843,1643],{"class":210},[194,3845,3578],{"class":207},[194,3847,3213],{"class":210},[194,3849,1451],{"class":457},[194,3851,1576],{"class":207},[194,3853,3854],{"class":457},"'\n",[194,3856,3857,3860,3862,3865,3868,3870,3872,3874,3876,3878,3880,3882,3884,3886],{"class":196,"line":2117},[194,3858,3859],{"class":338},"            product ",[194,3861,516],{"class":210},[194,3863,3864],{"class":338}," svc.get(",[194,3866,3867],{"class":457},"\"product\"",[194,3869,172],{"class":338},[194,3871,2048],{"class":457},[194,3873,775],{"class":338},[194,3875,852],{"class":210},[194,3877,3839],{"class":338},[194,3879,3575],{"class":210},[194,3881,1643],{"class":210},[194,3883,3578],{"class":207},[194,3885,3213],{"class":210},[194,3887,3216],{"class":457},[194,3889,3890,3892],{"class":196,"line":2131},[194,3891,2134],{"class":210},[194,3893,3894],{"class":338}," product:\n",[194,3896,3897,3900,3903,3906,3908],{"class":196,"line":2159},[194,3898,3899],{"class":338},"                label ",[194,3901,3902],{"class":210},"+=",[194,3904,3905],{"class":457}," \" \"",[194,3907,1434],{"class":210},[194,3909,3910],{"class":338}," product\n",[194,3912,3913],{"class":196,"line":2165},[194,3914,3915],{"class":338},"            services.append(label)\n",[194,3917,3918,3920],{"class":196,"line":2180},[194,3919,1640],{"class":210},[194,3921,3922],{"class":338}," ip:\n",[194,3924,3925,3928,3930,3932,3934,3937,3939,3942,3945,3947,3950],{"class":196,"line":2186},[194,3926,3927],{"class":338},"            found[ip] ",[194,3929,516],{"class":210},[194,3931,1338],{"class":338},[194,3933,1944],{"class":457},[194,3935,3936],{"class":338},": mac, ",[194,3938,3703],{"class":457},[194,3940,3941],{"class":338},": vendor, ",[194,3943,3944],{"class":457},"\"services\"",[194,3946,710],{"class":338},[194,3948,3949],{"class":457},"\"; \"",[194,3951,3952],{"class":338},".join(services)}\n",[194,3954,3955,3957],{"class":196,"line":2195},[194,3956,1707],{"class":210},[194,3958,3533],{"class":338},[194,3960,3961],{"class":196,"line":2204},[194,3962,394],{"emptyLinePlaceholder":393},[194,3964,3965],{"class":196,"line":2244},[194,3966,394],{"emptyLinePlaceholder":393},[194,3968,3969,3971,3973],{"class":196,"line":2253},[194,3970,1401],{"class":210},[194,3972,2401],{"class":1404},[194,3974,2404],{"class":338},[194,3976,3977,3979,3981,3983,3985,3987,3990],{"class":196,"line":2267},[194,3978,2410],{"class":338},[194,3980,516],{"class":210},[194,3982,2415],{"class":338},[194,3984,2418],{"class":369},[194,3986,516],{"class":210},[194,3988,3989],{"class":457},"\"Merge passive discovery exports into one inventory keyed by MAC address.\"",[194,3991,440],{"class":338},[194,3993,3994,3996,3999,4001,4003,4005,4007,4009,4011,4013,4016],{"class":196,"line":2289},[194,3995,2431],{"class":338},[194,3997,3998],{"class":457},"\"--dir\"",[194,4000,172],{"class":338},[194,4002,2449],{"class":369},[194,4004,516],{"class":210},[194,4006,1862],{"class":457},[194,4008,172],{"class":338},[194,4010,2457],{"class":369},[194,4012,516],{"class":210},[194,4014,4015],{"class":457},"\"Folder holding the exports\"",[194,4017,440],{"class":338},[194,4019,4020,4022,4025,4027,4029,4031,4034],{"class":196,"line":2295},[194,4021,2431],{"class":338},[194,4023,4024],{"class":457},"\"--nmap\"",[194,4026,172],{"class":338},[194,4028,2457],{"class":369},[194,4030,516],{"class":210},[194,4032,4033],{"class":457},"\"nmap -oX file from the scoped scan\"",[194,4035,440],{"class":338},[194,4037,4038,4040,4043,4045,4048,4050,4053,4055,4057,4059,4062],{"class":196,"line":2302},[194,4039,2431],{"class":338},[194,4041,4042],{"class":457},"\"--uplink-threshold\"",[194,4044,172],{"class":338},[194,4046,4047],{"class":369},"type",[194,4049,516],{"class":210},[194,4051,4052],{"class":207},"int",[194,4054,172],{"class":338},[194,4056,2449],{"class":369},[194,4058,516],{"class":210},[194,4060,4061],{"class":207},"5",[194,4063,387],{"class":338},[194,4065,4066,4069,4071,4074],{"class":196,"line":2307},[194,4067,4068],{"class":369},"                        help",[194,4070,516],{"class":210},[194,4072,4073],{"class":457},"\"Ports that learned more MACs than this are treated as uplinks\"",[194,4075,440],{"class":338},[194,4077,4078,4080,4082],{"class":196,"line":2312},[194,4079,2563],{"class":338},[194,4081,516],{"class":210},[194,4083,2568],{"class":338},[194,4085,4086,4089,4091,4094],{"class":196,"line":2323},[194,4087,4088],{"class":338},"    path ",[194,4090,516],{"class":210},[194,4092,4093],{"class":210}," lambda",[194,4095,4096],{"class":338}," name: os.path.join(args.dir, name)\n",[194,4098,4099],{"class":196,"line":2355},[194,4100,394],{"emptyLinePlaceholder":393},[194,4102,4103,4106,4108,4111,4114,4117,4120],{"class":196,"line":2374},[194,4104,4105],{"class":338},"    inv ",[194,4107,516],{"class":210},[194,4109,4110],{"class":338}," defaultdict(",[194,4112,4113],{"class":210},"lambda",[194,4115,4116],{"class":338},": defaultdict(",[194,4118,4119],{"class":207},"str",[194,4121,1989],{"class":338},[194,4123,4124,4127,4129,4131,4134],{"class":196,"line":2380},[194,4125,4126],{"class":338},"    sources ",[194,4128,516],{"class":210},[194,4130,4110],{"class":338},[194,4132,4133],{"class":207},"set",[194,4135,440],{"class":338},[194,4137,4138],{"class":196,"line":2386},[194,4139,394],{"emptyLinePlaceholder":393},[194,4141,4142,4144,4146,4148,4151,4153],{"class":196,"line":2391},[194,4143,1627],{"class":210},[194,4145,3273],{"class":338},[194,4147,525],{"class":210},[194,4149,4150],{"class":338}," read_csv(path(",[194,4152,2698],{"class":457},[194,4154,4155],{"class":338},")):\n",[194,4157,4158,4160,4162,4164,4166],{"class":196,"line":2396},[194,4159,1890],{"class":338},[194,4161,516],{"class":210},[194,4163,3392],{"class":338},[194,4165,1944],{"class":457},[194,4167,2718],{"class":338},[194,4169,4170,4172],{"class":196,"line":2407},[194,4171,1640],{"class":210},[194,4173,1902],{"class":338},[194,4175,4176,4179,4181,4183,4185,4188,4190,4192,4194,4196,4198],{"class":196,"line":2428},[194,4177,4178],{"class":338},"            inv[mac][",[194,4180,1928],{"class":457},[194,4182,3172],{"class":338},[194,4184,516],{"class":210},[194,4186,4187],{"class":338}," inv[mac][",[194,4189,1928],{"class":457},[194,4191,3172],{"class":338},[194,4193,3139],{"class":210},[194,4195,3263],{"class":338},[194,4197,1928],{"class":457},[194,4199,4200],{"class":338},"]\n",[194,4202,4203,4206,4209],{"class":196,"line":2467},[194,4204,4205],{"class":338},"            sources[mac].add(",[194,4207,4208],{"class":457},"\"arp\"",[194,4210,440],{"class":338},[194,4212,4213],{"class":196,"line":2500},[194,4214,394],{"emptyLinePlaceholder":393},[194,4216,4217,4219,4221,4223,4225,4228],{"class":196,"line":2533},[194,4218,1627],{"class":210},[194,4220,3273],{"class":338},[194,4222,525],{"class":210},[194,4224,4150],{"class":338},[194,4226,4227],{"class":457},"\"dhcp-leases.csv\"",[194,4229,4155],{"class":338},[194,4231,4232,4234,4236,4238,4241],{"class":196,"line":2560},[194,4233,1890],{"class":338},[194,4235,516],{"class":210},[194,4237,3392],{"class":338},[194,4239,4240],{"class":457},"\"ClientId\"",[194,4242,2718],{"class":338},[194,4244,4245,4247,4249],{"class":196,"line":2571},[194,4246,1640],{"class":210},[194,4248,1643],{"class":210},[194,4250,1902],{"class":338},[194,4252,4253],{"class":196,"line":2576},[194,4254,1885],{"class":210},[194,4256,4257,4260,4263,4265,4267,4269,4271,4273,4275,4277,4280],{"class":196,"line":2592},[194,4258,4259],{"class":338},"        inv[mac][",[194,4261,4262],{"class":457},"\"hostname\"",[194,4264,3172],{"class":338},[194,4266,516],{"class":210},[194,4268,4187],{"class":338},[194,4270,4262],{"class":457},[194,4272,3172],{"class":338},[194,4274,3139],{"class":210},[194,4276,3263],{"class":338},[194,4278,4279],{"class":457},"\"HostName\"",[194,4281,4200],{"class":338},[194,4283,4284,4286,4288,4291,4293,4295,4298,4301],{"class":196,"line":2602},[194,4285,1640],{"class":210},[194,4287,3306],{"class":338},[194,4289,4290],{"class":457},"\"AddressState\"",[194,4292,172],{"class":338},[194,4294,2048],{"class":457},[194,4296,4297],{"class":338},").startswith(",[194,4299,4300],{"class":457},"\"Active\"",[194,4302,1416],{"class":338},[194,4304,4305,4307,4309,4311,4313,4315,4317,4319,4321,4323,4326],{"class":196,"line":2613},[194,4306,4178],{"class":338},[194,4308,1928],{"class":457},[194,4310,3172],{"class":338},[194,4312,516],{"class":210},[194,4314,4187],{"class":338},[194,4316,1928],{"class":457},[194,4318,3172],{"class":338},[194,4320,3139],{"class":210},[194,4322,3263],{"class":338},[194,4324,4325],{"class":457},"\"IPAddress\"",[194,4327,4200],{"class":338},[194,4329,4330,4332,4335],{"class":196,"line":2618},[194,4331,4205],{"class":338},[194,4333,4334],{"class":457},"\"dhcp\"",[194,4336,440],{"class":338},[194,4338,4339,4342],{"class":196,"line":2633},[194,4340,4341],{"class":210},"        else",[194,4343,1554],{"class":338},[194,4345,4346,4348,4351,4354],{"class":196,"line":2664},[194,4347,4205],{"class":338},[194,4349,4350],{"class":457},"\"dhcp-history\"",[194,4352,4353],{"class":338},")  ",[194,4355,4356],{"class":200},"# expired, declined or offered: seen once, not proof it is here now\n",[194,4358,4359],{"class":196,"line":2692},[194,4360,394],{"emptyLinePlaceholder":393},[194,4362,4363,4365,4368,4370,4373,4375],{"class":196,"line":2721},[194,4364,1627],{"class":210},[194,4366,4367],{"class":338}," mac, port ",[194,4369,525],{"class":210},[194,4371,4372],{"class":338}," edge_ports(read_csv(path(",[194,4374,2726],{"class":457},[194,4376,4377],{"class":338},")), args.uplink_threshold).items():\n",[194,4379,4380],{"class":196,"line":2761},[194,4381,4382],{"class":338},"        inv[mac].update(port)\n",[194,4384,4385,4388,4390],{"class":196,"line":2796},[194,4386,4387],{"class":338},"        sources[mac].add(",[194,4389,2732],{"class":457},[194,4391,440],{"class":338},[194,4393,4394],{"class":196,"line":2801},[194,4395,394],{"emptyLinePlaceholder":393},[194,4397,4398,4401,4403,4406,4408,4411,4413,4416,4418,4421,4423,4426,4428],{"class":196,"line":2806},[194,4399,4400],{"class":338},"    ip_to_mac ",[194,4402,516],{"class":210},[194,4404,4405],{"class":338}," {row[",[194,4407,1928],{"class":457},[194,4409,4410],{"class":338},"]: mac ",[194,4412,1790],{"class":210},[194,4414,4415],{"class":338}," mac, row ",[194,4417,525],{"class":210},[194,4419,4420],{"class":338}," inv.items() ",[194,4422,852],{"class":210},[194,4424,4425],{"class":338}," row[",[194,4427,1928],{"class":457},[194,4429,4430],{"class":338},"]}\n",[194,4432,4433,4436,4438,4441],{"class":196,"line":2822},[194,4434,4435],{"class":338},"    ad_ips ",[194,4437,516],{"class":210},[194,4439,4440],{"class":207}," set",[194,4442,4443],{"class":338},"()\n",[194,4445,4447,4449,4451,4453,4455,4458],{"class":196,"line":4446},113,[194,4448,1627],{"class":210},[194,4450,3273],{"class":338},[194,4452,525],{"class":210},[194,4454,4150],{"class":338},[194,4456,4457],{"class":457},"\"ad-computers.csv\"",[194,4459,4155],{"class":338},[194,4461,4463,4466,4468,4470,4473,4475,4477],{"class":196,"line":4462},114,[194,4464,4465],{"class":338},"        ip ",[194,4467,516],{"class":210},[194,4469,3306],{"class":338},[194,4471,4472],{"class":457},"\"IPv4Address\"",[194,4474,172],{"class":338},[194,4476,2048],{"class":457},[194,4478,440],{"class":338},[194,4480,4482,4484],{"class":196,"line":4481},115,[194,4483,1640],{"class":210},[194,4485,3922],{"class":338},[194,4487,4489],{"class":196,"line":4488},116,[194,4490,4491],{"class":338},"            ad_ips.add(ip)\n",[194,4493,4495,4497,4499],{"class":196,"line":4494},117,[194,4496,1890],{"class":338},[194,4498,516],{"class":210},[194,4500,4501],{"class":338}," ip_to_mac.get(ip)\n",[194,4503,4505,4507],{"class":196,"line":4504},118,[194,4506,1640],{"class":210},[194,4508,1902],{"class":338},[194,4510,4512,4514,4517,4519,4521,4523,4526],{"class":196,"line":4511},119,[194,4513,4178],{"class":338},[194,4515,4516],{"class":457},"\"ad_name\"",[194,4518,3172],{"class":338},[194,4520,516],{"class":210},[194,4522,3263],{"class":338},[194,4524,4525],{"class":457},"\"Name\"",[194,4527,4200],{"class":338},[194,4529,4531,4533,4536,4538,4540,4542,4545,4547,4549],{"class":196,"line":4530},120,[194,4532,4178],{"class":338},[194,4534,4535],{"class":457},"\"os\"",[194,4537,3172],{"class":338},[194,4539,516],{"class":210},[194,4541,3306],{"class":338},[194,4543,4544],{"class":457},"\"OperatingSystem\"",[194,4546,172],{"class":338},[194,4548,2048],{"class":457},[194,4550,440],{"class":338},[194,4552,4554,4556,4559],{"class":196,"line":4553},121,[194,4555,4205],{"class":338},[194,4557,4558],{"class":457},"\"ad\"",[194,4560,440],{"class":338},[194,4562,4564],{"class":196,"line":4563},122,[194,4565,394],{"emptyLinePlaceholder":393},[194,4567,4569,4571,4574,4576],{"class":196,"line":4568},123,[194,4570,1627],{"class":210},[194,4572,4573],{"class":338}," ip, hit ",[194,4575,525],{"class":210},[194,4577,4578],{"class":338}," read_nmap(args.nmap).items():\n",[194,4580,4582,4584,4586,4589,4591,4593,4595,4598,4600,4602,4605,4607,4610,4612],{"class":196,"line":4581},124,[194,4583,1890],{"class":338},[194,4585,516],{"class":210},[194,4587,4588],{"class":338}," hit[",[194,4590,1944],{"class":457},[194,4592,3172],{"class":338},[194,4594,3139],{"class":210},[194,4596,4597],{"class":338}," ip_to_mac.get(ip) ",[194,4599,3139],{"class":210},[194,4601,3802],{"class":210},[194,4603,4604],{"class":457},"\"ip:",[194,4606,1570],{"class":207},[194,4608,4609],{"class":338},"ip",[194,4611,1576],{"class":207},[194,4613,725],{"class":457},[194,4615,4617,4619,4621,4623,4625,4627,4629,4631,4633],{"class":196,"line":4616},125,[194,4618,4259],{"class":338},[194,4620,1928],{"class":457},[194,4622,3172],{"class":338},[194,4624,516],{"class":210},[194,4626,4187],{"class":338},[194,4628,1928],{"class":457},[194,4630,3172],{"class":338},[194,4632,3139],{"class":210},[194,4634,4635],{"class":338}," ip\n",[194,4637,4639,4641,4643,4645,4647,4649,4651],{"class":196,"line":4638},126,[194,4640,4259],{"class":338},[194,4642,3703],{"class":457},[194,4644,3172],{"class":338},[194,4646,516],{"class":210},[194,4648,4588],{"class":338},[194,4650,3703],{"class":457},[194,4652,4200],{"class":338},[194,4654,4656,4658,4660,4662,4664,4666,4668],{"class":196,"line":4655},127,[194,4657,4259],{"class":338},[194,4659,3944],{"class":457},[194,4661,3172],{"class":338},[194,4663,516],{"class":210},[194,4665,4588],{"class":338},[194,4667,3944],{"class":457},[194,4669,4200],{"class":338},[194,4671,4673,4675,4678],{"class":196,"line":4672},128,[194,4674,4387],{"class":338},[194,4676,4677],{"class":457},"\"nmap\"",[194,4679,440],{"class":338},[194,4681,4683],{"class":196,"line":4682},129,[194,4684,394],{"emptyLinePlaceholder":393},[194,4686,4688,4691,4693,4695,4697,4699,4701,4703,4705,4707,4709,4711,4713,4715,4717,4719,4721,4723,4725,4727,4729,4731,4733,4735,4738],{"class":196,"line":4687},130,[194,4689,4690],{"class":338},"    fields ",[194,4692,516],{"class":210},[194,4694,1463],{"class":338},[194,4696,1944],{"class":457},[194,4698,172],{"class":338},[194,4700,1928],{"class":457},[194,4702,172],{"class":338},[194,4704,4262],{"class":457},[194,4706,172],{"class":338},[194,4708,4516],{"class":457},[194,4710,172],{"class":338},[194,4712,4535],{"class":457},[194,4714,172],{"class":338},[194,4716,3703],{"class":457},[194,4718,172],{"class":338},[194,4720,2732],{"class":457},[194,4722,172],{"class":338},[194,4724,3453],{"class":457},[194,4726,172],{"class":338},[194,4728,2737],{"class":457},[194,4730,172],{"class":338},[194,4732,3944],{"class":457},[194,4734,172],{"class":338},[194,4736,4737],{"class":457},"\"sources\"",[194,4739,4200],{"class":338},[194,4741,4743,4745,4747,4750,4753,4755,4757,4759,4761,4763,4765,4767,4769],{"class":196,"line":4742},131,[194,4744,2326],{"class":210},[194,4746,2329],{"class":207},[194,4748,4749],{"class":338},"(path(",[194,4751,4752],{"class":457},"\"inventory.csv\"",[194,4754,2282],{"class":338},[194,4756,2335],{"class":457},[194,4758,172],{"class":338},[194,4760,2340],{"class":369},[194,4762,516],{"class":210},[194,4764,2048],{"class":457},[194,4766,775],{"class":338},[194,4768,2349],{"class":210},[194,4770,2352],{"class":338},[194,4772,4774,4776,4778,4780,4782,4784],{"class":196,"line":4773},132,[194,4775,2358],{"class":338},[194,4777,516],{"class":210},[194,4779,2363],{"class":338},[194,4781,2366],{"class":369},[194,4783,516],{"class":210},[194,4785,2371],{"class":338},[194,4787,4789],{"class":196,"line":4788},133,[194,4790,2377],{"class":338},[194,4792,4794,4796,4798,4800,4803,4806,4809,4812,4815,4817,4819,4821,4824],{"class":196,"line":4793},134,[194,4795,2102],{"class":210},[194,4797,3403],{"class":338},[194,4799,525],{"class":210},[194,4801,4802],{"class":207}," sorted",[194,4804,4805],{"class":338},"(inv, ",[194,4807,4808],{"class":369},"key",[194,4810,4811],{"class":210},"=lambda",[194,4813,4814],{"class":338}," m: ipaddress.ip_address(inv[m][",[194,4816,1928],{"class":457},[194,4818,3172],{"class":338},[194,4820,3139],{"class":210},[194,4822,4823],{"class":457}," \"0.0.0.0\"",[194,4825,4155],{"class":338},[194,4827,4829,4832,4834,4836,4839,4842,4844,4847,4849,4852,4854,4857,4859,4861,4864,4866,4868,4871,4873,4876],{"class":196,"line":4828},135,[194,4830,4831],{"class":338},"            writer.writerow({",[194,4833,1944],{"class":457},[194,4835,3936],{"class":338},[194,4837,4838],{"class":210},"**",[194,4840,4841],{"class":338},"{f: inv[mac][f] ",[194,4843,1790],{"class":210},[194,4845,4846],{"class":338}," f ",[194,4848,525],{"class":210},[194,4850,4851],{"class":338}," fields[",[194,4853,1938],{"class":207},[194,4855,4856],{"class":338},":",[194,4858,488],{"class":210},[194,4860,1938],{"class":207},[194,4862,4863],{"class":338},"]}, ",[194,4865,4737],{"class":457},[194,4867,710],{"class":338},[194,4869,4870],{"class":457},"\"+\"",[194,4872,2214],{"class":338},[194,4874,4875],{"class":207},"sorted",[194,4877,4878],{"class":338},"(sources[mac]))})\n",[194,4880,4882],{"class":196,"line":4881},136,[194,4883,394],{"emptyLinePlaceholder":393},[194,4885,4887,4890,4892,4894,4897,4899,4901,4903,4905,4907],{"class":196,"line":4886},137,[194,4888,4889],{"class":338},"    gaps ",[194,4891,516],{"class":210},[194,4893,4802],{"class":207},[194,4895,4896],{"class":338},"({row[",[194,4898,1928],{"class":457},[194,4900,3172],{"class":338},[194,4902,1790],{"class":210},[194,4904,4415],{"class":338},[194,4906,525],{"class":210},[194,4908,4909],{"class":338}," inv.items()\n",[194,4911,4913,4916,4918,4920,4922,4924,4927,4929,4932,4934,4937,4939,4941,4943,4945,4947,4949,4951,4953,4955,4958,4960,4962],{"class":196,"line":4912},138,[194,4914,4915],{"class":210},"                   if",[194,4917,4425],{"class":338},[194,4919,1928],{"class":457},[194,4921,3172],{"class":338},[194,4923,3406],{"class":210},[194,4925,4926],{"class":457}," \"arp\"",[194,4928,3316],{"class":210},[194,4930,4931],{"class":338}," sources[mac] ",[194,4933,3406],{"class":210},[194,4935,4936],{"class":457}," \"dhcp\"",[194,4938,1643],{"class":210},[194,4940,3316],{"class":210},[194,4942,4931],{"class":338},[194,4944,3406],{"class":210},[194,4946,4425],{"class":338},[194,4948,1928],{"class":457},[194,4950,3172],{"class":338},[194,4952,3313],{"class":210},[194,4954,3316],{"class":210},[194,4956,4957],{"class":338}," ad_ips}, ",[194,4959,4808],{"class":369},[194,4961,516],{"class":210},[194,4963,4964],{"class":338},"ipaddress.ip_address)\n",[194,4966,4968,4970,4972,4974,4977,4979,4981,4983,4985],{"class":196,"line":4967},139,[194,4969,2326],{"class":210},[194,4971,2329],{"class":207},[194,4973,4749],{"class":338},[194,4975,4976],{"class":457},"\"gaps.txt\"",[194,4978,2282],{"class":338},[194,4980,2335],{"class":457},[194,4982,775],{"class":338},[194,4984,2349],{"class":210},[194,4986,2352],{"class":338},[194,4988,4990,4993,4995,4998,5000,5003,5005,5007,5009,5011,5013,5015,5018,5020,5022],{"class":196,"line":4989},140,[194,4991,4992],{"class":338},"        handle.write(",[194,4994,1595],{"class":457},[194,4996,4997],{"class":207},"\\n",[194,4999,1595],{"class":457},[194,5001,5002],{"class":338},".join(gaps) ",[194,5004,1649],{"class":210},[194,5006,1062],{"class":338},[194,5008,1595],{"class":457},[194,5010,4997],{"class":207},[194,5012,1595],{"class":457},[194,5014,1442],{"class":210},[194,5016,5017],{"class":338}," gaps ",[194,5019,1448],{"class":210},[194,5021,1451],{"class":457},[194,5023,1989],{"class":338},[194,5025,5027,5029,5031,5033,5035,5037,5040,5042,5045,5047,5050,5052,5055],{"class":196,"line":5026},141,[194,5028,2764],{"class":207},[194,5030,366],{"class":338},[194,5032,1564],{"class":210},[194,5034,1595],{"class":457},[194,5036,2773],{"class":207},[194,5038,5039],{"class":338},"(inv)",[194,5041,1576],{"class":207},[194,5043,5044],{"class":457}," devices in inventory.csv, ",[194,5046,2773],{"class":207},[194,5048,5049],{"class":338},"(gaps)",[194,5051,1576],{"class":207},[194,5053,5054],{"class":457}," unexplained IPs in gaps.txt\"",[194,5056,440],{"class":338},[194,5058,5060],{"class":196,"line":5059},142,[194,5061,394],{"emptyLinePlaceholder":393},[194,5063,5065],{"class":196,"line":5064},143,[194,5066,394],{"emptyLinePlaceholder":393},[194,5068,5070,5072,5074,5076,5078],{"class":196,"line":5069},144,[194,5071,852],{"class":210},[194,5073,2811],{"class":207},[194,5075,2814],{"class":210},[194,5077,2817],{"class":457},[194,5079,1554],{"class":338},[194,5081,5083],{"class":196,"line":5082},145,[194,5084,2825],{"class":338},[11,5086,5087],{},"The output looks like this:",[185,5089,5093],{"className":5090,"code":5092,"language":1521,"meta":190},[5091],"language-text","mac,ip,hostname,ad_name,os,vendor,switch,port,vlan,services,sources\n00:50:56:ab:cd:01,10.0.0.1,,,,,,,,,arp\n00:50:56:ab:cd:20,10.0.0.20,WS-ACCT-07.corp.example,WS-ACCT-07,Windows 11 Enterprise,,sw1,Gi1\u002F0\u002F1,10,,ad+arp+dhcp+switch\nb0:7b:25:00:00:30,10.0.0.30,,,,Dell,sw1,Gi1\u002F0\u002F48,10,443\u002Fhttps lighttpd,arp+nmap+switch\n",[24,5094,5092],{"__ignoreMap":190},[11,5096,5097,5098,5101,5102,5105,5106,5109],{},"The ",[24,5099,5100],{},"sources"," column is the useful part. A row with ",[24,5103,5104],{},"ad+arp+dhcp+switch"," is fully explained. A row with only ",[24,5107,5108],{},"arp"," is a question for the people who run the place.",[62,5111,5113],{"id":5112},"passive-before-active","Passive Before Active",[11,5115,5116,5117,5119],{},"I run passive discovery first, before anything that actively probes the network. An undocumented environment is exactly the kind of place where an aggressive subnet-wide scan finds a piece of ancient building-management or industrial gear that falls over the moment something unexpected talks to it. Nmap's own documentation makes the relevant point: crashes are rare with the default timing, and \"omitting version detection is far more effective than playing with timing values at reducing these problems.\" So the active step is narrow on purpose: only the addresses in ",[24,5118,46],{},", with anything known to be fragile excluded, and version detection kept light.",[185,5121,5123],{"className":2828,"code":5122,"language":2830,"meta":190,"style":190},"#!\u002Fusr\u002Fbin\u002Fenv bash\n# Scoped active pass: only the addresses passive discovery could not explain.\nset -euo pipefail\nDIR=\u002Fsrv\u002Fdiscovery\u002F2026-07-15\n\n# fragile.txt: one address or range per line (PLCs, BMS controllers, old UPS cards).\ntouch \"$DIR\u002Ffragile.txt\"\n\n# 1. Confirm which gap addresses are alive. On a local Ethernet segment nmap uses ARP for this.\nnmap -sn -iL \"$DIR\u002Fgaps.txt\" --excludefile \"$DIR\u002Ffragile.txt\" -oX \"$DIR\u002Fping.xml\"\n\n# 2. Light service detection on the top 100 ports, rate-capped.\nnmap -sV --version-light --top-ports 100 -T3 --max-rate 100 \\\n    -iL \"$DIR\u002Fgaps.txt\" --excludefile \"$DIR\u002Ffragile.txt\" -oX \"$DIR\u002Fservices.xml\"\n\n# 3. Fold the results back into the inventory.\npython3 merge_inventory.py --dir \"$DIR\" --nmap \"$DIR\u002Fservices.xml\"\n",[24,5124,5125,5130,5135,5145,5155,5159,5164,5177,5181,5186,5223,5227,5232,5258,5286,5290,5295],{"__ignoreMap":190},[194,5126,5127],{"class":196,"line":197},[194,5128,5129],{"class":200},"#!\u002Fusr\u002Fbin\u002Fenv bash\n",[194,5131,5132],{"class":196,"line":204},[194,5133,5134],{"class":200},"# Scoped active pass: only the addresses passive discovery could not explain.\n",[194,5136,5137,5139,5142],{"class":196,"line":214},[194,5138,4133],{"class":207},[194,5140,5141],{"class":207}," -euo",[194,5143,5144],{"class":457}," pipefail\n",[194,5146,5147,5150,5152],{"class":196,"line":220},[194,5148,5149],{"class":338},"DIR",[194,5151,516],{"class":210},[194,5153,5154],{"class":457},"\u002Fsrv\u002Fdiscovery\u002F2026-07-15\n",[194,5156,5157],{"class":196,"line":228},[194,5158,394],{"emptyLinePlaceholder":393},[194,5160,5161],{"class":196,"line":234},[194,5162,5163],{"class":200},"# fragile.txt: one address or range per line (PLCs, BMS controllers, old UPS cards).\n",[194,5165,5166,5169,5171,5174],{"class":196,"line":240},[194,5167,5168],{"class":1404},"touch",[194,5170,963],{"class":457},[194,5172,5173],{"class":338},"$DIR",[194,5175,5176],{"class":457},"\u002Ffragile.txt\"\n",[194,5178,5179],{"class":196,"line":246},[194,5180,394],{"emptyLinePlaceholder":393},[194,5182,5183],{"class":196,"line":252},[194,5184,5185],{"class":200},"# 1. Confirm which gap addresses are alive. On a local Ethernet segment nmap uses ARP for this.\n",[194,5187,5188,5190,5193,5196,5198,5200,5203,5206,5208,5210,5213,5216,5218,5220],{"class":196,"line":263},[194,5189,53],{"class":1404},[194,5191,5192],{"class":207}," -sn",[194,5194,5195],{"class":207}," -iL",[194,5197,963],{"class":457},[194,5199,5173],{"class":338},[194,5201,5202],{"class":457},"\u002Fgaps.txt\"",[194,5204,5205],{"class":207}," --excludefile",[194,5207,963],{"class":457},[194,5209,5173],{"class":338},[194,5211,5212],{"class":457},"\u002Ffragile.txt\"",[194,5214,5215],{"class":207}," -oX",[194,5217,963],{"class":457},[194,5219,5173],{"class":338},[194,5221,5222],{"class":457},"\u002Fping.xml\"\n",[194,5224,5225],{"class":196,"line":269},[194,5226,394],{"emptyLinePlaceholder":393},[194,5228,5229],{"class":196,"line":279},[194,5230,5231],{"class":200},"# 2. Light service detection on the top 100 ports, rate-capped.\n",[194,5233,5234,5236,5239,5242,5245,5248,5251,5254,5256],{"class":196,"line":285},[194,5235,53],{"class":1404},[194,5237,5238],{"class":207}," -sV",[194,5240,5241],{"class":207}," --version-light",[194,5243,5244],{"class":207}," --top-ports",[194,5246,5247],{"class":207}," 100",[194,5249,5250],{"class":207}," -T3",[194,5252,5253],{"class":207}," --max-rate",[194,5255,5247],{"class":207},[194,5257,2865],{"class":207},[194,5259,5260,5263,5265,5267,5269,5271,5273,5275,5277,5279,5281,5283],{"class":196,"line":295},[194,5261,5262],{"class":207},"    -iL",[194,5264,963],{"class":457},[194,5266,5173],{"class":338},[194,5268,5202],{"class":457},[194,5270,5205],{"class":207},[194,5272,963],{"class":457},[194,5274,5173],{"class":338},[194,5276,5212],{"class":457},[194,5278,5215],{"class":207},[194,5280,963],{"class":457},[194,5282,5173],{"class":338},[194,5284,5285],{"class":457},"\u002Fservices.xml\"\n",[194,5287,5288],{"class":196,"line":301},[194,5289,394],{"emptyLinePlaceholder":393},[194,5291,5292],{"class":196,"line":309},[194,5293,5294],{"class":200},"# 3. Fold the results back into the inventory.\n",[194,5296,5297,5299,5302,5305,5307,5309,5311,5314,5316,5318],{"class":196,"line":315},[194,5298,2850],{"class":1404},[194,5300,5301],{"class":457}," merge_inventory.py",[194,5303,5304],{"class":207}," --dir",[194,5306,963],{"class":457},[194,5308,5173],{"class":338},[194,5310,1595],{"class":457},[194,5312,5313],{"class":207}," --nmap",[194,5315,963],{"class":457},[194,5317,5173],{"class":338},[194,5319,5285],{"class":457},[11,5321,5322,5325,5326,5329,5330,5333,5334,5337,5338,5341,5342,5345,5346,5349,5350,5353,5354,5357,5358,5361,5362,5365,5366,5369],{},[24,5323,5324],{},"-sn"," does host discovery only, with no port scan. ",[24,5327,5328],{},"-iL"," reads targets from a file and ",[24,5331,5332],{},"--excludefile"," removes addresses from any range you give it. ",[24,5335,5336],{},"--version-light"," is shorthand for ",[24,5339,5340],{},"--version-intensity 2",", faster and a little less likely to identify a service than the default intensity of 7. ",[24,5343,5344],{},"--max-rate 100"," caps sending at 100 packets per second. ",[24,5347,5348],{},"-oX"," writes the XML the merge script parses: each ",[24,5351,5352],{},"\u003Chost>"," has ",[24,5355,5356],{},"\u003Caddress addrtype=\"ipv4\">"," and, on a local segment, ",[24,5359,5360],{},"\u003Caddress addrtype=\"mac\" vendor=\"…\">",", and each open ",[24,5363,5364],{},"\u003Cport>"," carries a ",[24,5367,5368],{},"\u003Cservice name product version>",". Run nmap as root (or with raw-socket capability) so it can use ARP and SYN probes.",[11,5371,5372,5373,5378,5379,5382,5383,5387],{},"For deeper per-device fingerprinting of what's left, the ",[5374,5375,5377],"a",{"href":5376},"\u002F2026\u002F01\u002F07\u002Fpython-discovery-fingerprint-every-device-on-a-subnet\u002F","subnet fingerprinting script"," adds vendor lookups, SSH banners and HTTP ",[24,5380,5381],{},"Server"," headers, and the ",[5374,5384,5386],{"href":5385},"\u002F2026\u002F02\u002F11\u002Fnode-js-discovery-scanning-a-subnet-for-open-rdp-and-ssh\u002F","RDP and SSH scanner"," answers the narrower \"where can someone log in remotely\" question.",[62,5389,5391],{"id":5390},"interview-people-to-fill-gaps-not-to-start-the-picture","Interview People to Fill Gaps, Not to Start the Picture",[11,5393,5394],{},"Once discovery has done as much as it can on its own, that is when I bring in the people who have been running the place. But the conversation changes completely when you show up with \"here is what I found, help me understand these three things I could not resolve on my own\" instead of \"tell me what you remember about the network.\" The first version respects their time and gets specific, useful answers. The second version puts someone on the spot to reconstruct years of undocumented decisions from memory, and the gaps in what they say get treated as gaps in the network rather than gaps in recall.",[11,5396,5397,5398,5400,5401,176,5404,5407],{},"In practice the interview sheet is just the inventory filtered to rows whose ",[24,5399,5100],{}," column lacks ",[24,5402,5403],{},"dhcp",[24,5405,5406],{},"ad",", sorted by switch and port, so someone can walk to the closet with it:",[185,5409,5411],{"className":2828,"code":5410,"language":2830,"meta":190,"style":190},"python3 - \u003C\u003C'EOF'\nimport csv\nrows = [r for r in csv.DictReader(open(\"\u002Fsrv\u002Fdiscovery\u002F2026-07-15\u002Finventory.csv\"))\n        if not {\"dhcp\", \"ad\"} & set(r[\"sources\"].split(\"+\"))]\nfor r in sorted(rows, key=lambda r: (r[\"switch\"], r[\"port\"])):\n    print(f'{r[\"switch\"]:\u003C12} {r[\"port\"]:\u003C12} {r[\"ip\"]:\u003C15} {r[\"mac\"]}  {r[\"vendor\"]}  {r[\"services\"]}')\nEOF\n",[24,5412,5413,5425,5430,5435,5440,5445,5450],{"__ignoreMap":190},[194,5414,5415,5417,5419,5422],{"class":196,"line":197},[194,5416,2850],{"class":1404},[194,5418,482],{"class":457},[194,5420,5421],{"class":210}," \u003C\u003C",[194,5423,5424],{"class":457},"'EOF'\n",[194,5426,5427],{"class":196,"line":204},[194,5428,5429],{"class":457},"import csv\n",[194,5431,5432],{"class":196,"line":214},[194,5433,5434],{"class":457},"rows = [r for r in csv.DictReader(open(\"\u002Fsrv\u002Fdiscovery\u002F2026-07-15\u002Finventory.csv\"))\n",[194,5436,5437],{"class":196,"line":220},[194,5438,5439],{"class":457},"        if not {\"dhcp\", \"ad\"} & set(r[\"sources\"].split(\"+\"))]\n",[194,5441,5442],{"class":196,"line":228},[194,5443,5444],{"class":457},"for r in sorted(rows, key=lambda r: (r[\"switch\"], r[\"port\"])):\n",[194,5446,5447],{"class":196,"line":234},[194,5448,5449],{"class":457},"    print(f'{r[\"switch\"]:\u003C12} {r[\"port\"]:\u003C12} {r[\"ip\"]:\u003C15} {r[\"mac\"]}  {r[\"vendor\"]}  {r[\"services\"]}')\n",[194,5451,5452],{"class":196,"line":240},[194,5453,5454],{"class":457},"EOF\n",[62,5456,5458],{"id":5457},"documentation-that-outlives-the-discovery-effort","Documentation That Outlives the Discovery Effort",[11,5460,5461],{},"The point of doing this discovery-first is not just to produce one good diagram, it is to leave behind a process that can be re-run. I hand off the inventory and the scripts that built it, not just a static drawing, so that six months later when something has inevitably changed, the next person does not have to start from zero and a stale document again. An undocumented network usually got that way because documentation was treated as a one-time deliverable instead of a habit; discovery-first work only pays off long term if it becomes the habit, not the exception.",[11,5463,5464,5465,5468],{},"Making it a habit is mostly scheduling. A weekly run into a dated folder, committed to a Git repository, turns ",[24,5466,5467],{},"git diff"," into a change report:",[185,5470,5472],{"className":2828,"code":5471,"language":2830,"meta":190,"style":190},"# \u002Fetc\u002Fcron.d\u002Fdiscovery: Sundays 02:00, passive collection only\n0 2 * * 0 discovery \u002Fsrv\u002Fdiscovery\u002Frun-weekly.sh >> \u002Fvar\u002Flog\u002Fdiscovery.log 2>&1\n",[24,5473,5474,5479],{"__ignoreMap":190},[194,5475,5476],{"class":196,"line":197},[194,5477,5478],{"class":200},"# \u002Fetc\u002Fcron.d\u002Fdiscovery: Sundays 02:00, passive collection only\n",[194,5480,5481,5483,5485,5488,5490,5492,5495,5498,5501,5504],{"class":196,"line":204},[194,5482,1922],{"class":1404},[194,5484,3169],{"class":207},[194,5486,5487],{"class":207}," *",[194,5489,5487],{"class":207},[194,5491,1551],{"class":207},[194,5493,5494],{"class":457}," discovery",[194,5496,5497],{"class":457}," \u002Fsrv\u002Fdiscovery\u002Frun-weekly.sh",[194,5499,5500],{"class":210}," >>",[194,5502,5503],{"class":457}," \u002Fvar\u002Flog\u002Fdiscovery.log",[194,5505,5506],{"class":210}," 2>&1\n",[185,5508,5510],{"className":2828,"code":5509,"language":2830,"meta":190,"style":190},"#!\u002Fusr\u002Fbin\u002Fenv bash\n# \u002Fsrv\u002Fdiscovery\u002Frun-weekly.sh\nset -euo pipefail\n. \u002Fsrv\u002Fdiscovery\u002Fenv   # exports SNMP_COMMUNITY, ROUTERS, SWITCHES, VLANS (mode 600)\nDIR=\u002Fsrv\u002Fdiscovery\u002F$(date +%F)\nmkdir -p \"$DIR\"\ncp \u002Fsrv\u002Fdiscovery\u002Flatest\u002Fdhcp-leases.csv \u002Fsrv\u002Fdiscovery\u002Flatest\u002Fad-computers.csv \"$DIR\"\u002F 2>\u002Fdev\u002Fnull || true\npython3 \u002Fsrv\u002Fdiscovery\u002Fsnmp_collect.py --routers $ROUTERS --switches $SWITCHES --vlans $VLANS --out \"$DIR\"\npython3 \u002Fsrv\u002Fdiscovery\u002Fmerge_inventory.py --dir \"$DIR\"\nln -sfn \"$DIR\" \u002Fsrv\u002Fdiscovery\u002Flatest\ncd \u002Fsrv\u002Fdiscovery\n# env holds the community string: it is listed in .gitignore and never staged.\ngit add -- \"$DIR\" latest\ngit diff --cached --quiet || git commit -qm \"discovery $(date +%F)\"\n",[24,5511,5512,5516,5521,5529,5539,5559,5573,5603,5635,5650,5667,5675,5680,5700],{"__ignoreMap":190},[194,5513,5514],{"class":196,"line":197},[194,5515,5129],{"class":200},[194,5517,5518],{"class":196,"line":204},[194,5519,5520],{"class":200},"# \u002Fsrv\u002Fdiscovery\u002Frun-weekly.sh\n",[194,5522,5523,5525,5527],{"class":196,"line":214},[194,5524,4133],{"class":207},[194,5526,5141],{"class":207},[194,5528,5144],{"class":457},[194,5530,5531,5533,5536],{"class":196,"line":220},[194,5532,180],{"class":207},[194,5534,5535],{"class":457}," \u002Fsrv\u002Fdiscovery\u002Fenv",[194,5537,5538],{"class":200},"   # exports SNMP_COMMUNITY, ROUTERS, SWITCHES, VLANS (mode 600)\n",[194,5540,5541,5543,5545,5548,5551,5554,5557],{"class":196,"line":228},[194,5542,5149],{"class":338},[194,5544,516],{"class":210},[194,5546,5547],{"class":457},"\u002Fsrv\u002Fdiscovery\u002F",[194,5549,5550],{"class":338},"$(",[194,5552,5553],{"class":1404},"date",[194,5555,5556],{"class":457}," +%F",[194,5558,440],{"class":338},[194,5560,5561,5564,5567,5569,5571],{"class":196,"line":234},[194,5562,5563],{"class":1404},"mkdir",[194,5565,5566],{"class":207}," -p",[194,5568,963],{"class":457},[194,5570,5173],{"class":338},[194,5572,725],{"class":457},[194,5574,5575,5578,5581,5584,5586,5588,5591,5594,5597,5600],{"class":196,"line":240},[194,5576,5577],{"class":1404},"cp",[194,5579,5580],{"class":457}," \u002Fsrv\u002Fdiscovery\u002Flatest\u002Fdhcp-leases.csv",[194,5582,5583],{"class":457}," \u002Fsrv\u002Fdiscovery\u002Flatest\u002Fad-computers.csv",[194,5585,963],{"class":457},[194,5587,5173],{"class":338},[194,5589,5590],{"class":457},"\"\u002F",[194,5592,5593],{"class":210}," 2>",[194,5595,5596],{"class":457},"\u002Fdev\u002Fnull",[194,5598,5599],{"class":210}," ||",[194,5601,5602],{"class":207}," true\n",[194,5604,5605,5607,5610,5612,5615,5618,5621,5623,5626,5629,5631,5633],{"class":196,"line":246},[194,5606,2850],{"class":1404},[194,5608,5609],{"class":457}," \u002Fsrv\u002Fdiscovery\u002Fsnmp_collect.py",[194,5611,2856],{"class":207},[194,5613,5614],{"class":338}," $ROUTERS ",[194,5616,5617],{"class":207},"--switches",[194,5619,5620],{"class":338}," $SWITCHES ",[194,5622,1138],{"class":207},[194,5624,5625],{"class":338}," $VLANS ",[194,5627,5628],{"class":207},"--out",[194,5630,963],{"class":457},[194,5632,5173],{"class":338},[194,5634,725],{"class":457},[194,5636,5637,5639,5642,5644,5646,5648],{"class":196,"line":252},[194,5638,2850],{"class":1404},[194,5640,5641],{"class":457}," \u002Fsrv\u002Fdiscovery\u002Fmerge_inventory.py",[194,5643,5304],{"class":207},[194,5645,963],{"class":457},[194,5647,5173],{"class":338},[194,5649,725],{"class":457},[194,5651,5652,5655,5658,5660,5662,5664],{"class":196,"line":263},[194,5653,5654],{"class":1404},"ln",[194,5656,5657],{"class":207}," -sfn",[194,5659,963],{"class":457},[194,5661,5173],{"class":338},[194,5663,1595],{"class":457},[194,5665,5666],{"class":457}," \u002Fsrv\u002Fdiscovery\u002Flatest\n",[194,5668,5669,5672],{"class":196,"line":269},[194,5670,5671],{"class":207},"cd",[194,5673,5674],{"class":457}," \u002Fsrv\u002Fdiscovery\n",[194,5676,5677],{"class":196,"line":279},[194,5678,5679],{"class":200},"# env holds the community string: it is listed in .gitignore and never staged.\n",[194,5681,5682,5685,5688,5691,5693,5695,5697],{"class":196,"line":285},[194,5683,5684],{"class":1404},"git",[194,5686,5687],{"class":457}," add",[194,5689,5690],{"class":207}," --",[194,5692,963],{"class":457},[194,5694,5173],{"class":338},[194,5696,1595],{"class":457},[194,5698,5699],{"class":457}," latest\n",[194,5701,5702,5704,5707,5710,5713,5715,5718,5721,5724,5727,5729],{"class":196,"line":295},[194,5703,5684],{"class":1404},[194,5705,5706],{"class":457}," diff",[194,5708,5709],{"class":207}," --cached",[194,5711,5712],{"class":207}," --quiet",[194,5714,5599],{"class":210},[194,5716,5717],{"class":1404}," git",[194,5719,5720],{"class":457}," commit",[194,5722,5723],{"class":207}," -qm",[194,5725,5726],{"class":457}," \"discovery $(",[194,5728,5553],{"class":1404},[194,5730,5731],{"class":457}," +%F)\"\n",[11,5733,5734,5735,5739],{},"The Windows half runs as a scheduled task on a management server and drops its two CSVs into the share the Linux collector reads. Keep the active nmap step out of the schedule. It stays a deliberate, scoped action someone chooses to run; to flag new hosts between those runs, ",[5374,5736,5738],{"href":5737},"\u002F2026\u002F03\u002F11\u002Fnode-js-discovery-diffing-two-network-scans-to-flag-new-hosts\u002F","diff two scans"," instead.",[11,5741,5742,5743,5747,5748,5752,5753,1113,5757,5761],{},"From here the inventory feeds everything else: ",[5374,5744,5746],{"href":5745},"\u002F2025\u002F12\u002F10\u002Fpython-discovery-inventory-vmware-hosts-and-vms-via-pyvmomi\u002F","VMware inventory"," for the virtual side, ",[5374,5749,5751],{"href":5750},"\u002F2026\u002F01\u002F28\u002Fpython-discovery-crawling-dns-zones-for-orphaned-records\u002F","orphaned DNS records"," cross-checked against live IPs, a ",[5374,5754,5756],{"href":5755},"\u002F2026\u002F09\u002F02\u002Fwindows-management-building-a-discovery-inventory-nobody-has-to-maintain-by-hand\u002F","Windows inventory that maintains itself",[5374,5758,5760],{"href":5759},"\u002F2025\u002F08\u002F20\u002Fpowershell-connectwise-sync-configuration-items-from-discovery-scans\u002F","ConnectWise configuration items"," synced from the results so the PSA matches reality.",[62,5763,5765],{"id":5764},"references","References",[1051,5767,5768,5776,5782,5790,5798,5808,5816,5823],{},[21,5769,5770,5775],{},[5374,5771,151],{"href":5772,"rel":5773},"https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fpowershell\u002Fmodule\u002Fdhcpserver\u002Fget-dhcpserverv4lease",[5774],"nofollow"," (Microsoft Learn)",[21,5777,5778,5775],{},[5374,5779,163],{"href":5780,"rel":5781},"https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fpowershell\u002Fmodule\u002Factivedirectory\u002Fget-adcomputer",[5774],[21,5783,5784,5789],{},[5374,5785,5788],{"href":5786,"rel":5787},"https:\u002F\u002Fwww.rfc-editor.org\u002Frfc\u002Frfc4188",[5774],"RFC 4188: Definitions of Managed Objects for Bridges"," (BRIDGE-MIB)",[21,5791,5792,5797],{},[5374,5793,5796],{"href":5794,"rel":5795},"https:\u002F\u002Fwww.rfc-editor.org\u002Frfc\u002Frfc4293",[5774],"RFC 4293: Management Information Base for the Internet Protocol"," (IP-MIB)",[21,5799,5800,1062,5805,701],{},[5374,5801,5804],{"href":5802,"rel":5803},"https:\u002F\u002Fwww.rfc-editor.org\u002Frfc\u002Frfc1213",[5774],"RFC 1213: MIB-II",[24,5806,5807],{},"ipNetToMediaTable",[21,5809,5810,5815],{},[5374,5811,5814],{"href":5812,"rel":5813},"https:\u002F\u002Fwww.cisco.com\u002Fc\u002Fen\u002Fus\u002Fsupport\u002Fdocs\u002Fip\u002Fsimple-network-management-protocol-snmp\u002F40367-camsnmp40367.html",[5774],"Use SNMP to obtain the CAM table on Catalyst switches"," (Cisco, community string indexing)",[21,5817,5818],{},[5374,5819,5822],{"href":5820,"rel":5821},"https:\u002F\u002Fwww.net-snmp.org\u002Fdocs\u002Fman\u002Fsnmpcmd.html",[5774],"Net-SNMP snmpcmd options",[21,5824,5825,5826,172,5831,172,5836,172,5841,172,5846],{},"Nmap reference guide: ",[5374,5827,5830],{"href":5828,"rel":5829},"https:\u002F\u002Fnmap.org\u002Fbook\u002Fman-host-discovery.html",[5774],"host discovery",[5374,5832,5835],{"href":5833,"rel":5834},"https:\u002F\u002Fnmap.org\u002Fbook\u002Fman-target-specification.html",[5774],"target specification",[5374,5837,5840],{"href":5838,"rel":5839},"https:\u002F\u002Fnmap.org\u002Fbook\u002Fman-version-detection.html",[5774],"version detection",[5374,5842,5845],{"href":5843,"rel":5844},"https:\u002F\u002Fnmap.org\u002Fbook\u002Fman-performance.html",[5774],"timing and performance",[5374,5847,5850],{"href":5848,"rel":5849},"https:\u002F\u002Fnmap.org\u002Fbook\u002Foutput-formats-xml-output.html",[5774],"XML output",[5852,5853,5854],"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 .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}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);}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}",{"title":190,"searchDepth":204,"depth":204,"links":5856},[5857,5858,5859,5860,5861,5862,5863,5864],{"id":64,"depth":204,"text":65},{"id":144,"depth":204,"text":145},{"id":1045,"depth":204,"text":1046},{"id":2899,"depth":204,"text":2900},{"id":5112,"depth":204,"text":5113},{"id":5390,"depth":204,"text":5391},{"id":5457,"depth":204,"text":5458},{"id":5764,"depth":204,"text":5765},"techcolumnist",[5867,5868],"engineering","strategy","2026-07-15T14:00:00Z","Document an undocumented network from what it already knows: DHCP leases, AD, ARP and switch MAC tables, then a scoped nmap pass. Scripts included.","md",false,null,{},"\u002Fblog\u002F2026\u002F07\u002F15\u002Finfrastructure-a-discovery-first-approach-to-documenting-an-undocumented-network",{"title":6,"description":5870},[5865],"blog\u002F2026\u002F07\u002F15\u002Finfrastructure-a-discovery-first-approach-to-documenting-an-undocumented-network",[5880,5881,1177],"discovery","network","\u002F2026\u002F07\u002F15\u002Finfrastructure-a-discovery-first-approach-to-documenting-an-undocumented-network\u002F","sWjPecxqXzdAUQr2UwAqnEmmWawbeKHzNyebD_9MwsI",{"title":5885,"description":5886,"date":5887,"url":5888,"categories":5889,"tags":5890,"image":5873,"readingTime":246,"canonical":5865,"sites":5893,"series":5873,"seriesOrder":5873},"Infrastructure: What a Real Disaster Recovery Test Actually Reveals","What a real DR failover exposes that a tabletop never catches, with a Hyper-V Replica test failover script that checks each recovered VM and a runbook checklist.","2026-07-22T14:00:00Z","\u002F2026\u002F07\u002F22\u002Finfrastructure-what-a-real-disaster-recovery-test-actually-reveals\u002F",[5867,5868],[5891,5892],"backup","hyper-v",[5865],{"title":5895,"description":5896,"date":5897,"url":5898,"categories":5899,"tags":5900,"image":5873,"readingTime":240,"canonical":5865,"sites":5903,"series":5873,"seriesOrder":5873},"Kubernetes: Running Talos Alongside a Legacy Hyper-V Estate","Introducing a Talos Kubernetes cluster into an existing Hyper-V estate: VM build script, Image Factory schematic, VLAN, VIP, firewall and storage boundaries.","2026-07-08T14:00:00Z","\u002F2026\u002F07\u002F08\u002Fkubernetes-running-talos-alongside-a-legacy-hyper-v-estate\u002F",[5867],[5901,5902,5892],"talos","kubernetes",[5865],[5905,5914,5921],{"title":5906,"description":5907,"date":5908,"url":5750,"categories":5909,"tags":5911,"image":5873,"readingTime":214,"canonical":5865,"sites":5913,"series":5873,"seriesOrder":5873},"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",[5910,5867],"scripts",[1177,5880,5881,5912],"windows",[5865],{"title":5915,"description":5916,"date":5917,"url":5376,"categories":5918,"tags":5919,"image":5873,"readingTime":214,"canonical":5865,"sites":5920,"series":5873,"seriesOrder":5873},"Python: Discovery – Fingerprint Every Device on a Subnet","A Python script that ARP-sweeps a local subnet with Scapy, maps MACs to IEEE vendors, probes common ports for banners, and writes one CSV row per device.","2026-01-07T14:00:00Z",[5910,5867],[1177,5880,5881],[5865],{"title":5922,"description":5923,"date":5924,"url":5755,"categories":5925,"tags":5926,"image":5873,"readingTime":228,"canonical":5865,"sites":5928,"series":5873,"seriesOrder":5873},"Windows Management: A Discovery Inventory Nobody Maintains by Hand","Why a self-refreshing Windows inventory built from AD and CIM discovery outlasts a spreadsheet, with the sweep script, staleness report and schedule.","2026-09-02T14:00:00Z",[5867,5868],[5880,5912,5927],"active-directory",[5865],{"doc":5873,"posts":5930},[],1790052514066]