Skip to content

Commit

Permalink
add age distribution
Browse files Browse the repository at this point in the history
  • Loading branch information
badra001 committed Jan 9, 2026
1 parent 74cc3ea commit 665cdd1
Showing 1 changed file with 85 additions and 18 deletions.
103 changes: 85 additions & 18 deletions local-app/python-tools/cross-organization/assess_check_ecr.py
Original file line number Diff line number Diff line change
@@ -1,54 +1,121 @@
#!/usr/bin/env python

import json, argparse, sys, os, glob
from datetime import datetime
from datetime import datetime, timezone
from collections import Counter

# --- VERSIONING ---
__version__ = "1.0.0"
__version__ = "1.1.0"

def find_latest_file(pattern):
files = glob.glob(pattern)
return max(files, key=os.path.getctime) if files else None

def get_days_ago(iso_str):
"""Calculates days between now and an ISO date string."""
if not iso_str or iso_str == "N/A":
return None
try:
dt = datetime.fromisoformat(iso_str)
# Handle timezone awareness
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
now = datetime.now(timezone.utc)
return (now - dt).days
except:
return None

def bucket_age(days, counters):
"""Increments the appropriate age bucket counter."""
if days is None: return
if days >= 365: counters['365+'] += 1
elif days >= 180: counters['180-364'] += 1
elif days >= 90: counters['90-179'] += 1
elif days >= 60: counters['60-89'] += 1
elif days >= 30: counters['30-59'] += 1
else: counters['<30'] += 1

def main():
parser = argparse.ArgumentParser(description="AWS ECR Audit Assessor")
parser = argparse.ArgumentParser(description="AWS ECR Audit Assessor - Data Aging Edition")
parser.add_argument("--input", help="JSON audit file")
args = parser.parse_args()

input_file = args.input or find_latest_file("audit_results.check_ecr.*.json")
if not input_file: print("Error: No file found."); sys.exit(1)
if not input_file:
print("Error: No file found."); sys.exit(1)

with open(input_file, 'r') as f: data = json.load(f)
with open(input_file, 'r') as f:
data = json.load(f)

report_width = 220
print("-" * report_width)
print(f"ECR REPOSITORY ASSESSMENT | Total Accounts: {len(data)}")
print(f"ECR COMPREHENSIVE ASSESSMENT | Total Accounts: {len(data)} | Input: {os.path.basename(input_file)}")
print("-" * report_width)
print(f"{'Idx':<5} | {'Account ID':<15} | {'Region':<12} | {'Repo Name':<40} | {'Mutability':<12} | {'Lifecycle':<10} | {'Img Count'}")
print(f"{'Idx':<5} | {'Account ID':<15} | {'Region':<12} | {'Repo Name':<45} | {'Mutability':<12} | {'Lifecycle':<10} | {'Img Count'}")
print("-" * report_width)

stats = {"total_repos": 0, "total_images": 0, "no_lifecycle": 0}
# Summary Statistics
stats = {
"repos": 0, "images": 0, "no_lifecycle": 0,
"mutable": 0, "immutable": 0,
"push_ages": Counter(), "pull_ages": Counter(),
"total_push_days": 0, "push_day_count": 0
}

for idx, account in enumerate(data, 1):
checks = account.get("data", {})
for key, val in checks.items():
if key == "account_summary" or ":" not in key: continue

region = key.split(":")[0]
stats["total_repos"] += 1
img_count = len(val.get("images", []))
stats["total_images"] += img_count
stats["repos"] += 1

# Mutability Tracking
mut = val.get("mutability", "MUTABLE")
if mut == "IMMUTABLE": stats["immutable"] += 1
else: stats["mutable"] += 1

lifecycle_status = "YES" if val.get("has_lifecycle") == "True" else "NO"
if lifecycle_status == "NO": stats["no_lifecycle"] += 1
# Lifecycle Tracking
has_lc = val.get("has_lifecycle") == "True"
if not has_lc: stats["no_lifecycle"] += 1

print(f"{idx:<5} | {account['account_id']:<15} | {region:<12} | {val['repo_name']:<40} | {val['mutability']:<12} | {lifecycle_status:<10} | {img_count}")
# Image Age Tracking
images = val.get("images", [])
stats["images"] += len(images)

for img in images:
# Push Age
p_days = get_days_ago(img.get("pushed_at"))
if p_days is not None:
bucket_age(p_days, stats["push_ages"])
stats["total_push_days"] += p_days
stats["push_day_count"] += 1

# Pull Age
l_days = get_days_ago(img.get("last_pulled_at"))
bucket_age(l_days, stats["pull_ages"])

print(f"{idx:<5} | {account['account_id']:<15} | {region:<12} | {val['repo_name']:<45} | {mut:<12} | {'YES' if has_lc else 'NO':<10} | {len(images)}")

# Footer Logic
avg_push_age = stats["total_push_days"] / stats["push_day_count"] if stats["push_day_count"] > 0 else 0

print("-" * report_width)
print(f"ORGANIZATION ECR FOOTPRINT SUMMARY")
print(f" Total Repositories: {stats['total_repos']}")
print(f" Total Images: {stats['total_images']}")
print(f" Repos w/o Lifecycle Policies: {stats['no_lifecycle']} (RISK)")
print(f" --- Repository Config ---")
print(f" Total Repositories: {stats['repos']} | Immutable: {stats['immutable']} | Mutable: {stats['mutable']}")
print(f" Repos Missing Lifecycle Policies: {stats['no_lifecycle']} (Action Required)")

print(f"\n --- Image Aging & Usage ---")
print(f" Total Images: {stats['images']} | Average Age (Since Push): {avg_push_age:.1f} days")
print(f" Images Older than 1 Year: {stats['push_ages']['365+']}")

print(f"\n Age Distribution (Days Since Action):")
print(f" Bucket | Pushed Count | Last Pull Count")
print(f" ----------- | ------------ | ---------------")
for b in ['<30', '30-59', '60-89', '90-179', '180-364', '365+']:
print(f" {b:<11} | {stats['push_ages'][b]:<12} | {stats['pull_ages'][b]}")
print("-" * report_width)

if __name__ == "__main__": main()
if __name__ == "__main__":
main()

0 comments on commit 665cdd1

Please sign in to comment.