-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
1 changed file
with
85 additions
and
18 deletions.
There are no files selected for viewing
103 changes: 85 additions & 18 deletions
103
local-app/python-tools/cross-organization/assess_check_ecr.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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() |