From b0a97608d88b05c83f4a7c1ccb0bd0d218da7ae4 Mon Sep 17 00:00:00 2001 From: badra001 Date: Fri, 9 Jan 2026 15:14:07 -0500 Subject: [PATCH] add cve checking --- .../cross-organization/assess_check_ecr.py | 102 +++++++++--------- .../cross-organization/check_ecr.py | 22 +++- 2 files changed, 66 insertions(+), 58 deletions(-) diff --git a/local-app/python-tools/cross-organization/assess_check_ecr.py b/local-app/python-tools/cross-organization/assess_check_ecr.py index e16670f4..f52a407e 100755 --- a/local-app/python-tools/cross-organization/assess_check_ecr.py +++ b/local-app/python-tools/cross-organization/assess_check_ecr.py @@ -1,11 +1,10 @@ #!/usr/bin/env python - import json, argparse, sys, os, glob from datetime import datetime, timezone from collections import Counter, defaultdict # --- VERSIONING --- -__version__ = "1.1.1" +__version__ = "1.3.1" def find_latest_file(pattern): files = glob.glob(pattern) @@ -16,8 +15,7 @@ def get_days_ago(iso_str): try: dt = datetime.fromisoformat(iso_str) if dt.tzinfo is None: dt = dt.replace(tzinfo=timezone.utc) - now = datetime.now(timezone.utc) - return (now - dt).days + return (datetime.now(timezone.utc) - dt).days except: return None def bucket_age(days, counters): @@ -30,7 +28,7 @@ def bucket_age(days, counters): else: counters['<30'] += 1 def main(): - parser = argparse.ArgumentParser(description="AWS ECR Audit Assessor - Image Statistics Edition") + parser = argparse.ArgumentParser(description="ECR Full Spectrum Assessor - v1.3.1") parser.add_argument("--input", help="JSON audit file") args = parser.parse_args() @@ -39,85 +37,83 @@ def main(): with open(input_file, 'r') as f: data = json.load(f) - report_width = 230 + report_width = 240 print("-" * report_width) - print(f"ECR IMAGE STATISTICS ASSESSMENT | Total Accounts: {len(data)}") + print(f"ECR COMPREHENSIVE AUDIT | Accounts: {len(data)} | Input: {os.path.basename(input_file)}") print("-" * report_width) - print(f"{'Idx':<5} | {'Account ID':<15} | {'Region':<12} | {'Repo Name':<45} | {'Img Count':<10} | {'Mutability':<12} | {'Lifecycle'}") + print(f"{'Idx':<4} | {'Account ID':<15} | {'Region':<12} | {'Repo Name':<40} | {'Size (GB)':<10} | {'Mutability':<11} | {'L/Cycle':<7} | {'CRITICAL':<8} | {'HIGH':<8}") print("-" * report_width) - # Core Stats stats = { - "repos": 0, "total_images": 0, "total_bytes": 0, + "total_repos": 0, "total_images": 0, "total_bytes": 0, "no_lc": 0, + "mut": {"IMMUTABLE": 0, "MUTABLE": 0}, "region_bytes": defaultdict(int), "push_ages": Counter(), "pull_ages": Counter(), - "total_push_days": 0, "push_day_count": 0, - "mutable": 0, "immutable": 0, "no_lifecycle": 0 + "total_push_days": 0, "push_count": 0, + "org_vulns": Counter(), "scanned_imgs": 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["repos"] += 1 + stats["total_repos"] += 1 + + repo_mut = val.get("mutability", "MUTABLE") + stats["mut"][repo_mut] += 1 + if val.get("has_lifecycle") == "False": stats["no_lc"] += 1 - # Mutability & Lifecycle - mut = val.get("mutability", "MUTABLE") - if mut == "IMMUTABLE": stats["immutable"] += 1 - else: stats["mutable"] += 1 - if val.get("has_lifecycle") == "False": stats["no_lifecycle"] += 1 + repo_size = val.get("repo_size_bytes", 0) + stats["total_bytes"] += repo_size + stats["region_bytes"][region] += repo_size - # Process Images + repo_vulns = Counter() images = val.get("images", []) stats["total_images"] += len(images) for img in images: - # Size calculation - img_size = img.get("size_bytes", 0) - stats["total_bytes"] += img_size - stats["region_bytes"][region] += img_size - - # Age calculation + # Security + # FIX: Ensure img is a dictionary before calling .get() + # If img is a single-element list containing a dict, use img[0] + if isinstance(img, list) and len(img) > 0: + img = img[0] + + counts = img.get("severity_counts", {}) + if counts: + stats["scanned_imgs"] += 1 + for sev, count in counts.items(): + repo_vulns[sev] += count + stats["org_vulns"][sev] += count + # Aging 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 - - l_days = get_days_ago(img.get("last_pulled_at")) - bucket_age(l_days, stats["pull_ages"]) + stats["push_count"] += 1 + bucket_age(get_days_ago(img.get("last_pulled_at")), stats["pull_ages"]) - print(f"{idx:<5} | {account['account_id']:<15} | {region:<12} | {val['repo_name']:<45} | {len(images):<10} | {mut:<12} | {'YES' if val.get('has_lifecycle')=='True' else 'NO'}") + print(f"{idx:<4} | {account['account_id']:<15} | {region:<12} | {val['repo_name']:<40} | {repo_size/(1024**3):<10.2f} | {repo_mut:<11} | {'YES' if val.get('has_lifecycle')=='True' else 'NO':<7} | {repo_vulns['CRITICAL']:<8} | {repo_vulns['HIGH']:<8}") - # Aggregated Results - total_gb = stats["total_bytes"] / (1024**3) + # Footers avg_img_mb = (stats["total_bytes"] / stats["total_images"]) / (1024**2) if stats["total_images"] > 0 else 0 - avg_push_age = stats["total_push_days"] / stats["push_day_count"] if stats["push_day_count"] > 0 else 0 + avg_push_age = stats["total_push_days"] / stats["push_count"] if stats["push_count"] > 0 else 0 print("-" * report_width) - print(f"ORGANIZATION IMAGE FOOTPRINT SUMMARY") - print(f" --- Image Storage Statistics ---") - print(f" Total Images Found: {stats['total_images']:,}") - print(f" Total Image Storage: {total_gb:.2f} GB") - print(f" Average Image Size: {avg_img_mb:.2f} MB") - - print(f"\n --- Aging & Lifecycle ---") - print(f" Average Image Age: {avg_push_age:.1f} days") - print(f" Images Older > 1yr: {stats['push_ages']['365+']} pushed | {stats['pull_ages']['365+']} pulled") - print(f" Repos w/o Lifecycle: {stats['no_lifecycle']} (Critical Gap)") + print(f"ORGANIZATION ECR SUMMARY\n") + print(f" --- Config & Storage ---") + print(f" Repos: {stats['total_repos']} ({stats['mut']['IMMUTABLE']} Immutable / {stats['mut']['MUTABLE']} Mutable) | Missing Lifecycle: {stats['no_lc']}") + print(f" Total Data: {stats['total_bytes']/(1024**3):.2f} GB | Average Image Size: {avg_img_mb:.2f} MB\n") - print(f"\n --- Regional Image Storage Breakdown ---") - for reg, r_bytes in sorted(stats["region_bytes"].items(), key=lambda x: x[1], reverse=True): - print(f" - {reg:<15}: {r_bytes/(1024**3):>8.2f} GB") + print(f" --- Security & Vulnerabilities ---") + print(f" Scan Coverage: {stats['scanned_imgs']} of {stats['total_images']} images scanned") + print(f" Org Totals: " + " | ".join([f"{s}: {stats['org_vulns'][s]:,}" for s in ["CRITICAL", "HIGH", "MEDIUM", "LOW"]])) - print(f"\n Age Distribution (Days Since Action):") - print(f" Bucket | Pushed Count | Last Pull Count") - print(f" ----------- | ------------ | ---------------") + print(f"\n --- Age Distribution (Days since Push vs Pull) ---") + print(f" Average Image Age: {avg_push_age:.1f} days") + print(f" Bucket | Pushed Count | Pulled Count") 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(f" {b:<11} | {stats['push_ages'][b]:<12} | {stats['pull_ages'][b]}") print("-" * report_width) -if __name__ == "__main__": - main() +if __name__ == "__main__": main() diff --git a/local-app/python-tools/cross-organization/check_ecr.py b/local-app/python-tools/cross-organization/check_ecr.py index 2f8f6100..d5764455 100644 --- a/local-app/python-tools/cross-organization/check_ecr.py +++ b/local-app/python-tools/cross-organization/check_ecr.py @@ -4,24 +4,36 @@ from datetime import datetime # --- VERSIONING --- -__version__ = "1.0.0" +__version__ = "1.2.0" def get_repo_images(ecr_client, repo_name): - """Fetches details for all images in a repository.""" + """Fetches images and their scan results.""" images = [] + repo_total_size = 0 try: + # describe_images provides the findingSeverityCounts summary directly paginator = ecr_client.get_paginator('describe_images') for page in paginator.paginate(repositoryName=repo_name): for img in page['imageDetails']: + size = img.get('imageSizeInBytes', 0) + repo_total_size += size + + # Extract scan summary if available + scan_summary = img.get('imageScanFindingsSummary', {}) + severity_counts = scan_summary.get('findingSeverityCounts', {}) + images.append({ "image_tags": img.get('imageTags', []), + "image_digest": img.get('imageDigest'), "pushed_at": img['imagePushedAt'].isoformat() if 'imagePushedAt' in img else "N/A", "last_pulled_at": img['lastRecordedPullTime'].isoformat() if 'lastRecordedPullTime' in img else "N/A", - "status": img.get('imageStatus', 'ACTIVE'), - "size_bytes": img.get('imageSizeInBytes', 0) + "scan_status": img.get('imageScanStatus', {}).get('status', 'NO_SCAN'), + "severity_counts": severity_counts, # Restored: CVE Severity Counts + "size_bytes": size }) except: pass - return images + return images, repo_total_size + def get_lifecycle_policy(ecr_client, repo_name): """Checks for lifecycle policy and counts rules."""