Skip to content

Commit

Permalink
initial
Browse files Browse the repository at this point in the history
  • Loading branch information
badra001 committed Feb 27, 2026
1 parent 0584ea6 commit 7eb637f
Show file tree
Hide file tree
Showing 5 changed files with 964 additions and 0 deletions.
235 changes: 235 additions & 0 deletions local-app/python-tools/route53/describe_zone.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
#!/usr/bin/env python3
"""
describe_zone.py v1.0.0
Describe a Route 53 hosted zone in detail: metadata, record count,
delegation set, associated VPCs, tags, and DNSSEC status.
Usage:
python describe_zone.py --zone-id Z1234567890ABC [--profile myprofile] [--output-json zone_info.json]
"""

import argparse
import boto3
import json
import sys
from typing import Optional

VERSION = "1.0.0"


def get_client(profile: Optional[str], region: str):
session = boto3.Session(profile_name=profile, region_name=region)
return session.client('route53')


def get_zone_details(client, zone_id: str) -> dict:
"""Fetch core hosted zone metadata."""
try:
response = client.get_hosted_zone(Id=zone_id)
except client.exceptions.NoSuchHostedZone:
print(f"ERROR: Hosted zone '{zone_id}' not found.", file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"ERROR fetching zone details: {e}", file=sys.stderr)
sys.exit(1)

return {
'HostedZone': response['HostedZone'],
'DelegationSet': response.get('DelegationSet', {}),
'VPCs': response.get('VPCs', []),
}


def get_record_count(client, zone_id: str) -> int:
"""
Return the exact record count by paginating all RRs.
The ResourceRecordSetCount in GetHostedZone is an estimate and can lag.
"""
count = 0
paginator = client.get_paginator('list_resource_record_sets')
for page in paginator.paginate(HostedZoneId=zone_id):
count += len(page['ResourceRecordSets'])
return count


def get_tags(client, zone_id: str) -> dict:
"""Fetch tags for the hosted zone."""
try:
response = client.list_tags_for_resource(
ResourceType='hostedzone',
ResourceId=zone_id
)
return {tag['Key']: tag['Value'] for tag in response['ResourceTagSet']['Tags']}
except Exception as e:
print(f"WARNING: Could not fetch tags: {e}", file=sys.stderr)
return {}


def get_dnssec_status(client, zone_id: str) -> dict:
"""Fetch DNSSEC signing status (public zones only)."""
try:
response = client.get_dnssec(HostedZoneId=zone_id)
return {
'Status': response['Status'].get('ServeSignature', 'UNKNOWN'),
'KeySigningKeys': [
{
'Name': ksk['Name'],
'Status': ksk['Status'],
'Algorithm': ksk.get('SigningAlgorithmMnemonic', ''),
'CreatedOn': str(ksk.get('CreatedDate', '')),
}
for ksk in response.get('KeySigningKeys', [])
]
}
except client.exceptions.NoSuchHostedZone:
return {'Status': 'NOT_APPLICABLE', 'KeySigningKeys': []}
except Exception:
# Private zones and GovCloud may not support DNSSEC
return {'Status': 'NOT_APPLICABLE', 'KeySigningKeys': []}


def get_account_id(profile: Optional[str]) -> str:
"""Resolve the AWS account ID for the active credentials."""
try:
session = boto3.Session(profile_name=profile)
sts = session.client('sts')
return sts.get_caller_identity()['Account']
except Exception as e:
return f"UNKNOWN ({e})"


def build_report(zone_id: str, details: dict, record_count: int, tags: dict,
dnssec: dict, account_id: str) -> dict:
"""Assemble all collected data into a structured report dict."""
hz = details['HostedZone']
config = hz.get('Config', {})
delegation = details.get('DelegationSet', {})
vpcs = details.get('VPCs', [])

return {
'SchemaVersion': '1.0',
'ToolVersion': VERSION,
'AccountId': account_id,
'HostedZone': {
'Id': zone_id,
'Name': hz.get('Name', ''),
'Comment': config.get('Comment', ''),
'PrivateZone': config.get('PrivateZone', False),
'CallerReference': hz.get('CallerReference', ''),
},
'RecordCount': {
'Exact': record_count,
'Estimate': hz.get('ResourceRecordSetCount', 'N/A'),
},
'DelegationSet': {
'Id': delegation.get('Id', ''),
'CallerReference': delegation.get('CallerReference', ''),
'NameServers': delegation.get('NameServers', []),
},
'AssociatedVPCs': [
{'VPCId': vpc.get('VPCId', ''), 'VPCRegion': vpc.get('VPCRegion', '')}
for vpc in vpcs
],
'Tags': tags,
'DNSSEC': dnssec,
}


def print_report(report: dict):
"""Print a human-readable summary of the zone report."""
hz = report['HostedZone']
rc = report['RecordCount']
ds = report['DelegationSet']
vpcs = report['AssociatedVPCs']
dnssec = report['DNSSEC']

print(f"\ndescribe_zone.py v{VERSION}")
print("=" * 60)
print("HOSTED ZONE SUMMARY")
print("=" * 60)

print(f"\n{'Zone ID':<22} {hz['Id']}")
print(f"{'Zone Name':<22} {hz['Name']}")
print(f"{'Account ID':<22} {report['AccountId']}")
print(f"{'Comment':<22} {hz['Comment'] or '(none)'}")
print(f"{'Private Zone':<22} {hz['PrivateZone']}")
print(f"{'Caller Reference':<22} {hz['CallerReference']}")

print(f"\n--- Record Count {'---':-<42}")
print(f" {'Exact (paginated)':<22} {rc['Exact']}")
print(f" {'AWS Estimate':<22} {rc['Estimate']}")

print(f"\n--- Delegation Set {'---':-<41}")
if ds['NameServers']:
print(f" {'Set ID':<22} {ds['Id'] or '(default)'}")
for i, ns in enumerate(ds['NameServers']):
label = 'Name Servers' if i == 0 else ''
print(f" {label:<22} {ns}")
else:
print(" (Private zone — no delegation set)")

print(f"\n--- Associated VPCs {'---':-<40}")
if vpcs:
print(f" {'VPC ID':<24} {'Region'}")
print(f" {'-'*24} {'-'*15}")
for vpc in vpcs:
print(f" {vpc['VPCId']:<24} {vpc['VPCRegion']}")
else:
print(" (Public zone — no VPC associations)")

print(f"\n--- Tags {'---':-<51}")
if report['Tags']:
for k, v in sorted(report['Tags'].items()):
print(f" {k:<30} {v}")
else:
print(" (no tags)")

print(f"\n--- DNSSEC {'---':-<49}")
print(f" {'Status':<22} {dnssec['Status']}")
if dnssec['KeySigningKeys']:
for ksk in dnssec['KeySigningKeys']:
print(f" {'KSK':<22} {ksk['Name']} | {ksk['Status']} | {ksk['Algorithm']} | created {ksk['CreatedOn']}")
else:
print(" (no key signing keys)")

print()


def export_json(report: dict, output_file: str):
with open(output_file, 'w') as f:
json.dump(report, f, indent=2, default=str)
print(f"Report exported to {output_file}")


def main():
parser = argparse.ArgumentParser(
description=f'Describe a Route 53 hosted zone in detail. v{VERSION}'
)
parser.add_argument('--zone-id', required=True, help='Hosted Zone ID (e.g., Z1234567890ABC)')
parser.add_argument('--profile', help='AWS profile name')
parser.add_argument('--region', default='us-gov-east-1', help='AWS region (default: us-gov-east-1)')
parser.add_argument('--output-json', metavar='FILE', help='Export report to JSON file')
parser.add_argument('--quiet', action='store_true', help='Suppress console output (use with --output-json)')
args = parser.parse_args()

zone_id = args.zone_id.split('/')[-1]

client = get_client(args.profile, args.region)
account_id = get_account_id(args.profile)
details = get_zone_details(client, zone_id)
record_count = get_record_count(client, zone_id)
tags = get_tags(client, zone_id)
dnssec = get_dnssec_status(client, zone_id)

report = build_report(zone_id, details, record_count, tags, dnssec, account_id)

if not args.quiet:
print_report(report)

if args.output_json:
export_json(report, args.output_json)


if __name__ == '__main__':
main()
Loading

0 comments on commit 7eb637f

Please sign in to comment.