<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[vishnu sai]]></title><description><![CDATA[vishnu sai]]></description><link>https://vishnusai.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Mon, 26 Oct 2026 03:33:07 GMT</lastBuildDate><atom:link href="https://vishnusai.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[       AWS HOST AUDIT & CLEANUP ]]></title><description><![CDATA[DevOps • Linux • Docker • AWS IAM Security]]></description><link>https://vishnusai.hashnode.dev/aws-host-audit-cleanup</link><guid isPermaLink="true">https://vishnusai.hashnode.dev/aws-host-audit-cleanup</guid><dc:creator><![CDATA[vishnu sai]]></dc:creator><pubDate>Sun, 18 Oct 2026 10:13:53 GMT</pubDate><content:encoded><![CDATA[<h3><strong>PROBLEM</strong></h3>
<p>Managing an AWS EC2 server means monitoring system resources, Docker containers, disk usage, running processes, and IAM security. Without a proper audit, unused resources can consume storage, and outdated access keys or missing MFA can create security risks.</p>
<h3><strong>HOW I DEBUGGED IT</strong></h3>
<p>● I built a Python-based audit tool to investigate the server step by step.</p>
<p>●  Checked CPU, RAM, and disk usage. Reviewed Docker containers, images, networks, and volumes.</p>
<p>●   Identified large files and reviewed long-running or idle processes.Audited IAM users, MFA status, access keys, and administrative permissions</p>
<p><strong>THE SOLUTION    EC2 → Audit Checks → Safe Cleanup → Reports</strong></p>
<p>I built a Python audit tool with Dry-Run mode, audit logs, and JSON reports to identify issues and quickly review cleanup actions.</p>
<p><strong>Has anyone else faced this problem?</strong></p>
<p><strong>How did you fix it?</strong></p>
<p><strong>HashnodProfile:</strong><a href="https://hashnode.com/@vishnusaiK">https://hashnode.com/@vishnusaiK</a><br /><strong>GitHub :</strong> <a href="https://github.com/ksaivishnusaikvs/DevOps-Engineering/tree/main/Linkedin/Post">https://github.com/ksaivishnusaikvs/DevOps-Engineering/tree/main/Linkedin/Post</a></p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/e31421a7-e9cb-4ebf-a7ce-8ba70afacd00.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/b7613b2d-4982-4853-b486-5d5437d931a4.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/cc4e9091-d154-46b5-884e-5bed3245c908.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Step1:</strong> System Monitoring: Displays server details, hostname, IP address, and logged-in user. Monitors CPU, RAM, and disk usage while running in Dry-Run mode without making changes.</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/5a4df77e-b06b-41e3-a400-7ce33c3917ae.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Step2:</strong> The system resource screen provides a live view of CPU, memory utilization, and disk usage across mounted partitions. This helps identify servers or resources that are consuming high CPU, RAM, or disk capacity.</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/a515b8e9-dd63-4519-8d77-8b5625900416.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Step3:</strong> The document clearly shows Docker containers, images, networks, and volumes. However, the production cleanup is highly destructive because it can remove all containers, images, networks, and volumes. AWSMONITORING AUDIT(2)Add a clear warning before the production screenshot: “Production cleanup should be performed only after validating that the resources are unused.” sime and sho</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/4ff2b0a3-7650-455c-8295-563eec641512.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/30d4e113-847f-4303-b5f8-f58cc0f7addf.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Step4:</strong> Good coverage of partitions, total/used/free space, and large files. The /var, /tmp, and /home analysis is useful for identifying storage-consuming files. AWS MONITORING AUDIT(2): Add the actual finding/result below the screenshot instead of only showing the command output.</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/8268b4a1-02c6-40c5-bd09-771fef131c29.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Step5: T</strong>he script checks idle and long-running processes and has a protected-process list. AWS MONITORING AUDIT(2): This is good from a safety perspective, but the document should explain why a process was identified as a cleanup candidate before showing termination.</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/8e9a5c92-b9ec-41bf-9f09-77c977cfc6d3.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/e8400dca-a8d1-4a1e-bf4d-76979359153f.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Step6:</strong> This is one of the strongest sections. It checks <strong>MFA, inactive users, access-key age, admin access, policies and groups</strong>, and generates an IAM report. AWS MONITORING AUDIT(2): The recommendations such as MFA enablement, key rotation, and admin-access review are clearly presented.</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/be7103e7-ecec-42c0-86db-f7623ea5c6c9.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Step7:</strong> IAM Security &amp; Audit Logs: The IAM audit reviews users, MFA, access keys, and flagged accounts, while recording audit activities.The summary provides the security status and generates a detailed JSON audit report.</p>
<pre><code class="language-python">======================================================================
ubuntu@ip-172-31-44-197:~$ cat awsaudit.py 
#!/usr/bin/env python3
"""
╔══════════════════════════════════════════════════════════════════╗
║        ENTERPRISE PRODUCTION CLEANUP SCRIPT  v4.0               ║
║  Docker · Processes · Disk · IAM User Tracker                   ║
║  Dry-Run + Production · Audit Trail · NDJSON Logs               ║
╚══════════════════════════════════════════════════════════════════╝
"""

import os
import sys
import json
import signal
import socket
import datetime
import platform
import argparse
import subprocess
import textwrap
from pathlib import Path
from typing import List, Dict, Tuple, Optional

# ── Auto-install dependencies ──────────────────────────────────────
def _pip(pkg):
    subprocess.check_call([sys.executable, "-m", "pip", "install", pkg, "--quiet",
                           "--break-system-packages"], stderr=subprocess.DEVNULL)

try:
    from rich.console import Console
    from rich.table import Table
    from rich.panel import Panel
    from rich.prompt import Prompt, Confirm
    from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn, TimeElapsedColumn
    from rich import box
    from rich.rule import Rule
except ImportError:
    _pip("rich")
    from rich.console import Console
    from rich.table import Table
    from rich.panel import Panel
    from rich.prompt import Prompt, Confirm
    from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn, TimeElapsedColumn
    from rich import box
    from rich.rule import Rule

try:
    import psutil
except ImportError:
    _pip("psutil"); import psutil

try:
    import boto3
    from botocore.exceptions import ClientError, NoCredentialsError
except ImportError:
    _pip("boto3")
    import boto3
    from botocore.exceptions import ClientError, NoCredentialsError

# ══════════════════════════════════════════════════════════════════
# GLOBALS
# ══════════════════════════════════════════════════════════════════
console      = Console()
SCRIPT_START = datetime.datetime.now(datetime.timezone.utc)
LOG_DIR      = Path.home() / "logs" / "prod_cleanup"
RUN_TAG      = SCRIPT_START.strftime("%Y%m%dT%H%M%SZ")
LOG_DIR.mkdir(parents=True, exist_ok=True)

RUN_LOG       = LOG_DIR / f"cleanup_{RUN_TAG}.ndjson"
AUDIT_LOG     = LOG_DIR / "audit_master.ndjson"
DELETIONS_LOG = LOG_DIR / f"deletions_{RUN_TAG}.json"
UNPROC_LOG    = LOG_DIR / f"unprocessed_{RUN_TAG}.json"
IAM_REPORT    = LOG_DIR / f"iam_report_{RUN_TAG}.json"

DELETIONS  : List[dict] = []
UNPROCESSED: List[dict] = []
STATS: Dict = {
    "docker_containers_killed": 0,
    "docker_images_removed":    0,
    "docker_networks_removed":  0,
    "docker_volumes_removed":   0,
    "processes_killed":         0,
    "iam_users_scanned":        0,
    "iam_keys_flagged":         0,
    "iam_inactive_users":       0,
    "iam_no_mfa":               0,
}

PROTECTED_PROCS = {
    "init","systemd","kthreadd","kworker","ksoftirqd","migration","rcu_sched","rcu_bh",
    "watchdog","sshd","bash","python3","python","journald","dbus-daemon","rsyslogd",
    "chronyd","cron","atd","auditd","containerd","dockerd","docker","kubelet","etcd",
    "aws","amazon-ssm-agent","amazon-cloudwatch-agent","prod.py","prod_cleanup.py","cl.py",
}

IDLE_THRESHOLD_HOURS = 2
LONG_RUNNING_HOURS   = 24
KEY_WARN_DAYS        = 90
KEY_CRIT_DAYS        = 180
INACTIVE_DAYS        = 90

# ══════════════════════════════════════════════════════════════════
# LOGGING HELPERS
# ══════════════════════════════════════════════════════════════════
def _now_iso():
    return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")

def log_event(event_type, payload):
    entry = {"ts": _now_iso(), "type": event_type, **payload}
    for f in (RUN_LOG, AUDIT_LOG):
        with open(f, "a") as fp:
            fp.write(json.dumps(entry) + "\n")

def record_deletion(kind, ident, details=None):
    rec = {"ts": _now_iso(), "kind": kind, "id": ident, "details": details or {}}
    DELETIONS.append(rec); log_event("DELETE", rec)

def record_unprocessed(kind, ident, reason):
    rec = {"ts": _now_iso(), "kind": kind, "id": ident, "reason": reason}
    UNPROCESSED.append(rec); log_event("SKIP", rec)

def flush_json_logs():
    with open(DELETIONS_LOG, "w") as f: json.dump(DELETIONS, f, indent=2)
    with open(UNPROC_LOG,    "w") as f: json.dump(UNPROCESSED, f, indent=2)

# ══════════════════════════════════════════════════════════════════
# UTILS
# ══════════════════════════════════════════════════════════════════
def run(cmd, capture=True):
    r = subprocess.run(cmd, shell=True, capture_output=capture, text=True)
    return r.returncode, r.stdout.strip(), r.stderr.strip()

def docker_available():
    return run("docker info")[0] == 0

def bytes_to_mb(b):
    return round(b / 1_048_576, 2)

def bar(pct, width=30):
    f = int(pct / 100 * width)
    return "[green]" + chr(9608) * f + "[/green]" + chr(9617) * (width - f)

def days_ago(dt):
    if dt is None: return None
    if dt.tzinfo is None: dt = dt.replace(tzinfo=datetime.timezone.utc)
    return (datetime.datetime.now(datetime.timezone.utc) - dt).days

# ══════════════════════════════════════════════════════════════════
# SECTION 1 — RUNNER IDENTITY
# ══════════════════════════════════════════════════════════════════
def show_identity():
    hostname = socket.gethostname()
    uname    = platform.uname()
    try:    local_ip = socket.gethostbyname(hostname)
    except: local_ip = "unknown"
    rc, pub_ip, _ = run("curl -s --max-time 5 https://api.ipify.org")
    pub_ip = pub_ip if rc == 0 else "unavailable"
    try:
        import pwd
        pw = pwd.getpwuid(os.getuid())
        username, home, shell = pw.pw_name, pw.pw_dir, pw.pw_shell
    except:
        username = os.environ.get("USER","unknown")
        home, shell = os.path.expanduser("~"), "unknown"

    lines = [
        f"[bold cyan]Hostname:[/bold cyan]   {hostname}",
        f"[bold cyan]OS/Kernel:[/bold cyan]  {uname.system} {uname.release} ({uname.machine})",
        f"[bold cyan]Started:[/bold cyan]    {SCRIPT_START.strftime('%Y-%m-%dT%H:%M:%SZ')}",
        "",
        "[bold yellow]-- Network ----------------------------------------[/bold yellow]",
        f"[bold cyan]Local IP:[/bold cyan]   {local_ip}",
        f"[bold cyan]Public IP:[/bold cyan]  {pub_ip}",
        "",
        "[bold yellow]-- Linux User -------------------------------------[/bold yellow]",
        f"[bold cyan]Username:[/bold cyan]   {username} (uid={os.getuid()}, euid={os.geteuid()})",
        f"[bold cyan]Root?:[/bold cyan]      {'[bold red]Yes[/bold red]' if os.geteuid()==0 else 'No'}",
        f"[bold cyan]Home:[/bold cyan]       {home} | {shell}",
        "",
        "[bold yellow]-- Log Folder -------------------------------------[/bold yellow]",
        f"[bold cyan]Log Dir:[/bold cyan]    {LOG_DIR}",
        f"[bold cyan]IAM Report:[/bold cyan] {IAM_REPORT.name}",
    ]
    console.print(Panel("\n".join(lines), title="[bold]RUNNER IDENTITY[/bold]", border_style="blue"))
    log_event("IDENTITY", {"hostname": hostname, "public_ip": pub_ip, "user": username})

# ══════════════════════════════════════════════════════════════════
# SECTION 2 — RESOURCE USAGE
# ══════════════════════════════════════════════════════════════════
def show_resource_usage():
    cpu = psutil.cpu_percent(interval=1)
    mem = psutil.virtual_memory()
    lines = [
        f"[bold]CPU[/bold]  {bar(cpu)}  {cpu:.1f}%\n",
        f"[bold]RAM[/bold]  {bar(mem.percent)}  {mem.percent:.1f}%  "
        f"{bytes_to_mb(mem.used):.0f} MB / {bytes_to_mb(mem.total):.0f} MB\n",
        "[bold]Disk[/bold]",
    ]
    for p in psutil.disk_partitions():
        try:
            u = psutil.disk_usage(p.mountpoint)
            lines.append(f"  {p.mountpoint:&lt;20} {bar(u.percent, 20)}  "
                         f"{u.percent:.0f}%  {u.used/1e9:.1f}G / {u.total/1e9:.1f}G")
        except PermissionError:
            pass
    console.print(Panel("\n".join(lines), title="[bold]RESOURCE USAGE[/bold]", border_style="cyan"))

# ══════════════════════════════════════════════════════════════════
# SECTION 3 — DOCKER CLEANUP
# ══════════════════════════════════════════════════════════════════
DOCKER_ONE_LINER = (
    "docker ps -aq | xargs -r docker stop &amp;&amp; "
    "docker ps -aq | xargs -r docker rm -f &amp;&amp; "
    "docker images -q | xargs -r docker rmi -f &amp;&amp; "
    "docker network ls --filter type=custom -q | xargs -r docker network rm &amp;&amp; "
    "docker volume ls -q | xargs -r docker volume rm &amp;&amp; "
    "docker system prune -af --volumes"
)

def docker_cleanup(dry_run, interactive):
    if not docker_available():
        console.print("[yellow]  Docker daemon not reachable -- skipping.[/yellow]"); return
    console.print(Rule("[bold red]DOCKER FULL CLEANUP[/bold red]"))

    _, cr, _ = run("docker ps -a --format '{{.ID}}\t{{.Names}}\t{{.Status}}\t{{.Image}}'")
    _, ir, _ = run("docker images --format '{{.ID}}\t{{.Repository}}\t{{.Tag}}\t{{.Size}}'")
    _, nr, _ = run("docker network ls --filter type=custom --format '{{.ID}}\t{{.Name}}\t{{.Driver}}'")
    _, vr, _ = run("docker volume ls --format '{{.Name}}\t{{.Driver}}'")

    containers = [l.split("\t") for l in cr.splitlines() if l]
    images     = [l.split("\t") for l in ir.splitlines() if l]
    networks   = [l.split("\t") for l in nr.splitlines() if l]
    volumes    = [l.split("\t") for l in vr.splitlines() if l]

    def make_table(title, headers, rows, style="cyan"):
        t = Table(title=title, box=box.ROUNDED, border_style=style, show_lines=True)
        for h in headers: t.add_column(h, style="white")
        for r in rows:    t.add_row(*r[:len(headers)])
        return t

    console.print(make_table("Containers", ["ID","Name","Status","Image"], containers)
                  if containers else "[green]  No containers.[/green]")
    console.print(make_table("Images",    ["ID","Repository","Tag","Size"], images, "yellow")
                  if images     else "[green]  No images.[/green]")
    console.print(make_table("Networks",  ["ID","Name","Driver"], networks, "magenta")
                  if networks   else "[green]  No custom networks.[/green]")
    if volumes:
        vt = Table(title="Volumes", box=box.ROUNDED, border_style="red")
        vt.add_column("Name"); vt.add_column("Driver")
        for v in volumes: vt.add_row(*v[:2])
        console.print(vt)
    else:
        console.print("[green]  No volumes.[/green]")

    console.print(Panel(
        f"[bold yellow]{DOCKER_ONE_LINER}{'  [DRY-RUN]' if dry_run else ''}[/bold yellow]",
        title="DOCKER NUKE ONE-LINER", border_style="red"))

    if not (containers or images or networks or volumes):
        console.print("[green]  Docker already clean.[/green]"); return

    if dry_run:
        console.print("[bold yellow]  [DRY-RUN] No changes made.[/bold yellow]")
        for c in containers: record_unprocessed("docker_container", c[0], "dry-run")
        for i in images:     record_unprocessed("docker_image",     i[0], "dry-run")
        return

    if interactive and not Confirm.ask(
        f"[bold red]Remove {len(containers)} containers, {len(images)} images, "
        f"{len(networks)} networks, {len(volumes)} volumes?[/bold red]", default=False):
        console.print("[yellow]  Skipped.[/yellow]"); return

    steps = [
        ("Stop all containers",    "docker ps -aq | xargs -r docker stop"),
        ("Remove all containers",  "docker ps -aq | xargs -r docker rm -f"),
        ("Remove ALL images",      "docker images -q | xargs -r docker rmi -f"),
        ("Remove custom networks", "docker network ls --filter type=custom -q | xargs -r docker network rm"),
        ("Remove all volumes",     "docker volume ls -q | xargs -r docker volume rm"),
        ("System prune -af",       "docker system prune -af --volumes"),
    ]
    with Progress(SpinnerColumn(), TextColumn("[progress.description]{task.description}"),
                  BarColumn(), TimeElapsedColumn(), console=console) as prog:
        task = prog.add_task("Docker cleanup...", total=len(steps))
        for label, cmd in steps:
            prog.update(task, description=f"[cyan]{label}[/cyan]")
            rc, out, err = run(cmd)
            log_event("DOCKER_STEP", {"cmd": cmd, "rc": rc})
            prog.advance(task)

    console.print("[bold green]  Docker cleanup complete.[/bold green]")
    for c in containers:
        record_deletion("docker_container", c[0], {"name": c[1] if len(c)&gt;1 else ""})
        STATS["docker_containers_killed"] += 1
    for i in images:
        record_deletion("docker_image", i[0]); STATS["docker_images_removed"] += 1
    for n in networks:
        record_deletion("docker_network", n[0]); STATS["docker_networks_removed"] += 1
    for v in volumes:
        record_deletion("docker_volume", v[0]); STATS["docker_volumes_removed"] += 1

# ══════════════════════════════════════════════════════════════════
# SECTION 4 — PROCESS CLEANUP
# ══════════════════════════════════════════════════════════════════
def _is_protected(name):
    nl = name.lower()
    return any(p in nl for p in PROTECTED_PROCS)

def _proc_age_h(proc):
    try: return (datetime.datetime.now().timestamp() - proc.create_time()) / 3600
    except: return 0.0

def _idle_h(proc):
    try:
        ct = proc.cpu_times()
        age = _proc_age_h(proc)
        return age if age &gt; 0 and (ct.user + ct.system) &lt; 0.5 else 0.0
    except: return 0.0

def collect_suspicious_processes():
    idle, long_r = [], []
    for proc in psutil.process_iter(["pid","name","username","status",
                                      "cpu_percent","memory_info","create_time","cmdline"]):
        try:
            info = proc.info
            name = info["name"] or ""
            if _is_protected(name) or info["pid"] in (0,1): continue
            if info["username"] in ("root", None) and name.startswith("k"): continue
            age_h = round(_proc_age_h(proc), 1)
            ih    = round(_idle_h(proc), 1)
            mem   = bytes_to_mb(info["memory_info"].rss) if info["memory_info"] else 0
            cmd   = " ".join(info["cmdline"] or [])[:120]
            entry = {"pid": info["pid"], "name": name, "user": info["username"] or "?",
                     "status": info["status"] or "?", "age_h": age_h,
                     "idle_h": ih, "mem_mb": mem, "cmd": cmd}
            if ih    &gt; IDLE_THRESHOLD_HOURS: idle.append(entry)
            if age_h &gt; LONG_RUNNING_HOURS:   long_r.append(entry)
        except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): pass
    idle.sort(key=lambda x: x["idle_h"], reverse=True)
    long_r.sort(key=lambda x: x["age_h"], reverse=True)
    return idle, long_r

def show_process_table(title, procs, color="cyan"):
    if not procs:
        console.print(f"[green]  None found for: {title}[/green]"); return
    t = Table(title=title, box=box.ROUNDED, border_style=color, show_lines=True)
    for col, st, w in [("PID","bold white",8),("Name","cyan",18),("User","yellow",12),
                        ("Status","white",10),("Age(h)","magenta",8),("Idle(h)","red",8),
                        ("MemMB","green",8),("Command","dim",38)]:
        t.add_column(col, style=st, width=w)
    for p in procs[:50]:
        t.add_row(str(p["pid"]),p["name"][:18],p["user"][:12],p["status"],
                  str(p["age_h"]),str(p["idle_h"]),str(p["mem_mb"]),p["cmd"][:38])
    console.print(t)

def kill_processes(procs, dry_run, interactive, label):
    if not procs: return
    if interactive and not dry_run:
        console.print(f"\n[bold yellow]Found {len(procs)} {label} processes.[/bold yellow]")
        pids_input = Prompt.ask("PIDs to kill (comma-sep / all / none)", default="none")
        if pids_input.strip().lower() == "none":
            console.print("[dim]  Skipped.[/dim]"); return
        elif pids_input.strip().lower() == "all":
            targets = procs
        else:
            try:
                chosen  = {int(x.strip()) for x in pids_input.split(",")}
                targets = [p for p in procs if p["pid"] in chosen]
            except ValueError:
                console.print("[red]  Invalid -- skipping.[/red]"); return
    else:
        targets = procs

    killed = 0
    for p in targets:
        if dry_run:
            console.print(f"  [DRY-RUN] Would kill PID {p['pid']} ({p['name']})")
            record_unprocessed("process", str(p["pid"]), "dry-run"); continue
        try:
            os.kill(p["pid"], signal.SIGTERM)
            record_deletion("process", str(p["pid"]), {"name": p["name"]})
            STATS["processes_killed"] += 1; killed += 1
            console.print(f"  [green]  SIGTERM -&gt; PID {p['pid']} ({p['name']})[/green]")
        except (ProcessLookupError, PermissionError) as e:
            console.print(f"  [red]  PID {p['pid']} -- {e}[/red]")
            record_unprocessed("process", str(p["pid"]), str(e))
    if not dry_run:
        console.print(f"[bold green]  Killed {killed}/{len(targets)}.[/bold green]")

def process_cleanup(dry_run, interactive):
    console.print(Rule("[bold yellow]PROCESS CLEANUP[/bold yellow]"))
    with console.status("[cyan]Scanning processes...[/cyan]"):
        idle_procs, long_procs = collect_suspicious_processes()
    console.print(f"\n[bold]Idle (&gt;{IDLE_THRESHOLD_HOURS}h zero CPU):[/bold]")
    show_process_table("IDLE PROCESSES", idle_procs, "yellow")
    console.print(f"\n[bold]Long-running (&gt;{LONG_RUNNING_HOURS}h alive):[/bold]")
    show_process_table("LONG-RUNNING PROCESSES", long_procs, "red")
    kill_processes(idle_procs,  dry_run, interactive, "idle")
    kill_processes(long_procs,  dry_run, interactive, "long-running")

# ══════════════════════════════════════════════════════════════════
# SECTION 5 — DISK ANALYSIS
# ══════════════════════════════════════════════════════════════════
def show_disk_analysis():
    console.print(Rule("[bold blue]DISK &amp; EBS ANALYSIS[/bold blue]"))
    t = Table(box=box.ROUNDED, border_style="blue", title="Disk Partitions", show_lines=True)
    for col in ("Mount","Device","FSType","Total","Used","Free","Use%"):
        t.add_column(col, style="red" if col=="Use%" else "white")
    for p in psutil.disk_partitions(all=False):
        try:
            u = psutil.disk_usage(p.mountpoint)
            t.add_row(p.mountpoint, p.device, p.fstype,
                      f"{u.total/1e9:.1f}G", f"{u.used/1e9:.1f}G",
                      f"{u.free/1e9:.1f}G", f"{u.percent:.0f}%")
        except PermissionError: pass
    console.print(t)
    rc, out, _ = run("du -ahx --max-depth=4 /var /tmp /home 2&gt;/dev/null | sort -rh | head -20")
    if out:
        lf = Table(box=box.SIMPLE, show_header=False)
        lf.add_column("size"); lf.add_column("path")
        for line in out.splitlines():
            parts = line.split(None, 1)
            if len(parts) == 2: lf.add_row(f"[yellow]{parts[0]}[/yellow]", parts[1])
        console.print(lf)

# ══════════════════════════════════════════════════════════════════
# SECTION 6 — IAM USER TRACKER
# ══════════════════════════════════════════════════════════════════
def _iam_client():
    try:
        c = boto3.client("iam")
        c.get_account_summary()
        return c
    except Exception:
        return None

def iam_tracker(dry_run):
    console.print(Rule("[bold magenta]IAM USER TRACKER[/bold magenta]"))

    iam = _iam_client()
    if iam is None:
        console.print(Panel(
            "[yellow]No AWS credentials found.\n\n"
            "To enable IAM tracking:\n"
            "  export AWS_ACCESS_KEY_ID=...\n"
            "  export AWS_SECRET_ACCESS_KEY=...\n"
            "  export AWS_DEFAULT_REGION=ap-south-1\n\n"
            "Or attach an IAM role to this EC2 instance.[/yellow]",
            title="AWS Credentials Missing", border_style="yellow")); return

    # Generate + fetch credential report
    with console.status("[magenta]Requesting IAM credential report...[/magenta]"):
        import time
        for _ in range(30):
            resp = iam.generate_credential_report()
            if resp["State"] == "COMPLETE": break
            time.sleep(1)

    csv_text = iam.get_credential_report()["Content"].decode("utf-8")
    rows     = [r.split(",") for r in csv_text.strip().splitlines()]
    headers  = rows[0]
    users_csv= rows[1:]

    def col(row, name):
        try:    return row[headers.index(name)]
        except: return ""

    # Fetch all users, policies, groups
    with console.status("[magenta]Listing users, policies, groups...[/magenta]"):
        all_users = []
        for page in iam.get_paginator("list_users").paginate():
            all_users.extend(page["Users"])

        policy_map = {}
        group_map  = {}
        for u in all_users:
            un = u["UserName"]
            attached = iam.list_attached_user_policies(UserName=un)["AttachedPolicies"]
            inline   = iam.list_user_policies(UserName=un)["PolicyNames"]
            policy_map[un] = [p["PolicyName"] for p in attached] + inline
            group_map[un]  = [g["GroupName"] for g in
                              iam.list_groups_for_user(UserName=un)["Groups"]]

    STATS["iam_users_scanned"] = len(all_users)

    def parse_dt(val):
        if val in ("N/A","no_information","","not_supported"): return None
        try:    return datetime.datetime.fromisoformat(val.replace("Z","+00:00"))
        except: return None

    def key_age(raw):
        dt = parse_dt(raw)
        return days_ago(dt) if dt else None

    enriched = []
    for row in users_csv:
        uname = col(row, "user")
        if uname == "&lt;root_account&gt;": uname = "ROOT"

        pwd_last   = parse_dt(col(row, "password_last_used"))
        k1_last    = parse_dt(col(row, "access_key_1_last_used_date"))
        k2_last    = parse_dt(col(row, "access_key_2_last_used_date"))
        k1_age     = key_age(col(row, "access_key_1_last_rotated"))
        k2_age     = key_age(col(row, "access_key_2_last_rotated"))

        has_console = col(row, "password_enabled") == "true"
        has_mfa     = col(row, "mfa_active") == "true"
        k1_active   = col(row, "access_key_1_active") == "true"
        k2_active   = col(row, "access_key_2_active") == "true"

        pwd_days  = days_ago(pwd_last)
        k1_u_days = days_ago(k1_last)
        k2_u_days = days_ago(k2_last)

        last_active = min(
            (d for d in [pwd_days, k1_u_days, k2_u_days] if d is not None),
            default=None)

        policies = policy_map.get(uname, [])
        groups   = group_map.get(uname,  [])
        is_admin = "AdministratorAccess" in policies

        flags = []
        if has_console and not has_mfa:
            flags.append("NO_MFA"); STATS["iam_no_mfa"] += 1
        if last_active is not None and last_active &gt; INACTIVE_DAYS:
            flags.append(f"INACTIVE_{last_active}d"); STATS["iam_inactive_users"] += 1
        if last_active is None and has_console:
            flags.append("NEVER_LOGGED_IN")
        if k1_active and k1_age is not None:
            if k1_age &gt; KEY_CRIT_DAYS:
                flags.append(f"KEY1_CRITICAL_{k1_age}d"); STATS["iam_keys_flagged"] += 1
            elif k1_age &gt; KEY_WARN_DAYS:
                flags.append(f"KEY1_WARN_{k1_age}d"); STATS["iam_keys_flagged"] += 1
        if k2_active and k2_age is not None:
            if k2_age &gt; KEY_CRIT_DAYS:
                flags.append(f"KEY2_CRITICAL_{k2_age}d"); STATS["iam_keys_flagged"] += 1
            elif k2_age &gt; KEY_WARN_DAYS:
                flags.append(f"KEY2_WARN_{k2_age}d"); STATS["iam_keys_flagged"] += 1
        if is_admin:
            flags.append("ADMIN_ACCESS")

        enriched.append({
            "user": uname, "console": has_console, "mfa": has_mfa,
            "k1_active": k1_active, "k2_active": k2_active,
            "k1_age": k1_age, "k2_age": k2_age,
            "last_active_d": last_active, "pwd_days": pwd_days,
            "policies": policies, "groups": groups,
            "is_admin": is_admin, "flags": flags,
        })

    # ── Table 1: All Users Overview ──────────────────────────────
    t_all = Table(title="IAM Users Overview", box=box.ROUNDED,
                  border_style="magenta", show_lines=True)
    t_all.add_column("User",         style="bold cyan",  width=20)
    t_all.add_column("Console",      style="white",      width=9)
    t_all.add_column("MFA",          style="white",      width=6)
    t_all.add_column("Keys",         style="white",      width=8)
    t_all.add_column("Last Active",  style="white",      width=13)
    t_all.add_column("K1 Age(d)",    style="yellow",     width=11)
    t_all.add_column("K2 Age(d)",    style="yellow",     width=11)
    t_all.add_column("Admin",        style="red",        width=7)
    t_all.add_column("Groups",       style="dim",        width=22)
    t_all.add_column("Flags",        style="red",        width=32)

    for u in enriched:
        mfa_s    = "[green]YES[/green]" if u["mfa"]     else "[red]NO[/red]"
        con_s    = "[green]YES[/green]" if u["console"] else "[dim]no[/dim]"
        keys_s   = f"{'K1 ' if u['k1_active'] else ''}{'K2' if u['k2_active'] else ''}".strip() or "[dim]none[/dim]"
        act_s    = (f"{u['last_active_d']}d ago" if u["last_active_d"] is not None
                    else "[dim]never[/dim]")
        admin_s  = "[bold red]YES[/bold red]" if u["is_admin"] else "[dim]no[/dim]"
        groups_s = ", ".join(u["groups"][:3]) or "[dim]–[/dim]"
        flags_s  = " ".join(f"[red]{f}[/red]" for f in u["flags"]) or "[green]OK[/green]"
        t_all.add_row(u["user"], con_s, mfa_s, keys_s, act_s,
                      str(u["k1_age"]) if u["k1_age"] is not None else "–",
                      str(u["k2_age"]) if u["k2_age"] is not None else "–",
                      admin_s, groups_s, flags_s)
    console.print(t_all)

    # ── Table 2: Flagged / Risky Users ───────────────────────────
    risky = [u for u in enriched if u["flags"]]
    if risky:
        console.print(f"\n[bold red]  {len(risky)} users have security flags:[/bold red]")
        t_risk = Table(title="FLAGGED USERS", box=box.ROUNDED,
                       border_style="red", show_lines=True)
        t_risk.add_column("User",           style="bold cyan", width=20)
        t_risk.add_column("Flags",          style="red",       width=36)
        t_risk.add_column("Policies",       style="yellow",    width=32)
        t_risk.add_column("Recommendation", style="green",     width=38)

        for u in risky:
            recs = []
            if "NO_MFA"        in u["flags"]: recs.append("Enable MFA immediately")
            if any("INACTIVE"  in f for f in u["flags"]): recs.append("Disable or delete user")
            if any("CRITICAL"  in f for f in u["flags"]): recs.append("Rotate access keys NOW")
            if any("WARN"      in f for f in u["flags"]): recs.append("Schedule key rotation")
            if "ADMIN_ACCESS"  in u["flags"]: recs.append("Audit admin necessity")
            if "NEVER_LOGGED_IN" in u["flags"]: recs.append("Remove console access")
            t_risk.add_row(u["user"],
                           "\n".join(u["flags"]),
                           "\n".join(u["policies"][:5]) or "–",
                           "\n".join(recs))
        console.print(t_risk)
    else:
        console.print("[green]  No security flags found.[/green]")

    # ── Table 3: Access Keys Age ─────────────────────────────────
    key_users = [u for u in enriched if u["k1_active"] or u["k2_active"]]
    if key_users:
        t_keys = Table(title="Active Access Keys", box=box.ROUNDED,
                       border_style="yellow", show_lines=True)
        t_keys.add_column("User",       style="bold cyan", width=20)
        t_keys.add_column("Key",        style="white",     width=5)
        t_keys.add_column("Age (days)", style="yellow",    width=12)
        t_keys.add_column("Status",     style="white",     width=28)

        for u in key_users:
            for key_n, active, age in [("KEY1",u["k1_active"],u["k1_age"]),
                                        ("KEY2",u["k2_active"],u["k2_age"])]:
                if not active: continue
                if age and age &gt; KEY_CRIT_DAYS:
                    status = "[bold red]CRITICAL -- rotate immediately[/bold red]"
                elif age and age &gt; KEY_WARN_DAYS:
                    status = "[yellow]WARNING -- rotate soon[/yellow]"
                else:
                    status = "[green]OK[/green]"
                t_keys.add_row(u["user"], key_n, str(age) if age else "?", status)
        console.print(t_keys)

    # ── IAM Summary Panel ─────────────────────────────────────────
    console.print(Panel(
        "\n".join([
            f"[bold cyan]Total users scanned:[/bold cyan]      {len(enriched)}",
            f"[bold cyan]Console access users:[/bold cyan]     {sum(1 for u in enriched if u['console'])}",
            f"[bold cyan]Users WITHOUT MFA:[/bold cyan]        [bold red]{STATS['iam_no_mfa']}[/bold red]",
            f"[bold cyan]Inactive (&gt;{INACTIVE_DAYS}d):[/bold cyan]         [bold yellow]{STATS['iam_inactive_users']}[/bold yellow]",
            f"[bold cyan]Access keys flagged:[/bold cyan]      [bold yellow]{STATS['iam_keys_flagged']}[/bold yellow]",
            f"[bold cyan]Admin users:[/bold cyan]              [bold red]{sum(1 for u in enriched if u['is_admin'])}[/bold red]",
            f"[bold cyan]Total flagged users:[/bold cyan]      [bold red]{len(risky)}[/bold red]",
            "",
            f"[dim]Full JSON report -&gt; {IAM_REPORT}[/dim]",
        ]),
        title="IAM SECURITY SUMMARY", border_style="magenta"))

    # ── Save JSON report ──────────────────────────────────────────
    try:
        account_id = boto3.client("sts").get_caller_identity().get("Account","?")
    except Exception:
        account_id = "?"

    with open(IAM_REPORT, "w") as f:
        json.dump({"generated": _now_iso(), "account_id": account_id,
                   "users": enriched,
                   "stats": {"total": len(enriched), "no_mfa": STATS["iam_no_mfa"],
                              "inactive": STATS["iam_inactive_users"],
                              "keys_flagged": STATS["iam_keys_flagged"],
                              "admin_users": sum(1 for u in enriched if u["is_admin"]),
                              "flagged": len(risky)}},
                  f, indent=2, default=str)

    console.print(f"[green]  IAM report saved -&gt; {IAM_REPORT}[/green]")
    log_event("IAM_SCAN", {"users": len(enriched), "flagged": len(risky)})

# ══════════════════════════════════════════════════════════════════
# SECTION 7 — FINAL SUMMARY
# ══════════════════════════════════════════════════════════════════
def show_summary(dry_run, elapsed):
    mode_tag = "[bold red]  DRY-RUN[/bold red]" if dry_run else "[bold green]  PRODUCTION[/bold green]"
    t = Table(box=box.DOUBLE_EDGE, border_style="green", title="CLEANUP SUMMARY", show_lines=True)
    t.add_column("Metric",        style="bold cyan",  width=36)
    t.add_column("Count / Value", style="bold white", width=26)
    for k, v in [
        ("Mode",                         mode_tag),
        ("Docker containers killed",     str(STATS["docker_containers_killed"])),
        ("Docker images removed",        str(STATS["docker_images_removed"])),
        ("Docker networks removed",      str(STATS["docker_networks_removed"])),
        ("Docker volumes removed",       str(STATS["docker_volumes_removed"])),
        ("Processes killed",             str(STATS["processes_killed"])),
        ("IAM users scanned",            str(STATS["iam_users_scanned"])),
        ("IAM users without MFA",        str(STATS["iam_no_mfa"])),
        ("IAM inactive users",           str(STATS["iam_inactive_users"])),
        ("IAM access keys flagged",      str(STATS["iam_keys_flagged"])),
        ("Total duration",               f"{elapsed:.1f}s"),
        ("Audit log",                    str(AUDIT_LOG)),
        ("IAM report",                   str(IAM_REPORT)),
    ]: t.add_row(k, v)
    console.print(t)
    log_event("SUMMARY", {**STATS, "duration_s": elapsed, "dry_run": dry_run})

# ══════════════════════════════════════════════════════════════════
# INTERACTIVE MENU
# ══════════════════════════════════════════════════════════════════
MENU_ITEMS = [
    ("1", "Full Auto    (Docker + Processes + Disk + IAM)",   "auto"),
    ("2", "Docker Only  (containers + images + nets + vols)", "docker"),
    ("3", "Processes    (idle + long-running)",               "procs"),
    ("4", "Disk         (EBS + large files)",                 "disk"),
    ("5", "IAM Tracker  (users, keys, MFA, inactive)",        "iam"),
    ("6", "Resources    (CPU / RAM / disk live)",             "resources"),
    ("7", "Exit",                                             "exit"),
]
MENU_ACTION = {k: v for k, _, v in MENU_ITEMS}
MENU_COLORS = ["yellow","cyan","cyan","cyan","magenta","cyan","red"]

def interactive_menu(dry_run):
    mode_label = "[bold red]DRY-RUN[/bold red]" if dry_run else "[bold green]PRODUCTION[/bold green]"
    body = f"Mode: {mode_label}\n\n" + "\n".join(
        f"  [{c}]{k}[/{c}]  {label}"
        for (k, label, _), c in zip(MENU_ITEMS, MENU_COLORS))
    console.print(Panel(body, title="[bold]ENTERPRISE CLEANUP MENU[/bold]", border_style="blue"))
    return MENU_ACTION[Prompt.ask("Select", choices=[k for k,_,_ in MENU_ITEMS], default="1")]

# ══════════════════════════════════════════════════════════════════
# MAIN
# ══════════════════════════════════════════════════════════════════
def main():
    # Declare ALL globals FIRST before any usage
    global IDLE_THRESHOLD_HOURS, LONG_RUNNING_HOURS
    global INACTIVE_DAYS, KEY_WARN_DAYS, KEY_CRIT_DAYS

    parser = argparse.ArgumentParser(
        description="Enterprise Cleanup + IAM Tracker v4.0",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=textwrap.dedent("""
        Examples:
          sudo python3 prod_cleanup.py --dry-run
          sudo python3 prod_cleanup.py --production
          sudo python3 prod_cleanup.py --production --auto
          sudo python3 prod_cleanup.py --iam-only
          sudo python3 prod_cleanup.py --production --inactive-days 60 --key-crit 120
        """))

    mode_grp = parser.add_mutually_exclusive_group()
    mode_grp.add_argument("--dry-run",       action="store_true")
    mode_grp.add_argument("--production",    action="store_true")
    parser.add_argument("--auto",            action="store_true", help="No prompts (CI/CD)")
    parser.add_argument("--docker-only",     action="store_true")
    parser.add_argument("--procs-only",      action="store_true")
    parser.add_argument("--disk-only",       action="store_true")
    parser.add_argument("--iam-only",        action="store_true")
    parser.add_argument("--idle-hours",      type=float, default=IDLE_THRESHOLD_HOURS)
    parser.add_argument("--long-hours",      type=float, default=LONG_RUNNING_HOURS)
    parser.add_argument("--inactive-days",   type=int,   default=INACTIVE_DAYS,
                        help="Flag IAM users inactive &gt; N days (default 90)")
    parser.add_argument("--key-warn",        type=int,   default=KEY_WARN_DAYS,
                        help="Access key warning age days (default 90)")
    parser.add_argument("--key-crit",        type=int,   default=KEY_CRIT_DAYS,
                        help="Access key critical age days (default 180)")

    args = parser.parse_args()

    IDLE_THRESHOLD_HOURS = args.idle_hours
    LONG_RUNNING_HOURS   = args.long_hours
    INACTIVE_DAYS        = args.inactive_days
    KEY_WARN_DAYS        = args.key_warn
    KEY_CRIT_DAYS        = args.key_crit

    dry_run     = not args.production
    interactive = not args.auto

    console.print(Panel(
        "[bold yellow]  DRY-RUN MODE -- no changes will be made.[/bold yellow]"
        if dry_run else
        "[bold red]  PRODUCTION MODE -- destructive operations WILL execute.[/bold red]",
        border_style="yellow" if dry_run else "red"))

    show_identity()
    show_resource_usage()

    if args.iam_only:
        iam_tracker(dry_run)
    elif args.docker_only:
        docker_cleanup(dry_run, interactive)
    elif args.procs_only:
        process_cleanup(dry_run, interactive)
    elif args.disk_only:
        show_disk_analysis()
    elif args.auto:
        docker_cleanup(dry_run, interactive)
        process_cleanup(dry_run, interactive)
        show_disk_analysis()
        iam_tracker(dry_run)
    else:
        while True:
            action = interactive_menu(dry_run)
            if action == "exit": break
            elif action == "auto":
                docker_cleanup(dry_run, interactive)
                process_cleanup(dry_run, interactive)
                show_disk_analysis()
                iam_tracker(dry_run)
            elif action == "docker":    docker_cleanup(dry_run, interactive)
            elif action == "procs":     process_cleanup(dry_run, interactive)
            elif action == "disk":      show_disk_analysis()
            elif action == "iam":       iam_tracker(dry_run)
            elif action == "resources": show_resource_usage()
            if not Confirm.ask("\n[cyan]Return to menu?[/cyan]", default=True): break

    elapsed = (datetime.datetime.now(datetime.timezone.utc) - SCRIPT_START).total_seconds()
    flush_json_logs()
    show_summary(dry_run, elapsed)
    console.print(Panel(
        f"[bold green]  Completed![/bold green]  Logs -&gt; [cyan]{LOG_DIR}[/cyan]",
        border_style="green"))


if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        console.print("\n[yellow]  Interrupted. Flushing logs...[/yellow]")
        flush_json_logs()
        elapsed = (datetime.datetime.now(datetime.timezone.utc) - SCRIPT_START).total_seconds()
        show_summary(dry_run=True, elapsed=elapsed)
        sys.exit(130)
    except Exception:
        console.print_exception()
        flush_json_logs()
        sys.exit(1)
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/db153d61-bbb5-49e5-89f2-0de1b11bbf04.png" alt="" style="display:block;margin:0 auto" />]]></content:encoded></item><item><title><![CDATA[AI Security in Action :Exploring Perplexity Bumblebee & Numbat
]]></title><description><![CDATA[Over the weekend, I explored R&D AI coding agents and learned about AI security tools like Numbat and Bumblebee. I learned how to scan MCP servers…]]></description><link>https://vishnusai.hashnode.dev/ai-security-in-action-exploring-perplexity-bumblebee-numbat</link><guid isPermaLink="true">https://vishnusai.hashnode.dev/ai-security-in-action-exploring-perplexity-bumblebee-numbat</guid><dc:creator><![CDATA[vishnu sai]]></dc:creator><pubDate>Tue, 13 Oct 2026 01:41:36 GMT</pubDate><content:encoded><![CDATA[<h3><strong>PROBLEM</strong></h3>
<p>Over the weekend, I explored R&amp;D AI coding agents and learned about AI security tools like Numbat and Bumblebee. I learned how to scan MCP servers, configurations, and extensions for potential security risks. My goal was to understand how to make AI-assisted development more secure.</p>
<h3><strong>HOW I DEBUGGED IT</strong></h3>
<p>I installed and tested two open-source security tools: Bumblebee and Numbat on an Ubuntu EC2 instance.</p>
<p>● Bumblebee  Scanned packages, MCP configurations, and developer extensions.</p>
<p>●  Numbat  Scanned supported AI agent artifacts for potential security issues.</p>
<h3><strong>THE SOLUTION</strong></h3>
<p>I used <strong>Bumblebee</strong> to check packages, MCP configurations, and developer extensions. I used <strong>Numbat</strong> to discover and analyze supported AI agent artifacts.</p>
<p>Testing both tools on Ubuntu helped me understand how to identify potential security risks in AI development environments.</p>
<p><strong>EC2 → Bumblebee &amp; Numbat → Scan → Identify Risks → Secure AI Development</strong></p>
<p><strong>Has anyone else faced this problem?</strong></p>
<p><strong>How did you fix it? What tools do you use to secure AI coding agents?</strong></p>
<p><strong>GitHub :</strong> <a href="https://github.com/ksaivishnusaikvs/DevOps-Engineering/tree/main/Linkedin/Post">https://github.com/ksaivishnusaikvs/DevOps-Engineering/tree/main/Linkedin/Post</a></p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/c18a8cf7-7676-484b-979f-2fa89c526767.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Bumblebee</strong> is also open source and free to run. It's a read-only security scanner for developer endpoints, including packages, MCP configs, editor extensions, and browser extensions.  <a href="https://github.com/perplexityai/bumblebee">https://github.com/perplexityai/bumblebee</a></p>
<p><strong>Step1:</strong> Install prerequisites: <strong>Bumblebee</strong> requires Go 1.25+ and is a read-only scanner. GitHub</p>
<p><strong>sudo apt update; sudo apt install -y git; go version</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/b022aee2-c9f9-430f-a98d-2c8625a90745.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Step 2:</strong> Install Bumblebee</p>
<p>go install github.com/perplexityai/bumblebee/cmd/bumblebee@latest</p>
<p>Add it to PATH:</p>
<p>echo 'export PATH="\(HOME/go/bin:\)PATH"' &gt;&gt; ~/.bashrc</p>
<p>source ~/.bashrc</p>
<p>Verify: bumblebee version</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/a5b2b79b-98e0-44d7-86cb-2fca1fb6fc31.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Step 3:</strong> Run the self-test</p>
<p>bumblebee selftest</p>
<p>Your result:</p>
<p>selftest OK</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/d9497211-44f6-45f1-82e4-eb6b68cc6626.png" alt="" style="display:block;margin:0 auto" />

<p>means the installation is working.</p>
<p><strong>Step 4:</strong> Baseline scan</p>
<p>This is the recommended lightweight inventory scan:</p>
<p>bumblebee scan --profile baseline &gt; ~/bumblebee-baseline.ndjson</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/f38b5cc0-50a6-4b8b-b32b-3acec946cd15.png" alt="" style="display:block;margin:0 auto" />

<p>Check the final summary:</p>
<p>grep '"record_type":"scan_summary"' ~/bumblebee-baseline.ndjson</p>
<p>Bumblebee's output is NDJSON. Diagnostics are written separately to stderr. GitHub</p>
<p><strong>Step5.</strong> Deep scan</p>
<p>For an on-demand scan of specific directories:</p>
<p>bumblebee scan \</p>
<p>  --profile deep \</p>
<p>  --root /home/ubuntu \</p>
<p>  --root /opt \</p>
<p>  --root /srv \</p>
<p>  &gt; ~/bumblebee-deep.ndjson</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/0db92231-8481-4a19-9ae9-79858319cea2.png" alt="" style="display:block;margin:0 auto" />

<p>deep is specifically designed for explicit --root paths.</p>
<p><strong>Numbat</strong>  <a href="https://github.com/perplexityai/numbat">https://github.com/perplexityai/numbat</a> </p>
<p>is the more interesting one for a DevOps/security project: it's a single binary for Linux, macOS, and Windows, and provides AI-agent monitoring, local detection, optional blocking, and forensic reconstruction. Its GitHub repo is Apache 2.0.</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/277d61d7-e69e-4379-89c7-a774395924f2.png" alt="" style="display:block;margin:0 auto" />

<p> <strong>Step 1:</strong> numbat agents --all. Select one agent based on the scan</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/7082b53e-4b44-4446-8ac1-cba666fb1b77.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Step2:</strong> log in to the Gemini CLI command gemini shown below</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/af9871d1-5b75-4d85-ade9-4f1c97ee6be4.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Step3 :</strong> Click on the Sign in with Google area; select based on your options. I selected 1 option: Sign in with Google</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/5e562bda-cfa3-43e9-87cd-cbe516e47d00.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Click Sign in it shows the URL in the web, then we can log in</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/7d70403b-c689-4ddc-b93e-7f9ec586702a.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Step3 :</strong> Test Numbat in the first CLI; it does not show <strong>Image 1:</strong> no agent artifacts to <a href="http://analyze.in">analyze</a> in that dir. <strong>Image 2:</strong> Numbat Agent Scan: Numbat successfully discovered and parsed 6 artifacts with 0 findings.</p>
<p><strong>Image 1:</strong> Gemini CLI Integration: Numbat scanned the DevOps directory but found no agent artifacts to analyze. The scan returned an error because it found no supported artifacts.</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/10634285-25c6-4e10-8f17-667834d5a838.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Image 2:</strong> Numbat Agent Scan: Numbat successfully discovered and parsed 6 artifacts with 0 findings. The scan completed successfully, confirming that artifact discovery and parsing worked in this run.</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/217221d9-75b4-424d-9bad-a731ac5baa01.png" alt="" style="display:block;margin:0 auto" />

<p>full scrreen shot</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/5bed1698-e058-40d5-b831-c210e55c801a.png" alt="" style="display:block;margin:0 auto" />]]></content:encoded></item><item><title><![CDATA[AWS cost Report to Slack notification]]></title><description><![CDATA[AWS Cost Explorer • Python • Slack • Automation THE PROBLEM I wanted an easier way to see AWS costs without opening the AWS console and checking the report…]]></description><link>https://vishnusai.hashnode.dev/aws-cost-report-to-slack-notification</link><guid isPermaLink="true">https://vishnusai.hashnode.dev/aws-cost-report-to-slack-notification</guid><dc:creator><![CDATA[vishnu sai]]></dc:creator><pubDate>Wed, 07 Oct 2026 01:12:15 GMT</pubDate><content:encoded><![CDATA[<p><strong>AWS Cost Explorer • Python • Slack • Automation</strong></p>
<p><strong>THE PROBLEM</strong></p>
<p>I wanted an easier way to see AWS costs without opening the AWS console and checking the report manually every day.</p>
<p><strong>WHAT I WANTED</strong></p>
<p>●       See AWS costs by service. Send the report directly to Slack</p>
<p>●       Make the information easy to read. Reduce repetitive manual work</p>
<p><strong>HOW I DEBUGGED IT</strong></p>
<p>I started by generating the AWS cost report and checking the output. Then I tested the Slack setup, permissions, and channel configuration step by step until the report was reaching Slack correctly.</p>
<p><strong>THE SOLUTION</strong></p>
<p>I connected AWS Cost Explorer → AWS Cost CLI → Python → Slack and tested the complete flow. After that, I added scheduled automation so the report could be generated and sent automatically.     Copy of AWS cost Report to Slack     Copy of AWS cost Report to Slack</p>
<p><strong>THE WORKFLOW</strong></p>
<p>AWS Cost → Report → Python → Slack → Automation</p>
<p><strong>Has anyone else faced this problem?</strong></p>
<p><strong>How do you share AWS costs without giving everyone Console access?</strong></p>
<p><strong>How did you fix it?</strong></p>
<p><strong>HashnodProfile:</strong><a href="https://hashnode.com/@vishnusaiK">https://hashnode.com/@vishnusaiK</a><br /><strong>GitHub :</strong> <a href="https://github.com/ksaivishnusaikvs/DevOps-Engineering/tree/main/Linkedin/Post">https://github.com/ksaivishnusaikvs/DevOps-Engineering/tree/main/Linkedin/Post</a></p>
<p>➢    <strong>AWS cost notification to Slack channel</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/0676e3af-694b-479b-8e78-f089a9fae634.png" alt="" style="display:block;margin:0 auto" />

<p><strong>To check clearly: select the 3 dots, click on View raw</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/1409abe1-36d9-4210-a6dd-dbb7a578bced.png" alt="" style="display:block;margin:0 auto" />

<p><strong>STEP1:</strong> Set up an EC2 instance first. Launch the EC2 instance in an AWS region, run apt-get update, and update the packages</p>
<p>Install Node.js from <a href="https://nodejs.org/en/download">https://nodejs.org/en/download</a>, select os and use it. In my case</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/ee861dfa-38b0-4688-91ab-70a6706005c4.png" alt="" style="display:block;margin:0 auto" />

<p>Installation looks like this.</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/8f7e7102-4646-4421-84ab-91a59f42aca2.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Step2:</strong> Installation. Install the package globally, or you can also use npx  <br /><strong>npm install -g aws-cost-cli <br />  (Or)</strong></p>
<p><strong>Docker</strong>You can build this Dockerfile using the docker build command and then run a container from the built image. For example:</p>
<p>docker build -t aws-cost-cli.  <br />docker run aws-cost-cli</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/b3eaeb97-1c36-4410-a2ad-ad0511210d2d.png" alt="" style="display:block;margin:0 auto" />

<p>Install the command we need to run</p>
<p><strong>aws-cost: This</strong> will show the AWS billing cost</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/1d741a0a-c762-4ffd-be47-750cb4a53d94.png" alt="" style="display:block;margin:0 auto" />

<p>●      aws-cost --help, it will help you find commands</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/8fdd2a64-f91c-4074-bc5c-2924186d80da.png" alt="" style="display:block;margin:0 auto" />

<h3><strong>How to integrate with Slack</strong></h3>
<p><strong>Step1:</strong> Install the AWS CLI in your instance</p>
<p># Install AWS CLI</p>
<p>curl "<a href="https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip">https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip</a>" -o "<a href="http://awscliv2.zip">awscliv2.zip</a>"</p>
<p>sudo apt-get install unzip -y</p>
<p>unzip <a href="http://awscliv2.zip">awscliv2.zip</a></p>
<p>sudo ./aws/install</p>
<p><a href="https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html">https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html</a></p>
<p>Try the command</p>
<p>AWS configure or IAM roles.</p>
<p><strong>Step2:</strong> Go to Slack —&gt; first we need to create a workspace, then create the channel  → click on Next</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/a4f863ab-59b4-4d17-b99a-85adb05f3f79.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Step3:</strong> Give the channel a name  → select the visibility (public or private best practices</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/197e361d-2b6b-41d3-ac1e-da62294fea34.png" alt="" style="display:block;margin:0 auto" />

<p>Click on Create the channel • click Create</p>
<p><strong>Step4:</strong></p>
<p>Go to <a href="https://api.slack.com/apps">https://api.slack.com/apps</a> → click on the new app →select from scratch</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/ed1b78fe-5580-4358-aa4c-24a0e9b68562.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Step 5:</strong> Give your app a name and select the workspace in Slack. If no workspace is created, create a workspace.</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/017cd534-e04d-42d1-8eef-79742446b9ac.png" alt="" style="display:block;margin:0 auto" />

<p>Once it is created, it looks like this.</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/e2e505ad-a898-48c2-8479-c4a839850563.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Step6</strong>:</p>
<p>The image text describes how to set Slack app permissions in the app dashboard:</p>
<p>Go to "OAuth &amp; Permissions".</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/be5e7f58-bd1e-4e37-86b3-67f65719e039.png" alt="" style="display:block;margin:0 auto" />

<p>Scroll to "Scopes".</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/0b64aa2a-8c33-4e4d-97d7-5b6323e938aa.png" alt="" style="display:block;margin:0 auto" />

<p>Under "Bot Token Scopes", add:</p>
<p>chat. write → Allows the bot to post messages.</p>
<p>chat. write. public → (Optional) Allows posting in public channels without being invited.</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/5787fa18-c4ce-4128-b4a4-77137b40c196.png" alt="" style="display:block;margin:0 auto" />

<p>Once they add all</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/fc07e7fb-8eb7-4d30-bfaf-75aeb20406e0.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Step 7:</strong> Install the App in Your Workspace</p>
<ol>
<li><p>Still in OAuth &amp; Permissions, click Install App to Workspace.</p>
</li>
<li><p>Approve the permissions.</p>
</li>
<li><p>Copy the Bot User OAuth Token:</p>
</li>
</ol>
<p>○ Starts with xoxb-... → You'll need this for aws-cost-cli. Approve the permissions.</p>
<p>Approve the permissions.</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/6cfc85b1-a831-4d5a-a0fe-aeccf0250a5c.png" alt="" style="display:block;margin:0 auto" />

<p>Then we need to see the token; it starts with xoxb-... → You'll need this for aws-cost-cli.</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/9654eb2e-c53d-45be-ad10-13b15eb45315.png" alt="" style="display:block;margin:0 auto" />

<p>Step8: Find Your Slack Channel ID</p>
<ol>
<li><p>Open Slack.</p>
</li>
<li><p>Go to the channel you want to post in.</p>
</li>
<li><p>Click on the channel name at the top → View channel details.</p>
</li>
<li><p>Copy the Channel ID (starts with C...).</p>
</li>
</ol>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/73d3ef2a-bfb5-4cab-8db0-530f3a6153c8.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/a3244dba-bf75-4ad8-899a-bd77f1c0c7d7.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Step9:</strong> Test the Slack integration with <strong>aws-cost-cli</strong></p>
<p>aws-cost --slack-token xoxb-884434558977825-12ZAU3Pl9zCCkJyeQH5oK8tO --slack-channel C08QKDNCW59</p>
<p>Change the token and channel name.</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/20156d7a-b491-4c65-a712-af00b7b8ed57.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Verify that we received the message on Slack.</strong></p>
<p><strong>Note</strong>: If your Slack invitation fails to send a message to the Slack channel not found, we need to invert the process for the first time (or) failed Slack not found</p>
<p><strong>Step 9.1</strong>: /invite @ your app name that shows in the image</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/d0d610b1-7798-4635-a5db-1d651b2d3a9f.png" alt="" style="display:block;margin:0 auto" />

<p>Run the command</p>
<p>aws-cost --slack-token xoxb-8844374581409-8844558977825-12ZAU3Pl9zCCkJyeQH5oK8tO --slack-channel C08QKDNCW59</p>
<p><strong>Now we get a message on Slack</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/fcf6268b-25ec-4589-a8da-2555f1651fea.png" alt="" style="display:block;margin:0 auto" />

<p><strong>This output is not clear format. We need to print the file format.</strong></p>
<p><strong>Step10:</strong> /invite @AWS-COST-Report</p>
<p>aws-cost --text &gt; cost-report.txt &amp;&amp; \</p>
<p>curl -F file=@cost-report.txt \</p>
<p>-F "initial_comment=AWS Cost Report" \</p>
<p>-F channels=C08PFVZV9PF \</p>
<p>-H "Authorization: Bearer xoxb-6800278956247-8805347127382-SQlckF4aKrzil8ODyqARQRA" \</p>
<p><a href="https://slack.com/api/files.upload">https://slack.com/api/files.upload</a></p>
<p>This command generates an AWS cost report, saves it to a text file, and then uploads it to a <strong>Slack channel</strong> using Slack's API.</p>
<p><strong>Step10.1:</strong> We need to print in text format. We need to add the go for the <a href="https://api.slack.com/apps/A08QYJ503A6/oauth">https://api.slack.com/apps/A08QYJ503A6/oauth</a>?</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/e73e3a44-7b0e-419b-b806-ad5c51b097ca.png" alt="" style="display:block;margin:0 auto" />

<p>We need to print the file format in Slack. We add on the go→ scope → add the file write.</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/cfe970c9-256f-4bf2-b6a7-aecbde7ef6f8.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/efc84837-d521-4d76-8ab6-90f7442ea1a4.png" alt="" style="display:block;margin:0 auto" />

<p>After adding the file, write in the scope we need to reinstall the app, click on the reinstall app in top.</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/e7d5b7de-21ce-4359-9753-eb5b05666c2c.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Click on Allow</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/b65af70d-c272-4540-8619-eaad5a3fde50.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Step 11:</strong> Install Python &amp; pip (if not already) sudo apt update sudo apt install -y python3 python3-pip python3-venv 2. Create a Virtual Environment (Optional but Recommended) python3 -m venv venv source venv/bin/activate</p>
<p><strong>Step12:</strong> We install pip install slack-sdk</p>
<ol>
<li>Create Python Script (upload_cost_report.py):</li>
</ol>
<p>from slack_sdk import WebClient from slack_sdk.errors import SlackApiError</p>
<p>Your Slack Bot Token</p>
<p>slack_token = "xoxb-6800278956247-DyqARQRA" client = WebClient(token=slack_token)</p>
<p>try: # Upload the file response = client.files_upload( channels="C08PFVZV9PF", file="cost-report.txt", initial_comment="AWS Cost Report" ) print("File uploaded successfully:", response)</p>
<p>except SlackApiError as e: print(f"Error uploading file: {e.response['error']}")</p>
<pre><code class="language-python">from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError

# Your Slack Bot Token
slack_token = "xoxb-6800278956247-DyqARQRA"
client = WebClient(token=slack_token)

try:
    # Upload the file
    response = client.files_upload(
        channels="C08PFVZV9PF",
        file="cost-report.txt",
        initial_comment="AWS Cost Report"
    )
    print("File uploaded successfully:", response)

except SlackApiError as e:
    print(f"Error uploading file: {e.response['error']}")
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/8594e842-52b0-4e16-9710-cd08683db9b9.png" alt="" style="display:block;margin:0 auto" />

<p><img src="align=%22center%22" alt="" /></p>
<p><img src="align=%22center%22" alt="" /></p>
<p><strong>Step12:</strong> creat file vi aws_cost_<a href="http://slack.py">slack.py</a>, add script  give the executed permissions</p>
<p><strong>Run the AWS Cost CLI + Python Script:</strong></p>
<p><strong>Generate cost report</strong> aws-cost --text &gt; cost-report.txt</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/655ea672-b8ff-402a-8d10-db4f780aecec.png" alt="" style="display:block;margin:0 auto" />

<p>Every time we run the command, we run the script; it gets a new report to text.</p>
<h4><strong>Upload to Slack</strong></h4>
<p>python3 upload_cost_<a href="http://report.py">report.py</a></p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/36152d2d-cdc8-4b36-9f7b-a8e2cc62f6df.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/4a71ad96-d69d-4bac-bd6a-7acb218a9977.png" alt="" style="display:block;margin:0 auto" />

<p>After the run, we receive the notification in the channel.</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/12f4bd65-a8e1-4789-8b40-c8b1c6ad2468.png" alt="" style="display:block;margin:0 auto" />

<p>To check clearly: select the 3 dots, click on the view ra.w</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/d31c9fb8-6a28-4ceb-8150-3031a64b5efd.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Option 1: Using cron (on Linux/Ubuntu EC2) Step-by-step: Open crontab:</strong></p>
<p>crontab -e Add a cron job to run the script every day at 8 AM (or your preferred time):</p>
<p>0 8 * * * /usr/bin/aws-cost --text &gt; /root/cost-report.txt &amp;&amp; /usr/bin/python3 /root/aws_cost_slack.py ○ Adjust paths if aws-cost or python3 are in different locations.</p>
<p>○ Use which aws-cost and which python3 to find full paths.</p>
<p><strong>Option 2: Using a Shell Script + cron</strong></p>
<p>Create a shell script: aws_daily_report.sh:</p>
<p>#!/bin/bash /usr/bin/aws-cost --text &gt; /root/cost-report.txt /usr/bin/python3 /root/aws_cost_slack.py</p>
<ol>
<li>Make it executable:</li>
</ol>
<p>chmod +x aws_daily_report.sh Schedule it via cron:</p>
<p>crontab -e</p>
<p>0 8 * * * /root/aws_daily_report.sh</p>
<p>**Schedule it via cron:<br />**crontab -e<br />0 8 * * * /root/aws_daily_<a href="http://report.sh">report.sh</a></p>
<h2><strong>Option 3: AWS Lambda + CloudWatch (serverless, scalable)</strong></h2>
<p>Use AWS Lambda to:</p>
<p>●      Generate the report</p>
<p>●      Upload it to Slack</p>
<p>●      Schedule with <strong>CloudWatch Events</strong></p>
<p>This is better for <strong>multi-user orgs</strong> or <strong>100+ team-wide notifications</strong>.</p>
<p>If you're interested, I can generate an <strong>end-to-end Lambda version</strong> of your script.</p>
<p><strong>One-Time Setup</strong></p>
<p>You first run this <strong>one-time command</strong> to get daily AWS cost data grouped by service for a <strong>past month</strong> (April 2024):</p>
<p>aws ce get-cost-and-usage \</p>
<p>--time-period Start=2024-04-01,End=2024-04-30 \</p>
<p>--granularity DAILY \</p>
<p>--metrics "UnblendedCost" \</p>
<p>--group-by Type=SERVICE \</p>
<p>--output table</p>
<p>This is a <strong>historical report</strong>.</p>
<h2><strong>Breakdown alias aws-cost="..."</strong></h2>
<p>Creates a <strong>command shortcut</strong>. After this, you can just type aws-cost in your terminal, and it will run the entire AWS CLI command.</p>
<p><strong>aws ce get-cost-and-usage</strong></p>
<p>This is the AWS CLI command for <strong>Cost Explorer</strong>, used to fetch billing and usage data.</p>
<p>●       ce = Cost Explorer</p>
<p>●       get-cost-and-usage = Retrieves AWS cost data</p>
<h3><strong>--time-period Start=...,End=...</strong></h3>
<h3>Specifies the <strong>date range</strong> of the cost report.</h3>
<p>Start=$(date -d 'yesterday' +%Y-%m-%d)</p>
<p>End=$(date +%Y-%m-%d)</p>
<p>●       Start = <strong>yesterday's date</strong> (e.g., 2025-05-23)</p>
<p>●       EEnd= <strong>today's date</strong> (e.g., 2025-05-24)</p>
<p>This means: <strong>Get costs for yesterday</strong>.</p>
<h3><strong>--granularity DAILY</strong></h3>
<p>This tells AWS to give you cost data <strong>broken down per day</strong>. Since you're only asking for one day (yesterday), it gives the cost just for that one day.</p>
<h3><strong>🔹 --metrics UnblendedCost</strong></h3>
<p>Specifies the cost metric you want:</p>
<p>●       UnblendedCost = Actual cost before discounts or credits are applied.</p>
<p>Other options (not used here): BlendedCost, UsageQuantity.</p>
<h3><strong>🔹 --group-by Type=SERVICE</strong></h3>
<p>This groups the output by <strong>AWS service</strong>, so you can see how much each service (like EC2, S3, etc.) costs on that day.</p>
<h3><strong>🔹 --output table</strong></h3>
<p>This formats the output in a <strong>readable text table</strong> format.<br /> Alternatives:</p>
<p>●       json (for scripting)</p>
<p>●       text</p>
<p>●       csv</p>
<p><strong>Example Output:</strong></p>
<p>When you run:</p>
<p>aws-cost</p>
<p>+--------------------------------------+-----------+</p>
<p>| Service                              | Amount    |</p>
<p>+--------------------------------------+-----------+</p>
<p>| Amazon Elastic Compute Cloud - EC2   | $1.11     |</p>
<p>| Amazon S3                            | $0.04     |</p>
<p>+--------------------------------------+-----------+</p>
<h2><strong>Summary</strong></h2>
<table style="min-width:100px"><colgroup><col style="min-width:25px"></col><col style="min-width:25px"></col><col style="min-width:25px"></col><col style="min-width:25px"></col></colgroup><tbody><tr><td><p></p></td><td><p><strong>Option</strong></p></td><td><p></p></td><td><p><strong>Purpose</strong></p></td></tr><tr><td><p>alias aws-cost=...</p></td><td><p>Creates a shortcut</p></td><td><p></p></td><td><p></p></td></tr><tr><td><p>Start=$(...)</p></td><td><p>Uses <strong>yesterday's</strong> date</p></td><td><p></p></td><td><p></p></td></tr><tr><td><p>--granularity DAILY</p></td><td><p>Gives daily cost details</p></td><td><p></p></td><td><p></p></td></tr><tr><td><p>--group-by SERVICE</p></td><td><p>Breaks are cut down <strong>by the service</strong></p></td><td><p></p></td><td><p></p></td></tr><tr><td><p>--output table</p></td><td><p>Human-readable report</p></td><td><p></p></td><td><p></p></td></tr></tbody></table>

<h3><strong>Dynamic command</strong></h3>
<p><strong>alias aws-cost="aws ce get-cost-and-usage \</strong></p>
<p><strong>--time-period Start=\((date -d 'yesterday' +%Y-%m-%d),End=\)(date +%Y-%m-%d) \</strong></p>
<p><strong>--granularity DAILY \</strong></p>
<p><strong>--metrics UnblendedCost \</strong></p>
<p><strong>--group-by Type=SERVICE \</strong></p>
<p><strong>--output table"</strong></p>
<h3><strong>It is dynamic, and here’s what it does:</strong></h3>
<p>●       **Start=$(date -d 'yesterday' +%Y-%m-%d)<br />** Gets <strong>yesterday’s date</strong> in the format YYYY-MM-DD.</p>
<p>●       **End=$(date +%Y-%m-%d)<br />** Gets <strong>today’s date</strong>, so the report ends at midnight today.</p>
<p>●       **--granularity DAILY<br />** Ensures the data is broken down <strong>by day</strong>.</p>
<p>●       **--group-by Type=SERVICE<br />** Breaks down cost <strong>by AWS service</strong> (as shown in your screenshot).</p>
<p>●       **--output table<br />** Formats the output in a <strong>readable text table</strong> (like your screenshot).</p>
<h3><strong>Result</strong></h3>
<p>Each time you run:</p>
<p>aws-cost &gt; cost-report.txt</p>
<p>It will dynamically generate a <strong>yesterday-only report</strong> for all AWS services and store it in cost-report.txt, in the same <strong>table format</strong> shown in your screenshot.</p>
<h3><strong>Example Output (based on your screenshot):</strong></h3>
<p>Each day, you'll get something like:</p>
<p>nginx</p>
<p>Service                          Last Month  This Month  Last 7 Days  Yesterday</p>
<p>Amazon EC2                      $XX.XX      $XX.XX      $X.XX        $X.XX</p>
<p>Amazon S3                       $0.27       $0.05       $0.04        $0.04</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/a1128638-0f01-4332-ab29-fc464779004a.png" alt="" style="display:block;margin:0 auto" />

<h3><strong>Summary</strong></h3>
<p>●        Yes, your AWS cost alias <strong>will dynamically generate</strong> this report <strong>every day</strong> for <strong>yesterday</strong>.</p>
<p>●       The output is formatted as a readable <strong>text table</strong>.</p>
<p>●        You can automate or script it for Slack, email, or log files.</p>
<p>Let me know if you want to:</p>
<p>●       Convert it to JSON/CSV for scripting.</p>
<p>●       Filter to only non-zero services.</p>
<p>●       Format it for Slack better.</p>
<h3><strong>Summary</strong></h3>
<p>●        Yes, your AWS cost alias <strong>will dynamically generate</strong> this report <strong>every day</strong> for <strong>yesterday</strong>.</p>
<p>●       The output is formatted as a readable <strong>text table</strong>.</p>
<p>●        You can automate or script it for Slack, email, or log files.</p>
<p>Let me know if you want to:</p>
<p>●       Convert it to JSON/CSV for scripting.</p>
<p>●       Filter to only non-zero services.</p>
<p>●       Format it for Slack better.</p>
<h2><strong>Step 1: Modify Your AWS-Cost Alias to Output JSON</strong></h2>
<p>You’ll use <strong>JSON output</strong> (easier to process programmatically):</p>
<p>alias aws-cost="aws ce get-cost-and-usage \</p>
<p>--time-period Start=\((date -d 'yesterday' +%Y-%m-%d),End=\)(date +%Y-%m-%d) \</p>
<p>--granularity DAILY \</p>
<p>--metrics UnblendedCost \</p>
<p>--group-by Type=SERVICE \</p>
<p>--output json"</p>
<p>Add this alias to your ~/.bashrc or ~/.zshrc, then run source ~/.bashrc (or equivalent shell).</p>
<p><strong>Step 2: Run the Alias &amp; Save Output</strong></p>
<p>aws-cost &gt; cost-report.json</p>
<p><strong>Modified Command for CSV Output:</strong></p>
<p>alias aws-cost="aws ce get-cost-and-usage \</p>
<p>--time-period Start=\((date -d 'yesterday' +%Y-%m-%d),End=\)(date +%Y-%m-%d) \</p>
<p>--granularity DAILY \</p>
<p>--metrics UnblendedCost \</p>
<p>--group-by Type=SERVICE \</p>
<p>--output text | tr '\t' ',' &gt; cost-report.csv"</p>
<p><strong>How This Works:</strong></p>
<h3><strong>--output text</strong></h3>
<p>This produces output in <strong>plain tab-delimited text</strong> (not real CSV yet).</p>
<h3><strong>tr '\t' ','</strong></h3>
<p>This converts <strong>tabs to commas</strong>, making it <strong>CSV-compatible</strong>.</p>
<h3><strong>&gt; cost-report.csv</strong></h3>
<p>Redirects the output into a file.</p>
<h2><strong>Result:</strong></h2>
<p>When you run: aws-cos</p>
<p>It will generate a file cost-report.csv with a format like:</p>
<p>yaml</p>
<p>CopyEdit</p>
<p>2025-05-23, Amazon Elastic Compute Cloud - Compute,1.15, USD</p>
<p>2025-05-23,Amazon S3,0.04,USD</p>
<h2><strong>Notes:</strong></h2>
<p>●   AWS CLI does <strong>not</strong> directly support --output csv.</p>
<p>●   Using --output text | tr '\t' ',' is the most reliable workaround.</p>
<p>●   If you need proper headers or processing, use Python to post-process the file.</p>
<p>Great! Here's a complete <strong>step-by-step guide</strong> to:</p>
<ol>
<li><p>Get AWS cost data dynamically in JSON or CSV</p>
</li>
<li><p>Filter for non-zero cost services</p>
</li>
<li><p>Format it nicely for Slack messages</p>
</li>
<li><p>Automate if needed</p>
</li>
</ol>
<p><strong>Step 1:</strong> Modify Your AWS-Cost Alias to Output JSON You’ll use JSON output (easier to process programmatically): bash CopyEdit alias aws-cost="aws ce get-cost-and-usage<br />--time-period Start=\((date -d 'yesterday' +%Y-%m-%d),End=\)(date +%Y-%m-%d)<br />--granularity DAILY<br />--metrics UnblendedCost<br />--group-by Type=SERVICE<br />--output json" Add this alias to your ~/.bashrc or ~/.zshrc, then run source ~/.bashrc (or equivalent shell).</p>
<p><strong>Step 2:</strong> Run the Alias &amp; Save Output</p>
<p>aws-cost &gt; cost-report.json.</p>
<p><strong>Step 3:</strong> Python Script to Convert JSON → Filtered Slack Message</p>
<pre><code class="language-python">import json
import requests

# Load cost report
with open('cost-report.json') as f:
    data = json.load(f)

# Extract relevant data
results = data['ResultsByTime'][0]['Groups']
date_range = data['ResultsByTime'][0]['TimePeriod']
start = date_range['Start']
end = date_range['End']

non_zero_services = []
for group in results:
    service = group['Keys'][0]
    amount = float(group['Metrics']['UnblendedCost']['Amount'])
    if amount &gt; 0:
        non_zero_services.append((service, amount))

# Format for Slack
if not non_zero_services:
    message = f"*AWS Cost Report for {start} to {end}*\nNo charges yesterday. 🎉"
else:
    message = f"*AWS Cost Report for {start} to {end}*"
    for service, amount in sorted(non_zero_services, key=lambda x: x[1], reverse=True):
        message += f"\n• `{service}`: *${amount:.2f}*"

# Send to Slack
SLACK_WEBHOOK_URL = "https://hooks.slack.com/services/your/webhook/url"

payload = {"text": message}

response = requests.post(SLACK_WEBHOOK_URL, json=payload)
print("Slack Response:", response.status_code, response.text)

</code></pre>
<p><strong>Step 4: Add to Cron (Optional Automation)</strong></p>
<p>Run daily at 8 AM:</p>
<p>crontab -e</p>
<p>Add this line:</p>
<p>0 8 * * * /bin/bash -c 'source ~/.bashrc &amp;&amp; aws-cost &gt; ~/cost-report.json &amp;&amp; python3 ~/aws_cost_<a href="http://slack.py">slack.py</a>'</p>
<h2><strong>Optional: CSV Export (if needed)</strong></h2>
<p>You can also convert cost-report.json to CSV using Python:</p>
<p>import csv</p>
<p>with open('cost-report.json') as f:</p>
<p>data = json.load(f)</p>
<p>results = data['ResultsByTime'][0]['Groups']</p>
<p>with open('cost-report.csv', 'w', newline='') as csvfile:</p>
<p>writer = csv.writer(csvfile)</p>
<p>writer.writerow(['Service', 'Amount'])</p>
<p>for group in results:</p>
<p>service = group['Keys'][0]</p>
<p>amount = float(group['Metrics']['UnblendedCost']['Amount'])</p>
<p>if amount &gt; 0:</p>
<p>writer.writerow([service, f"${amount:.2f}"])</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/7bcbe0de-8bcf-4406-977d-8688a4261c15.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/6a78f20b-5d02-419f-bb5f-3b9595e9c4f0.png" alt="" style="display:block;margin:0 auto" />]]></content:encoded></item><item><title><![CDATA[      EC2 Cleanup & Monitoring
   Monitor • Review • Clean • Log ]]></title><description><![CDATA[PROBLEMAn EC2 server was running low on disk space. I needed to clean it up without accidentally deleting important files or affecting running…]]></description><link>https://vishnusai.hashnode.dev/ec2-cleanup-monitoring-monitor-review-clean-log</link><guid isPermaLink="true">https://vishnusai.hashnode.dev/ec2-cleanup-monitoring-monitor-review-clean-log</guid><dc:creator><![CDATA[vishnu sai]]></dc:creator><pubDate>Wed, 30 Sep 2026 04:28:16 GMT</pubDate><content:encoded><![CDATA[<p><strong>PROBLEM</strong><br />An EC2 server was running low on disk space. I needed to clean it up without accidentally deleting important files or affecting running applications.</p>
<p><strong>HOW I DEBUGGED IT</strong><br />I started by checking CPU, memory, disk usage, EBS volumes, large files, and running processes before taking any action.</p>
<p><strong>THE SOLUTION</strong><br />I created a Python-based <strong>EC2 Cleanup &amp; Monitoring script</strong> with:</p>
<ul>
<li><p><strong>Auto Mode</strong> – performs predefined cleanup tasks</p>
</li>
<li><p><strong>Interactive Mode</strong> – reviews actions before applying them</p>
</li>
<li><p><strong>JSON Logging</strong> – records cleanup activities</p>
</li>
</ul>
<p><strong>MY APPROACH</strong></p>
<p><strong>Check → Review → Approve → Clean → Log</strong></p>
<p>This keeps the cleanup process controlled and makes it easier to understand what happened.</p>
<p><strong>Has anyone else faced a similar EC2 cleanup problem?</strong><br /><strong>How did you handle it?</strong><br /><strong>How did you fix it?</strong></p>
<p><strong>HashnodProfile:</strong><a href="https://hashnode.com/@vishnusaiK">https://hashnode.com/@vishnusaiK</a> <br /><strong>GitHubProfile :</strong> <a href="https://github.com/ksaivishnusaikvs/DevOps-Engineering/tree/main/Linkedin/Post">https://github.com/ksaivishnusaikvs/DevOps-Engineering/tree/main/Linkedin/Post</a></p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/a548b580-53a0-4803-8687-32aaf3f80a17.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Step1: Check System Health</strong>   Run the cleanup script  Select execution mode ./<a href="http://cleanup.py">cleanup.py</a></p>
<p>1 → Auto Mode</p>
<p>2 → Interactive Mode    <strong>Interactive Mode is recommended for production</strong> because it asks for approval before critical actions</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/b8b52352-90de-408d-8239-6022c1cd2182.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Step2: Check High-Resource Processes</strong></p>
<p>Next, identify the processes consuming the most system resources. The script displays the <strong>Top 20 CPU-consuming processes and Top 20 memory-consuming processes</strong>, including the <strong>PID, command, CPU percentage, and memory percentage</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/87b587f8-af4c-4896-b670-bf29fc254f29.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Step3: Review EBS Volumes</strong></p>
<p>After checking the EC2 instance, review the attached EBS volumes. The script displays important information such as the Volume ID, volume state, size, and Availability Zone.</p>
<p>Next, <strong>identify large files/directories and review processes or files</strong> before killing processes or deleting unnecessary files.</p>
<p>Enter the required <strong>folder path</strong>, and the script scans the path and displays the <strong>top storage-consuming files and directories</strong></p>
<p><strong>Review Processes</strong> – Approve PIDs before killing them.</p>
<p><strong>Review Files</strong> – Approve files before deleting them.</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/c66ee554-e2b2-4628-ac7a-e33eaeb00667.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Step4: Check Cleanup Logs</strong></p>
<p>All important cleanup activities are recorded in JSON log format. The logs capture details such as the Linux user, sudo user, Linux groups, EUID, IAM role, event, timestamp, and action details, providing an audit trail of the cleanup activity</p>
<p><strong>Check logs :</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/90075ac1-b6ff-4ae0-821f-9700c2d33421.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/09306534-f0fe-4130-9cac-19c3e1c4a1e1.png" alt="" style="display:block;margin:0 auto" />

<pre><code class="language-py">#!/usr/bin/env python3
"""
Production Cleanup &amp; Monitoring Script
"""                                                                 

import os
import json
import subprocess
import datetime
import getpass
import grp
from pathlib import Path
from rich.console import Console
from rich.prompt import Prompt
from rich.table import Table

console = Console()

# Log file setup: try /var/log first, else fallback to ~/logs
DEFAULT_LOG = Path("/var/log/prod_cleanup.log")
FALLBACK_LOG = Path.home() / "logs" / "prod_cleanup.log"
LOG_FILE = DEFAULT_LOG if os.access(DEFAULT_LOG.parent, os.W_OK) else FALLBACK_LOG


def show_description():
    console.print("""
[bold cyan]==========================================================[/bold cyan]
[bold yellow]Script Modes:[/bold yellow]

[bold green]1. Auto Mode (1)[/bold green]
   - The script runs automatically with predefined actions.
   - Kills processes, deletes files, scans disks, and logs everything.
   - No prompts appear for user input.
   - [red]Use with caution:[/red] only for safe/staging environments or
     pre-approved process/file lists.

[bold green]2. Interactive Mode (2) [Default][/bold green]
   - The script pauses and asks for input before every critical action.
     - Which PIDs to kill
     - Which files to delete
     - Folder paths for disk scan
   - Requires approval from an authorized user before killing/deleting.
   - [yellow]Safer for production environments.[/yellow]

[bold magenta]Note:[/bold magenta]
- Interactive mode is recommended for production.
- Auto mode is faster but may act on processes/files without confirmation.
[bold cyan]==========================================================[/bold cyan]
""")


def log_event(event, detail):
    """Log event in JSON format with safe IAM role detection"""
    ts = datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
    linux_user = getpass.getuser()
    sudo_user = os.environ.get("SUDO_USER", linux_user)
    linux_groups = ",".join(grp.getgrgid(g).gr_name for g in os.getgroups())
    euid = os.geteuid()

    # Detect IAM role safely
    try:
        role_name = subprocess.check_output(
            ["curl", "-s", "http://169.254.169.254/latest/meta-data/iam/security-credentials/"],
            text=True, timeout=2
        ).strip()
        if role_name and "Unauthorized" not in role_name:
            iam_role = role_name
        else:
            iam_role = "none"
    except Exception:
        iam_role = "none"

    log_data = {
        "ts": ts,
        "linux_user": linux_user,
        "sudo_user": sudo_user,
        "linux_groups": linux_groups,
        "euid": euid,
        "iam_role": iam_role,
        "event": event,
        "detail": detail
    }
    LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
    with LOG_FILE.open("a") as f:
        f.write(json.dumps(log_data) + "\n")


def run_cmd(cmd):
    try:
        return subprocess.check_output(cmd, shell=True, text=True)
    except subprocess.CalledProcessError:
        return ""


def system_monitor():
    console.print("\n[bold cyan]======= SYSTEM MONITOR =======[/bold cyan]")
    uptime_output = run_cmd("uptime | awk -F'load average:' '{print $2}'").strip()
    console.print(f"[bold yellow]CPU Load:[/bold yellow] {uptime_output}")
    console.print(run_cmd("free -h"))
    console.print(run_cmd("df -h | grep -E '^/dev|Filesystem'"))
    log_event("monitor", "System stats displayed")


def show_top_processes():
    # CPU
    console.print("\n[bold cyan]======= TOP 20 CPU PROCESSES =======[/bold cyan]")
    cpu_output = run_cmd("ps -eo pid,ppid,comm,%cpu,%mem --sort=-%cpu | head -n 21").splitlines()
    table_cpu = Table(show_header=True, header_style="bold magenta")
    table_cpu.add_column("PID", justify="right")
    table_cpu.add_column("PPID", justify="right")
    table_cpu.add_column("Command", overflow="fold")
    table_cpu.add_column("%CPU", justify="right")
    table_cpu.add_column("%MEM", justify="right")

    for line in cpu_output[1:]:
        parts = line.split(None, 4)
        if len(parts) == 5:
            table_cpu.add_row(*parts)
    console.print(table_cpu)

    # Memory
    console.print("\n[bold cyan]======= TOP 20 MEMORY PROCESSES =======[/bold cyan]")
    mem_output = run_cmd("ps -eo pid,ppid,comm,%cpu,%mem --sort=-%mem | head -n 21").splitlines()
    table_mem = Table(show_header=True, header_style="bold magenta")
    table_mem.add_column("PID", justify="right")
    table_mem.add_column("PPID", justify="right")
    table_mem.add_column("Command", overflow="fold")
    table_mem.add_column("%CPU", justify="right")
    table_mem.add_column("%MEM", justify="right")

    for line in mem_output[1:]:
        parts = line.split(None, 4)
        if len(parts) == 5:
            table_mem.add_row(*parts)
    console.print(table_mem)

    log_event("monitor", "Top CPU and memory processes displayed")
    return cpu_output, mem_output


def kill_processes(process_list, label):
    pids = Prompt.ask(f"Enter PIDs to kill from {label} list (space separated, blank to skip)", default="")
    if pids:
        for pid in pids.split():
            try:
                os.kill(int(pid), 9)
                console.print(f"[green]  Killed process {pid}[/green]")
                log_event("kill", f"Process {pid} killed from {label}")
            except Exception:
                console.print(f"[red]  Failed to kill {pid}[/red]")


def list_ebs_volumes():
    console.print("\n[bold cyan]======= EBS VOLUMES (Top 20) =======[/bold cyan]")
    if run_cmd("which aws"):
        volumes = run_cmd(
            "aws ec2 describe-volumes --query \"Volumes[*].{ID:VolumeId,State:State,Size:Size,AZ:AvailabilityZone}\" --output table | head -n 22"
        )
        console.print(volumes if volumes else "[yellow]No volumes found[/yellow]")
        log_event("ebs", "EBS volumes listed")
    else:
        console.print("[yellow]AWS CLI not installed. Skipping EBS volume listing.[/yellow]")
        log_event("ebs", "AWS CLI not installed")


def disk_usage():
    path = Prompt.ask("Enter folder path to scan", default="/")
    console.print(f"\n[bold cyan]======= DISK USAGE ({path}) =======[/bold cyan]")
    usage = run_cmd(f"du -ah {path} 2&gt;/dev/null | sort -rh | head -n 20")
    console.print(usage)
    log_event("disk", f"Top storage usage scanned in path {path}")


def delete_files():
    files = Prompt.ask("Enter full file paths to delete (space separated, blank to skip)", default="")
    if files:
        for f in files.split():
            try:
                os.remove(f)
                console.print(f"[green]  Deleted {f}[/green]")
                log_event("delete", f"File {f} deleted")
            except FileNotFoundError:
                console.print(f"[yellow]   File {f} not found[/yellow]")


def main():
    show_description()
    mode = Prompt.ask("Choose Mode: [green]1=Auto[/green], [yellow]2=Interactive[/yellow]", default="2")

    system_monitor()
    cpu_list, mem_list = show_top_processes()
    list_ebs_volumes()

    if mode == "2":
        kill_processes(cpu_list, "CPU")
        kill_processes(mem_list, "Memory")
        disk_usage()
        delete_files()

    log_event("finish", "Cleanup completed")
    console.print("\n[bold green]   Cleanup Completed![/bold green]")


if __name__ == "__main__":
    main()   

If run the script 






</code></pre>
]]></content:encoded></item><item><title><![CDATA[
        Linux Troubleshooting with TUI Tools
                    Logs • System Health • Disk]]></title><description><![CDATA[While troubleshooting Linux servers, I was jumping between different commands to check logs, services, CPU, memory, and disk usage. It was taking more time…]]></description><link>https://vishnusai.hashnode.dev/linux-troubleshooting-with-tui-tools-logs-system-health-disk</link><guid isPermaLink="true">https://vishnusai.hashnode.dev/linux-troubleshooting-with-tui-tools-logs-system-health-disk</guid><dc:creator><![CDATA[vishnu sai]]></dc:creator><pubDate>Tue, 22 Sep 2026 04:33:36 GMT</pubDate><content:encoded><![CDATA[<h3><strong>PROBLEM</strong></h3>
<p>While troubleshooting Linux servers, I was jumping between different commands to check logs, services, CPU, memory, and disk usage. It was taking more time than I wanted.</p>
<p><strong>HOW I DEBUGGED IT</strong></p>
<p>I started trying a few TUI tools to get a quick view of what was happening on the server.</p>
<p>This made it easier to investigate logs, system health, and disk usage directly from the terminal.</p>
<h3><strong>THE SOLUTION</strong></h3>
<p>These tools helped me spot service errors and resource issues faster. I could see more information in one place instead of switching between multiple commands. It made my Linux troubleshooting workflow much simpler and faster.</p>
<p><strong>Linux Issue → Check Logs &amp; System Health → TUI Tools → Identify the Problem → Fix</strong></p>
<p><strong>Have you faced a similar problem?<br />What tools or commands do you use for Linux troubleshooting?</strong></p>
<p><strong>How did you fix it?</strong><br /><strong>HashnodProfile:</strong><a href="https://hashnode.com/@vishnusaiK">https://hashnode.com/@vishnusaiK</a><br /><strong>GitHub :</strong> <a href="https://github.com/ksaivishnusaikvs">https://github.com/ksaivishnusaikvs</a></p>
<p><strong>Lnav: lnav</strong>      What I like about lnav</p>
<p>Search &amp; filter logs quickly — find errors, warnings, and specific events without chaining multiple commands.</p>
<p>Easy log navigation — work with timestamps and follow live logs in an interactive terminal UI.</p>
<p>One place for troubleshooting — combines many common log-viewing tasks into a simple TUI instead of switching between grep, tail, awk, and less.</p>
<p>lnav file = analyze log files; helps you investigate those logs interactively.</p>
<p>journalctl | lnav = analyze systemd journal logs; get  the Nginx service logs.</p>
<p><strong>Problem                               Command                                                  What It Solves</strong></p>
<p><strong>--------------------------------------------------------------------------------</strong></p>
<p><strong>Service Failing</strong>         journalctl -u nginx.service | lnav              Investigate systemd service errors</p>
<p><strong>Container Issue</strong>            docker logs nginx 2&gt;&amp;1 | lnav             Investigate container logs</p>
<p><strong>Live Container Issue</strong>    docker logs -f nginx 2&gt;&amp;1 | lnav          Watch errors as they happen</p>
<p><strong>Application Issue</strong>         lnav /var/log/nginx/access.log              Analyze a specific log file</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/e5da13ce-4509-40f2-bdde-4945c0afc3a7.png" alt="" style="display:block;margin:0 auto" />

<p><strong>tui-logs</strong></p>
<p><strong>tui-logs mainly solves the problem of manually searching through large log files using multiple Linux commands.</strong></p>
<p><strong>What it solves</strong>Too many logs → quickly search and filter the logs you need.Hard to troubleshoot errors → easily find errors, warnings, and related events.</p>
<p>Live-log monitoring → watch new logs in real time from one interactive terminal.</p>
<p>Too many commands → reduces the need to switch between cat, grep, tail, less, and awk.</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/14ac3a68-95d6-4993-896b-3149ff8e8cd7.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/7b0f6782-8f84-4495-84d9-f34ca16ff31d.png" alt="" style="display:block;margin:0 auto" />

<p><strong>SysWatch:</strong>     <strong>syswatch</strong></p>
<p>Gives a single live view of Linux CPU, memory, disk, network, processes, and services.</p>
<p><strong>Main problem:</strong> Quickly identifies failed services, resource issues, and system health problems.</p>
<p><strong>Production benefit:</strong> Enables faster troubleshooting and helps prevent server/application downtime.</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/a07fceeb-8801-4d30-89de-e4d7972b42a4.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/55286f69-b873-48ec-992d-65a703909cbd.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Tui-disk: tui-disk</strong></p>
<p>Shows all disks, partitions, mounts, usage, and health in one simple view.</p>
<p>Production benefit: Quickly finds <strong>f</strong>ull disks, mount problems, and storage issues before they cause outages.</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/849799ac-54d0-43ec-b6d7-9b2e5739398d.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/a1f5b6fb-b5d8-49d5-83af-d496d2c761cb.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Superfile</strong> is a simple TUI file manager that brings a visual file-browser experience directly into the terminal.</p>
<p>●   📁 <strong>Easy navigation</strong> — browse files and folders quickly.</p>
<p>●  🔄 <strong>File management</strong> — copy, move, rename, and delete files from one interface.</p>
<p>● ⚡ <strong>Less typing</strong> — reduces the need for repeated ls, cd, cp, mv, and rm commands.</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/d0850470-74f9-4abe-9c07-6ba45253c525.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/f94c32e9-d463-4400-b8e9-dce8bc486252.png" alt="" style="display:block;margin:0 auto" />]]></content:encoded></item><item><title><![CDATA[     Terraform and CDK (CDKTF) ]]></title><description><![CDATA[Deployed AWS Infrastructure with Python + Terraform CDKTF PROBLEM I wanted to create an AWS S3 bucket using Python + CDKTF instead of writing Terraform…]]></description><link>https://vishnusai.hashnode.dev/terraform-and-cdk-cdktf</link><guid isPermaLink="true">https://vishnusai.hashnode.dev/terraform-and-cdk-cdktf</guid><dc:creator><![CDATA[vishnu sai]]></dc:creator><pubDate>Wed, 09 Sep 2026 06:51:45 GMT</pubDate><content:encoded><![CDATA[<p><strong>Deployed AWS Infrastructure with Python + Terraform CDKTF</strong></p>
<p><strong>PROBLEM</strong></p>
<p>I wanted to create an AWS S3 bucket using <strong>Python + CDKTF</strong> instead of writing Terraform HCL. I also needed versioning, encryption, public access fizefsh, and proper tags.</p>
<p><strong>HOW I DEBUGGED IT</strong></p>
<p>First, I checked my AWS access. I checked the Python and package versions first.</p>
<p>Instead of changing the system Python,</p>
<p>I created a <strong>virtual environment (venv)</strong> and installed the required packages there.</p>
<p>Normal/system Python → you tried it → it did NOT work</p>
<p>Python venv → you tried it → it WORKED   Then I tested the Python code and ran</p>
<p><strong>cdktf synth</strong></p>
<p>After that, I ran:</p>
<p><strong>cdktf diff</strong>       to make sure everything looked right before deployment.</p>
<p><strong>THE SOLUTION</strong></p>
<p><strong>cdktf deploy</strong></p>
<p>The S3 bucket was created successfully with versioning, encryption, public access blocked, and proper tags.</p>
<p><strong>Has anyone else faced this problem?</strong></p>
<p><strong>Have you used Python + CDKTF for AWS infrastructure?</strong></p>
<p><strong>How did you fix it?</strong></p>
<p><strong>Note:</strong> All infrastructure images, official docs, and videos links  are included on the last page<br />**GitHub:**<a href="https://github.com/ksaivishnusaikvs/DevOpsEngineering/tree/main/Linkedin">https://github.com/ksaivishnusaikvs/DevOpsEngineering/tree/main/Linkedin</a></p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/0a99e25f-144f-41e5-879b-4056d20262dd.png" alt="terraform platform" style="display:block;margin:0 auto" />

<p><strong>2. Prerequisites</strong></p>
<p>Ubuntu EC2 instance, Python 3, pip, Node.js/npm, CDKTF CLI, Terraform CLIAWS CLIAWS credentials or an IAM role with permission to create the required S3 resources</p>
<p><strong>Verify AWS Access</strong> aws sts get-caller-identity</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/a62ea38d-d379-4cb4-b7d6-8eee7dd47a4c.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Step 1:</strong> Create infrastructure: Create the CDKTF Project</p>
<p>cd ~<br />rm -rf prod-s3<br />mkdir prod-s3<br />cd prod-s3<br />python3 -m venv .venv<br />source .venv/bin/activate</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/0e6bca97-0038-477a-801b-3e2c1a1fc96d.png" alt="" style="display:block;margin:0 auto" />

<p>pip install --upgrade pip<br />pip install cdktf constructs cdktf-cdktf-provider-aws</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/e9945b56-82a2-45b0-a990-5432be5341ff.png" alt="" style="display:block;margin:0 auto" />

<p><strong>STEP2:</strong>  Create the CDKTF configuration</p>
<p>cat &gt; cdktf.json &lt;&lt;'EOF'<br />{<br />  "language": "python",<br />  "app": "python3 <a href="http://main.py">main.py</a>"<br />}<br />EOF</p>
<p>This tells CDKTF that the application entry point is <a href="http://main.py">main.py</a> and that Python is the projectlanguage.</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/0a8ffd60-de63-41ae-9cc3-5c27f5ea8bcb.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/d4df95e8-7535-492a-88d4-5684c24ebc8b.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Step 3:</strong> Write code for infra Python Infrastructure Code. Create the infrastructure file</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/cdc0227a-355b-47c2-a560-ff1c60be8103.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/9aca6a4a-e3d5-4a92-92f6-7414bccecfd2.png" alt="" style="display:block;margin:0 auto" />

<pre><code class="language-python">from cdktf import App, TerraformStack
from constructs import Construct

from cdktf_cdktf_provider_aws.provider import AwsProvider
from cdktf_cdktf_provider_aws.s3_bucket import S3Bucket
from cdktf_cdktf_provider_aws.s3_bucket_versioning import S3BucketVersioningA
from cdktf_cdktf_provider_aws.s3_bucket_public_access_block import (
	S3BucketPublicAccessBlock
)
from cdktf_cdktf_provider_aws.s3_bucket_server_side_encryption_configuration import (
    S3BucketServerSideEncryptionConfigurationA
)


class ProdS3Stack(TerraformStack):

	def __init__(self, scope: Construct, stack_id: str):
    	super().__init__(scope, stack_id)

    	# AWS provider
    	AwsProvider(
        	self,
        	"aws",
        	region="ap-south-1"
    	)

    	# S3 bucket
    	bucket = S3Bucket(
        	self,
        	"prod-bucket",
            bucket="kv-prod-s3-demo-20260909",
        	tags={
            	"Environment": "prod",
            	"Application": "my-app",
            	"ManagedBy": "cdktf"
        	}
    	)

    	# Versioning
    	S3BucketVersioningA(
        	self,
        	"versioning",
        	bucket=bucket.id,
        	versioning_configuration={
            	"status": "Enabled"
        	}
    	)

    	# Block public access
    	S3BucketPublicAccessBlock(
        	self,
        	"public-access",
        	bucket=bucket.id,
        	block_public_acls=True,
        	block_public_policy=True,
        	ignore_public_acls=True,
        	restrict_public_buckets=True
    	)

    	# Server-side encryption
        S3BucketServerSideEncryptionConfigurationA(
        	self,
        	"encryption",
        	bucket=bucket.id,
        	rule=[
            	{
                    "apply_server_side_encryption_by_default": [
                    	{
                            "sse_algorithm": "AES256"
                    	}
                	]
            	}
        	]
    	)


app = App()

ProdS3Stack(app, "prod-s3")

app.synth()




</code></pre>
<p><strong>Step 4:</strong> What the Python Code Creates</p>
<p><strong>AWS Provider:</strong> Uses ap-south-1 as the AWS region.</p>
<p><strong>S3 Bucket:</strong> Creates the production bucket.</p>
<p><strong>Tags:</strong> Adds Environment, Application, and ManagedBy tags for ownership and FinOps tracking.</p>
<p><strong>Versioning:</strong> Keeps object versions so accidental changes/deletions can be recovered.</p>
<p><strong>Public Access Block:</strong> Blocks public ACLs and public bucket policies.</p>
<p><strong>Encryption:</strong> Enables S3 server-side encryption using AES256.</p>
<p><strong>Step5:   python3</strong> <a href="http://main.py"><strong>main.py</strong></a></p>
<p>Test the Python Code. If there is no Python traceback/error, the CDKTF application code is valid enough to synthesize.</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/8c1a95ee-a028-4e55-a15d-5963a3e04ee4.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Step6: cdktf synth</strong> CDKTF converts the Python infrastructure definition into Terraform configuration under the generated CDKTF output.</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/28af576b-3c01-4f84-9078-c73e7a004465.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Step7: cdktf diff</strong> Review the resources before deployment. In the successful setup, the plan showed 4 resources to add and 0 to change or destroy.</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/1e334eb1-ed28-4d05-9abc-a18474a6bbf3.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/84b33ea4-e4f1-45bf-bc5b-ccec70c045b2.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Step8: cdktf deploy</strong> When CDKTF asks for approval, choose Approve/yes. Terraform then creates the AWS resources</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/b81ffa55-c249-4776-92c8-6dd4b3da2867.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Step 9:</strong> See the image below; infra was created with Terraform/CDKTF using Python, and the data was validated</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/c2ea9de4-7534-4a85-bb77-9948158ff637.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Step 10:</strong> Verify whether the S3 Bucket  infra is created or not in the cloud are not state file  create not in local</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/f21b6df0-97cd-4879-a8e3-cb40623f945c.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Step 11:</strong> Verify in the cloud ui the infrastructure resource is crete are not  in cloud UI</p>
<p>my s3 create throw CDKTF; name: s3    <strong>kv-prod-s3-demo-20260909</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/342b3246-f0db-45dc-b24a-12d48d3ebfe8.png" alt="" style="display:block;margin:0 auto" />

<p>TAGS show the Terraform file; compare the two images</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/1d0deed8-980c-4c7f-9280-a31a30dfc01e.png" alt="" style="display:block;margin:0 auto" />

<p>compare the tags in the below image both are matched</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/cab6f911-8e07-495a-a837-c749095c22c4.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/04454c09-d1cb-41c2-a1da-ec33f599c0e4.png" alt="" style="display:block;margin:0 auto" />

<p>bucker version has enable</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/b9e1c63b-f323-4635-a81e-afe4c10b1c5f.png" alt="" style="display:block;margin:0 auto" />

<p>block the public acess</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/43be12e9-b788-440d-a3fa-b1b2ffc9dfac.png" alt="" style="display:block;margin:0 auto" />

<p>encryption has enabled</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/6a111b81-8b4b-4df0-b6c6-824010a01dfc.png" alt="" style="display:block;margin:0 auto" />

<p><strong>REFDOC:</strong></p>
<p><strong>Hashicorp Ofiical :</strong> <a href="https://developer.hashicorp.com/terraform/cdktf">https://developer.hashicorp.com/terraform/cdktf</a></p>
<p><strong>AWS:</strong> <a href="https://aws.amazon.com/blogs/opensource/announcing-cdk-for-terraform-on-aws/">https://aws.amazon.com/blogs/opensource/announcing-cdk-for-terraform-on-aws/</a></p>
<p><strong>installsteps:</strong> 1:<a href="https://spacelift.io/blog/terraform-cdk">https://spacelift.io/blog/terraform-cdk</a></p>
<p>2:<a href="https://dev.to/aws-builders/cdk-for-terraform-cdktf-on-aws-how-to-configure-an-s3-remote-backend-and-deploy-a-lambda-function-url-using-python-3okk">https://dev.to/aws-builders/cdk-for-terraform-cdktf-on-aws-how-to-configure-an-s3-remote-backend-and-deploy-a-lambda-function-url-using-python-3okk</a></p>
<p><strong>CDKTF official HashiCorp channel:</strong> <a href="https://www.youtube.com/watch?v=Y9suaDVT-SY&amp;t=27s">https://www.youtube.com/watch?v=Y9suaDVT-SY&amp;t=27s</a></p>
<p><a href="https://developer.hashicorp.com/terraform/tutorials/cdktf/cdktf-install">https://developer.hashicorp.com/terraform/tutorials/cdktf/cdktf-install</a></p>
<p><strong>DOCsteps:</strong>  <a href="https://spacelift.io/blog/terraform-cdk">https://spacelift.io/blog/terraform-cdk</a></p>
]]></content:encoded></item><item><title><![CDATA[         Automated EC2 Health Monitoring 
                                             &
                        Multi-Channel Alerts]]></title><description><![CDATA[When managing multiple EC2 instances, manually checking every server is time-consuming. If an instance stops or has a health issue, it can easily go…]]></description><link>https://vishnusai.hashnode.dev/automated-ec2-health-monitoring-multi-channel-alerts</link><guid isPermaLink="true">https://vishnusai.hashnode.dev/automated-ec2-health-monitoring-multi-channel-alerts</guid><dc:creator><![CDATA[vishnu sai]]></dc:creator><pubDate>Sat, 29 Aug 2026 03:32:01 GMT</pubDate><content:encoded><![CDATA[<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/68e7aaf1-d2f7-4ea4-86bb-7370001fd5a7.png" alt="" style="display:block;margin:0 auto" />

<h3>PROBLEM</h3>
<p>When managing multiple EC2 instances, manually checking every server is time-consuming. If an instance <strong>stops or has a health issue</strong>, it can easily go unnoticed.</p>
<p><strong>HOW I DEBUGGED IT</strong></p>
<p>I wanted to avoid checking each instance manually, so I automated the health checks. I monitor the Running/Stopped state, 1/2 Status Checks, CPU, Memory, and Disk utilization across all EC2 instances.</p>
<p><strong>THE SOLUTION</strong></p>
<p><strong>EventBridge → Lambda → EC2 / CloudWatch / SSM → Slack</strong></p>
<p>Every 5 minutes, Lambda checks all EC2 instances. If an instance stops or any issue is detected, it automatically sends the details to Slack.</p>
<p>This helps me quickly identify which server has the problem, what went wrong, and where to start troubleshooting, without manually checking every instance.</p>
<p>Result: Less manual monitoring, faster issue detection, and quicker troubleshooting.</p>
<p><strong>Has anyone else faced this problem?</strong></p>
<p><strong>How did you fix it?</strong></p>
<p><strong>Step 1:</strong> Create the Lambda function that will perform the EC2 health checks. Configure the function with the required Python runtime and prepare it to execute the monitoring code.</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/f165e0e2-c41e-4471-8e09-46475f2ab4f7.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Step 2:</strong> Configure Lambda. Configure the Lambda function with the required environment variables, including EC2 instance IDs, AWS Region, alert thresholds, Slack/Teams webhook URLs, and SNS topic ARN.</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/098564b0-60f3-4f58-a1ec-4c0f5ea98b33.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Step 3:</strong> Configure the Lambda function with the required environment variables, including EC2 instance IDs</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/dcff626b-3f7b-4973-a7c5-8e502494a155.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Step 4:</strong> Deploy the Lambda function and run a test execution. Verify that the function can successfully collect the EC2 health information and return the expected response Now we test and deploy</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/1bf36518-c361-43c9-a83c-c4a3e14241c2.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Step 5:</strong> Status. After running the test, check the execution result and logs. Confirm that the Lambda successfully checks the configured EC2 instance and produces the health information required for the alert.Status</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/40e663fd-8f11-4c70-9900-572f15226a53.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/906dd68c-2c89-4fc2-937b-23917ebd93c0.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Step 6:</strong> Click <strong>Deploy</strong> to deploy the Lambda function. Review the <strong>Slack notification</strong> shown below. With one click, we can see the data in the Slack Notification</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/e950e8d3-10f5-47e4-befa-25baf1526274.png" alt="" style="display:block;margin:0 auto" />

<p>one cick the data we can see slack Notification open the channal</p>
<p><strong>Step 7:</strong> Receive the EC2 alert in Slack with clear information about the alert/warning, instance ID, server name, region, status, and timestamp.</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/6c7c5aea-c627-499d-b7dd-80594af77ff0.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Step 8:</strong> Click on the navigation buttons for the EC2 Console. Check the EC2 instance status to confirm whether the server is Running, Stopped, or in a transitional state before performing the recovery action.Click on the navigation buttons <strong>EC2 Console</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/5431ec92-8777-4dbf-bd74-2ae5704b1e15.png" alt="" style="display:block;margin:0 auto" />

<p>●       Click on the navigation buttons <strong>Cloudwatch</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/f437edb3-d47b-4dc5-ba02-bd51709dcdec.png" alt="" style="display:block;margin:0 auto" />

<p>Navigate the <strong>CloudWatch logs</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/703d03d3-62af-4e7c-bf25-f381273f1be0.png" alt="" style="display:block;margin:0 auto" />

<p><strong>SessionManager</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/63e06ac4-14c7-43b5-8822-cfff395d2d4f.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/ace30c55-d0ef-43b8-829b-e858bb3d181f.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/dfda06ce-d78d-4f05-a76e-ce43e984079f.png" alt="" style="display:block;margin:0 auto" />

<pre><code class="language-pyton">Ec2_monitor.py

==============
Lambda function — runs on a schedule (EventBridge every 5 min).

Checks for EACH EC2 instance (public + private):
  1. CPU utilization        — CloudWatch metric AWS/EC2 (always free, no agent)
  2. Status checks 1/2 &amp; 2/2 — CloudWatch metric AWS/EC2 (always free, no agent)
  3. Memory utilization     — SSM Run Command reads /proc/meminfo (no agent needed)
  4. Disk utilization       — SSM Run Command reads df (no agent needed)

Sends rich alerts to:
  - Slack  (Block Kit cards with action buttons)
  - Teams  (Adaptive Cards with action buttons)
  - Email  (via SNS — already subscribed in your infra)

Environment variables required:
  SLACK_WEBHOOK_URL     Slack incoming webhook
  TEAMS_WEBHOOK_URL     Teams incoming webhook
  SNS_TOPIC_ARN         SNS topic ARN for email
  INSTANCE_IDS          Comma-separated: i-aaa,i-bbb,i-ccc
  CPU_WARN_THRESHOLD    Default 80
  CPU_CRIT_THRESHOLD    Default 95
  MEM_WARN_THRESHOLD    Default 85
  DISK_WARN_THRESHOLD   Default 85
  AWS_REGION_NAME       e.g. ap-south-1  (note: not AWS_REGION — that's reserved)
"""

import json
import os
import urllib.request
import urllib.error
import boto3
import time
from datetime import datetime, timedelta, timezone
from typing import Optional

# ── Config from environment ────────────────────────────────────────────────────
SLACK_WEBHOOK_URL  = os.environ.get("SLACK_WEBHOOK_URL", "https://hooks.slack.com/services/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")
TEAMS_WEBHOOK_URL  = os.environ.get("TEAMS_WEBHOOK_URL", "")
SNS_TOPIC_ARN      = os.environ.get("SNS_TOPIC_ARN", "")
REGION             = os.environ.get("AWS_REGION_NAME", "eu-north-1")
ENVIRONMENT        = os.environ.get("ENVIRONMENT", "prod")

INSTANCE_IDS: list[str] = [
    i.strip() for i in os.environ.get("INSTANCE_IDS", "i-00430b1a074d82e2c").split(",") if i.strip()
]

CPU_WARN  = float(os.environ.get("CPU_WARN_THRESHOLD",  "80"))
CPU_CRIT  = float(os.environ.get("CPU_CRIT_THRESHOLD",  "80"))
MEM_WARN  = float(os.environ.get("MEM_WARN_THRESHOLD",  "80"))
DISK_WARN = float(os.environ.get("DISK_WARN_THRESHOLD", "80"))

# ── AWS clients ────────────────────────────────────────────────────────────────
cw  = boto3.client("cloudwatch",  region_name=REGION)
ec2 = boto3.client("ec2",         region_name=REGION)
ssm = boto3.client("ssm",         region_name=REGION)
sns = boto3.client("sns",         region_name=REGION)


# ══════════════════════════════════════════════════════════════════════════════
# METRIC COLLECTORS
# ══════════════════════════════════════════════════════════════════════════════

def get_cloudwatch_metric(instance_id: str, metric_name: str, namespace: str = "AWS/EC2") -&gt; Optional[float]:
    """
    Fetch the latest average value of a CloudWatch metric for an instance.
    Uses the last 10 minutes window to get the most recent data point.
    Returns None if no data is available.
    """
    now = datetime.now(timezone.utc)
    resp = cw.get_metric_statistics(
        Namespace=namespace,
        MetricName=metric_name,
        Dimensions=[{"Name": "InstanceId", "Value": instance_id}],
        StartTime=now - timedelta(minutes=10),
        EndTime=now,
        Period=300,
        Statistics=["Average", "Maximum"],
    )
    datapoints = resp.get("Datapoints", [])
    if not datapoints:
        return None
    latest = max(datapoints, key=lambda d: d["Timestamp"])
    return round(latest["Average"], 2)


def get_status_checks(instance_id: str) -&gt; dict:
    """
    Returns status check results directly from EC2 API.
    status = 'ok' | 'impaired' | 'insufficient-data' | 'not-applicable'
    """
    try:
        resp = ec2.describe_instance_status(
            InstanceIds=[instance_id],
            IncludeAllInstances=True,
        )
        statuses = resp.get("InstanceStatuses", [])
        if not statuses:
            return {"system": "unknown", "instance": "unknown", "state": "unknown"}

        s = statuses[0]
        return {
            "state":    s["InstanceState"]["Name"],
            "system":   s["SystemStatus"]["Status"],    # ok | impaired | ...
            "instance": s["InstanceStatus"]["Status"],  # ok | impaired | ...
        }
    except Exception as e:
        print(f"[WARN] Status check failed for {instance_id}: {e}")
        return {"system": "error", "instance": "error", "state": "unknown"}


def get_instance_name(instance_id: str) -&gt; str:
    """Return the Name tag of an instance, or the instance ID if no tag."""
    try:
        resp = ec2.describe_instances(InstanceIds=[instance_id])
        tags = resp["Reservations"][0]["Instances"][0].get("Tags", [])
        for tag in tags:
            if tag["Key"] == "Name":
                return tag["Value"]
    except Exception:
        pass
    return instance_id


def run_ssm_command(instance_id: str, command: str, timeout: int = 20) -&gt; Optional[str]:
    """
    Execute a shell command on an EC2 instance via SSM Run Command.
    Returns stdout string or None on failure.
    Requires:
      - EC2 instance has SSM Agent running (pre-installed on Amazon Linux 2/2023, Ubuntu 16+)
      - EC2 IAM role has AmazonSSMManagedInstanceCore policy
    """
    try:
        resp = ssm.send_command(
            InstanceIds=[instance_id],
            DocumentName="AWS-RunShellScript",
            Parameters={"commands": [command]},
            TimeoutSeconds=timeout,
        )
        command_id = resp["Command"]["CommandId"]

        # Poll for result (max 25 seconds)
        for _ in range(5):
            time.sleep(5)
            result = ssm.get_command_invocation(
                CommandId=command_id,
                InstanceId=instance_id,
            )
            status = result["Status"]
            if status == "Success":
                return result["StandardOutputContent"].strip()
            if status in ("Failed", "Cancelled", "TimedOut"):
                print(f"[WARN] SSM command {status} on {instance_id}")
                return None
        print(f"[WARN] SSM command timed out polling on {instance_id}")
        return None
    except ssm.exceptions.InvalidInstanceId:
        print(f"[WARN] {instance_id} not registered with SSM (agent not running or no IAM role)")
        return None
    except Exception as e:
        print(f"[WARN] SSM error on {instance_id}: {e}")
        return None


def get_memory_via_ssm(instance_id: str) -&gt; Optional[float]:
    """
    Read memory usage % from /proc/meminfo via SSM Run Command.
    No CloudWatch Agent needed.
    """
    cmd = (
        "awk '/MemTotal/{t=$2} /MemAvailable/{a=$2} "
        "END{printf \"%.2f\", (t-a)/t*100}' /proc/meminfo"
    )
    output = run_ssm_command(instance_id, cmd)
    if output is None:
        return None
    try:
        return float(output)
    except ValueError:
        return None


def get_disk_via_ssm(instance_id: str) -&gt; Optional[float]:
    """
    Read root disk usage % via SSM Run Command.
    Returns the highest used % across all mounted filesystems.
    No CloudWatch Agent needed.
    """
    cmd = "df -h --output=pcent / | tail -1 | tr -d ' %'"
    output = run_ssm_command(instance_id, cmd)
    if output is None:
        return None
    try:
        return float(output)
    except ValueError:
        return None


# ══════════════════════════════════════════════════════════════════════════════
# HEALTH EVALUATOR
# ══════════════════════════════════════════════════════════════════════════════

def evaluate_instance(instance_id: str) -&gt; dict:
    """
    Collect all metrics for one instance and return a health report dict.
    """
    name = get_instance_name(instance_id)
    print(f"  Checking {name} ({instance_id})...")

    cpu    = get_cloudwatch_metric(instance_id, "CPUUtilization")
    status = get_status_checks(instance_id)
    memory = get_memory_via_ssm(instance_id)
    disk   = get_disk_via_ssm(instance_id)

    alerts = []

    # CPU
    if cpu is not None:
        if cpu &gt;= CPU_CRIT:
            alerts.append({"metric": "CPU", "value": cpu, "threshold": CPU_CRIT, "severity": "CRITICAL"})
        elif cpu &gt;= CPU_WARN:
            alerts.append({"metric": "CPU", "value": cpu, "threshold": CPU_WARN, "severity": "WARNING"})

    # Memory
    if memory is not None:
        if memory &gt;= MEM_WARN:
            alerts.append({"metric": "Memory", "value": memory, "threshold": MEM_WARN, "severity": "WARNING"})

    # Disk
    if disk is not None:
        if disk &gt;= DISK_WARN:
            alerts.append({"metric": "Disk", "value": disk, "threshold": DISK_WARN, "severity": "WARNING"})

    # Status checks
    if status["system"] not in ("ok", "unknown"):
        alerts.append({
            "metric": "Status Check (1/2 System)",
            "value": status["system"],
            "threshold": "ok",
            "severity": "CRITICAL",
            "note": "AWS hardware issue — may require stop/start to migrate host"
        })
    if status["instance"] not in ("ok", "unknown"):
        alerts.append({
            "metric": "Status Check (2/2 Instance)",
            "value": status["instance"],
            "threshold": "ok",
            "severity": "CRITICAL",
            "note": "OS/kernel issue — check system logs or reboot"
        })

    return {
        "instance_id": instance_id,
        "name": name,
        "state": status.get("state", "unknown"),
        "metrics": {
            "cpu":    cpu,
            "memory": memory,
            "disk":   disk,
            "status_system":   status["system"],
            "status_instance": status["instance"],
        },
        "alerts": alerts,
        "healthy": len(alerts) == 0,
    }


# ══════════════════════════════════════════════════════════════════════════════
# NOTIFICATION BUILDERS
# ══════════════════════════════════════════════════════════════════════════════

SEVERITY_COLORS = {
    "CRITICAL": {"hex": "#D32F2F", "teams": "attention", "emoji": "🔴"},
    "WARNING":  {"hex": "#F57C00", "teams": "warning",   "emoji": "🟠"},
    "OK":       {"hex": "#388E3C", "teams": "good",       "emoji": "✅"},
}

def _metric_line(report: dict) -&gt; str:
    m = report["metrics"]
    cpu_str  = f"{m['cpu']:.1f}%"    if m["cpu"]    is not None else "N/A"
    mem_str  = f"{m['memory']:.1f}%" if m["memory"] is not None else "N/A (no SSM)"
    disk_str = f"{m['disk']:.1f}%"   if m["disk"]   is not None else "N/A (no SSM)"
    sys_str  = m["status_system"]
    ins_str  = m["status_instance"]
    return (
        f"CPU: {cpu_str}  |  Mem: {mem_str}  |  "
        f"Disk: {disk_str}  |  Status: {sys_str}/{ins_str}"
    )


def build_slack_message(report: dict) -&gt; dict:
    instance_id = report["instance_id"]
    name        = report["name"]
    alerts      = report["alerts"]
    env         = ENVIRONMENT.upper()
    region      = REGION

    top_severity = "CRITICAL" if any(a["severity"] == "CRITICAL" for a in alerts) else "WARNING"
    color = SEVERITY_COLORS[top_severity]["hex"]
    emoji = SEVERITY_COLORS[top_severity]["emoji"]

    header = f"{emoji} [{env}] EC2 ALERT — {name} ({instance_id})"

    alert_lines = []
    for a in alerts:
        note = f" — {a['note']}" if "note" in a else ""
        alert_lines.append(
            f"• *{a['metric']}*: `{a['value']}` (threshold: {a['threshold']}) [{a['severity']}]{note}"
        )

    ec2_url = f"https://{region}.console.aws.amazon.com/ec2/home?region={region}#Instances:instanceId={instance_id}"
    cw_url  = f"https://{region}.console.aws.amazon.com/cloudwatch/home?region={region}#alarmsV2:"
    logs_url = f"https://{region}.console.aws.amazon.com/systems-manager/run-command?region={region}"

    blocks = [
        {
            "type": "header",
            "text": {"type": "plain_text", "text": header[:150], "emoji": True}
        },
        {
            "type": "section",
            "text": {
                "type": "mrkdwn",
                "text": "\n".join(alert_lines)
            }
        },
        {"type": "divider"},
        {
            "type": "section",
            "fields": [
                {"type": "mrkdwn", "text": f"*Instance*\n`{instance_id}`"},
                {"type": "mrkdwn", "text": f"*Name*\n{name}"},
                {"type": "mrkdwn", "text": f"*State*\n{report['state']}"},
                {"type": "mrkdwn", "text": f"*Region*\n{region}"},
            ]
        },
        {
            "type": "section",
            "text": {
                "type": "mrkdwn",
                "text": f"*Current metrics*\n`{_metric_line(report)}`"
            }
        },
        {
            "type": "context",
            "elements": [
                {"type": "mrkdwn", "text": f"Time: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S')} UTC | Env: {env}"}
            ]
        },
        {
            "type": "actions",
            "elements": [
                {
                    "type": "button",
                    "text": {"type": "plain_text", "text": "EC2 Console"},
                    "url": ec2_url,
                    "style": "danger"
                },
                {
                    "type": "button",
                    "text": {"type": "plain_text", "text": "CloudWatch"},
                    "url": cw_url
                },
                {
                    "type": "button",
                    "text": {"type": "plain_text", "text": "SSM Run Command"},
                    "url": logs_url
                },
            ]
        }
    ]

    return {
        "attachments": [{
            "color": color,
            "blocks": blocks,
            "fallback": header
        }]
    }


def build_teams_message(report: dict) -&gt; dict:
    instance_id = report["instance_id"]
    name        = report["name"]
    alerts      = report["alerts"]
    env         = ENVIRONMENT.upper()
    region      = REGION

    top_severity = "CRITICAL" if any(a["severity"] == "CRITICAL" for a in alerts) else "WARNING"
    sev          = SEVERITY_COLORS[top_severity]
    title        = f"{sev['emoji']} [{env}] EC2 ALERT — {name} ({instance_id})"

    ec2_url  = f"https://{region}.console.aws.amazon.com/ec2/home?region={region}#Instances:instanceId={instance_id}"
    cw_url   = f"https://{region}.console.aws.amazon.com/cloudwatch/home?region={region}#alarmsV2:"

    alert_facts = []
    for a in alerts:
        note = f" ({a.get('note', '')})" if "note" in a else ""
        alert_facts.append({
            "title": f"{a['severity']} — {a['metric']}",
            "value": f"{a['value']} (threshold: {a['threshold']}){note}"
        })

    m = report["metrics"]

    return {
        "type": "message",
        "attachments": [{
            "contentType": "application/vnd.microsoft.card.adaptive",
            "content": {
                "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
                "type": "AdaptiveCard",
                "version": "1.4",
                "body": [
                    {
                        "type": "TextBlock",
                        "text": title,
                        "weight": "bolder",
                        "size": "medium",
                        "wrap": True,
                        "color": sev["teams"]
                    },
                    {
                        "type": "FactSet",
                        "facts": alert_facts
                    },
                    {"type": "TextBlock", "text": "Current metrics", "weight": "bolder", "size": "small"},
                    {
                        "type": "FactSet",
                        "facts": [
                            {"title": "CPU",     "value": f"{m['cpu']:.1f}%"    if m["cpu"]    is not None else "N/A"},
                            {"title": "Memory",  "value": f"{m['memory']:.1f}%" if m["memory"] is not None else "N/A (no SSM)"},
                            {"title": "Disk",    "value": f"{m['disk']:.1f}%"   if m["disk"]   is not None else "N/A (no SSM)"},
                            {"title": "Status",  "value": f"System: {m['status_system']} | Instance: {m['status_instance']}"},
                            {"title": "State",   "value": report["state"]},
                            {"title": "Region",  "value": region},
                            {"title": "Time",    "value": datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S") + " UTC"},
                        ]
                    }
                ],
                "actions": [
                    {"type": "Action.OpenUrl", "title": "Open EC2 Console",    "url": ec2_url},
                    {"type": "Action.OpenUrl", "title": "View CloudWatch",      "url": cw_url},
                ]
            }
        }]
    }


def build_sns_email(report: dict) -&gt; str:
    """Plain text email body for SNS."""
    env   = ENVIRONMENT.upper()
    name  = report["name"]
    iid   = report["instance_id"]
    lines = [
        f"[{env}] EC2 ALERT — {name} ({iid})",
        f"Time: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S')} UTC",
        f"Region: {REGION}",
        f"State: {report['state']}",
        "",
        "ALERTS:",
    ]
    for a in report["alerts"]:
        note = f" | Note: {a.get('note', '')}" if "note" in a else ""
        lines.append(f"  [{a['severity']}] {a['metric']}: {a['value']} (threshold: {a['threshold']}){note}")
    m = report["metrics"]
    lines += [
        "",
        "CURRENT METRICS:",
        f"  CPU:    {m['cpu']:.1f}%"    if m["cpu"]    is not None else "  CPU:    N/A",
        f"  Memory: {m['memory']:.1f}%" if m["memory"] is not None else "  Memory: N/A (SSM not available)",
        f"  Disk:   {m['disk']:.1f}%"   if m["disk"]   is not None else "  Disk:   N/A (SSM not available)",
        f"  Status: System={m['status_system']} | Instance={m['status_instance']}",
        "",
        f"EC2 Console: https://{REGION}.console.aws.amazon.com/ec2/home?region={REGION}#Instances:instanceId={iid}",
    ]
    return "\n".join(lines)


# ══════════════════════════════════════════════════════════════════════════════
# NOTIFICATION SENDERS
# ══════════════════════════════════════════════════════════════════════════════

def post_webhook(url: str, payload: dict, label: str) -&gt; bool:
    if not url:
        print(f"  [SKIP] {label}: no webhook URL configured")
        return False
    data = json.dumps(payload).encode("utf-8")
    req  = urllib.request.Request(url, data=data, headers={"Content-Type": "application/json"}, method="POST")
    try:
        with urllib.request.urlopen(req, timeout=10) as r:
            print(f"  [OK] {label}: HTTP {r.getcode()}")
            return r.getcode() == 200
    except urllib.error.HTTPError as e:
        print(f"  [ERROR] {label}: HTTP {e.code} — {e.read().decode()[:200]}")
        return False
    except Exception as e:
        print(f"  [ERROR] {label}: {e}")
        return False


def send_sns_email(report: dict) -&gt; bool:
    if not SNS_TOPIC_ARN:
        print("  [SKIP] Email: SNS_TOPIC_ARN not configured")
        return False
    top = "CRITICAL" if any(a["severity"] == "CRITICAL" for a in report["alerts"]) else "WARNING"
    subject = f"[{ENVIRONMENT.upper()}] {top} — EC2 {report['name']} ({report['instance_id']})"
    try:
        sns.publish(
            TopicArn=SNS_TOPIC_ARN,
            Subject=subject[:100],
            Message=build_sns_email(report),
        )
        print(f"  [OK] Email via SNS")
        return True
    except Exception as e:
        print(f"  [ERROR] SNS email: {e}")
        return False


def notify(report: dict):
    """Send alert to all channels."""
    post_webhook(SLACK_WEBHOOK_URL, build_slack_message(report), "Slack")
    post_webhook(TEAMS_WEBHOOK_URL, build_teams_message(report), "Teams")
    send_sns_email(report)


# ══════════════════════════════════════════════════════════════════════════════
# LAMBDA ENTRY POINT
# ══════════════════════════════════════════════════════════════════════════════

def lambda_handler(event, context):
    """
    Triggered by EventBridge every 5 minutes.
    Checks all instances and sends alerts for any that are unhealthy.
    """
    if not INSTANCE_IDS:
        print("[ERROR] INSTANCE_IDS environment variable is empty. Nothing to check.")
        return {"statusCode": 400, "message": "No instance IDs configured"}

    print(f"[START] Checking {len(INSTANCE_IDS)} instances: {INSTANCE_IDS}")
    results  = []
    alerted  = 0

    for instance_id in INSTANCE_IDS:
        report = evaluate_instance(instance_id)
        results.append(report)

        if not report["healthy"]:
            print(f"  [ALERT] {report['name']} has {len(report['alerts'])} issue(s) — sending notifications")
            notify(report)
            alerted += 1
        else:
            print(f"  [OK] {report['name']} is healthy")

    summary = {
        "checked":   len(INSTANCE_IDS),
        "healthy":   len(INSTANCE_IDS) - alerted,
        "alerted":   alerted,
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "results":   [
            {
                "id":      r["instance_id"],
                "name":    r["name"],
                "healthy": r["healthy"],
                "alerts":  len(r["alerts"]),
            }
            for r in results
        ],
    }
    print(f"[DONE] {summary}")
    return {"statusCode": 200, "summary": summary}
</code></pre>
]]></content:encoded></item><item><title><![CDATA[ Linux  Monitoring, Cleanup,Logging 
                                       & 
                             Auditing]]></title><description><![CDATA[Managing Linux servers can become difficult when you have to check CPU, memory, disk usage, processes, Docker resources, and EBS volumes separately. I…]]></description><link>https://vishnusai.hashnode.dev/linux-monitoring-cleanup-logging-auditing</link><guid isPermaLink="true">https://vishnusai.hashnode.dev/linux-monitoring-cleanup-logging-auditing</guid><dc:creator><![CDATA[vishnu sai]]></dc:creator><pubDate>Fri, 21 Aug 2026 08:55:25 GMT</pubDate><content:encoded><![CDATA[<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/77f599b3-e07b-42fb-9b52-9a5b27d8149e.png" alt="" style="display:block;margin:0 auto" />

<h2><strong>PROBLEM</strong></h2>
<p>Managing Linux servers can become difficult when you have to check CPU, memory, disk usage, processes, Docker resources, and EBS volumes separately. I wanted a simple way to bring these everyday monitoring and cleanup tasks into one place.</p>
<p><strong>HOW I DEBUGGED IT</strong></p>
<p>I built a Python script that collects system information, monitors resources, identifies unused processes, checks Docker resources, and gives visibility into what is happening on the server.</p>
<p><strong>THE SOLUTION</strong></p>
<p>I added <strong>Auto, Interactive, and Dry-Run modes</strong> so cleanup actions can be controlled safely. The script also keeps <strong>logs and audit records</strong> to make every important action easier to track and review.</p>
<p><strong>Monitor → Detect → Decide → Cleanup → Log → Audit</strong></p>
<h3>Step 1 — Script Overview</h3>
<p>Shows the script modes and main features such as monitoring, cleanup, process detection, and logging.</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/54920ec1-abed-4088-9e2f-a158b5fba06f.png" alt="" style="display:block;margin:0 auto" />

<h3>Step 2 — Runner Identity</h3>
<p>Collects information about the server, Linux user, IP address, AWS instance, region, and IAM identity.</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/441b3df5-d1e6-4d9d-bcd2-bae066206835.png" alt="" style="display:block;margin:0 auto" />

<h3>Step 3 — Resource Monitoring</h3>
<p>Checks CPU, memory, swap, disk usage, and system load to understand the server's current health.</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/79addea0-0c2b-481d-b880-264dd569cc6d.png" alt="" style="display:block;margin:0 auto" />

<h3>Step 4 — Process Detection</h3>
<p>Finds idle, sleeping, and zombie processes and supports Auto, Manual, and Dry-Run modes.</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/e14e15e3-7bc1-4327-910b-6091b105435c.png" alt="" style="display:block;margin:0 auto" />

<h3>Step 4 — Process Detection</h3>
<p>Finds idle, sleeping, and zombie processes and supports Auto, Manual, and Dry-Run modes. Lists attached EBS volumes with their volume ID, availability zone, size, and current state. Lists attached EBS volumes with their volume ID, availability zone, size, and current state</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/5b8f5ec6-0faa-4e5f-af76-c0ded4f8089c.png" alt="" style="display:block;margin:0 auto" />

<h3>Step 5 — Docker Cleanup</h3>
<p>Checks stopped containers, dangling images, and unused Docker volumes for possible cleanup.</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/1a3f7740-7e1e-46fe-9b1d-0eeff748cc87.png" alt="" style="display:block;margin:0 auto" />

<h3>Step 6— Cleanup &amp; Audit Logs</h3>
<p>Performs selected cleanup actions and records deleted, skipped, failed, and restore-related information.</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/4acf66fb-9205-47fe-80d5-9a37272dd61f.png" alt="" style="display:block;margin:0 auto" />

<h3>Step 7— Final Report</h3>
<p>Displays the total runtime, section timing, audit logs, unprocessed items, and final cleanup status.</p>
<img src="https://cdn.hashnode.com/uploads/covers/695a3935fed5332c35faf7a1/990343cd-21a5-4063-936d-de280dbf0375.png" alt="" style="display:block;margin:0 auto" />

<h1>============================================================</h1>
<h1>PRODUCTION LINUX &amp; DOCKER CLEANUP — CRON JOBS</h1>
<h1>============================================================</h1>
<h1>------------------------------------------------------------</h1>
<h1>LINUX</h1>
<h1>------------------------------------------------------------</h1>
<h1>CPU — every 5 minutes</h1>
<p>*/5 * * * * /usr/bin/python3 /opt/prod_cleanup.py --mode auto --linux cpu &gt;&gt; /var/log/prod_cleanup.log 2&gt;&amp;1</p>
<h1>Memory — every 5 minutes</h1>
<p>*/5 * * * * /usr/bin/python3 /opt/prod_cleanup.py --mode auto --linux memory &gt;&gt; /var/log/prod_cleanup.log 2&gt;&amp;1</p>
<h1>Disk — daily at 2:00 AM</h1>
<p>0 2 * * * /usr/bin/python3 /opt/prod_cleanup.py --mode auto --linux disk &gt;&gt; /var/log/prod_cleanup.log 2&gt;&amp;1</p>
<h1>Processes — daily at 3:00 AM</h1>
<p>0 3 * * * /usr/bin/python3 /opt/prod_cleanup.py --mode auto --linux processes &gt;&gt; /var/log/prod_cleanup.log 2&gt;&amp;1</p>
<h1>EBS — daily at 4:00 AM</h1>
<p>0 4 * * * /usr/bin/python3 /opt/prod_cleanup.py --mode auto --linux ebs &gt;&gt; /var/log/prod_cleanup.log 2&gt;&amp;1</p>
<h1>All Linux operations — every Sunday at 5:00 AM</h1>
<p>0 5 * * 0 /usr/bin/python3 /opt/prod_cleanup.py --mode auto --linux all &gt;&gt; /var/log/prod_cleanup.log 2&gt;&amp;1</p>
<h1>------------------------------------------------------------</h1>
<h1>DOCKER</h1>
<h1>------------------------------------------------------------</h1>
<h1>Stopped containers — daily at 6:00 AM</h1>
<p>0 6 * * * /usr/bin/python3 /opt/prod_cleanup.py --mode auto --docker container-only &gt;&gt; /var/log/prod_cleanup.log 2&gt;&amp;1</p>
<h1>Dangling images — daily at 7:00 AM</h1>
<p>0 7 * * * /usr/bin/python3 /opt/prod_cleanup.py --mode auto --docker image-only &gt;&gt; /var/log/prod_cleanup.log 2&gt;&amp;1</p>
<h1>Unused volumes — every Sunday at 8:00 AM</h1>
<p>0 8 * * 0 /usr/bin/python3 /opt/prod_cleanup.py --mode auto --docker volume-only &gt;&gt; /var/log/prod_cleanup.log 2&gt;&amp;1</p>
<h1>All Docker operations — every Sunday at 9:00 AM</h1>
<p>0 9 * * 0 /usr/bin/python3 /opt/prod_cleanup.py --mode auto --docker all &gt;&gt; /var/log/prod_cleanup.log 2&gt;&amp;1</p>
<h1>------------------------------------------------------------</h1>
<h1>EVERYTHING</h1>
<h1>------------------------------------------------------------</h1>
<h1>Complete Linux + Docker cleanup — every Sunday at 10:00 AM</h1>
<p>0 10 * * 0 /usr/bin/python3 /opt/prod_cleanup.py --mode auto --linux all --docker all &gt;&gt; /var/log/prod_cleanup.log 2&gt;&amp;1</p>
<pre><code class="language-python">#!/usr/bin/env python3
"""
Production Cleanup &amp; Monitoring Script — Enhanced v2.0
Fixes:  Dynamic file/directory deletion (IsADirectoryError resolved)
New:    Docker mode selector, live runtime clock, ASCII bar graphs,
        restoration log, unprocessed items tracker, unified audit log folder,
        Unused Process Detection mode selector (independent of global mode)
"""

import os
import json
import shutil
import socket
import subprocess
import datetime
import getpass
import grp
import pwd
import time
import platform
import signal
import sys
from pathlib import Path
from rich.console import Console
from rich.prompt import Prompt, Confirm
from rich.table import Table
from rich.panel import Panel
from rich.live import Live
from rich.text import Text
from rich.columns import Columns
from rich import box

console = Console()

# ─── Unified log folder (best practice: one dir, rotate-friendly) ─────────────
LOG_ROOT_CANDIDATES = [Path("/var/log/prod_cleanup"), Path.home() / "logs" / "prod_cleanup"]
LOG_DIR: Path = next((p for p in LOG_ROOT_CANDIDATES
                       if (p.exists() and os.access(p, os.W_OK))
                       or (not p.exists() and os.access(p.parent, os.W_OK))), LOG_ROOT_CANDIDATES[1])

RUN_TS   = datetime.datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")
LOG_FILE      = LOG_DIR / f"cleanup_{RUN_TS}.ndjson"   # per-run structured log
AUDIT_FILE    = LOG_DIR / "audit_master.ndjson"         # append-only master audit
DELETION_LOG  = LOG_DIR / f"deletions_{RUN_TS}.json"   # what was deleted + restore hints
UNPROCESSED   = LOG_DIR / f"unprocessed_{RUN_TS}.json" # items user skipped / failed

# ─── Global timing ────────────────────────────────────────────────────────────
SCRIPT_START   = time.time()
SECTION_TIMINGS: dict = {}

# ─── Unprocessed + deletion tracker ─────────────────────────────────────────
UNPROCESSED_ITEMS: list = []
DELETED_ITEMS:     list = []

# ─── Global state ─────────────────────────────────────────────────────────────
RUNNER_IDENTITY: dict = {}
DOCKER_MODE: str = "2"   # 1=Auto, 2=Manual — set during startup


# ══════════════════════════════════════════════════════════════════════════════
#  HELPERS
# ══════════════════════════════════════════════════════════════════════════════

def elapsed_since(start: float) -&gt; str:
    secs = time.time() - start
    if secs &lt; 60:
        return f"{secs:.1f}s"
    mins, s = divmod(int(secs), 60)
    return f"{mins}m {s}s"


def runtime_str() -&gt; str:
    """Human-friendly total elapsed since script start."""
    return elapsed_since(SCRIPT_START)


def now_utc() -&gt; str:
    return datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")


def section_timer(name: str, start: float):
    elapsed = time.time() - start
    SECTION_TIMINGS[name] = elapsed
    console.print(f"[dim]⏱  {name} completed in {elapsed:.1f}s  (total runtime: {runtime_str()})[/dim]")


def run_cmd(cmd, timeout=30) -&gt; str:
    try:
        return subprocess.check_output(cmd, shell=True, text=True,
                                       stderr=subprocess.DEVNULL, timeout=timeout)
    except Exception:
        return ""


def docker_available() -&gt; bool:
    return bool(run_cmd("which docker").strip())


# ══════════════════════════════════════════════════════════════════════════════
#  LOG SETUP  — ensures log folder exists with README
# ══════════════════════════════════════════════════════════════════════════════

def init_log_dir():
    LOG_DIR.mkdir(parents=True, exist_ok=True)
    readme = LOG_DIR / "README.txt"
    if not readme.exists():
        readme.write_text(
            "Production Cleanup Script — Audit Log Folder\n"
            "=============================================\n"
            "cleanup_*.ndjson   → Per-run structured event log (one JSON per line)\n"
            "audit_master.ndjson→ Append-only master audit trail (all runs)\n"
            "deletions_*.json   → Files/dirs deleted + size + restore hints\n"
            "unprocessed_*.json → Items that were skipped or failed\n"
        )


def log_event(event: str, detail: str, extra: dict | None = None):
    ri  = RUNNER_IDENTITY
    geo = ri.get("geo", {})
    lu  = ri.get("linux_user", {})
    aws = ri.get("aws", {})
    ssh = ri.get("session", {})

    record = {
        "ts":           now_utc(),
        "run_id":       RUN_TS,
        "event":        event,
        "detail":       detail,
        "runtime_secs": round(time.time() - SCRIPT_START, 2),
        "hostname":     ri.get("hostname", socket.gethostname()),
        "local_ip":     ri.get("local_ip", ""),
        "public_ip":    ri.get("public_ip", ""),
        "geo_country":  geo.get("country", ""),
        "geo_city":     geo.get("city", ""),
        "linux_user":   lu.get("username", getpass.getuser()),
        "uid":          lu.get("uid", os.getuid()),
        "is_root":      lu.get("is_root", os.geteuid() == 0),
        "sudo_user":    ssh.get("sudo_user", ""),
        "aws_instance": aws.get("instance_id", ""),
        "aws_region":   aws.get("region", ""),
        "aws_arn":      aws.get("aws_arn", ""),
    }
    if extra:
        record.update(extra)

    LOG_DIR.mkdir(parents=True, exist_ok=True)
    line = json.dumps(record) + "\n"
    LOG_FILE.open("a").write(line)
    AUDIT_FILE.open("a").write(line)


# ══════════════════════════════════════════════════════════════════════════════
#  ASCII BAR GRAPHS
# ══════════════════════════════════════════════════════════════════════════════

def _bar(pct: float, width: int = 30, color: str = "green") -&gt; str:
    """Return a rich-markup ASCII progress bar for `pct` (0–100)."""
    pct = max(0.0, min(100.0, pct))
    filled = int(round(pct / 100 * width))
    bar_color = "green" if pct &lt; 60 else ("yellow" if pct &lt; 85 else "red")
    bar = f"[{bar_color}]{'█' * filled}[/{bar_color}]{'░' * (width - filled)}"
    return f"{bar} [bold]{pct:5.1f}&lt;a class="embed-card" href="/bold"&gt;/bold&lt;/a&gt;"


def show_resource_graphs():
    """Print ASCII bar-graph dashboard for CPU, RAM, Disk, load average."""
    console.print("\n[bold cyan]======= RESOURCE USAGE GRAPHS =======[/bold cyan]")

    def _cpu_pct() -&gt; list[tuple[str, float]]:
        lines1 = Path("/proc/stat").read_text().splitlines()
        time.sleep(0.4)
        lines2 = Path("/proc/stat").read_text().splitlines()
        result = []
        for l1, l2 in zip(lines1, lines2):
            if not l1.startswith("cpu"):
                continue
            p1 = list(map(int, l1.split()[1:]))
            p2 = list(map(int, l2.split()[1:]))
            idle1, idle2 = p1[3], p2[3]
            tot1,  tot2  = sum(p1), sum(p2)
            dtot  = tot2 - tot1
            if dtot == 0:
                continue
            pct = 100.0 * (1 - (idle2 - idle1) / dtot)
            result.append((l1.split()[0], pct))
        return result

    cpu_data = _cpu_pct()

    mem_info = {}
    for line in Path("/proc/meminfo").read_text().splitlines():
        k, v = line.split(":")[0], line.split(":")[1].strip().split()[0]
        mem_info[k] = int(v)
    mem_total = mem_info.get("MemTotal", 1)
    mem_avail = mem_info.get("MemAvailable", mem_total)
    mem_used  = mem_total - mem_avail
    mem_pct   = 100.0 * mem_used / mem_total
    swap_total = mem_info.get("SwapTotal", 0)
    swap_free  = mem_info.get("SwapFree", swap_total)
    swap_used  = swap_total - swap_free
    swap_pct   = 100.0 * swap_used / swap_total if swap_total else 0.0

    def _kb(kb: int) -&gt; str:
        if kb &gt; 1_048_576:
            return f"{kb/1_048_576:.1f} GB"
        return f"{kb/1024:.0f} MB"

    disk_data = []
    for line in run_cmd("df -h --output=target,pcent,used,avail,size").splitlines()[1:]:
        parts = line.split()
        if len(parts) &gt;= 5 and parts[0].startswith("/"):
            mnt  = parts[0]
            try:
                pct = float(parts[1].rstrip("%"))
            except ValueError:
                continue
            disk_data.append((mnt, pct, parts[2], parts[3], parts[4]))

    load_raw = Path("/proc/loadavg").read_text().split()
    load1, load5, load15 = float(load_raw[0]), float(load_raw[1]), float(load_raw[2])
    cores = os.cpu_count() or 1
    load_pct1 = min(100.0, load1 / cores * 100)

    console.print(Panel(
        f"[bold]Script started:[/bold] {datetime.datetime.utcfromtimestamp(SCRIPT_START).strftime('%Y-%m-%d %H:%M:%S')} UTC\n"
        f"[bold]Current time:  [/bold] {datetime.datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} UTC\n"
        f"[bold]Elapsed:       [/bold] [yellow]{runtime_str()}[/yellow]",
        title="⏰  Runtime Clock", border_style="blue", padding=(0, 2)
    ))

    console.print("\n[bold yellow]  CPU[/bold yellow]")
    for label, pct in cpu_data[:9]:
        tag = "[bold]ALL[/bold]" if label == "cpu" else label
        console.print(f"  {tag:&gt;6}  {_bar(pct)}")

    console.print(f"\n[bold yellow]  Load Average[/bold yellow]  "
                  f"(cores={cores}):  "
                  f"[cyan]1m={load1}[/cyan]  [cyan]5m={load5}[/cyan]  [cyan]15m={load15}[/cyan]")
    console.print(f"  {'load':&gt;6}  {_bar(load_pct1)}")

    console.print(f"\n[bold yellow]  Memory[/bold yellow]")
    console.print(f"  {'RAM':&gt;6}  {_bar(mem_pct)}  {_kb(mem_used)} / {_kb(mem_total)}")
    if swap_total:
        console.print(f"  {'Swap':&gt;6}  {_bar(swap_pct)}  {_kb(swap_used)} / {_kb(swap_total)}")

    console.print(f"\n[bold yellow]  Disk Partitions[/bold yellow]")
    for mnt, pct, used, avail, size in disk_data[:8]:
        console.print(f"  {mnt:&gt;20}  {_bar(pct)}  {used} used / {size}  ({avail} free)")

    log_event("graphs", "Resource graphs displayed",
              {"load1": load1, "mem_pct": round(mem_pct, 1)})


# ══════════════════════════════════════════════════════════════════════════════
#  RUNNER IDENTITY
# ══════════════════════════════════════════════════════════════════════════════

def _fetch_imdsv2(path: str, token: str, timeout: int = 2) -&gt; str:
    try:
        return subprocess.check_output(
            ["curl", "-sf", "-H", f"X-aws-ec2-metadata-token: {token}",
             f"http://169.254.169.254/latest/meta-data/{path}"],
            text=True, timeout=timeout).strip()
    except Exception:
        return "unavailable"


def _get_imdsv2_token(timeout: int = 2) -&gt; str:
    try:
        return subprocess.check_output(
            ["curl", "-sf", "-X", "PUT",
             "-H", "X-aws-ec2-metadata-token-ttl-seconds: 60",
             "http://169.254.169.254/latest/api/token"],
            text=True, timeout=timeout).strip()
    except Exception:
        return ""


def _get_public_ip() -&gt; dict:
    try:
        raw = subprocess.check_output(
            ["curl", "-sf", "--max-time", "4",
             "http://ip-api.com/json?fields=status,message,country,countryCode,"
             "region,regionName,city,zip,lat,lon,timezone,isp,org,as,query"],
            text=True, timeout=5).strip()
        data = json.loads(raw)
        if data.get("status") == "success":
            return data
    except Exception:
        pass
    return {"query": "unavailable", "country": "unknown", "city": "unknown",
            "regionName": "unknown", "isp": "unknown", "timezone": "unknown"}


def _get_local_ip() -&gt; str:
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        s.connect(("8.8.8.8", 80))
        ip = s.getsockname()[0]; s.close(); return ip
    except Exception:
        return "unavailable"


def _get_linux_user_data() -&gt; dict:
    try:
        pw = pwd.getpwuid(os.getuid())
        all_groups = [{"gid": g.gr_gid, "name": g.gr_name}
                      for g in grp.getgrall()
                      if pw.pw_name in g.gr_mem or g.gr_gid == pw.pw_gid]
        return {"username": pw.pw_name, "uid": pw.pw_uid, "gid": pw.pw_gid,
                "gecos": pw.pw_gecos, "home_dir": pw.pw_dir, "shell": pw.pw_shell,
                "groups": all_groups, "euid": os.geteuid(), "egid": os.getegid(),
                "is_root": os.geteuid() == 0}
    except Exception as e:
        return {"error": str(e)}


def _get_ssh_session() -&gt; dict:
    return {k: os.environ.get(v, "") for k, v in [
        ("ssh_client", "SSH_CLIENT"), ("ssh_connection", "SSH_CONNECTION"),
        ("ssh_tty", "SSH_TTY"), ("term", "TERM"), ("tmux", "TMUX"),
        ("sudo_user", "SUDO_USER"), ("sudo_command", "SUDO_COMMAND"), ("logname", "LOGNAME")
    ]}


def _get_aws_identity() -&gt; dict:
    result = {}
    token = _get_imdsv2_token()
    fetch = (lambda p: _fetch_imdsv2(p, token)) if token else (lambda p: "unavailable")
    for key, path in [("instance_id", "instance-id"), ("instance_type", "instance-type"),
                      ("ami_id", "ami-id"), ("local_ipv4", "local-ipv4"),
                      ("availability_zone", "placement/availability-zone"),
                      ("region", "placement/region")]:
        result[key] = fetch(path)
    iam_role = fetch("iam/security-credentials/")
    result["iam_role"] = iam_role if iam_role and "unavailable" not in iam_role else "none"
    try:
        sts = json.loads(subprocess.check_output(
            ["aws", "sts", "get-caller-identity", "--output", "json"],
            text=True, timeout=5, stderr=subprocess.DEVNULL))
        result.update({"aws_account_id": sts.get("Account",""), "aws_arn": sts.get("Arn",""),
                        "aws_user_id": sts.get("UserId","")})
    except Exception:
        result.update({"aws_account_id": "unavailable", "aws_arn": "unavailable",
                        "aws_user_id": "unavailable"})
    return result


def collect_runner_identity() -&gt; dict:
    console.print("[dim]  Collecting runner identity...[/dim]", end="")
    geo = _get_public_ip()
    identity = {
        "script_start_utc": now_utc(),
        "hostname": socket.gethostname(), "fqdn": socket.getfqdn(),
        "os": platform.platform(), "kernel": platform.release(),
        "python": platform.python_version(),
        "local_ip": _get_local_ip(), "public_ip": geo.get("query", "unavailable"),
        "geo": {"country": geo.get("country",""), "country_code": geo.get("countryCode",""),
                "region": geo.get("regionName",""), "city": geo.get("city",""),
                "latitude": geo.get("lat"), "longitude": geo.get("lon"),
                "timezone": geo.get("timezone",""), "isp": geo.get("isp",""),
                "org": geo.get("org",""), "as_number": geo.get("as","")},
        "linux_user": _get_linux_user_data(),
        "session": _get_ssh_session(),
        "aws": _get_aws_identity(),
    }
    console.print(" [green]done[/green]")
    return identity


def show_runner_identity():
    ri = RUNNER_IDENTITY
    geo = ri.get("geo", {}); lu = ri.get("linux_user", {})
    aws = ri.get("aws", {}); ssh = ri.get("session", {})
    groups_str = ", ".join(f"{g['name']}({g['gid']})" for g in lu.get("groups", []))
    ssh_src = ""
    if ssh.get("ssh_client"):
        parts = ssh["ssh_client"].split()
        ssh_src = f"{parts[0]}:{parts[1]}" if len(parts) &gt;= 2 else ssh["ssh_client"]
    loc_str = ", ".join(filter(None, [geo.get("city"), geo.get("region"), geo.get("country")])) or "unknown"

    lines = [
        f"[bold white]Hostname:[/bold white]      {ri.get('hostname')} ({ri.get('fqdn')})",
        f"[bold white]OS/Kernel:[/bold white]     {ri.get('os')} — {ri.get('kernel')}",
        f"[bold white]Started:[/bold white]       {ri.get('script_start_utc')}  |  Elapsed: [yellow]{runtime_str()}[/yellow]",
        "", f"[bold cyan]── Network ──────────────────────────[/bold cyan]",
        f"[bold white]Local IP:[/bold white]      {ri.get('local_ip')}",
        f"[bold white]Public IP:[/bold white]     {ri.get('public_ip')}",
        f"[bold white]Location:[/bold white]      {loc_str}  |  TZ: {geo.get('timezone')}",
        f"[bold white]ISP/Org:[/bold white]       {geo.get('isp')} / {geo.get('org')}",
        "", f"[bold cyan]── Linux User ───────────────────────[/bold cyan]",
        f"[bold white]Username:[/bold white]      {lu.get('username')} (uid={lu.get('uid')}, euid={lu.get('euid')})",
        f"[bold white]Root?:[/bold white]         {'[red]YES — running as root![/red]' if lu.get('is_root') else '[green]No[/green]'}",
        f"[bold white]Home/Shell:[/bold white]    {lu.get('home_dir')} | {lu.get('shell')}",
        f"[bold white]Groups:[/bold white]        {groups_str or '(none)'}",
        f"[bold white]SUDO_USER:[/bold white]     {ssh.get('sudo_user') or '(not sudo)'}",
        "", f"[bold cyan]── SSH Session ──────────────────────[/bold cyan]",
        f"[bold white]SSH From:[/bold white]      {ssh_src or '(local / not SSH)'}",
        f"[bold white]TTY/TERM:[/bold white]      {ssh.get('ssh_tty') or '(none)'} | {ssh.get('term')}",
        f"[bold white]tmux:[/bold white]          {'yes' if ssh.get('tmux') else 'no'}",
        "", f"[bold cyan]── AWS Identity ─────────────────────[/bold cyan]",
        f"[bold white]Instance:[/bold white]      {aws.get('instance_id')} ({aws.get('instance_type')})",
        f"[bold white]Region/AZ:[/bold white]     {aws.get('region')} / {aws.get('availability_zone')}",
        f"[bold white]Account:[/bold white]       {aws.get('aws_account_id')}",
        f"[bold white]IAM Role:[/bold white]      {aws.get('iam_role')}",
        f"[bold white]STS ARN:[/bold white]       {aws.get('aws_arn')}",
        "", f"[bold cyan]── Log Folder ───────────────────────[/bold cyan]",
        f"[bold white]Log Dir:[/bold white]       {LOG_DIR}",
        f"[bold white]Run Log:[/bold white]       {LOG_FILE.name}",
        f"[bold white]Audit:  [/bold white]       {AUDIT_FILE.name}",
        f"[bold white]Deletions:[/bold white]     {DELETION_LOG.name}",
    ]
    console.print(Panel("\n".join(lines),
        title="[bold yellow]🔍  RUNNER IDENTITY — Who Is Running This Script?[/bold yellow]",
        border_style="yellow", padding=(1, 2)))


# ══════════════════════════════════════════════════════════════════════════════
#  DESCRIPTION
# ══════════════════════════════════════════════════════════════════════════════

def show_description():
    console.print(Panel("""
[bold yellow]Script Modes:[/bold yellow]

[bold green]1. Auto Mode[/bold green]   — runs all actions automatically (pre-approved lists only)
[bold green]2. Interactive Mode [Default][/bold green] — pauses and asks before every critical action

[bold cyan]Features v2.0:[/bold cyan]
  • [magenta]Dynamic Deletion[/magenta]              — files AND directories (recursive), safe + logged
  • [magenta]Docker Mode Selector[/magenta]          — choose Auto or Manual Docker cleanup at startup
  • [magenta]Unused Process Mode Selector[/magenta]  — independent kill mode per run (Auto/Manual/Dry-Run)
  • [magenta]ASCII Resource Graphs[/magenta]         — bar charts for CPU / RAM / Disk / Load
  • [magenta]Live Runtime Clock[/magenta]            — shows start time + elapsed at every step
  • [magenta]Runner Identity Logging[/magenta]       — IP, geo, IAM, Linux user, SSH session
  • [magenta]Unused Process Detection[/magenta]      — idle / zombie / sleeping processes
  • [magenta]Unified Log Folder[/magenta]            — per-run + master audit + deletion + unprocessed logs
  • [magenta]Restoration Hints[/magenta]             — deletion log records size &amp; location for recovery
  • [magenta]Unprocessed Tracker[/magenta]           — final report of everything skipped or failed
""",
        title="[bold cyan]Production Cleanup &amp; Monitoring v2.0[/bold cyan]",
        border_style="cyan"))


# ══════════════════════════════════════════════════════════════════════════════
#  SYSTEM MONITOR
# ══════════════════════════════════════════════════════════════════════════════

def system_monitor():
    t = time.time()
    console.print("\n[bold cyan]======= SYSTEM MONITOR =======[/bold cyan]")
    load = run_cmd("uptime | awk -F'load average:' '{print $2}'").strip()
    console.print(f"[bold yellow]Load Avg:[/bold yellow] {load}")
    console.print(run_cmd("free -h"))
    console.print(run_cmd("df -h | grep -E '^/dev|Filesystem'"))
    log_event("monitor", "System stats displayed")
    section_timer("System Monitor", t)


# ══════════════════════════════════════════════════════════════════════════════
#  TOP PROCESSES
# ══════════════════════════════════════════════════════════════════════════════

def show_top_processes():
    t = time.time()
    console.print("\n[bold cyan]======= TOP 20 CPU PROCESSES =======[/bold cyan]")
    cpu_output = run_cmd("ps -eo pid,ppid,comm,%cpu,%mem --sort=-%cpu | head -n 21").splitlines()
    _render_ps_table(cpu_output, "CPU")
    console.print("\n[bold cyan]======= TOP 20 MEMORY PROCESSES =======[/bold cyan]")
    mem_output = run_cmd("ps -eo pid,ppid,comm,%cpu,%mem --sort=-%mem | head -n 21").splitlines()
    _render_ps_table(mem_output, "MEM")
    log_event("monitor", "Top CPU/memory processes displayed")
    section_timer("Top Processes", t)
    return cpu_output, mem_output


def _render_ps_table(lines, sort_col):
    table = Table(show_header=True, header_style="bold magenta", box=box.SIMPLE)
    for col in ("PID", "PPID", "Command", "%CPU", "%MEM"):
        table.add_column(col, justify="right" if col != "Command" else "left", overflow="fold")
    for line in lines[1:]:
        parts = line.split(None, 4)
        if len(parts) == 5:
            table.add_row(*parts)
    console.print(table)


def kill_processes(process_list, label):
    pids = Prompt.ask(
        f"Enter PIDs to kill from [yellow]{label}[/yellow] list (space-separated, blank=skip)",
        default="")
    if pids:
        for pid in pids.split():
            try:
                os.kill(int(pid), 9)
                console.print(f"[green]  ✓ Killed PID {pid}[/green]")
                log_event("kill", f"Process {pid} killed from {label}")
            except Exception as e:
                console.print(f"[red]  ✗ Failed to kill {pid}: {e}[/red]")
                UNPROCESSED_ITEMS.append({"type": "kill", "pid": pid, "error": str(e)})


# ══════════════════════════════════════════════════════════════════════════════
#  UNUSED PROCESS MODE SELECTOR  ← NEW: independent of global mode
# ══════════════════════════════════════════════════════════════════════════════

def select_unused_process_mode() -&gt; str:
    """
    Ask the user to choose a kill mode specifically for unused/idle processes.
    Completely independent of the global mode — runs every time this section
    is reached, allowing fine-grained control without touching other sections.

    Returns
    -------
    "1"  Auto      — kill all zombies immediately, log &amp; skip the rest
    "2"  Manual    — interactive: choose by row, PID, or skip per process
    "3"  Dry-Run   — detect and display only, no kills performed
    """
    console.print(Panel(
        "[bold green]1. Auto[/bold green]      "
        "— kill all [red]zombie[/red] processes automatically; log sleeping/idle as unprocessed\n\n"
        "[bold yellow]2. Manual[/bold yellow]    "
        "— review each candidate: kill by row, enter PIDs, or skip individually\n\n"
        "[bold blue]3. Dry-Run[/bold blue]   "
        "— detect and [underline]display only[/underline]; no processes will be touched "
        "(safe for auditing)",
        title="[bold cyan]🔧  Unused Process Detection — Kill Mode[/bold cyan]",
        border_style="cyan",
        padding=(1, 3),
    ))
    choice = Prompt.ask(
        "  Select unused-process mode",
        choices=["1", "2", "3"],
        default="3",
    )
    labels = {"1": "Auto", "2": "Manual", "3": "Dry-Run"}
    console.print(
        f"[dim]  ✔  Unused-process mode set to: "
        f"[bold]{labels[choice]}[/bold][/dim]"
    )
    log_event("unused_proc_mode", f"Unused-process mode selected: {labels[choice]}",
              {"unused_proc_mode": choice})
    return choice


# ══════════════════════════════════════════════════════════════════════════════
#  UNUSED PROCESS DETECTION
# ══════════════════════════════════════════════════════════════════════════════

def detect_unused_processes():
    """
    Detect idle/zombie/sleeping processes and handle them according to a
    dedicated kill mode chosen right before scanning — independent of global mode.
    """
    t = time.time()
    console.print("\n[bold cyan]======= UNUSED / IDLE PROCESS DETECTION =======[/bold cyan]")

    # ── Pick the kill mode NOW, before scanning ──────────────────────────────
    u_mode = select_unused_process_mode()

    raw = run_cmd("ps -eo pid,stat,etime,%cpu,%mem,comm --no-headers").splitlines()
    unused = []
    for line in raw:
        parts = line.split(None, 5)
        if len(parts) &lt; 6:
            continue
        pid, stat, etime, cpu, mem, comm = parts
        try: cpu_f = float(cpu)
        except ValueError: cpu_f = 0.0
        is_zombie     = stat.startswith("Z")
        is_long_sleep = stat.startswith("S") and cpu_f == 0.0 and _etime_minutes(etime) &gt; 60
        is_idle       = stat.startswith("I")
        if is_zombie or is_long_sleep or is_idle:
            reason = ("Zombie" if is_zombie
                      else "Idle kernel thread" if is_idle
                      else f"Sleeping {_etime_minutes(etime):.0f}m, 0% CPU")
            unused.append((pid, stat, etime, cpu, mem, comm, reason))

    if not unused:
        console.print("[green]  ✓ No unused/zombie/idle processes found.[/green]")
        log_event("unused_procs", "None found")
        section_timer("Unused Process Detection", t)
        return

    # ── Render results table (always shown, even in dry-run) ─────────────────
    table = Table(
        title=f"Unused / Idle Processes — Mode: [bold cyan]{['','Auto','Manual','Dry-Run'][int(u_mode)]}[/bold cyan]",
        show_header=True,
        header_style="bold red",
        box=box.SIMPLE,
    )
    for col in ("#", "PID", "Stat", "Elapsed", "%CPU", "%MEM", "Command", "Reason", "Action"):
        table.add_column(col, overflow="fold")

    for i, (pid, stat, etime, cpu, mem, comm, reason) in enumerate(unused, 1):
        if u_mode == "3":                          # Dry-Run: show what WOULD happen
            action_hint = (
                "[yellow]Would kill (zombie)[/yellow]" if stat.startswith("Z")
                else "[dim]Would skip (non-zombie)[/dim]"
            )
        elif u_mode == "1":                        # Auto: label what will be done
            action_hint = (
                "[red]→ Kill[/red]" if stat.startswith("Z")
                else "[dim]→ Log &amp; skip[/dim]"
            )
        else:                                      # Manual: pending user decision
            action_hint = "[cyan]Pending[/cyan]"
        table.add_row(str(i), pid, stat, etime, cpu, mem, comm, reason, action_hint)

    console.print(table)
    console.print(f"[yellow]  Found {len(unused)} unused/idle process(es).[/yellow]")
    log_event("unused_procs", f"Found {len(unused)}",
              {"pids": [u[0] for u in unused], "mode": u_mode})

    # ── Dispatch by mode ─────────────────────────────────────────────────────
    if u_mode == "3":
        _dryrun_unused(unused)
    elif u_mode == "1":
        _auto_kill_unused(unused)
    else:
        _interactive_kill_unused(unused)

    section_timer("Unused Process Detection", t)


def _dryrun_unused(unused: list):
    """Dry-Run: report only — no process is touched."""
    zombies  = [u for u in unused if u[1].startswith("Z")]
    sleepers = [u for u in unused if u[1].startswith("S")]
    idles    = [u for u in unused if u[1].startswith("I")]

    console.print(Panel(
        f"[bold]Dry-Run Summary[/bold] — no changes made\n\n"
        f"  [red]Zombies[/red]       : {len(zombies):&gt;4}  "
        f"{'(would be killed in Auto mode)' if zombies else ''}\n"
        f"  [yellow]Long sleepers[/yellow] : {len(sleepers):&gt;4}  "
        f"(would be logged as unprocessed)\n"
        f"  [dim]Idle threads[/dim]  : {len(idles):&gt;4}  "
        f"(would be logged as unprocessed)\n\n"
        f"  Total detected : {len(unused)}",
        title="[bold blue]🔍  Dry-Run Result[/bold blue]",
        border_style="blue",
        padding=(0, 2),
    ))
    # Still record them so the unprocessed report reflects what was seen
    for pid, stat, etime, cpu, mem, comm, reason in unused:
        UNPROCESSED_ITEMS.append({
            "type": "dryrun_unused",
            "pid": pid,
            "stat": stat,
            "comm": comm,
            "reason": reason,
            "note": "Dry-run — not touched",
        })
    log_event("unused_procs_dryrun",
              f"Dry-run: {len(unused)} processes reported, none killed",
              {"zombies": len(zombies), "sleepers": len(sleepers), "idles": len(idles)})


def _etime_minutes(etime: str) -&gt; float:
    try:
        days = 0
        if "-" in etime:
            d, etime = etime.split("-"); days = int(d)
        parts = list(map(int, etime.split(":")))
        h, m, s = (parts + [0, 0, 0])[:3] if len(parts) == 3 else (0, parts[0], parts[1]) if len(parts) == 2 else (0, 0, 0)
        return days * 1440 + h * 60 + m + s / 60
    except Exception:
        return 0.0


def _auto_kill_unused(unused):
    console.print("\n[bold red][Auto Mode] Killing zombie processes...[/bold red]")
    for pid, stat, etime, cpu, mem, comm, reason in unused:
        if stat.startswith("Z"):
            try:
                os.kill(int(pid), 9)
                console.print(f"[green]  ✓ Killed zombie PID {pid} ({comm})[/green]")
                log_event("kill_unused_auto", f"Zombie PID {pid} ({comm}) killed")
            except Exception as e:
                console.print(f"[red]  ✗ Could not kill {pid}: {e}[/red]")
                UNPROCESSED_ITEMS.append({"type": "kill_zombie", "pid": pid, "error": str(e)})
        else:
            console.print(f"[yellow]  ⚠  Logging PID {pid} ({comm}) — {reason}[/yellow]")
            UNPROCESSED_ITEMS.append({"type": "skip_unused", "pid": pid, "reason": reason})


def _interactive_kill_unused(unused):
    console.print("\n[bold yellow][Manual Mode] Choose how to handle unused processes:[/bold yellow]")
    console.print("  [1] Kill all zombies automatically  [2] Pick by row  [3] Enter PIDs  [4] Skip all")
    choice = Prompt.ask("Your choice", choices=["1","2","3","4"], default="4")
    if choice == "1":
        _auto_kill_unused(unused)
    elif choice == "2":
        nums = Prompt.ask("Row numbers to kill (space-separated)", default="")
        for n in nums.split():
            idx = int(n) - 1
            if 0 &lt;= idx &lt; len(unused):
                pid, _, _, _, _, comm, _ = unused[idx]
                _kill_pid(pid, comm, "row")
    elif choice == "3":
        pids = Prompt.ask("PIDs to kill (space-separated)", default="")
        pid_map = {u[0]: u[5] for u in unused}
        for pid in pids.split():
            _kill_pid(pid, pid_map.get(pid,"?"), "manual")
    else:
        console.print("[dim]  All unused processes skipped.[/dim]")
        UNPROCESSED_ITEMS.append({"type": "unused_procs", "count": len(unused), "reason": "user skipped all"})


def _kill_pid(pid, comm, source):
    try:
        os.kill(int(pid), 9)
        console.print(f"[green]  ✓ Killed PID {pid} ({comm})[/green]")
        log_event(f"kill_unused_{source}", f"PID {pid} ({comm})")
    except Exception as e:
        console.print(f"[red]  ✗ Failed to kill {pid}: {e}[/red]")
        UNPROCESSED_ITEMS.append({"type": "kill_fail", "pid": pid, "error": str(e)})


# ══════════════════════════════════════════════════════════════════════════════
#  EBS VOLUMES
# ══════════════════════════════════════════════════════════════════════════════

def list_ebs_volumes():
    t = time.time()
    console.print("\n[bold cyan]======= EBS VOLUMES =======[/bold cyan]")
    if run_cmd("which aws"):
        out = run_cmd(
            'aws ec2 describe-volumes --query '
            '"Volumes[*].{ID:VolumeId,State:State,Size:Size,AZ:AvailabilityZone}" '
            '--output table | head -n 22')
        console.print(out if out else "[yellow]No volumes found[/yellow]")
        log_event("ebs", "Volumes listed")
    else:
        console.print("[yellow]AWS CLI not installed. Skipping.[/yellow]")
    section_timer("EBS Volumes", t)


# ══════════════════════════════════════════════════════════════════════════════
#  DISK USAGE
# ══════════════════════════════════════════════════════════════════════════════

def disk_usage():
    t = time.time()
    path = Prompt.ask("Enter folder path to scan", default="/")
    console.print(f"\n[bold cyan]======= DISK USAGE ({path}) =======[/bold cyan]")
    usage = run_cmd(f"du -ah {path} 2&gt;/dev/null | sort -rh | head -n 20")
    console.print(usage)
    log_event("disk", f"Top storage scanned in {path}")
    section_timer("Disk Usage", t)


# ══════════════════════════════════════════════════════════════════════════════
#  DYNAMIC DELETION  ← FIXED: handles files AND directories
# ══════════════════════════════════════════════════════════════════════════════

def _get_size(path: Path) -&gt; int:
    """Return total size in bytes (works for both files and directories)."""
    try:
        if path.is_file() or path.is_symlink():
            return path.stat().st_size
        return sum(f.stat().st_size for f in path.rglob("*") if f.is_file())
    except Exception:
        return 0


def _human_size(size_bytes: int) -&gt; str:
    for unit in ("B","KB","MB","GB","TB"):
        if size_bytes &lt; 1024.0:
            return f"{size_bytes:.1f} {unit}"
        size_bytes /= 1024.0
    return f"{size_bytes:.1f} PB"


def _delete_path(target: str) -&gt; bool:
    p = Path(target)
    if not p.exists() and not p.is_symlink():
        console.print(f"[yellow]  ⚠  Not found: {target}[/yellow]")
        UNPROCESSED_ITEMS.append({"type": "delete_notfound", "path": target})
        return False

    kind     = "symlink" if p.is_symlink() else ("directory" if p.is_dir() else "file")
    size_b   = _get_size(p)
    size_str = _human_size(size_b)
    parent   = str(p.parent)
    name     = p.name

    if kind == "directory":
        console.print(f"[bold red]  ⚠  {target} is a DIRECTORY ({size_str})[/bold red]")
        if not Confirm.ask(f"  Recursively delete entire directory [red]{target}[/red]?", default=False):
            console.print(f"[yellow]  ⚠  Skipped (directory deletion cancelled)[/yellow]")
            UNPROCESSED_ITEMS.append({"type": "delete_cancelled", "path": target, "kind": kind})
            return False

    try:
        if kind == "directory":
            shutil.rmtree(target)
        else:
            os.remove(target)

        console.print(f"[green]  ✓ Deleted {kind}: {target}  ({size_str})[/green]")
        record = {
            "ts":           now_utc(),
            "path":         target,
            "kind":         kind,
            "size_bytes":   size_b,
            "size_human":   size_str,
            "parent_dir":   parent,
            "name":         name,
            "restore_hint": f"Recover from backup at {parent}/{name} or check snapshots",
            "deleted_by":   RUNNER_IDENTITY.get("linux_user", {}).get("username", getpass.getuser()),
            "run_id":       RUN_TS,
        }
        DELETED_ITEMS.append(record)
        log_event("delete", f"Deleted {kind}: {target}",
                  {"kind": kind, "size_bytes": size_b, "path": target})
        return True

    except PermissionError as e:
        console.print(f"[red]  ✗ Permission denied: {target} — {e}[/red]")
        UNPROCESSED_ITEMS.append({"type": "delete_error", "path": target, "error": str(e)})
        return False
    except Exception as e:
        console.print(f"[red]  ✗ Error deleting {target}: {e}[/red]")
        UNPROCESSED_ITEMS.append({"type": "delete_error", "path": target, "error": str(e)})
        return False


def delete_files():
    t = time.time()
    console.print("\n[bold cyan]======= DYNAMIC FILE / DIRECTORY DELETION =======[/bold cyan]")
    raw = Prompt.ask(
        "Enter full paths to delete (space-separated, blank=skip)",
        default="")
    if not raw.strip():
        console.print("[dim]  Deletion skipped.[/dim]")
        return

    paths = raw.split()

    table = Table(title="Deletion Preview", show_header=True,
                  header_style="bold red", box=box.ROUNDED)
    table.add_column("#"); table.add_column("Path"); table.add_column("Type")
    table.add_column("Size", justify="right")
    for i, p in enumerate(paths, 1):
        pp = Path(p)
        if not pp.exists() and not pp.is_symlink():
            table.add_row(str(i), p, "[yellow]NOT FOUND[/yellow]", "—")
        else:
            kind = "symlink" if pp.is_symlink() else ("directory" if pp.is_dir() else "file")
            table.add_row(str(i), p, kind, _human_size(_get_size(pp)))
    console.print(table)

    if not Confirm.ask(
            f"[bold red]Proceed to delete these {len(paths)} path(s)?[/bold red]", default=False):
        console.print("[dim]  Deletion cancelled.[/dim]")
        UNPROCESSED_ITEMS.append({"type": "delete_batch_cancelled", "paths": paths})
        section_timer("Deletion", t)
        return

    for p in paths:
        _delete_path(p)

    DELETION_LOG.write_text(json.dumps({
        "run_id": RUN_TS,
        "deleted_at": now_utc(),
        "items": DELETED_ITEMS,
        "note": "Use restore_hint for recovery guidance."
    }, indent=2))
    console.print(f"\n[dim]  Deletion log saved → {DELETION_LOG}[/dim]")
    section_timer("Deletion", t)


# ══════════════════════════════════════════════════════════════════════════════
#  DOCKER CLEANUP  — with its own mode selector
# ══════════════════════════════════════════════════════════════════════════════

def select_docker_mode() -&gt; str:
    console.print(Panel(
        "[bold green]1. Manual[/bold green]  — review and confirm each Docker cleanup step\n"
        "[bold yellow]2. Auto[/bold yellow]   — prune all stopped containers, dangling images, unused volumes automatically",
        title="[bold cyan]Docker Cleanup Mode[/bold cyan]",
        border_style="cyan", padding=(0, 2)))
    choice = Prompt.ask("Docker mode", choices=["1","2"], default="1")
    log_event("docker_mode", f"Docker mode selected: {choice}")
    return choice


def docker_cleanup():
    t = time.time()
    console.print("\n[bold cyan]======= DOCKER CLEANUP =======[/bold cyan]")
    if not docker_available():
        console.print("[yellow]  Docker not installed or not in PATH. Skipping.[/yellow]")
        log_event("docker", "Docker not available")
        section_timer("Docker Cleanup", t)
        return

    d_mode = select_docker_mode()
    _docker_stopped_containers(d_mode)
    _docker_dangling_images(d_mode)
    _docker_unused_volumes(d_mode)
    section_timer("Docker Cleanup", t)


def _docker_stopped_containers(mode: str):
    console.print("\n[bold yellow]── Stopped Containers ──[/bold yellow]")
    raw = run_cmd(
        'docker ps -a --filter "status=exited" --filter "status=created" '
        '--format "{{.ID}}\t{{.Names}}\t{{.Image}}\t{{.Status}}\t{{.RunningFor}}"').strip()
    if not raw:
        console.print("  [green]✓ No stopped containers.[/green]"); return
    rows = [r.split("\t") for r in raw.splitlines() if r]
    table = Table(show_header=True, header_style="bold magenta", box=box.SIMPLE)
    for col in ("#","Container ID","Name","Image","Status","Stopped For"):
        table.add_column(col, overflow="fold")
    for i, r in enumerate(rows, 1):
        table.add_row(str(i), *r)
    console.print(table)
    log_event("docker_containers", f"Found {len(rows)} stopped containers")
    if mode == "2":
        out = run_cmd("docker container prune -f")
        console.print(out)
        log_event("docker_containers_auto", "All stopped containers pruned")
    else:
        choice = Prompt.ask("[1] Remove all  [2] Pick by row  [3] Skip",
                            choices=["1","2","3"], default="3")
        if choice == "1":
            console.print(run_cmd("docker container prune -f"))
            log_event("docker_containers", "All pruned (manual)")
        elif choice == "2":
            nums = Prompt.ask("Row numbers (space-separated)", default="")
            for n in nums.split():
                idx = int(n) - 1
                if 0 &lt;= idx &lt; len(rows):
                    cid = rows[idx][0]
                    out = run_cmd(f"docker rm -f {cid}")
                    status = "[green]✓[/green]" if out else "[red]✗[/red]"
                    console.print(f"  {status} Removed container {cid}")
                    log_event("docker_container_remove", f"Container {cid}")
        else:
            UNPROCESSED_ITEMS.append({"type": "docker_containers", "count": len(rows), "reason": "skipped"})


def _docker_dangling_images(mode: str):
    console.print("\n[bold yellow]── Dangling Images ──[/bold yellow]")
    raw = run_cmd(
        'docker images --filter "dangling=true" '
        '--format "{{.ID}}\t{{.Repository}}\t{{.Tag}}\t{{.Size}}\t{{.CreatedSince}}"').strip()
    if not raw:
        console.print("  [green]✓ No dangling images.[/green]"); return
    rows = [r.split("\t") for r in raw.splitlines() if r]
    table = Table(show_header=True, header_style="bold magenta", box=box.SIMPLE)
    for col in ("#","Image ID","Repository","Tag","Size","Created"):
        table.add_column(col, overflow="fold")
    for i, r in enumerate(rows, 1):
        table.add_row(str(i), *r)
    console.print(table)
    log_event("docker_images", f"Found {len(rows)} dangling images")
    if mode == "2":
        console.print(run_cmd("docker image prune -f"))
        log_event("docker_images_auto", "All dangling images pruned")
    else:
        choice = Prompt.ask("[1] Remove all  [2] Pick by row  [3] Skip",
                            choices=["1","2","3"], default="3")
        if choice == "1":
            console.print(run_cmd("docker image prune -f"))
        elif choice == "2":
            nums = Prompt.ask("Row numbers (space-separated)", default="")
            for n in nums.split():
                idx = int(n) - 1
                if 0 &lt;= idx &lt; len(rows):
                    iid = rows[idx][0]
                    out = run_cmd(f"docker rmi -f {iid}")
                    console.print(f"  {'[green]✓[/green]' if out else '[red]✗[/red]'} Removed image {iid}")
        else:
            UNPROCESSED_ITEMS.append({"type": "docker_images", "count": len(rows), "reason": "skipped"})


def _docker_unused_volumes(mode: str):
    console.print("\n[bold yellow]── Unused Volumes ──[/bold yellow]")
    raw = run_cmd('docker volume ls --filter "dangling=true" --format "{{.Name}}\t{{.Driver}}"').strip()
    if not raw:
        console.print("  [green]✓ No unused volumes.[/green]"); return
    rows = [r.split("\t") for r in raw.splitlines() if r]
    table = Table(show_header=True, header_style="bold magenta", box=box.SIMPLE)
    for col in ("#","Volume Name","Driver"):
        table.add_column(col, overflow="fold")
    for i, r in enumerate(rows, 1):
        table.add_row(str(i), *r)
    console.print(table)
    log_event("docker_volumes", f"Found {len(rows)} unused volumes")
    if mode == "2":
        console.print(run_cmd("docker volume prune -f"))
        log_event("docker_volumes_auto", "All unused volumes pruned")
    else:
        choice = Prompt.ask("[1] Remove all  [2] Pick by row  [3] Skip",
                            choices=["1","2","3"], default="3")
        if choice == "1":
            console.print(run_cmd("docker volume prune -f"))
        elif choice == "2":
            nums = Prompt.ask("Row numbers (space-separated)", default="")
            for n in nums.split():
                idx = int(n) - 1
                if 0 &lt;= idx &lt; len(rows):
                    vname = rows[idx][0]
                    out = run_cmd(f"docker volume rm {vname}")
                    console.print(f"  {'[green]✓[/green]' if out else '[red]✗[/red]'} Removed volume {vname}")
        else:
            UNPROCESSED_ITEMS.append({"type": "docker_volumes", "count": len(rows), "reason": "skipped"})


# ══════════════════════════════════════════════════════════════════════════════
#  UNPROCESSED ITEMS REPORT
# ══════════════════════════════════════════════════════════════════════════════

def show_unprocessed_report():
    console.print("\n[bold cyan]======= UNPROCESSED / SKIPPED ITEMS =======[/bold cyan]")
    if not UNPROCESSED_ITEMS:
        console.print("[green]  ✓ All items were processed successfully.[/green]")
    else:
        table = Table(title=f"⚠  {len(UNPROCESSED_ITEMS)} Unprocessed Item(s)",
                      show_header=True, header_style="bold yellow", box=box.ROUNDED)
        table.add_column("#"); table.add_column("Type"); table.add_column("Detail")
        for i, item in enumerate(UNPROCESSED_ITEMS, 1):
            detail = " | ".join(f"{k}={v}" for k, v in item.items() if k != "type")
            table.add_row(str(i), item.get("type","unknown"), detail)
        console.print(table)

    UNPROCESSED.parent.mkdir(parents=True, exist_ok=True)
    UNPROCESSED.write_text(json.dumps({
        "run_id": RUN_TS, "ts": now_utc(),
        "count": len(UNPROCESSED_ITEMS),
        "items": UNPROCESSED_ITEMS
    }, indent=2))
    console.print(f"[dim]  Unprocessed report → {UNPROCESSED}[/dim]")


# ══════════════════════════════════════════════════════════════════════════════
#  FINAL SUMMARY
# ══════════════════════════════════════════════════════════════════════════════

def show_summary():
    total = time.time() - SCRIPT_START
    start_dt = datetime.datetime.utcfromtimestamp(SCRIPT_START)
    end_dt   = datetime.datetime.utcnow()

    console.print(Panel(
        f"[bold white]Started:[/bold white]   {start_dt.strftime('%Y-%m-%d %H:%M:%S')} UTC\n"
        f"[bold white]Finished:[/bold white]  {end_dt.strftime('%Y-%m-%d %H:%M:%S')} UTC\n"
        f"[bold white]Duration:[/bold white]  [yellow]{runtime_str()}[/yellow]  ({total:.1f}s total)",
        title="⏰  Run Duration", border_style="yellow", padding=(0, 2)))

    table = Table(title="⏱  Section Timing Summary", show_header=True,
                  header_style="bold cyan", box=box.ROUNDED)
    table.add_column("Section", style="bold")
    table.add_column("Time", justify="right")
    table.add_column("% of Total", justify="right")
    for section, secs in SECTION_TIMINGS.items():
        pct = secs / total * 100 if total else 0
        bar = "█" * int(pct / 5)
        table.add_row(section, f"{secs:.1f}s", f"{bar} {pct:.0f}%")
    table.add_row("[bold yellow]TOTAL[/bold yellow]",
                  f"[bold yellow]{total:.1f}s[/bold yellow]", "")
    console.print(table)

    console.print(Panel(
        f"[bold white]Log folder:[/bold white]  {LOG_DIR}\n\n"
        f"  [cyan]{LOG_FILE.name}[/cyan]      — structured run events (NDJSON)\n"
        f"  [cyan]{AUDIT_FILE.name}[/cyan]  — master append-only audit trail\n"
        f"  [cyan]{DELETION_LOG.name}[/cyan]  — deleted items with restore hints\n"
        f"  [cyan]{UNPROCESSED.name}[/cyan] — skipped / failed items",
        title="📁  Audit Logs", border_style="green", padding=(0, 2)))


# ══════════════════════════════════════════════════════════════════════════════
#  MAIN
# ══════════════════════════════════════════════════════════════════════════════

def main():
    global RUNNER_IDENTITY

    init_log_dir()
    show_description()

    # ── Runner identity ──────────────────────────────────────────────────────
    console.print("\n[bold cyan]======= RUNNER IDENTITY =======[/bold cyan]")
    RUNNER_IDENTITY = collect_runner_identity()
    show_runner_identity()
    log_event("startup", "Script started — runner identity collected")

    # ── Global mode selection ────────────────────────────────────────────────
    mode = Prompt.ask(
        "\n[bold]Global Mode[/bold]: [green]1=Auto[/green]  [yellow]2=Interactive[/yellow]",
        choices=["1","2"], default="2")
    log_event("mode_selected", f"mode={mode}")

    # ── Resource graphs ──────────────────────────────────────────────────────
    show_resource_graphs()

    # ── Standard sections ────────────────────────────────────────────────────
    system_monitor()
    cpu_list, mem_list = show_top_processes()
    list_ebs_volumes()

    # ── Unused processes — own mode selector, independent of global mode ─────
    detect_unused_processes()          # mode chosen interactively inside

    # ── Docker — own mode selector ───────────────────────────────────────────
    docker_cleanup()

    # ── Interactive-only ─────────────────────────────────────────────────────
    if mode == "2":
        kill_processes(cpu_list, "CPU")
        kill_processes(mem_list, "Memory")
        disk_usage()
        delete_files()

    # ── Final reports ─────────────────────────────────────────────────────────
    show_unprocessed_report()
    log_event("finish", "Cleanup completed",
              {"deleted_count": len(DELETED_ITEMS),
               "unprocessed_count": len(UNPROCESSED_ITEMS)})

    console.print("\n[bold green]   ✓ Cleanup Completed![/bold green]")
    show_summary()


if __name__ == "__main__":
    main()
</code></pre>
]]></content:encoded></item></channel></rss>