diff options
author | Christian Cleberg <hello@cleberg.net> | 2025-06-20 13:55:54 -0500 |
---|---|---|
committer | Christian Cleberg <hello@cleberg.net> | 2025-06-20 13:55:54 -0500 |
commit | 785f42901f34aaf356f316c691e3f56138c8608d (patch) | |
tree | 5b8f7a6e33a6af410e511137fdd51b6fa60d0f83 /sections | |
download | aws-summary-report-785f42901f34aaf356f316c691e3f56138c8608d.tar.gz aws-summary-report-785f42901f34aaf356f316c691e3f56138c8608d.tar.bz2 aws-summary-report-785f42901f34aaf356f316c691e3f56138c8608d.zip |
initial commit
Diffstat (limited to 'sections')
-rw-r--r-- | sections/__init__.py | 1 | ||||
-rw-r--r-- | sections/acm.py | 41 | ||||
-rw-r--r-- | sections/cloudfront.py | 37 | ||||
-rw-r--r-- | sections/cloudwatch.py | 45 | ||||
-rw-r--r-- | sections/config.py | 37 | ||||
-rw-r--r-- | sections/costexplorer.py | 67 | ||||
-rw-r--r-- | sections/route53.py | 40 | ||||
-rw-r--r-- | sections/s3.py | 47 | ||||
-rw-r--r-- | sections/securityhub.py | 56 |
9 files changed, 371 insertions, 0 deletions
diff --git a/sections/__init__.py b/sections/__init__.py new file mode 100644 index 0000000..02276b8 --- /dev/null +++ b/sections/__init__.py @@ -0,0 +1 @@ +# sections/__init__.py diff --git a/sections/acm.py b/sections/acm.py new file mode 100644 index 0000000..3a62668 --- /dev/null +++ b/sections/acm.py @@ -0,0 +1,41 @@ +# acm.py +import boto3 +from datetime import datetime, timedelta, timezone +from tabulate import tabulate + + +def get_section(config): + profile = config["aws"].get("profile") + region = config["aws"]["region"] + session = boto3.Session( + profile_name=profile if profile else None, region_name=region + ) + client = session.client("acm") + + today = datetime.now(timezone.utc) + deadline = today + timedelta(days=30) + + certs = client.list_certificates(CertificateStatuses=["ISSUED"])[ + "CertificateSummaryList" + ] + rows = [] + + for cert in certs: + detail = client.describe_certificate(CertificateArn=cert["CertificateArn"])[ + "Certificate" + ] + not_after = detail.get("NotAfter") + if not_after and today <= not_after <= deadline: + rows.append([cert["DomainName"], not_after.strftime("%Y-%m-%d")]) + + if not rows: + return "Expiring TLS Certificates:\nNo certs expiring in the next 30 days." + + table = tabulate(rows, headers=["Domain", "Expires"], tablefmt="simple_grid") + lines = [ + "Expiring TLS Certificates (Next 30 Days):", + f"[https://{config['aws'].get('region')}.console.aws.amazon.com/acm/home#/certificates/list]", + table, + ] + + return "\n".join(lines) diff --git a/sections/cloudfront.py b/sections/cloudfront.py new file mode 100644 index 0000000..df161e6 --- /dev/null +++ b/sections/cloudfront.py @@ -0,0 +1,37 @@ +# cloudfront.py +import boto3 +from datetime import datetime, timedelta, timezone +from tabulate import tabulate + + +def get_section(config): + profile = config["aws"].get("profile") + session = boto3.Session(profile_name=profile if profile else None) + client = session.client("cloudfront") + + now = datetime.now(timezone.utc) + cutoff = now - timedelta(days=2) + + dists = client.list_distributions().get("DistributionList", {}).get("Items", []) + rows = [] + + for dist in dists: + last_mod = dist["LastModifiedTime"] + if last_mod >= cutoff: + rows.append( + [dist["Id"], dist["DomainName"], last_mod.strftime("%Y-%m-%d %H:%M")] + ) + + if not rows: + return "CloudFront Changes:\nNo distributions changed in the last 48h." + + table = tabulate( + rows, headers=["ID", "Domain", "Last Modified"], tablefmt="simple_grid" + ) + lines = [ + "CloudFront Distribution Changes (Last 48h):", + f"[https://{config['aws'].get('region')}.console.aws.amazon.com/cloudfront/v4/home#/distributions]", + table, + ] + + return "\n".join(lines) diff --git a/sections/cloudwatch.py b/sections/cloudwatch.py new file mode 100644 index 0000000..65a59d6 --- /dev/null +++ b/sections/cloudwatch.py @@ -0,0 +1,45 @@ +# cloudwatch.py +import boto3 +import datetime +from tabulate import tabulate + + +def get_section(config): + profile = config["aws"].get("profile") + region = config["aws"]["region"] + session = boto3.Session( + profile_name=profile if profile else None, region_name=region + ) + client = session.client("cloudwatch") + + now = datetime.datetime.utcnow() + yesterday = now - datetime.timedelta(days=1) + + alarms = client.describe_alarms(StateValue="ALARM")["MetricAlarms"] + rows = [] + + for alarm in alarms: + last_updated = alarm["StateUpdatedTimestamp"] + if last_updated >= yesterday: + rows.append( + [ + alarm["AlarmName"], + alarm["MetricName"], + alarm["StateValue"], + last_updated.strftime("%Y-%m-%d %H:%M UTC"), + ] + ) + + if not rows: + return "CloudWatch Alarms:\nNo alarms triggered in the last 24h." + + table = tabulate( + rows, headers=["Name", "Metric", "State", "Updated"], tablefmt="simple_grid" + ) + lines = [ + "CloudWatch Alarms (Last 24h):", + f"[https://{config['aws'].get('region')}.console.aws.amazon.com/cloudwatch/home#alarmsV2:]", + table, + ] + + return "\n".join(lines) diff --git a/sections/config.py b/sections/config.py new file mode 100644 index 0000000..28505fa --- /dev/null +++ b/sections/config.py @@ -0,0 +1,37 @@ +# config.py +import boto3 +from tabulate import tabulate + + +def get_section(config): + profile = config["aws"].get("profile") + region = config["aws"]["region"] + session = boto3.Session( + profile_name=profile if profile else None, region_name=region + ) + client = session.client("config") + + paginator = client.get_paginator("describe_compliance_by_resource") + page_iterator = paginator.paginate(ComplianceTypes=["NON_COMPLIANT"]) + + rows = [] + + for page in page_iterator: + for result in page.get("ComplianceByResources", []): + resource_type = result.get("ResourceType", "Unknown") + resource_id = result.get("ResourceId", "Unknown") + rows.append([resource_type, resource_id]) + + if not rows: + return "AWS Config Non-Compliance:\nAll resources are compliant." + + table = tabulate( + rows, headers=["Resource Type", "Resource ID"], tablefmt="simple_grid" + ) + lines = [ + "AWS Config Non-Compliant Resources:", + f"[https://{config['aws'].get('region')}.console.aws.amazon.com/config/home#/resources?complianceType=NON_COMPLIANT]", + table, + ] + + return "\n".join(lines) diff --git a/sections/costexplorer.py b/sections/costexplorer.py new file mode 100644 index 0000000..66747f0 --- /dev/null +++ b/sections/costexplorer.py @@ -0,0 +1,67 @@ +# costexplorer.py +import boto3 +import datetime +from tabulate import tabulate + + +def get_section(config): + profile = config["aws"].get("profile") + region = config["aws"]["region"] + + session = boto3.Session( + profile_name=profile if profile else None, region_name=region + ) + client = session.client("ce") + + today = datetime.date.today() + start = (today - datetime.timedelta(days=1)).strftime("%Y-%m-%d") + end = today.strftime("%Y-%m-%d") + + response = client.get_cost_and_usage( + TimePeriod={"Start": start, "End": end}, + Granularity="DAILY", + Metrics=["UnblendedCost"], + GroupBy=[{"Type": "DIMENSION", "Key": "SERVICE"}], + ) + + results = response["ResultsByTime"][0] + date = results["TimePeriod"]["Start"] + estimated = results.get("Estimated", False) + + rows = [] + groups = results.get("Groups", []) + if groups: + for group in groups: + service = group["Keys"][0] + cost = ( + group.get("Metrics", {}).get("UnblendedCost", {}).get("Amount", "0.00") + ) + rows.append([service, f"${float(cost):.2f}"]) + else: + rows.append(["No service-level data available", ""]) + + total_cost = results.get("Total", {}).get("UnblendedCost", {}).get("Amount", None) + if total_cost is None: + total_cost = sum( + float(group.get("Metrics", {}).get("UnblendedCost", {}).get("Amount", 0.0)) + for group in groups + ) + rows.append(["TOTAL", f"${float(total_cost):.2f}"]) + + table = tabulate( + rows, + headers=["Service", "Cost"], + tablefmt="simple_grid", + colalign=("left", "right"), + ) + + lines = [ + f"AWS Billing Report for {date}", + f"[https://{config['aws'].get('region')}.console.aws.amazon.com/costmanagement/]", + table, + ] + + if estimated: + lines.append("\nNote: Costs are estimated and may change.") + + return "\n".join(lines) diff --git a/sections/route53.py b/sections/route53.py new file mode 100644 index 0000000..d2da7cf --- /dev/null +++ b/sections/route53.py @@ -0,0 +1,40 @@ +# route53.py +import boto3 +from tabulate import tabulate + + +def get_section(config): + profile = config["aws"].get("profile") + region = config["aws"]["region"] # Not used by Route53, but kept for consistency + + session = boto3.Session(profile_name=profile if profile else None) + client = session.client("route53") + + health_checks = client.list_health_checks()["HealthChecks"] + rows = [] + + for hc in health_checks: + hc_id = hc["Id"] + name = hc.get("HealthCheckConfig", {}).get( + "FullyQualifiedDomainName", "Unnamed" + ) + status = client.get_health_check_status(HealthCheckId=hc_id) + status_summary = status["HealthCheckObservations"] + healthy = all( + obs["StatusReport"]["Status"].startswith("Success") + for obs in status_summary + ) + state = "HEALTHY" if healthy else "UNHEALTHY" + rows.append([name, state]) + + if not rows: + return "Route 53 Health Checks:\nNo health checks configured." + + table = tabulate(rows, headers=["Domain", "Status"], tablefmt="simple_grid") + lines = [ + "Route 53 Health Checks:", + f"[https://{config['aws'].get('region')}.console.aws.amazon.com/route53/v2/healthchecks/home]", + table, + ] + + return "\n".join(lines) diff --git a/sections/s3.py b/sections/s3.py new file mode 100644 index 0000000..c2c53e6 --- /dev/null +++ b/sections/s3.py @@ -0,0 +1,47 @@ +# s3.py +import boto3 +from tabulate import tabulate + + +def get_section(config): + profile = config["aws"].get("profile") + session = boto3.Session(profile_name=profile if profile else None) + client = session.client("s3") + + buckets = client.list_buckets()["Buckets"] + rows = [] + + for bucket in buckets: + name = bucket["Name"] + public = "Unknown" + encrypted = "No" + + try: + acl = client.get_bucket_acl(Bucket=name) + public = any( + grant["Grantee"].get("URI", "").endswith("AllUsers") + for grant in acl["Grants"] + ) + except Exception: + public = "Error" + + try: + enc = client.get_bucket_encryption(Bucket=name) + rules = enc["ServerSideEncryptionConfiguration"]["Rules"] + if rules: + encrypted = "Yes" + except client.exceptions.ClientError: + encrypted = "No" + + rows.append([name, "Yes" if public else "No", encrypted]) + + table = tabulate( + rows, headers=["Bucket", "Public", "Encrypted"], tablefmt="simple_grid" + ) + lines = [ + "S3 Bucket Access Summary:", + f"[https://{config['aws'].get('region')}.console.aws.amazon.com/s3/home]", + table, + ] + + return "\n".join(lines) diff --git a/sections/securityhub.py b/sections/securityhub.py new file mode 100644 index 0000000..0ccb5fe --- /dev/null +++ b/sections/securityhub.py @@ -0,0 +1,56 @@ +# securityhub.py +import boto3 +import datetime +from tabulate import tabulate + + +def get_section(config): + profile = config["aws"].get("profile") + region = config["aws"]["region"] + + session = boto3.Session( + profile_name=profile if profile else None, region_name=region + ) + client = session.client("securityhub") + + findings = [] + paginator = client.get_paginator("get_findings") + + response_iterator = paginator.paginate( + Filters={ + "CreatedAt": [{"DateRange": {"Value": 1, "Unit": "DAYS"}}], + "RecordState": [{"Value": "ACTIVE", "Comparison": "EQUALS"}], + "WorkflowStatus": [{"Value": "NEW", "Comparison": "EQUALS"}], + }, + ) + + for page in response_iterator: + findings.extend(page.get("Findings", [])) + + rows = [] + for finding in findings: + title = finding.get("Title", "No title") + severity = finding.get("Severity", {}).get("Label", "UNKNOWN") + product = finding.get("ProductName", "Unknown Product") + resource = finding.get("Resources", [{}])[0].get("Id", "Unknown Resource") + rows.append([severity, title[:50], product, resource[:30]]) + + if not rows: + lines = [ + "AWS Security Hub Findings (Last 24h)", + "No new findings in the past 24 hours.", + ] + else: + table = tabulate( + rows, + headers=["Severity", "Title", "Product", "Resource"], + tablefmt="simple_grid", + colalign=("center", "left", "left", "left"), + ) + lines = [ + f"AWS Security Hub Findings (Last 24h): {len(rows)} new finding(s)", + f"[https://{config['aws'].get('region')}.console.aws.amazon.com/securityhub/home?region=eu-west-1#/findings]", + table, + ] + + return "\n".join(lines) |