wraith-cli 1.2.0__tar.gz → 1.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. {wraith_cli-1.2.0 → wraith_cli-1.3.0}/.env.example +2 -0
  2. {wraith_cli-1.2.0 → wraith_cli-1.3.0}/.gitea/workflows/pages.yml +1 -0
  3. {wraith_cli-1.2.0 → wraith_cli-1.3.0}/.gitignore +1 -0
  4. {wraith_cli-1.2.0 → wraith_cli-1.3.0}/.pre-commit-config.yaml +7 -4
  5. {wraith_cli-1.2.0 → wraith_cli-1.3.0}/CHANGELOG.md +2 -0
  6. {wraith_cli-1.2.0 → wraith_cli-1.3.0}/PKG-INFO +5 -5
  7. {wraith_cli-1.2.0 → wraith_cli-1.3.0}/README.md +3 -3
  8. {wraith_cli-1.2.0 → wraith_cli-1.3.0}/mkdocs.yml +2 -0
  9. {wraith_cli-1.2.0 → wraith_cli-1.3.0}/pyproject.toml +5 -17
  10. {wraith_cli-1.2.0 → wraith_cli-1.3.0}/src/wraith_cli/main.py +88 -53
  11. wraith_cli-1.3.0/src/wraith_cli/shield.py +187 -0
  12. wraith_cli-1.3.0/tests/test_cli.py +301 -0
  13. {wraith_cli-1.2.0 → wraith_cli-1.3.0}/tests/test_qol.py +2 -1
  14. {wraith_cli-1.2.0 → wraith_cli-1.3.0}/tests/test_repo_make.py +33 -0
  15. wraith_cli-1.3.0/tests/test_shield.py +236 -0
  16. {wraith_cli-1.2.0 → wraith_cli-1.3.0}/uv.lock +27 -124
  17. wraith_cli-1.2.0/tests/test_cli.py +0 -143
  18. {wraith_cli-1.2.0 → wraith_cli-1.3.0}/.cz.yaml +0 -0
  19. {wraith_cli-1.2.0 → wraith_cli-1.3.0}/.gitea/CODEOWNERS.md +0 -0
  20. {wraith_cli-1.2.0 → wraith_cli-1.3.0}/.gitea/PULL_REQUEST_TEMPLATE.md +0 -0
  21. {wraith_cli-1.2.0 → wraith_cli-1.3.0}/.gitea/workflows/release.yml +0 -0
  22. {wraith_cli-1.2.0 → wraith_cli-1.3.0}/.gitea/workflows/test.yml +0 -0
  23. {wraith_cli-1.2.0 → wraith_cli-1.3.0}/.secrets.baseline +0 -0
  24. {wraith_cli-1.2.0 → wraith_cli-1.3.0}/CONTRIBUTING.md +0 -0
  25. {wraith_cli-1.2.0 → wraith_cli-1.3.0}/LICENSE +0 -0
  26. {wraith_cli-1.2.0 → wraith_cli-1.3.0}/bin/build.sh +0 -0
  27. {wraith_cli-1.2.0 → wraith_cli-1.3.0}/bin/run_tests.sh +0 -0
  28. {wraith_cli-1.2.0 → wraith_cli-1.3.0}/bin/setup_venv.sh +0 -0
  29. {wraith_cli-1.2.0 → wraith_cli-1.3.0}/docs/architecture.md +0 -0
  30. {wraith_cli-1.2.0 → wraith_cli-1.3.0}/docs/index.md +0 -0
  31. {wraith_cli-1.2.0 → wraith_cli-1.3.0}/docs/reference.md +0 -0
  32. {wraith_cli-1.2.0 → wraith_cli-1.3.0}/docs/security.md +0 -0
  33. {wraith_cli-1.2.0 → wraith_cli-1.3.0}/docs/usage.md +0 -0
  34. {wraith_cli-1.2.0 → wraith_cli-1.3.0}/src/wraith_cli/__init__.py +0 -0
  35. {wraith_cli-1.2.0 → wraith_cli-1.3.0}/src/wraith_cli/qol.py +0 -0
  36. {wraith_cli-1.2.0 → wraith_cli-1.3.0}/src/wraith_cli/repo_make.py +0 -0
@@ -9,3 +9,5 @@ GITEA_COMPOSE_PATH=/home/user/example/gitea
9
9
  GITEA_API_URL="http://your-gitea-instance:3000"
10
10
  GITEA_TOKEN="your_personal_access_token"
11
11
  GITEA_TEMPLATE_URL="https://gitea.domain.com/user/repo-template.git"
12
+
13
+ SCRUTINY_URL=http://127.0.0.1:8090
@@ -1,5 +1,6 @@
1
1
  name: Deploy Gitea Pages
2
2
  on:
3
+ workflow_dispatch:
3
4
  push:
4
5
  branches:
5
6
  - main
@@ -165,3 +165,4 @@ cython_debug/
165
165
  # and can be added to the global gitignore or merged into this file. For a more nuclear
166
166
  # option (not recommended) you can uncomment the following to ignore the entire idea folder.
167
167
  #.idea/
168
+ .aider*
@@ -14,11 +14,14 @@ repos:
14
14
  args: ["--fix"]
15
15
  - id: ruff-format
16
16
 
17
- - repo: https://github.com/pre-commit/mirrors-mypy
18
- rev: v1.15.0
17
+ - repo: local
19
18
  hooks:
20
- - id: mypy
21
- additional_dependencies: ['types-requests']
19
+ - id: ty
20
+ name: ty
21
+ entry: uv run ty check
22
+ language: system
23
+ types: [python]
24
+ pass_filenames: false
22
25
 
23
26
  - repo: https://github.com/Yelp/detect-secrets
24
27
  rev: v1.5.0
@@ -1,3 +1,5 @@
1
+ ## v1.3.0 (2026-04-04)
2
+
1
3
  ## v1.2.0 (2026-04-02)
2
4
 
3
5
  ## v1.1.0 (2026-03-31)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wraith-cli
3
- Version: 1.2.0
3
+ Version: 1.3.0
4
4
  Summary: Sovereign Command Centre for a Ghost Stack
5
5
  Project-URL: Homepage, https://git.thomaspeoples.com/thomaspeoples/wraith-cli
6
6
  Project-URL: Documentation, https://www.thomaspeoples.com/gitea-repos/wraith-cli/
@@ -42,19 +42,19 @@ Requires-Dist: detect-secrets; extra == 'dev'
42
42
  Requires-Dist: genbadge[coverage]>=1.1.1; extra == 'dev'
43
43
  Requires-Dist: mkdocs-material<=10.0; extra == 'dev'
44
44
  Requires-Dist: mkdocs<=2.0; extra == 'dev'
45
- Requires-Dist: mypy; extra == 'dev'
46
45
  Requires-Dist: pre-commit; extra == 'dev'
47
46
  Requires-Dist: pymdown-extensions>=10.7.0; extra == 'dev'
48
47
  Requires-Dist: pytest; extra == 'dev'
49
48
  Requires-Dist: pytest-cov; extra == 'dev'
50
49
  Requires-Dist: ruff; extra == 'dev'
50
+ Requires-Dist: ty; extra == 'dev'
51
51
  Requires-Dist: types-requests; extra == 'dev'
52
52
  Description-Content-Type: text/markdown
53
53
 
54
54
  [![Documentation](https://img.shields.io/badge/docs-live-brightgreen)](https://www.thomaspeoples.com/gitea-repos/wraith-cli/)
55
- ![PyPI - Version](https://img.shields.io/pypi/v/wraith_cli)
56
- ![PyPI - License](https://img.shields.io/pypi/l/wraith_cli)
57
- ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/wraith_cli)
55
+ ![PyPI - Version](https://img.shields.io/pypi/v/wraith-cli)
56
+ ![PyPI - License](https://img.shields.io/pypi/l/wraith-cli)
57
+ ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/wraith-cli)
58
58
 
59
59
 
60
60
  # 👻 Wraith-CLI
@@ -1,7 +1,7 @@
1
1
  [![Documentation](https://img.shields.io/badge/docs-live-brightgreen)](https://www.thomaspeoples.com/gitea-repos/wraith-cli/)
2
- ![PyPI - Version](https://img.shields.io/pypi/v/wraith_cli)
3
- ![PyPI - License](https://img.shields.io/pypi/l/wraith_cli)
4
- ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/wraith_cli)
2
+ ![PyPI - Version](https://img.shields.io/pypi/v/wraith-cli)
3
+ ![PyPI - License](https://img.shields.io/pypi/l/wraith-cli)
4
+ ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/wraith-cli)
5
5
 
6
6
 
7
7
  # 👻 Wraith-CLI
@@ -20,6 +20,8 @@ nav:
20
20
  - Home: index.md
21
21
  - Usage: usage.md
22
22
  - Technical Reference: reference.md
23
+ - Architecture Reference: architecture.md
24
+ - Security Statement: security.md
23
25
 
24
26
  plugins:
25
27
  - search
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "wraith-cli"
7
- version = "1.2.0"
7
+ version = "1.3.0"
8
8
  description = "Sovereign Command Centre for a Ghost Stack"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -41,11 +41,11 @@ wraith = "wraith_cli.main:app"
41
41
  [project.optional-dependencies]
42
42
  dev = [
43
43
  "ruff",
44
+ "ty",
44
45
  "pytest",
45
46
  "pytest-cov",
46
47
  "pre-commit",
47
48
  "commitizen",
48
- "mypy",
49
49
  "types-requests",
50
50
  "detect-secrets",
51
51
  "mkdocs<=2.0",
@@ -56,34 +56,22 @@ dev = [
56
56
 
57
57
  [tool.pytest.ini_options]
58
58
  xfail_strict = true
59
- addopts = ["--cov=src", "--cov-report=term-missing", "--tb=short"]
59
+ addopts = ["--cov=wraith_cli", "--cov-report=term-missing", "--tb=short"]
60
60
  pythonpath = ["src"]
61
61
 
62
62
  [tool.ruff]
63
63
  line-length = 79
64
64
  target-version = "py312"
65
+ preview = true
65
66
 
66
67
  [tool.ruff.lint]
67
- select = ["E", "F", "I"]
68
+ select = ["E", "F", "I", "N", "UP", "B"]
68
69
  ignore = []
69
70
 
70
71
  [tool.ruff.format]
71
72
  quote-style = "double"
72
73
  indent-style = "space"
73
74
 
74
- [tool.mypy]
75
- # The "Strict" setting enforces type hints everywhere
76
- strict = true
77
- python_version = "3.12"
78
-
79
- # Don't moan about missing stubs for libraries that don't have them
80
- ignore_missing_imports = true
81
-
82
- # Make sure we don't accidentally leave 'any' types in the code
83
- disallow_untyped_defs = true
84
- disallow_incomplete_defs = true
85
- warn_unused_ignores = true
86
-
87
75
  [tool.hatch.build.targets.wheel]
88
76
  packages = ["src/wraith_cli"]
89
77
 
@@ -2,7 +2,7 @@ import os
2
2
  import subprocess
3
3
  from importlib.metadata import version as get_local_version
4
4
  from pathlib import Path
5
- from typing import Optional
5
+ from typing import Annotated
6
6
 
7
7
  import requests
8
8
  import typer
@@ -12,7 +12,22 @@ from rich.markdown import Markdown
12
12
  from rich.panel import Panel
13
13
  from rich.text import Text
14
14
 
15
- from wraith_cli import qol, repo_make
15
+ from wraith_cli import qol, repo_make, shield
16
+
17
+ SOVEREIGN_ADVISORY = """
18
+ ## 🛡️ Sovereign Security Advisory – Please Read
19
+
20
+ **Wraith-CLI** is your stack’s nervous system and has sharp edges.
21
+ The commands in this toolkit can wipe runner data (`runner-reset`),
22
+ view across all containers (`ps`), and expose sensitive stack paths.
23
+
24
+ **A bad config or unintended command can destabilise your core stack.**
25
+
26
+ Recommended baseline:
27
+ - Use strong passphrases on private keys if exposed to Wraith.
28
+ - Sandboxed execution where possible for third-party modules.
29
+ - Audit environment variable scope (`GITEA_COMPOSE_PATH`, etc.)
30
+ """
16
31
 
17
32
  # Load environment variables from .env
18
33
  load_dotenv()
@@ -42,22 +57,6 @@ def version_callback(value: bool):
42
57
  raise typer.Exit()
43
58
 
44
59
 
45
- SECURITY_WARNING = """
46
- ## 🛡️ Sovereign Security Advisory – Please Read
47
-
48
- **Wraith-CLI** is your stack’s nervous system and has sharp edges.
49
- The commands in this toolkit can wipe runner data (`runner-reset`),
50
- view across all containers (`ps`), and expose sensitive stack paths.
51
-
52
- **A bad config or unintended command can destabilise your core stack.**
53
-
54
- Recommended baseline:
55
- - Use strong passphrases on private keys if exposed to Wraith.
56
- - Sandboxed execution where possible for third-party modules.
57
- - Audit environment variable scope (`GITEA_COMPOSE_PATH`, etc.)
58
- """
59
-
60
-
61
60
  def print_wraith_welcome(): # pragma: no cover
62
61
  """Renders the definitive OpenClaw-inspired Sovereign splash."""
63
62
  console = qol.console
@@ -85,27 +84,14 @@ def print_wraith_welcome(): # pragma: no cover
85
84
  )
86
85
  console.print("\n")
87
86
 
88
- SOVEREIGN_ADVISORY = """
89
- ## Sovereign Security Advisory – Please Read
90
-
91
- **Wraith-CLI** is your stack’s nervous system and has sharp edges.
92
- The commands in this toolkit can wipe runner data (`runner-reset`),
93
- view across all containers (`ps`), and expose sensitive stack paths.
94
-
95
- **A bad config or unintended command can destabilise your core stack.**
96
-
97
- Recommended baseline:
98
- - Use strong passphrases on private keys if exposed to Wraith.
99
- - Sandboxed execution where possible for third-party modules.
100
- - Audit environment variable scope (`GITEA_COMPOSE_PATH`, etc.)
101
- """
102
87
  md = Markdown(SOVEREIGN_ADVISORY)
103
88
 
104
89
  panel = Panel(
105
90
  md,
106
- title=f"""
107
- Wraith Orchestrator – [bright_black]v{local_v} Advisory[/bright_black]
108
- """,
91
+ title=(
92
+ "Wraith Orchestrator – "
93
+ f"[bright_black]v{local_v} Advisory[/bright_black]"
94
+ ),
109
95
  border_style="bright_black",
110
96
  padding=(1, 2),
111
97
  )
@@ -117,13 +103,15 @@ Wraith Orchestrator – [bright_black]v{local_v} Advisory[/bright_black]
117
103
  @app.callback(invoke_without_command=True)
118
104
  def main(
119
105
  ctx: typer.Context,
120
- version: Optional[bool] = typer.Option(
121
- None,
122
- "--version",
123
- callback=version_callback,
124
- is_eager=True,
125
- help="Show version and exit.",
126
- ),
106
+ version: Annotated[
107
+ bool | None,
108
+ typer.Option(
109
+ "--version",
110
+ callback=version_callback,
111
+ is_eager=True,
112
+ help="Show version and exit.",
113
+ ),
114
+ ] = None,
127
115
  ):
128
116
  """
129
117
  [bold green]Wraith-CLI[/bold green]
@@ -174,7 +162,9 @@ def update():
174
162
 
175
163
  @app.command()
176
164
  def ps(
177
- all: bool = typer.Option(False, "--all", "-a", help="Show all containers"),
165
+ all: Annotated[
166
+ bool, typer.Option("--all", "-a", help="Show all containers")
167
+ ] = False,
178
168
  ):
179
169
  """View Docker process list with Sovereign styling."""
180
170
  cmd = ["docker", "ps"]
@@ -198,13 +188,15 @@ def tail(
198
188
  service: str = typer.Argument(
199
189
  ..., help="Service name (e.g., ollama, gitea)"
200
190
  ),
201
- path: Optional[Path] = typer.Option(
202
- None,
203
- "--path",
204
- "-p",
205
- envvar="WRAITH_COMPOSE_PATH",
206
- help="Path to the directory containing docker-compose.yml",
207
- ),
191
+ path: Annotated[
192
+ Path | None,
193
+ typer.Option(
194
+ "--path",
195
+ "-p",
196
+ envvar="WRAITH_COMPOSE_PATH",
197
+ help="Path to directory with docker-compose.yml.",
198
+ ),
199
+ ] = None,
208
200
  ):
209
201
  """Tail logs for any service in your stack.
210
202
  Priority:
@@ -270,6 +262,10 @@ def spawn(
270
262
  )
271
263
  raise typer.Exit(1)
272
264
 
265
+ assert GITEA_API_URL is not None
266
+ assert GITEA_TOKEN is not None
267
+ assert GITEA_TEMPLATE_URL is not None
268
+
273
269
  target_dir = Path.cwd() / repo_name
274
270
  typer.echo(f"👻 Spawning '{repo_name}' from Repo Factory...")
275
271
 
@@ -294,11 +290,50 @@ def spawn(
294
290
 
295
291
  except FileExistsError as e:
296
292
  typer.secho(f"❌ Error: {e}", fg="red")
297
- raise typer.Exit(1)
293
+ raise typer.Exit(1) from e
298
294
  except Exception as e:
299
295
  typer.secho(f"❌ Critical Failure: {e}", fg="red")
300
- raise typer.Exit(1)
296
+ raise typer.Exit(1) from e
297
+
298
+
299
+ @app.command()
300
+ def logs(
301
+ health: Annotated[
302
+ bool,
303
+ typer.Option(
304
+ "--health", help="Pull SMART data summary from Scrutiny API"
305
+ ),
306
+ ] = False,
307
+ ):
308
+ """View stack logs or bare-metal health status."""
309
+ if health:
310
+ typer.echo("🔍 Polling Bare-Metal SMART data via Scrutiny...")
311
+ health_table = shield.get_scrutiny_health()
312
+ qol.console.print(health_table)
313
+ else:
314
+ typer.echo(
315
+ "Use 'wraith tail <service>' for container logs. Pass --health for drive health."
316
+ )
317
+
318
+
319
+ @app.command()
320
+ def mesh():
321
+ """Checks Tailscale status and lists active peers in the Ghost Mesh."""
322
+ typer.echo("🌐 Querying Ghost Mesh (Tailscale)...")
323
+ mesh_table = shield.get_tailscale_mesh()
324
+ qol.console.print(mesh_table)
325
+
326
+
327
+ @app.command()
328
+ def audit():
329
+ """
330
+ Checks for containers running as root or exposed ports not in your
331
+ Registry Spec.
332
+ """
333
+ typer.echo("🛡️ Initiating Sovereign Security Audit...")
334
+ audit_table = shield.run_security_audit()
335
+ qol.console.print(audit_table)
301
336
 
302
337
 
303
- if __name__ == "__main__":
338
+ if __name__ == "__main__": # pragma: no cover
304
339
  app()
@@ -0,0 +1,187 @@
1
+ import json
2
+ import os
3
+ import subprocess
4
+ import requests
5
+ from rich.table import Table
6
+
7
+ # Pull from .env, fallback to the Ghost Stack default port
8
+ SCRUTINY_URL = os.getenv("SCRUTINY_URL", "http://127.0.0.1:8090")
9
+
10
+ def get_scrutiny_health() -> Table:
11
+ """Fetches SMART data from the Scrutiny API and formats it.
12
+
13
+ Returns:
14
+ Table: A rich Table object containing device health information.
15
+ """
16
+ table = Table(
17
+ title="Scrutiny SMART Health",
18
+ border_style="bright_black",
19
+ header_style="bold green",
20
+ )
21
+ table.add_column("Device")
22
+ table.add_column("Capacity")
23
+ table.add_column("Temp")
24
+ table.add_column("Status")
25
+
26
+ try:
27
+ # Scrutiny summary API endpoint
28
+ response = requests.get(f"{SCRUTINY_URL}/api/summary", timeout=5)
29
+ response.raise_for_status()
30
+ data = response.json()
31
+
32
+ devices = data.get("data", {}).get("summary", {})
33
+ if not devices:
34
+ table.add_row("No devices found.", "-", "-", "-")
35
+ return table
36
+
37
+ for wwn, device in devices.items():
38
+ name = device.get("device", {}).get("name", "Unknown")
39
+ capacity_bytes = device.get("device", {}).get("capacity", 0)
40
+ capacity_tb = f"{(capacity_bytes / (10**12)):.1f} TB" if capacity_bytes else "Unknown"
41
+
42
+ temp = str(device.get("smart", {}).get("temp", "N/A")) + "°C"
43
+ status = device.get("smart", {}).get("status", "Unknown")
44
+
45
+ # Sovereign styling for status
46
+ status_color = "green" if status.lower() == "passed" else "red"
47
+ formatted_status = f"[{status_color}]{status.upper()}[/{status_color}]"
48
+
49
+ table.add_row(name, capacity_tb, temp, formatted_status)
50
+
51
+ except requests.exceptions.RequestException as e:
52
+ table.add_row(f"[red]API Error: {e}[/red]", "-", "-", "-")
53
+
54
+ return table
55
+
56
+
57
+ def get_tailscale_mesh() -> Table:
58
+ """Pulls Tailscale status via JSON and formats active peers.
59
+
60
+ Returns:
61
+ Table: A rich Table object containing Tailscale mesh peer status.
62
+ """
63
+ table = Table(
64
+ title="Ghost Mesh (Tailscale)",
65
+ border_style="bright_black",
66
+ header_style="bold green",
67
+ )
68
+ table.add_column("Hostname")
69
+ table.add_column("Tailscale IP")
70
+ table.add_column("OS")
71
+ table.add_column("Status")
72
+
73
+ try:
74
+ result = subprocess.run(
75
+ ["tailscale", "status", "--json"],
76
+ capture_output=True,
77
+ text=True,
78
+ check=True,
79
+ )
80
+ data = json.loads(result.stdout)
81
+
82
+ peers = data.get("Peer", {})
83
+ for peer_id, peer_data in peers.items():
84
+ hostname = peer_data.get("HostName", "Unknown")
85
+ ip = peer_data.get("TailscaleIPs", ["Unknown"])[0]
86
+ os_name = peer_data.get("OS", "Unknown")
87
+ is_online = peer_data.get("Online", False)
88
+
89
+ status_str = (
90
+ "[green]Online[/green]"
91
+ if is_online
92
+ else "[bright_black]Offline[/bright_black]"
93
+ )
94
+
95
+ table.add_row(hostname, ip, os_name, status_str)
96
+
97
+ except Exception as e:
98
+ table.add_row(f"[red]Mesh Error: {e}[/red]", "-", "-", "-")
99
+
100
+ return table
101
+
102
+
103
+ def run_security_audit() -> Table:
104
+ """Checks for containers running as root or highly exposed ports.
105
+
106
+ Returns:
107
+ Table: A rich Table object with security audit results.
108
+ """
109
+ table = Table(
110
+ title="Sovereign Security Audit",
111
+ border_style="bright_black",
112
+ header_style="bold green",
113
+ )
114
+ table.add_column("Container")
115
+ table.add_column("User")
116
+ table.add_column("Exposed Ports")
117
+ table.add_column("Verdict")
118
+
119
+ try:
120
+ # Get all running container IDs
121
+ ps_result = subprocess.run(
122
+ ["docker", "ps", "-q"],
123
+ capture_output=True,
124
+ text=True,
125
+ check=True,
126
+ )
127
+ container_ids = [cid for cid in ps_result.stdout.split("\n") if cid]
128
+
129
+ if not container_ids:
130
+ table.add_row("No running containers.", "-", "-", "-")
131
+ return table
132
+
133
+ # Inspect all at once
134
+ inspect_result = subprocess.run(
135
+ ["docker", "inspect"] + container_ids,
136
+ capture_output=True,
137
+ text=True,
138
+ check=True,
139
+ )
140
+ containers = json.loads(inspect_result.stdout)
141
+
142
+ for c in containers:
143
+ name = c["Name"].lstrip("/")
144
+
145
+ # Check User
146
+ user = c["Config"].get("User", "")
147
+ if not user or user == "0" or user == "root":
148
+ user_disp = "[red]ROOT[/red]"
149
+ is_root = True
150
+ else:
151
+ user_disp = f"[green]{user}[/green]"
152
+ is_root = False
153
+
154
+ # Check Ports (Simplified: grabs bindings)
155
+ ports = c["NetworkSettings"].get("Ports", {})
156
+ exposed = []
157
+ for port, bindings in ports.items():
158
+ if bindings:
159
+ for b in bindings:
160
+ host_ip = b.get("HostIp", "")
161
+ host_port = b.get("HostPort", "")
162
+ # Flag if exposed globally (0.0.0.0) vs. locally.
163
+ if host_ip == "0.0.0.0" or host_ip == "":
164
+ exposed.append(
165
+ f"[yellow]{host_port}->{port} (Global)[/yellow]"
166
+ )
167
+ else:
168
+ exposed.append(f"{host_port}->{port}")
169
+
170
+ port_disp = ", ".join(exposed) if exposed else "Internal Only"
171
+
172
+ # Determine Verdict
173
+ if is_root and "Global" in port_disp:
174
+ verdict = "[red]CRITICAL: Root + Global Port[/red]"
175
+ elif is_root:
176
+ verdict = "[yellow]WARNING: Root Context[/yellow]"
177
+ elif "Global" in port_disp:
178
+ verdict = "[yellow]WARNING: Global Exposure[/yellow]"
179
+ else:
180
+ verdict = "[green]SECURE[/green]"
181
+
182
+ table.add_row(name, user_disp, port_disp, verdict)
183
+
184
+ except Exception as e:
185
+ table.add_row(f"[red]Audit Error: {e}[/red]", "-", "-", "-")
186
+
187
+ return table