[{"data":1,"prerenderedAt":2731},["ShallowReactive",2],{"post:\u002F2026\u002F01\u002F28\u002Fpython-discovery-crawling-dns-zones-for-orphaned-records\u002F":3},{"post":4,"newer":2682,"older":2692,"related":2703,"series":2729},{"id":5,"title":6,"body":7,"canonical":2662,"categories":2663,"date":2666,"description":2667,"extension":2668,"featured":2669,"hero":2670,"image":2670,"meta":2671,"navigation":381,"path":2672,"readingTime":208,"seo":2673,"series":2670,"seriesOrder":2670,"sites":2674,"source":2670,"stem":2675,"tags":2676,"updated":2670,"url":2680,"__hash__":2681},"blog\u002Fblog\u002F2026\u002F01\u002F28\u002Fpython-discovery-crawling-dns-zones-for-orphaned-records.md","Python: Discovery – Crawling DNS Zones for Orphaned Records",{"type":8,"value":9,"toc":2655},"minimark",[10,14,23,28,93,97,100,132,135,168,176,183,729,733,2486,2490,2574,2578,2651],[11,12,13],"p",{},"Internal DNS zones only ever seem to grow. Decommissioned servers, one-off project VMs, printers that got replaced two refreshes ago: the A records outlive the hosts by years, and nobody wants to delete a record they can't prove is safe to remove. Rather than trust anyone's memory, this script pulls the zone straight from the nameserver with a zone transfer, then probes every A\u002FAAAA record for signs of life: a handful of common TCP ports and a reverse DNS lookup. Anything with neither gets written to a report as an orphan candidate. It also flags CNAMEs whose in-zone target has no address record left. The output is a starting list for manual triage, not an auto-delete list.",[11,15,16,17,22],{},"This is the DNS collector from the ",[18,19,21],"a",{"href":20},"\u002F2026\u002F07\u002F15\u002Finfrastructure-a-discovery-first-approach-to-documenting-an-undocumented-network\u002F","discovery-first approach to documenting an undocumented network",", where the zone export is one of the inputs merged into the inventory.",[24,25,27],"h2",{"id":26},"requirements","Requirements",[29,30,31,35,58,87,90],"ul",{},[32,33,34],"li",{},"Python 3.10 or later.",[32,36,37,41,42,45,46,49,50,53,54,57],{},[38,39,40],"code",{},"dnspython"," 2.1 or later (",[38,43,44],{},"pip install dnspython","). The script uses ",[38,47,48],{},"dns.query.inbound_xfr()",", which replaced the ",[38,51,52],{},"dns.query.xfr()"," call the first version of this script used; ",[38,55,56],{},"xfr()"," is deprecated as of dnspython 2.1.",[32,59,60,61,64,65,68,69,72,73,72,76,72,79,82,83,86],{},"Zone transfer (AXFR) permitted from the host running the script. On Windows DNS the per-zone setting is ",[38,62,63],{},"-SecureSecondaries"," on ",[38,66,67],{},"Set-DnsServerPrimaryZone"," (",[38,70,71],{},"NoTransfer",", ",[38,74,75],{},"TransferAnyServer",[38,77,78],{},"TransferToZoneNameServer",[38,80,81],{},"TransferToSecureServers","); BIND uses ",[38,84,85],{},"allow-transfer",". Allow a single automation host rather than opening transfers broadly, or use the PowerShell export in the Usage section instead.",[32,88,89],{},"Network line of sight from wherever the script runs to the address ranges being probed. Run it from a management subnet or jump host; a probe from an isolated segment reports plenty of false positives for hosts it simply can't reach.",[32,91,92],{},"Authorization to probe the address ranges the zone points at. It's a light TCP connect check, but it is still a scan.",[24,94,96],{"id":95},"usage","Usage",[11,98,99],{},"Transfer an internal zone and write only the orphan candidates:",[101,102,107],"pre",{"className":103,"code":104,"language":105,"meta":106,"style":106},"language-bash shiki shiki-themes github-dark","python3 dns_orphan_crawler.py corp.example.internal --server dns01.corp.example.internal\n","bash","",[38,108,109],{"__ignoreMap":106},[110,111,114,118,122,125,129],"span",{"class":112,"line":113},"line",1,[110,115,117],{"class":116},"svObZ","python3",[110,119,121],{"class":120},"sU2Wk"," dns_orphan_crawler.py",[110,123,124],{"class":120}," corp.example.internal",[110,126,128],{"class":127},"sDLfK"," --server",[110,130,131],{"class":120}," dns01.corp.example.internal\n",[11,133,134],{},"Write a full report, including records that were confirmed reachable, and add line-of-business ports to the probe list:",[101,136,138],{"className":103,"code":137,"language":105,"meta":106,"style":106},"python3 dns_orphan_crawler.py corp.example.internal --server 10.0.0.10 --ports 22,80,443,445,3389,1433,9100 --write-full --output full-report.csv\n",[38,139,140],{"__ignoreMap":106},[110,141,142,144,146,148,150,153,156,159,162,165],{"class":112,"line":113},[110,143,117],{"class":116},[110,145,121],{"class":120},[110,147,124],{"class":120},[110,149,128],{"class":127},[110,151,152],{"class":127}," 10.0.0.10",[110,154,155],{"class":127}," --ports",[110,157,158],{"class":120}," 22,80,443,445,3389,1433,9100",[110,160,161],{"class":127}," --write-full",[110,163,164],{"class":127}," --output",[110,166,167],{"class":120}," full-report.csv\n",[101,169,174],{"className":170,"code":172,"language":173,"meta":106},[171],"language-text","Transferring corp.example.internal from 10.0.0.10...\n412 A\u002FAAAA record(s) and 57 CNAME(s) found. Probing...\n38 orphan candidate(s) and 4 dangling CNAME(s) out of 469 record(s). Report: full-report.csv\n","text",[38,175,172],{"__ignoreMap":106},[11,177,178,179,182],{},"If the zone lives on Windows DNS and AXFR is locked down (the right default), you can read the zone over the DnsServer module's CIM interface instead, and export the ",[38,180,181],{},"Timestamp"," column too. Dynamically registered records carry an aging timestamp; static records don't, which is useful context for the Python script's candidate list:",[101,184,188],{"className":185,"code":186,"language":187,"meta":106,"style":106},"language-powershell shiki shiki-themes github-dark","\u003C#\n.SYNOPSIS\n    Export A and CNAME records from a Windows DNS zone to CSV.\n.DESCRIPTION\n    Reads A and CNAME records with Get-DnsServerResourceRecord and writes host name,\n    type, data, TTL and the aging timestamp (blank for static records).\n.PARAMETER ZoneName\n    DNS zone to export.\n.PARAMETER DnsServer\n    DNS server to query.\n.PARAMETER Path\n    Output CSV path.\n.EXAMPLE\n    .\\Export-DnsZoneRecords.ps1 -ZoneName corp.example.internal -DnsServer dns01 -Path .\\dns-records.csv\n.NOTES\n    Author  : Thomas Lasswell (https:\u002F\u002Fwww.techcolumnist.com)\n    Version : 1.0 (2026-01-28)\n    Requires: DnsServer module (RSAT DNS Server Tools), read access to the zone\n#>\nparam(\n    [Parameter(Mandatory = $true)]\n    [string]$ZoneName,\n\n    [Parameter(Mandatory = $true)]\n    [string]$DnsServer,\n\n    [string]$Path = \".\\dns-records.csv\"\n)\n\n$records = foreach ($type in \"A\", \"CName\") {\n    Get-DnsServerResourceRecord -ZoneName $ZoneName -ComputerName $DnsServer -RRType $type\n}\n\n$records | ForEach-Object {\n    $data = if ($_.RecordType -eq \"A\") { \"$($_.RecordData.IPv4Address)\" } else { $_.RecordData.HostNameAlias }\n    [pscustomobject]@{\n        HostName   = $_.HostName\n        RecordType = $_.RecordType\n        Data       = $data\n        TimeToLive = $_.TimeToLive\n        Timestamp  = $_.Timestamp\n    }\n} | Export-Csv -Path $Path -NoTypeInformation\n\nWrite-Output \"Wrote $((Import-Csv -Path $Path).Count) record(s) to $Path\"\n","powershell",[38,189,190,196,206,212,220,226,232,243,249,259,265,275,281,289,295,303,309,315,321,327,337,362,376,383,400,412,417,433,439,444,473,496,502,507,521,580,597,611,624,635,648,661,667,688,693],{"__ignoreMap":106},[110,191,192],{"class":112,"line":113},[110,193,195],{"class":194},"sAwPA","\u003C#\n",[110,197,199,202],{"class":112,"line":198},2,[110,200,201],{"class":127},".",[110,203,205],{"class":204},"snl16","SYNOPSIS\n",[110,207,209],{"class":112,"line":208},3,[110,210,211],{"class":194},"    Export A and CNAME records from a Windows DNS zone to CSV.\n",[110,213,215,217],{"class":112,"line":214},4,[110,216,201],{"class":127},[110,218,219],{"class":204},"DESCRIPTION\n",[110,221,223],{"class":112,"line":222},5,[110,224,225],{"class":194},"    Reads A and CNAME records with Get-DnsServerResourceRecord and writes host name,\n",[110,227,229],{"class":112,"line":228},6,[110,230,231],{"class":194},"    type, data, TTL and the aging timestamp (blank for static records).\n",[110,233,235,237,240],{"class":112,"line":234},7,[110,236,201],{"class":127},[110,238,239],{"class":204},"PARAMETER",[110,241,242],{"class":204}," ZoneName\n",[110,244,246],{"class":112,"line":245},8,[110,247,248],{"class":194},"    DNS zone to export.\n",[110,250,252,254,256],{"class":112,"line":251},9,[110,253,201],{"class":127},[110,255,239],{"class":204},[110,257,258],{"class":204}," DnsServer\n",[110,260,262],{"class":112,"line":261},10,[110,263,264],{"class":194},"    DNS server to query.\n",[110,266,268,270,272],{"class":112,"line":267},11,[110,269,201],{"class":127},[110,271,239],{"class":204},[110,273,274],{"class":204}," Path\n",[110,276,278],{"class":112,"line":277},12,[110,279,280],{"class":194},"    Output CSV path.\n",[110,282,284,286],{"class":112,"line":283},13,[110,285,201],{"class":127},[110,287,288],{"class":204},"EXAMPLE\n",[110,290,292],{"class":112,"line":291},14,[110,293,294],{"class":194},"    .\\Export-DnsZoneRecords.ps1 -ZoneName corp.example.internal -DnsServer dns01 -Path .\\dns-records.csv\n",[110,296,298,300],{"class":112,"line":297},15,[110,299,201],{"class":127},[110,301,302],{"class":204},"NOTES\n",[110,304,306],{"class":112,"line":305},16,[110,307,308],{"class":194},"    Author  : Thomas Lasswell (https:\u002F\u002Fwww.techcolumnist.com)\n",[110,310,312],{"class":112,"line":311},17,[110,313,314],{"class":194},"    Version : 1.0 (2026-01-28)\n",[110,316,318],{"class":112,"line":317},18,[110,319,320],{"class":194},"    Requires: DnsServer module (RSAT DNS Server Tools), read access to the zone\n",[110,322,324],{"class":112,"line":323},19,[110,325,326],{"class":194},"#>\n",[110,328,330,333],{"class":112,"line":329},20,[110,331,332],{"class":204},"param",[110,334,336],{"class":335},"s95oV","(\n",[110,338,340,343,346,349,353,356,359],{"class":112,"line":339},21,[110,341,342],{"class":335},"    [",[110,344,345],{"class":127},"Parameter",[110,347,348],{"class":335},"(",[110,350,352],{"class":351},"s9osk","Mandatory",[110,354,355],{"class":204}," =",[110,357,358],{"class":127}," $true",[110,360,361],{"class":335},")]\n",[110,363,365,367,370,373],{"class":112,"line":364},22,[110,366,342],{"class":335},[110,368,369],{"class":204},"string",[110,371,372],{"class":335},"]$ZoneName",[110,374,375],{"class":204},",\n",[110,377,379],{"class":112,"line":378},23,[110,380,382],{"emptyLinePlaceholder":381},true,"\n",[110,384,386,388,390,392,394,396,398],{"class":112,"line":385},24,[110,387,342],{"class":335},[110,389,345],{"class":127},[110,391,348],{"class":335},[110,393,352],{"class":351},[110,395,355],{"class":204},[110,397,358],{"class":127},[110,399,361],{"class":335},[110,401,403,405,407,410],{"class":112,"line":402},25,[110,404,342],{"class":335},[110,406,369],{"class":204},[110,408,409],{"class":335},"]$DnsServer",[110,411,375],{"class":204},[110,413,415],{"class":112,"line":414},26,[110,416,382],{"emptyLinePlaceholder":381},[110,418,420,422,424,427,430],{"class":112,"line":419},27,[110,421,342],{"class":335},[110,423,369],{"class":204},[110,425,426],{"class":335},"]$Path ",[110,428,429],{"class":204},"=",[110,431,432],{"class":120}," \".\\dns-records.csv\"\n",[110,434,436],{"class":112,"line":435},28,[110,437,438],{"class":335},")\n",[110,440,442],{"class":112,"line":441},29,[110,443,382],{"emptyLinePlaceholder":381},[110,445,447,450,452,455,458,461,464,467,470],{"class":112,"line":446},30,[110,448,449],{"class":335},"$records ",[110,451,429],{"class":204},[110,453,454],{"class":204}," foreach",[110,456,457],{"class":335}," ($type ",[110,459,460],{"class":204},"in",[110,462,463],{"class":120}," \"A\"",[110,465,466],{"class":204},",",[110,468,469],{"class":120}," \"CName\"",[110,471,472],{"class":335},") {\n",[110,474,476,479,482,485,488,491,493],{"class":112,"line":475},31,[110,477,478],{"class":127},"    Get-DnsServerResourceRecord",[110,480,481],{"class":204}," -",[110,483,484],{"class":335},"ZoneName $ZoneName ",[110,486,487],{"class":204},"-",[110,489,490],{"class":335},"ComputerName $DnsServer ",[110,492,487],{"class":204},[110,494,495],{"class":335},"RRType $type\n",[110,497,499],{"class":112,"line":498},32,[110,500,501],{"class":335},"}\n",[110,503,505],{"class":112,"line":504},33,[110,506,382],{"emptyLinePlaceholder":381},[110,508,510,512,515,518],{"class":112,"line":509},34,[110,511,449],{"class":335},[110,513,514],{"class":204},"|",[110,516,517],{"class":127}," ForEach-Object",[110,519,520],{"class":335}," {\n",[110,522,524,527,529,532,534,537,540,543,545,548,551,554,556,558,561,564,566,569,572,575,577],{"class":112,"line":523},35,[110,525,526],{"class":335},"    $data ",[110,528,429],{"class":204},[110,530,531],{"class":204}," if",[110,533,68],{"class":335},[110,535,536],{"class":127},"$_",[110,538,539],{"class":335},".RecordType ",[110,541,542],{"class":204},"-eq",[110,544,463],{"class":120},[110,546,547],{"class":335},") { ",[110,549,550],{"class":120},"\"",[110,552,553],{"class":204},"$",[110,555,348],{"class":120},[110,557,536],{"class":127},[110,559,560],{"class":335},".RecordData.IPv4Address",[110,562,563],{"class":120},")",[110,565,550],{"class":120},[110,567,568],{"class":335}," } ",[110,570,571],{"class":204},"else",[110,573,574],{"class":335}," { ",[110,576,536],{"class":127},[110,578,579],{"class":335},".RecordData.HostNameAlias }\n",[110,581,583,585,588,591,594],{"class":112,"line":582},36,[110,584,342],{"class":335},[110,586,587],{"class":204},"pscustomobject",[110,589,590],{"class":335},"]",[110,592,593],{"class":204},"@",[110,595,596],{"class":335},"{\n",[110,598,600,603,605,608],{"class":112,"line":599},37,[110,601,602],{"class":335},"        HostName   ",[110,604,429],{"class":204},[110,606,607],{"class":127}," $_",[110,609,610],{"class":335},".HostName\n",[110,612,614,617,619,621],{"class":112,"line":613},38,[110,615,616],{"class":335},"        RecordType ",[110,618,429],{"class":204},[110,620,607],{"class":127},[110,622,623],{"class":335},".RecordType\n",[110,625,627,630,632],{"class":112,"line":626},39,[110,628,629],{"class":335},"        Data       ",[110,631,429],{"class":204},[110,633,634],{"class":335}," $data\n",[110,636,638,641,643,645],{"class":112,"line":637},40,[110,639,640],{"class":335},"        TimeToLive ",[110,642,429],{"class":204},[110,644,607],{"class":127},[110,646,647],{"class":335},".TimeToLive\n",[110,649,651,654,656,658],{"class":112,"line":650},41,[110,652,653],{"class":335},"        Timestamp  ",[110,655,429],{"class":204},[110,657,607],{"class":127},[110,659,660],{"class":335},".Timestamp\n",[110,662,664],{"class":112,"line":663},42,[110,665,666],{"class":335},"    }\n",[110,668,670,673,675,678,680,683,685],{"class":112,"line":669},43,[110,671,672],{"class":335},"} ",[110,674,514],{"class":204},[110,676,677],{"class":127}," Export-Csv",[110,679,481],{"class":204},[110,681,682],{"class":335},"Path $Path ",[110,684,487],{"class":204},[110,686,687],{"class":335},"NoTypeInformation\n",[110,689,691],{"class":112,"line":690},44,[110,692,382],{"emptyLinePlaceholder":381},[110,694,696,699,702,704,707,710,712,715,718,721,724,726],{"class":112,"line":695},45,[110,697,698],{"class":127},"Write-Output",[110,700,701],{"class":120}," \"Wrote ",[110,703,553],{"class":204},[110,705,706],{"class":120},"((",[110,708,709],{"class":127},"Import-Csv",[110,711,481],{"class":204},[110,713,714],{"class":120},"Path ",[110,716,717],{"class":335},"$Path",[110,719,720],{"class":120},").Count)",[110,722,723],{"class":120}," record(s) to ",[110,725,717],{"class":335},[110,727,728],{"class":120},"\"\n",[24,730,732],{"id":731},"script","Script",[101,734,738],{"className":735,"code":736,"language":737,"meta":106,"style":106},"language-python shiki shiki-themes github-dark","\"\"\"\nCrawl a DNS zone via AXFR and flag records that no longer point at a live host.\n\nFor each A\u002FAAAA record in the transferred zone, the script attempts lightweight TCP\nconnection probes and a reverse (PTR) lookup against the record's address. Records\nwith no reachable port and no PTR are written as orphan candidates. CNAMEs whose\ntarget is inside the zone but has no A\u002FAAAA record are flagged as dangling. Nothing is\ndeleted.\n\nReads: the zone via AXFR from the given DNS server.\nWrites: a CSV report of candidates (and, with --write-full, every record checked).\nRequires: Python 3.10+, dnspython >= 2.1, AXFR permission on the DNS server.\n\"\"\"\n\nimport argparse\nimport csv\nimport ipaddress\nimport socket\nfrom concurrent.futures import ThreadPoolExecutor\n\nimport dns.exception\nimport dns.query\nimport dns.resolver\nimport dns.zone\n\nDEFAULT_PORTS = \"22,80,443,445,3389\"\n\n\ndef parse_args():\n    parser = argparse.ArgumentParser(description=\"Crawl a DNS zone for orphaned records.\")\n    parser.add_argument(\"zone\", help=\"Zone name to transfer, e.g. corp.example.internal\")\n    parser.add_argument(\"--server\", required=True, help=\"Nameserver (name or IP) to request the AXFR from\")\n    parser.add_argument(\"--ports\", default=DEFAULT_PORTS, help=f\"TCP ports to probe (default {DEFAULT_PORTS})\")\n    parser.add_argument(\"--timeout\", type=float, default=1.5, help=\"Per-connection timeout in seconds\")\n    parser.add_argument(\"--output\", default=\"orphan-candidates.csv\", help=\"CSV report path\")\n    parser.add_argument(\"--write-full\", action=\"store_true\", help=\"Also write rows for healthy records\")\n    parser.add_argument(\"--workers\", type=int, default=20, help=\"Concurrent probe workers\")\n    return parser.parse_args()\n\n\ndef server_address(server):\n    \"\"\"inbound_xfr() wants an IP address, so resolve a hostname first.\"\"\"\n    try:\n        ipaddress.ip_address(server)\n        return server\n    except ValueError:\n        return socket.getaddrinfo(server, 53, proto=socket.IPPROTO_TCP)[0][4][0]\n\n\ndef transfer_zone(zone_name, server_ip):\n    zone = dns.zone.Zone(zone_name)\n    dns.query.inbound_xfr(server_ip, zone, timeout=10, lifetime=120)\n    origin = zone.origin\n\n    addresses, cnames = [], []\n    for rdtype in (\"A\", \"AAAA\"):\n        for name, _ttl, rdata in zone.iterate_rdatas(rdtype):\n            addresses.append((name.derelativize(origin).to_text(), rdtype, rdata.address))\n    for name, _ttl, rdata in zone.iterate_rdatas(\"CNAME\"):\n        target = rdata.target.derelativize(origin)\n        cnames.append((name.derelativize(origin).to_text(), target))\n    return addresses, cnames, origin\n\n\ndef has_ptr(ip_address):\n    try:\n        return bool(dns.resolver.resolve_address(ip_address, lifetime=2.0))\n    except dns.exception.DNSException:\n        return False\n\n\ndef probe_reachable(ip_address, ports, timeout):\n    for port in ports:\n        try:\n            with socket.create_connection((ip_address, port), timeout=timeout):\n                return port\n        except OSError:\n            continue\n    return None\n\n\ndef check_record(record, ports, timeout):\n    fqdn, rdtype, ip_address = record\n    open_port = probe_reachable(ip_address, ports, timeout)\n    ptr_exists = has_ptr(ip_address)\n    return {\n        \"fqdn\": fqdn,\n        \"type\": rdtype,\n        \"data\": ip_address,\n        \"open_port\": open_port or \"\",\n        \"has_ptr\": ptr_exists,\n        \"finding\": \"\" if (open_port or ptr_exists) else \"orphan-candidate\",\n    }\n\n\ndef dangling_cnames(cnames, addresses, origin):\n    \"\"\"CNAMEs pointing inside the zone at a name with no A\u002FAAAA (and no further CNAME).\"\"\"\n    names_with_address = {fqdn for fqdn, _, _ in addresses}\n    cname_names = {fqdn for fqdn, _ in cnames}\n    rows = []\n    for fqdn, target in cnames:\n        target_text = target.to_text()\n        if target.is_subdomain(origin) and target_text not in names_with_address | cname_names:\n            rows.append({\"fqdn\": fqdn, \"type\": \"CNAME\", \"data\": target_text,\n                         \"open_port\": \"\", \"has_ptr\": \"\", \"finding\": \"dangling-cname\"})\n    return rows\n\n\ndef main():\n    args = parse_args()\n    ports = [int(p) for p in args.ports.split(\",\") if p.strip()]\n    server_ip = server_address(args.server)\n\n    print(f\"Transferring {args.zone} from {server_ip}...\")\n    addresses, cnames, origin = transfer_zone(args.zone, server_ip)\n    print(f\"{len(addresses)} A\u002FAAAA record(s) and {len(cnames)} CNAME(s) found. Probing...\")\n\n    with ThreadPoolExecutor(max_workers=args.workers) as pool:\n        results = list(pool.map(lambda r: check_record(r, ports, args.timeout), addresses))\n    results.extend(dangling_cnames(cnames, addresses, origin))\n\n    flagged = [row for row in results if row[\"finding\"]]\n    rows = results if args.write_full else flagged\n    with open(args.output, \"w\", newline=\"\", encoding=\"utf-8\") as handle:\n        writer = csv.DictWriter(handle, fieldnames=[\"fqdn\", \"type\", \"data\", \"open_port\", \"has_ptr\", \"finding\"])\n        writer.writeheader()\n        writer.writerows(sorted(rows, key=lambda row: row[\"fqdn\"]))\n\n    orphans = sum(1 for row in flagged if row[\"finding\"] == \"orphan-candidate\")\n    dangling = len(flagged) - orphans\n    print(f\"{orphans} orphan candidate(s) and {dangling} dangling CNAME(s) out of {len(addresses) + len(cnames)} record(s). \"\n          f\"Report: {args.output}\")\n\n\nif __name__ == \"__main__\":\n    main()\n","python",[38,739,740,745,750,754,759,764,769,774,779,783,788,793,798,802,806,814,821,828,835,848,852,859,866,873,880,884,894,898,902,913,933,953,981,1017,1054,1081,1109,1145,1153,1157,1161,1171,1176,1184,1189,1197,1208,1251,1256,1261,1272,1283,1309,1320,1325,1336,1360,1374,1380,1397,1408,1414,1422,1427,1432,1443,1450,1471,1479,1487,1492,1497,1508,1521,1529,1545,1554,1565,1571,1579,1584,1589,1600,1611,1622,1633,1640,1649,1658,1667,1684,1693,1722,1727,1732,1737,1748,1754,1776,1796,1807,1820,1831,1860,1887,1919,1927,1932,1937,1947,1958,1996,2007,2012,2049,2060,2094,2099,2122,2142,2148,2153,2184,2203,2243,2288,2294,2320,2325,2367,2386,2434,2454,2459,2464,2480],{"__ignoreMap":106},[110,741,742],{"class":112,"line":113},[110,743,744],{"class":120},"\"\"\"\n",[110,746,747],{"class":112,"line":198},[110,748,749],{"class":120},"Crawl a DNS zone via AXFR and flag records that no longer point at a live host.\n",[110,751,752],{"class":112,"line":208},[110,753,382],{"emptyLinePlaceholder":381},[110,755,756],{"class":112,"line":214},[110,757,758],{"class":120},"For each A\u002FAAAA record in the transferred zone, the script attempts lightweight TCP\n",[110,760,761],{"class":112,"line":222},[110,762,763],{"class":120},"connection probes and a reverse (PTR) lookup against the record's address. Records\n",[110,765,766],{"class":112,"line":228},[110,767,768],{"class":120},"with no reachable port and no PTR are written as orphan candidates. CNAMEs whose\n",[110,770,771],{"class":112,"line":234},[110,772,773],{"class":120},"target is inside the zone but has no A\u002FAAAA record are flagged as dangling. Nothing is\n",[110,775,776],{"class":112,"line":245},[110,777,778],{"class":120},"deleted.\n",[110,780,781],{"class":112,"line":251},[110,782,382],{"emptyLinePlaceholder":381},[110,784,785],{"class":112,"line":261},[110,786,787],{"class":120},"Reads: the zone via AXFR from the given DNS server.\n",[110,789,790],{"class":112,"line":267},[110,791,792],{"class":120},"Writes: a CSV report of candidates (and, with --write-full, every record checked).\n",[110,794,795],{"class":112,"line":277},[110,796,797],{"class":120},"Requires: Python 3.10+, dnspython >= 2.1, AXFR permission on the DNS server.\n",[110,799,800],{"class":112,"line":283},[110,801,744],{"class":120},[110,803,804],{"class":112,"line":291},[110,805,382],{"emptyLinePlaceholder":381},[110,807,808,811],{"class":112,"line":297},[110,809,810],{"class":204},"import",[110,812,813],{"class":335}," argparse\n",[110,815,816,818],{"class":112,"line":305},[110,817,810],{"class":204},[110,819,820],{"class":335}," csv\n",[110,822,823,825],{"class":112,"line":311},[110,824,810],{"class":204},[110,826,827],{"class":335}," ipaddress\n",[110,829,830,832],{"class":112,"line":317},[110,831,810],{"class":204},[110,833,834],{"class":335}," socket\n",[110,836,837,840,843,845],{"class":112,"line":323},[110,838,839],{"class":204},"from",[110,841,842],{"class":335}," concurrent.futures ",[110,844,810],{"class":204},[110,846,847],{"class":335}," ThreadPoolExecutor\n",[110,849,850],{"class":112,"line":329},[110,851,382],{"emptyLinePlaceholder":381},[110,853,854,856],{"class":112,"line":339},[110,855,810],{"class":204},[110,857,858],{"class":335}," dns.exception\n",[110,860,861,863],{"class":112,"line":364},[110,862,810],{"class":204},[110,864,865],{"class":335}," dns.query\n",[110,867,868,870],{"class":112,"line":378},[110,869,810],{"class":204},[110,871,872],{"class":335}," dns.resolver\n",[110,874,875,877],{"class":112,"line":385},[110,876,810],{"class":204},[110,878,879],{"class":335}," dns.zone\n",[110,881,882],{"class":112,"line":402},[110,883,382],{"emptyLinePlaceholder":381},[110,885,886,889,891],{"class":112,"line":414},[110,887,888],{"class":127},"DEFAULT_PORTS",[110,890,355],{"class":204},[110,892,893],{"class":120}," \"22,80,443,445,3389\"\n",[110,895,896],{"class":112,"line":419},[110,897,382],{"emptyLinePlaceholder":381},[110,899,900],{"class":112,"line":435},[110,901,382],{"emptyLinePlaceholder":381},[110,903,904,907,910],{"class":112,"line":441},[110,905,906],{"class":204},"def",[110,908,909],{"class":116}," parse_args",[110,911,912],{"class":335},"():\n",[110,914,915,918,920,923,926,928,931],{"class":112,"line":446},[110,916,917],{"class":335},"    parser ",[110,919,429],{"class":204},[110,921,922],{"class":335}," argparse.ArgumentParser(",[110,924,925],{"class":351},"description",[110,927,429],{"class":204},[110,929,930],{"class":120},"\"Crawl a DNS zone for orphaned records.\"",[110,932,438],{"class":335},[110,934,935,938,941,943,946,948,951],{"class":112,"line":475},[110,936,937],{"class":335},"    parser.add_argument(",[110,939,940],{"class":120},"\"zone\"",[110,942,72],{"class":335},[110,944,945],{"class":351},"help",[110,947,429],{"class":204},[110,949,950],{"class":120},"\"Zone name to transfer, e.g. corp.example.internal\"",[110,952,438],{"class":335},[110,954,955,957,960,962,965,967,970,972,974,976,979],{"class":112,"line":498},[110,956,937],{"class":335},[110,958,959],{"class":120},"\"--server\"",[110,961,72],{"class":335},[110,963,964],{"class":351},"required",[110,966,429],{"class":204},[110,968,969],{"class":127},"True",[110,971,72],{"class":335},[110,973,945],{"class":351},[110,975,429],{"class":204},[110,977,978],{"class":120},"\"Nameserver (name or IP) to request the AXFR from\"",[110,980,438],{"class":335},[110,982,983,985,988,990,993,995,997,999,1001,1003,1006,1009,1012,1015],{"class":112,"line":504},[110,984,937],{"class":335},[110,986,987],{"class":120},"\"--ports\"",[110,989,72],{"class":335},[110,991,992],{"class":351},"default",[110,994,429],{"class":204},[110,996,888],{"class":127},[110,998,72],{"class":335},[110,1000,945],{"class":351},[110,1002,429],{"class":204},[110,1004,1005],{"class":204},"f",[110,1007,1008],{"class":120},"\"TCP ports to probe (default ",[110,1010,1011],{"class":127},"{DEFAULT_PORTS}",[110,1013,1014],{"class":120},")\"",[110,1016,438],{"class":335},[110,1018,1019,1021,1024,1026,1029,1031,1034,1036,1038,1040,1043,1045,1047,1049,1052],{"class":112,"line":509},[110,1020,937],{"class":335},[110,1022,1023],{"class":120},"\"--timeout\"",[110,1025,72],{"class":335},[110,1027,1028],{"class":351},"type",[110,1030,429],{"class":204},[110,1032,1033],{"class":127},"float",[110,1035,72],{"class":335},[110,1037,992],{"class":351},[110,1039,429],{"class":204},[110,1041,1042],{"class":127},"1.5",[110,1044,72],{"class":335},[110,1046,945],{"class":351},[110,1048,429],{"class":204},[110,1050,1051],{"class":120},"\"Per-connection timeout in seconds\"",[110,1053,438],{"class":335},[110,1055,1056,1058,1061,1063,1065,1067,1070,1072,1074,1076,1079],{"class":112,"line":523},[110,1057,937],{"class":335},[110,1059,1060],{"class":120},"\"--output\"",[110,1062,72],{"class":335},[110,1064,992],{"class":351},[110,1066,429],{"class":204},[110,1068,1069],{"class":120},"\"orphan-candidates.csv\"",[110,1071,72],{"class":335},[110,1073,945],{"class":351},[110,1075,429],{"class":204},[110,1077,1078],{"class":120},"\"CSV report path\"",[110,1080,438],{"class":335},[110,1082,1083,1085,1088,1090,1093,1095,1098,1100,1102,1104,1107],{"class":112,"line":582},[110,1084,937],{"class":335},[110,1086,1087],{"class":120},"\"--write-full\"",[110,1089,72],{"class":335},[110,1091,1092],{"class":351},"action",[110,1094,429],{"class":204},[110,1096,1097],{"class":120},"\"store_true\"",[110,1099,72],{"class":335},[110,1101,945],{"class":351},[110,1103,429],{"class":204},[110,1105,1106],{"class":120},"\"Also write rows for healthy records\"",[110,1108,438],{"class":335},[110,1110,1111,1113,1116,1118,1120,1122,1125,1127,1129,1131,1134,1136,1138,1140,1143],{"class":112,"line":599},[110,1112,937],{"class":335},[110,1114,1115],{"class":120},"\"--workers\"",[110,1117,72],{"class":335},[110,1119,1028],{"class":351},[110,1121,429],{"class":204},[110,1123,1124],{"class":127},"int",[110,1126,72],{"class":335},[110,1128,992],{"class":351},[110,1130,429],{"class":204},[110,1132,1133],{"class":127},"20",[110,1135,72],{"class":335},[110,1137,945],{"class":351},[110,1139,429],{"class":204},[110,1141,1142],{"class":120},"\"Concurrent probe workers\"",[110,1144,438],{"class":335},[110,1146,1147,1150],{"class":112,"line":613},[110,1148,1149],{"class":204},"    return",[110,1151,1152],{"class":335}," parser.parse_args()\n",[110,1154,1155],{"class":112,"line":626},[110,1156,382],{"emptyLinePlaceholder":381},[110,1158,1159],{"class":112,"line":637},[110,1160,382],{"emptyLinePlaceholder":381},[110,1162,1163,1165,1168],{"class":112,"line":650},[110,1164,906],{"class":204},[110,1166,1167],{"class":116}," server_address",[110,1169,1170],{"class":335},"(server):\n",[110,1172,1173],{"class":112,"line":663},[110,1174,1175],{"class":120},"    \"\"\"inbound_xfr() wants an IP address, so resolve a hostname first.\"\"\"\n",[110,1177,1178,1181],{"class":112,"line":669},[110,1179,1180],{"class":204},"    try",[110,1182,1183],{"class":335},":\n",[110,1185,1186],{"class":112,"line":690},[110,1187,1188],{"class":335},"        ipaddress.ip_address(server)\n",[110,1190,1191,1194],{"class":112,"line":695},[110,1192,1193],{"class":204},"        return",[110,1195,1196],{"class":335}," server\n",[110,1198,1200,1203,1206],{"class":112,"line":1199},46,[110,1201,1202],{"class":204},"    except",[110,1204,1205],{"class":127}," ValueError",[110,1207,1183],{"class":335},[110,1209,1211,1213,1216,1219,1221,1224,1226,1229,1232,1235,1238,1241,1244,1246,1248],{"class":112,"line":1210},47,[110,1212,1193],{"class":204},[110,1214,1215],{"class":335}," socket.getaddrinfo(server, ",[110,1217,1218],{"class":127},"53",[110,1220,72],{"class":335},[110,1222,1223],{"class":351},"proto",[110,1225,429],{"class":204},[110,1227,1228],{"class":335},"socket.",[110,1230,1231],{"class":127},"IPPROTO_TCP",[110,1233,1234],{"class":335},")[",[110,1236,1237],{"class":127},"0",[110,1239,1240],{"class":335},"][",[110,1242,1243],{"class":127},"4",[110,1245,1240],{"class":335},[110,1247,1237],{"class":127},[110,1249,1250],{"class":335},"]\n",[110,1252,1254],{"class":112,"line":1253},48,[110,1255,382],{"emptyLinePlaceholder":381},[110,1257,1259],{"class":112,"line":1258},49,[110,1260,382],{"emptyLinePlaceholder":381},[110,1262,1264,1266,1269],{"class":112,"line":1263},50,[110,1265,906],{"class":204},[110,1267,1268],{"class":116}," transfer_zone",[110,1270,1271],{"class":335},"(zone_name, server_ip):\n",[110,1273,1275,1278,1280],{"class":112,"line":1274},51,[110,1276,1277],{"class":335},"    zone ",[110,1279,429],{"class":204},[110,1281,1282],{"class":335}," dns.zone.Zone(zone_name)\n",[110,1284,1286,1289,1292,1294,1297,1299,1302,1304,1307],{"class":112,"line":1285},52,[110,1287,1288],{"class":335},"    dns.query.inbound_xfr(server_ip, zone, ",[110,1290,1291],{"class":351},"timeout",[110,1293,429],{"class":204},[110,1295,1296],{"class":127},"10",[110,1298,72],{"class":335},[110,1300,1301],{"class":351},"lifetime",[110,1303,429],{"class":204},[110,1305,1306],{"class":127},"120",[110,1308,438],{"class":335},[110,1310,1312,1315,1317],{"class":112,"line":1311},53,[110,1313,1314],{"class":335},"    origin ",[110,1316,429],{"class":204},[110,1318,1319],{"class":335}," zone.origin\n",[110,1321,1323],{"class":112,"line":1322},54,[110,1324,382],{"emptyLinePlaceholder":381},[110,1326,1328,1331,1333],{"class":112,"line":1327},55,[110,1329,1330],{"class":335},"    addresses, cnames ",[110,1332,429],{"class":204},[110,1334,1335],{"class":335}," [], []\n",[110,1337,1339,1342,1345,1347,1349,1352,1354,1357],{"class":112,"line":1338},56,[110,1340,1341],{"class":204},"    for",[110,1343,1344],{"class":335}," rdtype ",[110,1346,460],{"class":204},[110,1348,68],{"class":335},[110,1350,1351],{"class":120},"\"A\"",[110,1353,72],{"class":335},[110,1355,1356],{"class":120},"\"AAAA\"",[110,1358,1359],{"class":335},"):\n",[110,1361,1363,1366,1369,1371],{"class":112,"line":1362},57,[110,1364,1365],{"class":204},"        for",[110,1367,1368],{"class":335}," name, _ttl, rdata ",[110,1370,460],{"class":204},[110,1372,1373],{"class":335}," zone.iterate_rdatas(rdtype):\n",[110,1375,1377],{"class":112,"line":1376},58,[110,1378,1379],{"class":335},"            addresses.append((name.derelativize(origin).to_text(), rdtype, rdata.address))\n",[110,1381,1383,1385,1387,1389,1392,1395],{"class":112,"line":1382},59,[110,1384,1341],{"class":204},[110,1386,1368],{"class":335},[110,1388,460],{"class":204},[110,1390,1391],{"class":335}," zone.iterate_rdatas(",[110,1393,1394],{"class":120},"\"CNAME\"",[110,1396,1359],{"class":335},[110,1398,1400,1403,1405],{"class":112,"line":1399},60,[110,1401,1402],{"class":335},"        target ",[110,1404,429],{"class":204},[110,1406,1407],{"class":335}," rdata.target.derelativize(origin)\n",[110,1409,1411],{"class":112,"line":1410},61,[110,1412,1413],{"class":335},"        cnames.append((name.derelativize(origin).to_text(), target))\n",[110,1415,1417,1419],{"class":112,"line":1416},62,[110,1418,1149],{"class":204},[110,1420,1421],{"class":335}," addresses, cnames, origin\n",[110,1423,1425],{"class":112,"line":1424},63,[110,1426,382],{"emptyLinePlaceholder":381},[110,1428,1430],{"class":112,"line":1429},64,[110,1431,382],{"emptyLinePlaceholder":381},[110,1433,1435,1437,1440],{"class":112,"line":1434},65,[110,1436,906],{"class":204},[110,1438,1439],{"class":116}," has_ptr",[110,1441,1442],{"class":335},"(ip_address):\n",[110,1444,1446,1448],{"class":112,"line":1445},66,[110,1447,1180],{"class":204},[110,1449,1183],{"class":335},[110,1451,1453,1455,1458,1461,1463,1465,1468],{"class":112,"line":1452},67,[110,1454,1193],{"class":204},[110,1456,1457],{"class":127}," bool",[110,1459,1460],{"class":335},"(dns.resolver.resolve_address(ip_address, ",[110,1462,1301],{"class":351},[110,1464,429],{"class":204},[110,1466,1467],{"class":127},"2.0",[110,1469,1470],{"class":335},"))\n",[110,1472,1474,1476],{"class":112,"line":1473},68,[110,1475,1202],{"class":204},[110,1477,1478],{"class":335}," dns.exception.DNSException:\n",[110,1480,1482,1484],{"class":112,"line":1481},69,[110,1483,1193],{"class":204},[110,1485,1486],{"class":127}," False\n",[110,1488,1490],{"class":112,"line":1489},70,[110,1491,382],{"emptyLinePlaceholder":381},[110,1493,1495],{"class":112,"line":1494},71,[110,1496,382],{"emptyLinePlaceholder":381},[110,1498,1500,1502,1505],{"class":112,"line":1499},72,[110,1501,906],{"class":204},[110,1503,1504],{"class":116}," probe_reachable",[110,1506,1507],{"class":335},"(ip_address, ports, timeout):\n",[110,1509,1511,1513,1516,1518],{"class":112,"line":1510},73,[110,1512,1341],{"class":204},[110,1514,1515],{"class":335}," port ",[110,1517,460],{"class":204},[110,1519,1520],{"class":335}," ports:\n",[110,1522,1524,1527],{"class":112,"line":1523},74,[110,1525,1526],{"class":204},"        try",[110,1528,1183],{"class":335},[110,1530,1532,1535,1538,1540,1542],{"class":112,"line":1531},75,[110,1533,1534],{"class":204},"            with",[110,1536,1537],{"class":335}," socket.create_connection((ip_address, port), ",[110,1539,1291],{"class":351},[110,1541,429],{"class":204},[110,1543,1544],{"class":335},"timeout):\n",[110,1546,1548,1551],{"class":112,"line":1547},76,[110,1549,1550],{"class":204},"                return",[110,1552,1553],{"class":335}," port\n",[110,1555,1557,1560,1563],{"class":112,"line":1556},77,[110,1558,1559],{"class":204},"        except",[110,1561,1562],{"class":127}," OSError",[110,1564,1183],{"class":335},[110,1566,1568],{"class":112,"line":1567},78,[110,1569,1570],{"class":204},"            continue\n",[110,1572,1574,1576],{"class":112,"line":1573},79,[110,1575,1149],{"class":204},[110,1577,1578],{"class":127}," None\n",[110,1580,1582],{"class":112,"line":1581},80,[110,1583,382],{"emptyLinePlaceholder":381},[110,1585,1587],{"class":112,"line":1586},81,[110,1588,382],{"emptyLinePlaceholder":381},[110,1590,1592,1594,1597],{"class":112,"line":1591},82,[110,1593,906],{"class":204},[110,1595,1596],{"class":116}," check_record",[110,1598,1599],{"class":335},"(record, ports, timeout):\n",[110,1601,1603,1606,1608],{"class":112,"line":1602},83,[110,1604,1605],{"class":335},"    fqdn, rdtype, ip_address ",[110,1607,429],{"class":204},[110,1609,1610],{"class":335}," record\n",[110,1612,1614,1617,1619],{"class":112,"line":1613},84,[110,1615,1616],{"class":335},"    open_port ",[110,1618,429],{"class":204},[110,1620,1621],{"class":335}," probe_reachable(ip_address, ports, timeout)\n",[110,1623,1625,1628,1630],{"class":112,"line":1624},85,[110,1626,1627],{"class":335},"    ptr_exists ",[110,1629,429],{"class":204},[110,1631,1632],{"class":335}," has_ptr(ip_address)\n",[110,1634,1636,1638],{"class":112,"line":1635},86,[110,1637,1149],{"class":204},[110,1639,520],{"class":335},[110,1641,1643,1646],{"class":112,"line":1642},87,[110,1644,1645],{"class":120},"        \"fqdn\"",[110,1647,1648],{"class":335},": fqdn,\n",[110,1650,1652,1655],{"class":112,"line":1651},88,[110,1653,1654],{"class":120},"        \"type\"",[110,1656,1657],{"class":335},": rdtype,\n",[110,1659,1661,1664],{"class":112,"line":1660},89,[110,1662,1663],{"class":120},"        \"data\"",[110,1665,1666],{"class":335},": ip_address,\n",[110,1668,1670,1673,1676,1679,1682],{"class":112,"line":1669},90,[110,1671,1672],{"class":120},"        \"open_port\"",[110,1674,1675],{"class":335},": open_port ",[110,1677,1678],{"class":204},"or",[110,1680,1681],{"class":120}," \"\"",[110,1683,375],{"class":335},[110,1685,1687,1690],{"class":112,"line":1686},91,[110,1688,1689],{"class":120},"        \"has_ptr\"",[110,1691,1692],{"class":335},": ptr_exists,\n",[110,1694,1696,1699,1702,1705,1707,1710,1712,1715,1717,1720],{"class":112,"line":1695},92,[110,1697,1698],{"class":120},"        \"finding\"",[110,1700,1701],{"class":335},": ",[110,1703,1704],{"class":120},"\"\"",[110,1706,531],{"class":204},[110,1708,1709],{"class":335}," (open_port ",[110,1711,1678],{"class":204},[110,1713,1714],{"class":335}," ptr_exists) ",[110,1716,571],{"class":204},[110,1718,1719],{"class":120}," \"orphan-candidate\"",[110,1721,375],{"class":335},[110,1723,1725],{"class":112,"line":1724},93,[110,1726,666],{"class":335},[110,1728,1730],{"class":112,"line":1729},94,[110,1731,382],{"emptyLinePlaceholder":381},[110,1733,1735],{"class":112,"line":1734},95,[110,1736,382],{"emptyLinePlaceholder":381},[110,1738,1740,1742,1745],{"class":112,"line":1739},96,[110,1741,906],{"class":204},[110,1743,1744],{"class":116}," dangling_cnames",[110,1746,1747],{"class":335},"(cnames, addresses, origin):\n",[110,1749,1751],{"class":112,"line":1750},97,[110,1752,1753],{"class":120},"    \"\"\"CNAMEs pointing inside the zone at a name with no A\u002FAAAA (and no further CNAME).\"\"\"\n",[110,1755,1757,1760,1762,1765,1768,1771,1773],{"class":112,"line":1756},98,[110,1758,1759],{"class":335},"    names_with_address ",[110,1761,429],{"class":204},[110,1763,1764],{"class":335}," {fqdn ",[110,1766,1767],{"class":204},"for",[110,1769,1770],{"class":335}," fqdn, _, _ ",[110,1772,460],{"class":204},[110,1774,1775],{"class":335}," addresses}\n",[110,1777,1779,1782,1784,1786,1788,1791,1793],{"class":112,"line":1778},99,[110,1780,1781],{"class":335},"    cname_names ",[110,1783,429],{"class":204},[110,1785,1764],{"class":335},[110,1787,1767],{"class":204},[110,1789,1790],{"class":335}," fqdn, _ ",[110,1792,460],{"class":204},[110,1794,1795],{"class":335}," cnames}\n",[110,1797,1799,1802,1804],{"class":112,"line":1798},100,[110,1800,1801],{"class":335},"    rows ",[110,1803,429],{"class":204},[110,1805,1806],{"class":335}," []\n",[110,1808,1810,1812,1815,1817],{"class":112,"line":1809},101,[110,1811,1341],{"class":204},[110,1813,1814],{"class":335}," fqdn, target ",[110,1816,460],{"class":204},[110,1818,1819],{"class":335}," cnames:\n",[110,1821,1823,1826,1828],{"class":112,"line":1822},102,[110,1824,1825],{"class":335},"        target_text ",[110,1827,429],{"class":204},[110,1829,1830],{"class":335}," target.to_text()\n",[110,1832,1834,1837,1840,1843,1846,1849,1852,1855,1857],{"class":112,"line":1833},103,[110,1835,1836],{"class":204},"        if",[110,1838,1839],{"class":335}," target.is_subdomain(origin) ",[110,1841,1842],{"class":204},"and",[110,1844,1845],{"class":335}," target_text ",[110,1847,1848],{"class":204},"not",[110,1850,1851],{"class":204}," in",[110,1853,1854],{"class":335}," names_with_address ",[110,1856,514],{"class":204},[110,1858,1859],{"class":335}," cname_names:\n",[110,1861,1863,1866,1869,1872,1875,1877,1879,1881,1884],{"class":112,"line":1862},104,[110,1864,1865],{"class":335},"            rows.append({",[110,1867,1868],{"class":120},"\"fqdn\"",[110,1870,1871],{"class":335},": fqdn, ",[110,1873,1874],{"class":120},"\"type\"",[110,1876,1701],{"class":335},[110,1878,1394],{"class":120},[110,1880,72],{"class":335},[110,1882,1883],{"class":120},"\"data\"",[110,1885,1886],{"class":335},": target_text,\n",[110,1888,1890,1893,1895,1897,1899,1902,1904,1906,1908,1911,1913,1916],{"class":112,"line":1889},105,[110,1891,1892],{"class":120},"                         \"open_port\"",[110,1894,1701],{"class":335},[110,1896,1704],{"class":120},[110,1898,72],{"class":335},[110,1900,1901],{"class":120},"\"has_ptr\"",[110,1903,1701],{"class":335},[110,1905,1704],{"class":120},[110,1907,72],{"class":335},[110,1909,1910],{"class":120},"\"finding\"",[110,1912,1701],{"class":335},[110,1914,1915],{"class":120},"\"dangling-cname\"",[110,1917,1918],{"class":335},"})\n",[110,1920,1922,1924],{"class":112,"line":1921},106,[110,1923,1149],{"class":204},[110,1925,1926],{"class":335}," rows\n",[110,1928,1930],{"class":112,"line":1929},107,[110,1931,382],{"emptyLinePlaceholder":381},[110,1933,1935],{"class":112,"line":1934},108,[110,1936,382],{"emptyLinePlaceholder":381},[110,1938,1940,1942,1945],{"class":112,"line":1939},109,[110,1941,906],{"class":204},[110,1943,1944],{"class":116}," main",[110,1946,912],{"class":335},[110,1948,1950,1953,1955],{"class":112,"line":1949},110,[110,1951,1952],{"class":335},"    args ",[110,1954,429],{"class":204},[110,1956,1957],{"class":335}," parse_args()\n",[110,1959,1961,1964,1966,1969,1971,1974,1976,1979,1981,1984,1987,1990,1993],{"class":112,"line":1960},111,[110,1962,1963],{"class":335},"    ports ",[110,1965,429],{"class":204},[110,1967,1968],{"class":335}," [",[110,1970,1124],{"class":127},[110,1972,1973],{"class":335},"(p) ",[110,1975,1767],{"class":204},[110,1977,1978],{"class":335}," p ",[110,1980,460],{"class":204},[110,1982,1983],{"class":335}," args.ports.split(",[110,1985,1986],{"class":120},"\",\"",[110,1988,1989],{"class":335},") ",[110,1991,1992],{"class":204},"if",[110,1994,1995],{"class":335}," p.strip()]\n",[110,1997,1999,2002,2004],{"class":112,"line":1998},112,[110,2000,2001],{"class":335},"    server_ip ",[110,2003,429],{"class":204},[110,2005,2006],{"class":335}," server_address(args.server)\n",[110,2008,2010],{"class":112,"line":2009},113,[110,2011,382],{"emptyLinePlaceholder":381},[110,2013,2015,2018,2020,2022,2025,2028,2031,2034,2037,2039,2042,2044,2047],{"class":112,"line":2014},114,[110,2016,2017],{"class":127},"    print",[110,2019,348],{"class":335},[110,2021,1005],{"class":204},[110,2023,2024],{"class":120},"\"Transferring ",[110,2026,2027],{"class":127},"{",[110,2029,2030],{"class":335},"args.zone",[110,2032,2033],{"class":127},"}",[110,2035,2036],{"class":120}," from ",[110,2038,2027],{"class":127},[110,2040,2041],{"class":335},"server_ip",[110,2043,2033],{"class":127},[110,2045,2046],{"class":120},"...\"",[110,2048,438],{"class":335},[110,2050,2052,2055,2057],{"class":112,"line":2051},115,[110,2053,2054],{"class":335},"    addresses, cnames, origin ",[110,2056,429],{"class":204},[110,2058,2059],{"class":335}," transfer_zone(args.zone, server_ip)\n",[110,2061,2063,2065,2067,2069,2071,2074,2077,2079,2082,2084,2087,2089,2092],{"class":112,"line":2062},116,[110,2064,2017],{"class":127},[110,2066,348],{"class":335},[110,2068,1005],{"class":204},[110,2070,550],{"class":120},[110,2072,2073],{"class":127},"{len",[110,2075,2076],{"class":335},"(addresses)",[110,2078,2033],{"class":127},[110,2080,2081],{"class":120}," A\u002FAAAA record(s) and ",[110,2083,2073],{"class":127},[110,2085,2086],{"class":335},"(cnames)",[110,2088,2033],{"class":127},[110,2090,2091],{"class":120}," CNAME(s) found. Probing...\"",[110,2093,438],{"class":335},[110,2095,2097],{"class":112,"line":2096},117,[110,2098,382],{"emptyLinePlaceholder":381},[110,2100,2102,2105,2108,2111,2113,2116,2119],{"class":112,"line":2101},118,[110,2103,2104],{"class":204},"    with",[110,2106,2107],{"class":335}," ThreadPoolExecutor(",[110,2109,2110],{"class":351},"max_workers",[110,2112,429],{"class":204},[110,2114,2115],{"class":335},"args.workers) ",[110,2117,2118],{"class":204},"as",[110,2120,2121],{"class":335}," pool:\n",[110,2123,2125,2128,2130,2133,2136,2139],{"class":112,"line":2124},119,[110,2126,2127],{"class":335},"        results ",[110,2129,429],{"class":204},[110,2131,2132],{"class":127}," list",[110,2134,2135],{"class":335},"(pool.map(",[110,2137,2138],{"class":204},"lambda",[110,2140,2141],{"class":335}," r: check_record(r, ports, args.timeout), addresses))\n",[110,2143,2145],{"class":112,"line":2144},120,[110,2146,2147],{"class":335},"    results.extend(dangling_cnames(cnames, addresses, origin))\n",[110,2149,2151],{"class":112,"line":2150},121,[110,2152,382],{"emptyLinePlaceholder":381},[110,2154,2156,2159,2161,2164,2166,2169,2171,2174,2176,2179,2181],{"class":112,"line":2155},122,[110,2157,2158],{"class":335},"    flagged ",[110,2160,429],{"class":204},[110,2162,2163],{"class":335}," [row ",[110,2165,1767],{"class":204},[110,2167,2168],{"class":335}," row ",[110,2170,460],{"class":204},[110,2172,2173],{"class":335}," results ",[110,2175,1992],{"class":204},[110,2177,2178],{"class":335}," row[",[110,2180,1910],{"class":120},[110,2182,2183],{"class":335},"]]\n",[110,2185,2187,2189,2191,2193,2195,2198,2200],{"class":112,"line":2186},123,[110,2188,1801],{"class":335},[110,2190,429],{"class":204},[110,2192,2173],{"class":335},[110,2194,1992],{"class":204},[110,2196,2197],{"class":335}," args.write_full ",[110,2199,571],{"class":204},[110,2201,2202],{"class":335}," flagged\n",[110,2204,2206,2208,2211,2214,2217,2219,2222,2224,2226,2228,2231,2233,2236,2238,2240],{"class":112,"line":2205},124,[110,2207,2104],{"class":204},[110,2209,2210],{"class":127}," open",[110,2212,2213],{"class":335},"(args.output, ",[110,2215,2216],{"class":120},"\"w\"",[110,2218,72],{"class":335},[110,2220,2221],{"class":351},"newline",[110,2223,429],{"class":204},[110,2225,1704],{"class":120},[110,2227,72],{"class":335},[110,2229,2230],{"class":351},"encoding",[110,2232,429],{"class":204},[110,2234,2235],{"class":120},"\"utf-8\"",[110,2237,1989],{"class":335},[110,2239,2118],{"class":204},[110,2241,2242],{"class":335}," handle:\n",[110,2244,2246,2249,2251,2254,2257,2259,2262,2264,2266,2268,2270,2272,2274,2277,2279,2281,2283,2285],{"class":112,"line":2245},125,[110,2247,2248],{"class":335},"        writer ",[110,2250,429],{"class":204},[110,2252,2253],{"class":335}," csv.DictWriter(handle, ",[110,2255,2256],{"class":351},"fieldnames",[110,2258,429],{"class":204},[110,2260,2261],{"class":335},"[",[110,2263,1868],{"class":120},[110,2265,72],{"class":335},[110,2267,1874],{"class":120},[110,2269,72],{"class":335},[110,2271,1883],{"class":120},[110,2273,72],{"class":335},[110,2275,2276],{"class":120},"\"open_port\"",[110,2278,72],{"class":335},[110,2280,1901],{"class":120},[110,2282,72],{"class":335},[110,2284,1910],{"class":120},[110,2286,2287],{"class":335},"])\n",[110,2289,2291],{"class":112,"line":2290},126,[110,2292,2293],{"class":335},"        writer.writeheader()\n",[110,2295,2297,2300,2303,2306,2309,2312,2315,2317],{"class":112,"line":2296},127,[110,2298,2299],{"class":335},"        writer.writerows(",[110,2301,2302],{"class":127},"sorted",[110,2304,2305],{"class":335},"(rows, ",[110,2307,2308],{"class":351},"key",[110,2310,2311],{"class":204},"=lambda",[110,2313,2314],{"class":335}," row: row[",[110,2316,1868],{"class":120},[110,2318,2319],{"class":335},"]))\n",[110,2321,2323],{"class":112,"line":2322},128,[110,2324,382],{"emptyLinePlaceholder":381},[110,2326,2328,2331,2333,2336,2338,2341,2344,2346,2348,2351,2353,2355,2357,2360,2363,2365],{"class":112,"line":2327},129,[110,2329,2330],{"class":335},"    orphans ",[110,2332,429],{"class":204},[110,2334,2335],{"class":127}," sum",[110,2337,348],{"class":335},[110,2339,2340],{"class":127},"1",[110,2342,2343],{"class":204}," for",[110,2345,2168],{"class":335},[110,2347,460],{"class":204},[110,2349,2350],{"class":335}," flagged ",[110,2352,1992],{"class":204},[110,2354,2178],{"class":335},[110,2356,1910],{"class":120},[110,2358,2359],{"class":335},"] ",[110,2361,2362],{"class":204},"==",[110,2364,1719],{"class":120},[110,2366,438],{"class":335},[110,2368,2370,2373,2375,2378,2381,2383],{"class":112,"line":2369},130,[110,2371,2372],{"class":335},"    dangling ",[110,2374,429],{"class":204},[110,2376,2377],{"class":127}," len",[110,2379,2380],{"class":335},"(flagged) ",[110,2382,487],{"class":204},[110,2384,2385],{"class":335}," orphans\n",[110,2387,2389,2391,2393,2395,2397,2399,2402,2404,2407,2409,2412,2414,2417,2419,2422,2425,2427,2429,2431],{"class":112,"line":2388},131,[110,2390,2017],{"class":127},[110,2392,348],{"class":335},[110,2394,1005],{"class":204},[110,2396,550],{"class":120},[110,2398,2027],{"class":127},[110,2400,2401],{"class":335},"orphans",[110,2403,2033],{"class":127},[110,2405,2406],{"class":120}," orphan candidate(s) and ",[110,2408,2027],{"class":127},[110,2410,2411],{"class":335},"dangling",[110,2413,2033],{"class":127},[110,2415,2416],{"class":120}," dangling CNAME(s) out of ",[110,2418,2073],{"class":127},[110,2420,2421],{"class":335},"(addresses) ",[110,2423,2424],{"class":204},"+",[110,2426,2377],{"class":127},[110,2428,2086],{"class":335},[110,2430,2033],{"class":127},[110,2432,2433],{"class":120}," record(s). \"\n",[110,2435,2437,2440,2443,2445,2448,2450,2452],{"class":112,"line":2436},132,[110,2438,2439],{"class":204},"          f",[110,2441,2442],{"class":120},"\"Report: ",[110,2444,2027],{"class":127},[110,2446,2447],{"class":335},"args.output",[110,2449,2033],{"class":127},[110,2451,550],{"class":120},[110,2453,438],{"class":335},[110,2455,2457],{"class":112,"line":2456},133,[110,2458,382],{"emptyLinePlaceholder":381},[110,2460,2462],{"class":112,"line":2461},134,[110,2463,382],{"emptyLinePlaceholder":381},[110,2465,2467,2469,2472,2475,2478],{"class":112,"line":2466},135,[110,2468,1992],{"class":204},[110,2470,2471],{"class":127}," __name__",[110,2473,2474],{"class":204}," ==",[110,2476,2477],{"class":120}," \"__main__\"",[110,2479,1183],{"class":335},[110,2481,2483],{"class":112,"line":2482},136,[110,2484,2485],{"class":335},"    main()\n",[24,2487,2489],{"id":2488},"notes","Notes",[29,2491,2492,2511,2521,2537,2562,2568],{},[32,2493,2494,2498,2499,2502,2503,2506,2507,2510],{},[2495,2496,2497],"strong",{},"Candidates, not verdicts."," A host that only listens on a port outside the probe list, or sits behind a host firewall that drops everything but its one service port, shows up as an orphan even though it's alive. Cross-check candidates against DHCP leases (",[38,2500,2501],{},"Get-DhcpServerv4Lease",") and AD computer objects (",[38,2504,2505],{},"Get-ADComputer"," with ",[38,2508,2509],{},"LastLogonDate",") before deleting anything. The discovery-first essay has scripts for both.",[32,2512,2513,2516,2517,2520],{},[2495,2514,2515],{},"A missing PTR proves little on its own."," Many environments never populated reverse zones, which is why the script only flags a record when both the port probe and the PTR lookup come back empty. The PTR lookup goes through the host's configured resolver, not ",[38,2518,2519],{},"--server",", so run the script somewhere the internal reverse zones resolve, or every record looks PTR-less.",[32,2522,2523,2526,2527,2530,2531,2533,2534,2536],{},[2495,2524,2525],{},"Transfer failures."," A refused transfer raises a dnspython exception (a ",[38,2528,2529],{},"dns.exception.DNSException"," subclass) rather than returning an empty zone, so the script fails loudly instead of reporting \"no orphans\". ",[38,2532,1291],{}," is per response message and ",[38,2535,1301],{}," caps the whole transfer.",[32,2538,2539,2542,2543,2545,2546,2549,2550,2553,2554,2557,2558,2561],{},[2495,2540,2541],{},"Relative names."," Zones transferred with dnspython are relativized to the origin by default (",[38,2544,593],{}," for the apex, ",[38,2547,2548],{},"www"," rather than ",[38,2551,2552],{},"www.corp.example.internal.","). The first version of this script built FQDNs with string concatenation, which produced names like ",[38,2555,2556],{},"@.corp.example.internal."," for apex records; ",[38,2559,2560],{},"derelativize(origin)"," fixes that.",[32,2563,2564,2567],{},[2495,2565,2566],{},"Dangling CNAMEs"," are only checked for targets inside the transferred zone. A CNAME pointing at another zone or an external SaaS hostname needs a resolver lookup of its own, and a dangling external CNAME is worth checking for subdomain takeover risk.",[32,2569,2570,2573],{},[2495,2571,2572],{},"Aging."," On Windows DNS, stale dynamically registered records are what aging and scavenging exist for. Static records get a zero timestamp and are never scavenged, so those are the ones this report is really for.",[24,2575,2577],{"id":2576},"references","References",[29,2579,2580,2595,2609,2619,2626,2635,2642],{},[32,2581,2582],{},[18,2583,2587,2588,2591,2592],{"href":2584,"rel":2585},"https:\u002F\u002Fdnspython.readthedocs.io\u002Fen\u002Flatest\u002Fquery.html",[2586],"nofollow","dnspython: ",[38,2589,2590],{},"dns.query.inbound_xfr"," and the deprecation of ",[38,2593,2594],{},"dns.query.xfr",[32,2596,2597],{},[18,2598,2601,2602,2605,2606],{"href":2599,"rel":2600},"https:\u002F\u002Fdnspython.readthedocs.io\u002Fen\u002Flatest\u002Fzone-class.html",[2586],"dnspython: the ",[38,2603,2604],{},"Zone"," class and ",[38,2607,2608],{},"iterate_rdatas",[32,2610,2611],{},[18,2612,2615,2616,563],{"href":2613,"rel":2614},"https:\u002F\u002Fdnspython.readthedocs.io\u002Fen\u002Flatest\u002Fresolver-functions.html",[2586],"dnspython: resolver functions (",[38,2617,2618],{},"resolve_address",[32,2620,2621],{},[18,2622,2625],{"href":2623,"rel":2624},"https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fpowershell\u002Fmodule\u002Fdnsserver\u002Fget-dnsserverresourcerecord",[2586],"Get-DnsServerResourceRecord (DnsServer)",[32,2627,2628],{},[18,2629,2632,2633],{"href":2630,"rel":2631},"https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fpowershell\u002Fmodule\u002Fdnsserver\u002Fset-dnsserverprimaryzone",[2586],"Set-DnsServerPrimaryZone: ",[38,2634,63],{},[32,2636,2637],{},[18,2638,2641],{"href":2639,"rel":2640},"https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fwindows-server\u002Fnetworking\u002Fdns\u002Faging-scavenging",[2586],"DNS aging and scavenging in Windows Server",[32,2643,2644],{},[18,2645,2648,2649],{"href":2646,"rel":2647},"https:\u002F\u002Fbind9.readthedocs.io\u002Fen\u002Flatest\u002Freference.html#namedconf-statement-allow-transfer",[2586],"BIND 9 configuration reference: ",[38,2650,85],{},[2652,2653,2654],"style",{},"html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}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 .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}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}",{"title":106,"searchDepth":198,"depth":198,"links":2656},[2657,2658,2659,2660,2661],{"id":26,"depth":198,"text":27},{"id":95,"depth":198,"text":96},{"id":731,"depth":198,"text":732},{"id":2488,"depth":198,"text":2489},{"id":2576,"depth":198,"text":2577},"techcolumnist",[2664,2665],"scripts","engineering","2026-01-28T14:00:00Z","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.","md",false,null,{},"\u002Fblog\u002F2026\u002F01\u002F28\u002Fpython-discovery-crawling-dns-zones-for-orphaned-records",{"title":6,"description":2667},[2662],"blog\u002F2026\u002F01\u002F28\u002Fpython-discovery-crawling-dns-zones-for-orphaned-records",[737,2677,2678,2679],"discovery","network","windows","\u002F2026\u002F01\u002F28\u002Fpython-discovery-crawling-dns-zones-for-orphaned-records\u002F","4SXLgZjHSB03KsaT6pkclviJG1e8LjsqcF1jWq9nOig",{"title":2683,"description":2684,"date":2685,"url":2686,"categories":2687,"tags":2688,"image":2670,"readingTime":222,"canonical":2662,"sites":2691,"series":2670,"seriesOrder":2670},"Python: Talos – Health-Checking a Kubernetes Cluster from a Cron Job","A cron-friendly Python health check for Talos clusters: talosctl health, etcd alarms, node readiness, cordons and crash-looping pods, with a webhook alert.","2026-02-04T14:00:00Z","\u002F2026\u002F02\u002F04\u002Fpython-talos-health-checking-a-kubernetes-cluster-from-a-cron-job\u002F",[2664,2665],[737,2689,2690],"talos","kubernetes",[2662],{"title":2693,"description":2694,"date":2695,"url":2696,"categories":2697,"tags":2698,"image":2670,"readingTime":208,"canonical":2662,"sites":2702,"series":2670,"seriesOrder":2670},"Python: Data Warehouse – Catching Schema Drift Before an ETL Run","A Python pre-flight check that snapshots Postgres column definitions from information_schema and fails the ETL run when a column is dropped, retyped, or resized.","2026-01-21T14:00:00Z","\u002F2026\u002F01\u002F21\u002Fpython-data-warehouse-catching-schema-drift-before-an-etl-run\u002F",[2664,2665],[737,2699,2700,2701],"data-warehouse","etl","sql",[2662],[2704,2712,2720],{"title":2705,"description":2706,"date":2707,"url":2708,"categories":2709,"tags":2710,"image":2670,"readingTime":208,"canonical":2662,"sites":2711,"series":2670,"seriesOrder":2670},"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","\u002F2026\u002F01\u002F07\u002Fpython-discovery-fingerprint-every-device-on-a-subnet\u002F",[2664,2665],[737,2677,2678],[2662],{"title":2713,"description":2714,"date":2715,"url":20,"categories":2716,"tags":2718,"image":2670,"readingTime":245,"canonical":2662,"sites":2719,"series":2670,"seriesOrder":2670},"Infrastructure: Documenting an Undocumented Network, Discovery First","Document an undocumented network from what it already knows: DHCP leases, AD, ARP and switch MAC tables, then a scoped nmap pass. Scripts included.","2026-07-15T14:00:00Z",[2665,2717],"strategy",[2677,2678,737],[2662],{"title":2721,"description":2722,"date":2723,"url":2724,"categories":2725,"tags":2726,"image":2670,"readingTime":208,"canonical":2662,"sites":2728,"series":2670,"seriesOrder":2670},"Node.js: Discovery – Diffing Two Network Scans to Flag New Hosts","A dependency-free Node.js script that diffs two nmap XML scans and flags new, missing and moved hosts plus port changes, with an exit code for cron or CI.","2026-03-11T14:00:00Z","\u002F2026\u002F03\u002F11\u002Fnode-js-discovery-diffing-two-network-scans-to-flag-new-hosts\u002F",[2664,2665],[2727,2677,2678],"nodejs",[2662],{"doc":2670,"posts":2730},[],1790052513415]