|
1 | | -import requests |
| 1 | +#!/usr/bin/env python3 |
| 2 | +import argparse |
2 | 3 | import json |
| 4 | +import sys |
| 5 | +from pathlib import Path |
| 6 | + |
| 7 | +import requests |
| 8 | +from alive_progress import alive_bar |
| 9 | + |
| 10 | + |
| 11 | +DEFAULT_URL = "http://localhost:2375" # Docker Engine API (no auth; enable with care) |
| 12 | + |
| 13 | + |
| 14 | +def fmt_bytes(n: int) -> str: |
| 15 | + """Human-friendly bytes.""" |
| 16 | + units = ["B", "KB", "MB", "GB", "TB", "PB"] |
| 17 | + size = float(n) |
| 18 | + for u in units: |
| 19 | + if size < 1024.0: |
| 20 | + return f"{size:.1f} {u}" |
| 21 | + size /= 1024.0 |
| 22 | + return f"{size:.1f} EB" |
| 23 | + |
3 | 24 |
|
4 | | -# Docker API URL (assuming Docker is running locally) |
5 | | -DOCKER_API_URL = "http://localhost:2375" |
| 25 | +def get_engine_info(base_url: str, timeout: int = 10) -> dict: |
| 26 | + """Return Docker Engine info from /info.""" |
| 27 | + try: |
| 28 | + resp = requests.get(f"{base_url.rstrip('/')}/info", timeout=timeout) |
| 29 | + resp.raise_for_status() |
| 30 | + return resp.json() |
| 31 | + except requests.RequestException as e: |
| 32 | + print(f"[!] Failed to get engine info from {base_url}: {e}", file=sys.stderr) |
| 33 | + return {} |
6 | 34 |
|
7 | | -def get_containers(): |
8 | | - # Get list of all containers |
9 | | - response = requests.get(f"{DOCKER_API_URL}/containers/json?all=true") |
10 | | - if response.status_code != 200: |
11 | | - print("Failed to get containers") |
| 35 | + |
| 36 | +def get_containers(base_url: str, timeout: int = 10): |
| 37 | + """Return list of container summary objects (like `docker ps -a`).""" |
| 38 | + try: |
| 39 | + resp = requests.get(f"{base_url.rstrip('/')}/containers/json?all=true", timeout=timeout) |
| 40 | + resp.raise_for_status() |
| 41 | + return resp.json() |
| 42 | + except requests.RequestException as e: |
| 43 | + print(f"[!] Failed to get containers from {base_url}: {e}", file=sys.stderr) |
12 | 44 | return [] |
13 | | - return response.json() |
14 | 45 |
|
15 | | -def get_container_env(container_id): |
16 | | - # Get environment variables for a specific container |
17 | | - response = requests.get(f"{DOCKER_API_URL}/containers/{container_id}/json") |
18 | | - if response.status_code != 200: |
19 | | - print(f"Failed to get info for container {container_id}") |
| 46 | + |
| 47 | +def get_container_env(base_url: str, container_id: str, timeout: int = 10): |
| 48 | + """Return list of Env strings for a container (e.g., ['KEY=VALUE', ...]).""" |
| 49 | + try: |
| 50 | + resp = requests.get(f"{base_url.rstrip('/')}/containers/{container_id}/json", timeout=timeout) |
| 51 | + resp.raise_for_status() |
| 52 | + data = resp.json() |
| 53 | + return data.get("Config", {}).get("Env", []) or [] |
| 54 | + except requests.RequestException as e: |
| 55 | + print(f"[!] Failed to get info for container {container_id[:12]}: {e}", file=sys.stderr) |
20 | 56 | return [] |
21 | | - |
22 | | - container_data = response.json() |
23 | | - env_vars = container_data.get('Config', {}).get('Env', []) |
24 | | - return env_vars |
| 57 | + |
| 58 | + |
| 59 | +def parse_args(): |
| 60 | + p = argparse.ArgumentParser( |
| 61 | + description="Enumerate Docker containers and print their environment variables." |
| 62 | + ) |
| 63 | + p.add_argument( |
| 64 | + "--url", |
| 65 | + default=DEFAULT_URL, |
| 66 | + help=f"Docker Engine API base URL (default: {DEFAULT_URL})", |
| 67 | + ) |
| 68 | + p.add_argument( |
| 69 | + "--out", |
| 70 | + metavar="FILE", |
| 71 | + help="Optional path to save results as JSON (e.g., results.json)", |
| 72 | + ) |
| 73 | + p.add_argument( |
| 74 | + "--timeout", |
| 75 | + type=int, |
| 76 | + default=10, |
| 77 | + help="HTTP timeout in seconds (default: 10)", |
| 78 | + ) |
| 79 | + p.add_argument( |
| 80 | + "--show-info-json", |
| 81 | + action="store_true", |
| 82 | + help="Also print the full /info JSON after the overview.", |
| 83 | + ) |
| 84 | + return p.parse_args() |
| 85 | + |
| 86 | + |
| 87 | +def print_engine_overview(info: dict): |
| 88 | + """Pretty, concise overview of key /info fields.""" |
| 89 | + if not info: |
| 90 | + print("Engine Info: (unavailable)") |
| 91 | + return |
| 92 | + |
| 93 | + server_ver = info.get("ServerVersion") |
| 94 | + os_name = info.get("OperatingSystem") |
| 95 | + os_type = info.get("OSType") |
| 96 | + arch = info.get("Architecture") |
| 97 | + ncpu = info.get("NCPU") |
| 98 | + mem = info.get("MemTotal") |
| 99 | + containers = info.get("Containers") |
| 100 | + images = info.get("Images") |
| 101 | + driver = info.get("Driver") |
| 102 | + swarm = info.get("Swarm", {}) |
| 103 | + swarm_state = (swarm.get("LocalNodeState") or "inactive").lower() |
| 104 | + |
| 105 | + print("=== Docker Engine Overview (/info) ===") |
| 106 | + print(f"Server Version : {server_ver}") |
| 107 | + print(f"OS / Arch : {os_name} ({os_type}/{arch})") |
| 108 | + print(f"CPUs / Memory : {ncpu} / {fmt_bytes(mem) if isinstance(mem, int) else mem}") |
| 109 | + print(f"Storage Driver : {driver}") |
| 110 | + print(f"Containers : {containers}") |
| 111 | + print(f"Images : {images}") |
| 112 | + print(f"Swarm State : {swarm_state}") |
| 113 | + if info.get("RegistryConfig", {}).get("InsecureRegistryCIDRs"): |
| 114 | + print("Insecure Registries configured") |
| 115 | + if info.get("ExperimentalBuild"): |
| 116 | + print("Experimental features: enabled") |
| 117 | + print("=" * 36) |
| 118 | + |
25 | 119 |
|
26 | 120 | def main(): |
27 | | - containers = get_containers() |
| 121 | + args = parse_args() |
| 122 | + |
| 123 | + # 1) Capture and print /info first |
| 124 | + engine_info = get_engine_info(args.url, timeout=args.timeout) |
| 125 | + print_engine_overview(engine_info) |
| 126 | + if args.show_info_json and engine_info: |
| 127 | + print(json.dumps(engine_info, indent=2)) |
| 128 | + |
| 129 | + # 2) Enumerate containers with a progress bar |
| 130 | + containers = get_containers(args.url, timeout=args.timeout) |
28 | 131 | if not containers: |
29 | 132 | print("No containers found.") |
30 | | - return |
31 | | - |
32 | | - for container in containers: |
33 | | - container_id = container['Id'] |
34 | | - container_name = container['Names'][0] |
35 | | - print(f"\nContainer Name: {container_name}") |
36 | | - print(f"Container ID: {container_id}") |
37 | | - |
38 | | - env_vars = get_container_env(container_id) |
39 | | - if env_vars: |
40 | | - print("Environment Variables:") |
41 | | - for env in env_vars: |
42 | | - print(f" - {env}") |
43 | | - else: |
44 | | - print("No environment variables found.") |
| 133 | + results = {"engine_info": engine_info, "containers": []} |
| 134 | + if args.out: |
| 135 | + try: |
| 136 | + Path(args.out).write_text(json.dumps(results, indent=2)) |
| 137 | + print(f"\nSaved results to {Path(args.out).resolve()}") |
| 138 | + except Exception as e: |
| 139 | + print(f"[!] Failed to write output file '{args.out}': {e}", file=sys.stderr) |
| 140 | + return 2 |
| 141 | + return 0 |
| 142 | + |
| 143 | + results = {"engine_info": engine_info, "containers": []} |
| 144 | + |
| 145 | + print(f"Found {len(containers)} containers. Inspecting…") |
| 146 | + with alive_bar(len(containers), title="Investigating containers") as bar: |
| 147 | + for c in containers: |
| 148 | + container_id = c.get("Id", "") or "" |
| 149 | + names = c.get("Names") or [] |
| 150 | + name = names[0] if names else "" |
| 151 | + if name.startswith("/"): |
| 152 | + name = name[1:] |
| 153 | + |
| 154 | + env_vars = get_container_env(args.url, container_id, timeout=args.timeout) |
| 155 | + |
| 156 | + # Print to stdout |
| 157 | + print(f"\nContainer Name: {name or '(unnamed)'}") |
| 158 | + print(f"Container ID: {container_id}") |
| 159 | + if env_vars: |
| 160 | + print("Environment Variables:") |
| 161 | + for env in env_vars: |
| 162 | + print(f" - {env}") |
| 163 | + else: |
| 164 | + print("No environment variables found.") |
| 165 | + |
| 166 | + # Append to results |
| 167 | + results["containers"].append( |
| 168 | + { |
| 169 | + "id": container_id, |
| 170 | + "name": name, |
| 171 | + "env": env_vars, |
| 172 | + } |
| 173 | + ) |
| 174 | + |
| 175 | + bar() # advance progress |
| 176 | + |
| 177 | + # 3) Save if requested |
| 178 | + if args.out: |
| 179 | + try: |
| 180 | + out_path = Path(args.out) |
| 181 | + out_path.write_text(json.dumps(results, indent=2)) |
| 182 | + print(f"\nSaved results to {out_path.resolve()}") |
| 183 | + except Exception as e: |
| 184 | + print(f"[!] Failed to write output file '{args.out}': {e}", file=sys.stderr) |
| 185 | + return 2 |
| 186 | + |
| 187 | + return 0 |
| 188 | + |
45 | 189 |
|
46 | 190 | if __name__ == "__main__": |
47 | | - main() |
| 191 | + sys.exit(main()) |
0 commit comments