Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ Deceptgold is ideal for cybersecurity teams, researchers, and critical infrastru

![Service list](docs/content/assets/deceptgold_service_list_new.jpeg)

## Dashboard
![Service list](docs/content/assets/dashboard.png)

## Features

- **Hybrid Web2 + Web3 Honeypots** - Traditional (Web2) and decentralized (Web3) service simulation
Expand Down
2 changes: 1 addition & 1 deletion deceptgold/.python-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.11
3.12.12
4,669 changes: 0 additions & 4,669 deletions deceptgold/poetry.lock

This file was deleted.

8 changes: 5 additions & 3 deletions deceptgold/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,15 @@ universal_build = true
requires = []

[tool.briefcase.app.deceptgold.linux]
requires = []
requires = ["llama-cpp-python"]

[tool.briefcase.app.deceptgold.linux.system.ubuntu]
system_requires = []
system_requires = ["build-essential", "cmake"]
system_runtime_requires = ["libpython3.12"]

[tool.briefcase.app.deceptgold.linux.system.debian]
docker_base_image = "debian:bookworm"
system_requires = []
system_requires = ["build-essential", "cmake"]
system_runtime_requires = ["libpython3.11", "python3.11"]

[tool.briefcase.app.deceptgold.linux.system.fedora]
Expand Down Expand Up @@ -113,6 +113,8 @@ web3 = "^7.11.1"
qrcode-terminal = "^0.8"
llama-cpp-python = "^0.3.0"
reportlab = "^4.0.0"
matplotlib = "^3.10.8"
numpy = "^2.4.2"

[build-system]
requires = ["poetry-core"]
Expand Down
2 changes: 2 additions & 0 deletions deceptgold/src/deceptgold/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ def init_app():
from deceptgold.commands.notify import notify_app
from deceptgold.commands.ai import ai_app
from deceptgold.commands.reports import reports_app
from deceptgold.commands.dashboard import dashboard_app
from deceptgold.configuration import log
from deceptgold.helper.notify.telemetry.send import exec_telemetry
from deceptgold.helper.ai_model import ensure_default_model_installed, list_installed_models
Expand Down Expand Up @@ -68,6 +69,7 @@ def init_app():
app.command(notify_app)
app.command(ai_app)
app.command(reports_app)
app.command(dashboard_app)

app()

Expand Down
306 changes: 306 additions & 0 deletions deceptgold/src/deceptgold/commands/dashboard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,306 @@
#!/usr/bin/env python3
"""
Web server for the Deceptgold dashboard.
Serves the HTML page and provides a real-time data API.
"""

import json
import os
import secrets
import socket
import signal
import subprocess
import sys
from datetime import datetime
from pathlib import Path
import socketserver
from cyclopts import App
import qrcode_terminal

from deceptgold.commands.dashboard_handler import DashboardHandler, set_dashboard_token

DEFAULT_HOST = "0.0.0.0"
RUNTIME_DIR = Path("/tmp/deceptgold_dashboard")
PID_FILE = RUNTIME_DIR / "dashboard.pid"
STATE_FILE = RUNTIME_DIR / "dashboard_state.json"
dashboard_app = App(name="dashboard", help="Dashboard commands")


class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
"""TCP server with threading support."""
allow_reuse_address = True
daemon_threads = True


def _ensure_runtime_dir():
RUNTIME_DIR.mkdir(parents=True, exist_ok=True)


def _read_pid():
try:
return int(PID_FILE.read_text(encoding="utf-8").strip())
except Exception:
return None


def _is_process_running(pid):
if not pid:
return False
try:
os.kill(pid, 0)
return True
except OSError:
return False


def _is_port_in_use(host, port):
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.settimeout(0.5)
return sock.connect_ex((_resolve_display_host(host), port)) == 0
except OSError:
return False


def _find_high_random_port(host):
for _ in range(100):
candidate = secrets.randbelow(9999) + 55000
if not _is_port_in_use(host, candidate):
return candidate
raise RuntimeError("Unable to allocate a high random port for the dashboard")


def _write_runtime_state(pid, host, port, token):
_ensure_runtime_dir()
started_at = datetime.now().isoformat()
PID_FILE.write_text(str(pid), encoding="utf-8")
STATE_FILE.write_text(
json.dumps(
{
"pid": pid,
"host": host,
"port": port,
"token": token,
"url": _build_access_url(host, port, token),
"started_at": started_at,
},
ensure_ascii=False,
indent=2,
),
encoding="utf-8",
)


def _read_runtime_state():
try:
return json.loads(STATE_FILE.read_text(encoding="utf-8"))
except Exception:
return None


def _format_uptime(started_at):
if not started_at:
return "unknown"
try:
started = datetime.fromisoformat(started_at)
delta = datetime.now() - started
total_seconds = int(delta.total_seconds())
hours, remainder = divmod(total_seconds, 3600)
minutes, seconds = divmod(remainder, 60)
if hours > 0:
return f"{hours}h {minutes}m {seconds}s"
if minutes > 0:
return f"{minutes}m {seconds}s"
return f"{seconds}s"
except Exception:
return "unknown"


def _clear_runtime_state():
for path in (PID_FILE, STATE_FILE):
try:
if path.exists():
path.unlink()
except OSError:

Check notice

Code scanning / CodeQL

Empty except Note

'except' clause does nothing but pass and there is no explanatory comment.

Copilot Autofix

AI 29 days ago

General approach: avoid completely empty except blocks. Either (1) explicitly justify ignoring the exception with a comment, or (2) handle it in some lightweight way, such as logging the error while still allowing execution to continue. We must preserve existing functional behaviour: _clear_runtime_state should still not raise on OSError.

Best fix here: keep swallowing the exception but log it at a low severity (e.g., logging.debug or logging.warning) so that operational issues can be diagnosed. Since we can add imports of standard libraries, we can use Python’s logging module. This preserves the “best effort” semantics while addressing the “does nothing” concern.

Concrete changes in deceptgold/src/deceptgold/commands/dashboard.py:

  • Add import logging alongside the existing imports.
  • Replace the except OSError: pass block in _clear_runtime_state with an except OSError as exc: that calls logging.debug (or logging.warning) to record the failure, including the path and the exception message, and then continues.

No new methods or complex definitions are needed beyond the import; we can rely on the root logger configuration that the application likely already has, or accept default behaviour.

Suggested changeset 1
deceptgold/src/deceptgold/commands/dashboard.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/deceptgold/src/deceptgold/commands/dashboard.py b/deceptgold/src/deceptgold/commands/dashboard.py
--- a/deceptgold/src/deceptgold/commands/dashboard.py
+++ b/deceptgold/src/deceptgold/commands/dashboard.py
@@ -14,6 +14,7 @@
 from datetime import datetime
 from pathlib import Path
 import socketserver
+import logging
 from cyclopts import App
 import qrcode_terminal
 
@@ -121,8 +122,9 @@
         try:
             if path.exists():
                 path.unlink()
-        except OSError:
-            pass
+        except OSError as exc:
+            # Best-effort cleanup: log and ignore failure to remove runtime files
+            logging.debug("Failed to remove runtime file %s: %s", path, exc)
 
 
 def _resolve_display_host(host):
EOF
@@ -14,6 +14,7 @@
from datetime import datetime
from pathlib import Path
import socketserver
import logging
from cyclopts import App
import qrcode_terminal

@@ -121,8 +122,9 @@
try:
if path.exists():
path.unlink()
except OSError:
pass
except OSError as exc:
# Best-effort cleanup: log and ignore failure to remove runtime files
logging.debug("Failed to remove runtime file %s: %s", path, exc)


def _resolve_display_host(host):
Copilot is powered by AI and may make mistakes. Always verify output.
pass


def _resolve_display_host(host):
if host not in {"0.0.0.0", "::"}:
return host
try:
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
sock.connect(("8.8.8.8", 80))
return sock.getsockname()[0]
except Exception:
return "127.0.0.1"


def _resolve_internal_bind_host():
try:
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
sock.connect(("8.8.8.8", 80))
candidate = sock.getsockname()[0]
import ipaddress
if ipaddress.ip_address(candidate).is_private:
return candidate
except Exception:

Check notice

Code scanning / CodeQL

Empty except Note

'except' clause does nothing but pass and there is no explanatory comment.

Copilot Autofix

AI 29 days ago

In general, to fix an empty except that only passes, either handle the exception meaningfully (logging, cleanup, fallback) or narrow the exception type and document why it is safe to ignore. The goal is to avoid completely hiding failures without explanation.

For _resolve_internal_bind_host, we should keep its current behavior—fall back to "127.0.0.1" on any error—but avoid silently swallowing the exception. The least intrusive fix is to add a debug/diagnostic message in the except block while still returning the same fallback. Since the function’s caller does not appear in the snippet, we should not change its return contract or add new exceptions. We also should not introduce new logging frameworks if unnecessary; a simple print to stderr or a comment can suffice.

Concretely, in deceptgold/src/deceptgold/commands/dashboard.py, modify the except Exception: block in _resolve_internal_bind_host (lines 147–148) to either:

  • log a brief message indicating that resolving the internal bind host failed and that "127.0.0.1" will be used, or
  • add a comment explicitly stating that the exception is intentionally ignored because we fall back to localhost.

To maintain functionality but improve diagnostics and satisfy CodeQL, we can replace pass with a print to sys.stderr using the already-imported sys. No new imports are required.

Suggested changeset 1
deceptgold/src/deceptgold/commands/dashboard.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/deceptgold/src/deceptgold/commands/dashboard.py b/deceptgold/src/deceptgold/commands/dashboard.py
--- a/deceptgold/src/deceptgold/commands/dashboard.py
+++ b/deceptgold/src/deceptgold/commands/dashboard.py
@@ -144,8 +144,8 @@
         import ipaddress
         if ipaddress.ip_address(candidate).is_private:
             return candidate
-    except Exception:
-        pass
+    except Exception as e:
+        print(f"Could not resolve internal bind host, falling back to 127.0.0.1: {e}", file=sys.stderr)
     return "127.0.0.1"
 
 
EOF
@@ -144,8 +144,8 @@
import ipaddress
if ipaddress.ip_address(candidate).is_private:
return candidate
except Exception:
pass
except Exception as e:
print(f"Could not resolve internal bind host, falling back to 127.0.0.1: {e}", file=sys.stderr)
return "127.0.0.1"


Copilot is powered by AI and may make mistakes. Always verify output.
pass
return "127.0.0.1"


def _build_access_url(host, port, token=None):
display_host = _resolve_display_host(host)
if token:
return f"http://{display_host}:{port}/?token={token}"
return f"http://{display_host}:{port}"


def start_dashboard_server(port=8080, host="0.0.0.0", token=None):
"""Start the dashboard server."""
if token:
set_dashboard_token(token)
try:
with ThreadedTCPServer((host, port), DashboardHandler) as httpd:
access_url = _build_access_url(host, port, token)
print("Dashboard started")
print(f"Static directory: {os.environ.get('DECEPTGOLD_DASHBOARD_DIR', 'default')}")
if token:
print(f"Access: {access_url}")
else:
generated = secrets.token_urlsafe(24)
print("Token not configured")
print(f"Suggested token: {generated}")
httpd.serve_forever()

except KeyboardInterrupt:
print("\nDashboard stopped")
_clear_runtime_state()
except OSError as e:
if e.errno == 98:
print(f"Port {port} is already in use")
else:
print(f"Error starting dashboard server: {e}")
except Exception as e:
print(f"Unexpected dashboard server error: {e}")


@dashboard_app.command(name="start", help="Start the dashboard server")
def start(public: bool = False, port: int = 0):
host = DEFAULT_HOST if public else _resolve_internal_bind_host()
running_pid = _read_pid()
if _is_process_running(running_pid):
print("Dashboard is already running")
return

selected_port = port or _find_high_random_port(host)

if _is_port_in_use(host, selected_port):
print(f"Dashboard port {selected_port} is already in use")
return

token = secrets.token_urlsafe(24)
access_url = _build_access_url(host, selected_port, token)

child_args = [
sys.executable,
"-c",
f"from deceptgold.commands.dashboard import main; main(['--host', '{host}', '--port', '{selected_port}', '--token', '{token}'])",
]

log_file = RUNTIME_DIR / "dashboard.log"
_ensure_runtime_dir()

with open(log_file, 'w') as log:
process = subprocess.Popen(
child_args,
stdout=log,
stderr=subprocess.STDOUT,
start_new_session=True,
)
if process.poll() is not None:
print("Failed to start dashboard")
print(f"Check logs at: {log_file}")
return
_write_runtime_state(process.pid, host, selected_port, token)

print("Dashboard started")
print(f"Mode: {'public' if public else 'internal'}")
print(f"Access: {access_url}")
print(f"Logs: {log_file}")
qrcode_terminal.draw(access_url)


@dashboard_app.command(name="stop", help="Stop the internal dashboard server")
def stop():
pid = _read_pid()
if not _is_process_running(pid):
_clear_runtime_state()
print("Dashboard is not running")
return

try:
os.kill(pid, signal.SIGTERM)
except OSError:

Check notice

Code scanning / CodeQL

Empty except Note

'except' clause does nothing but pass and there is no explanatory comment.

Copilot Autofix

AI 29 days ago

In general, empty except blocks should either (a) handle the exception in some meaningful way (logging, cleanup, recovery) or (b) explicitly document why it is safe to ignore the exception, often by narrowing the exception type and/or conditionally re-raising.

For this specific case, the best minimal fix is:

  • Catch OSError into a variable.
  • Print a clear message indicating that stopping the dashboard failed, including the error message.
  • Optionally, avoid printing "Dashboard stopped" when the os.kill call fails, to keep user feedback accurate.
  • Still clear the runtime state file, because from the CLI’s perspective once stop has been requested, we want to forget local state even if the OS-level kill failed; but we should be honest about that to the user.

Concretely, within deceptgold/src/deceptgold/commands/dashboard.py in the stop() function:

  • Replace the try/except with a try that sets a stopped flag on success and an except OSError as e: that reports the failure and keeps stopped as False.
  • After the try/except, call _clear_runtime_state() as before, but adjust the printed message depending on whether the kill was successful.

No new imports or helper functions are required.

Suggested changeset 1
deceptgold/src/deceptgold/commands/dashboard.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/deceptgold/src/deceptgold/commands/dashboard.py b/deceptgold/src/deceptgold/commands/dashboard.py
--- a/deceptgold/src/deceptgold/commands/dashboard.py
+++ b/deceptgold/src/deceptgold/commands/dashboard.py
@@ -239,13 +239,18 @@
         print("Dashboard is not running")
         return
 
+    stopped = False
     try:
         os.kill(pid, signal.SIGTERM)
-    except OSError:
-        pass
+        stopped = True
+    except OSError as e:
+        print(f"Failed to stop dashboard (pid {pid}): {e}")
 
     _clear_runtime_state()
-    print("Dashboard stopped")
+    if stopped:
+        print("Dashboard stopped")
+    else:
+        print("Dashboard state cleared, but the process may still be running")
 
 
 @dashboard_app.command(name="status", help="Show dashboard runtime status")
EOF
@@ -239,13 +239,18 @@
print("Dashboard is not running")
return

stopped = False
try:
os.kill(pid, signal.SIGTERM)
except OSError:
pass
stopped = True
except OSError as e:
print(f"Failed to stop dashboard (pid {pid}): {e}")

_clear_runtime_state()
print("Dashboard stopped")
if stopped:
print("Dashboard stopped")
else:
print("Dashboard state cleared, but the process may still be running")


@dashboard_app.command(name="status", help="Show dashboard runtime status")
Copilot is powered by AI and may make mistakes. Always verify output.
pass

_clear_runtime_state()
print("Dashboard stopped")


@dashboard_app.command(name="status", help="Show dashboard runtime status")
def status():
state = _read_runtime_state()
pid = _read_pid()

if not state or not _is_process_running(pid):
_clear_runtime_state()
print("Dashboard is not running")
return

url = state.get("url")
host = state.get("host", DEFAULT_HOST)
port = state.get("port")
started_at = state.get("started_at")
token = state.get("token", "")

print("Dashboard status")
print(f"Running: yes")
print(f"PID: {pid}")
print(f"Host: {host}")
print(f"Port: {port}")
print(f"Uptime: {_format_uptime(started_at)}")
print(f"Token length: {len(token)}")
print(f"Access: {url}")
qrcode_terminal.draw(url)


def main(argv=None):
if argv is None:
argv = sys.argv[1:]

port = 0
host = DEFAULT_HOST
token = None

if len(argv) > 0:
for i, arg in enumerate(argv):
if arg == "--port" and i + 1 < len(argv):
try:
port = int(argv[i + 1])
except ValueError:
print("Port must be a number")
sys.exit(1)
if arg == "--host" and i + 1 < len(argv):
host = argv[i + 1]
if arg == "--token" and i + 1 < len(argv):
token = argv[i + 1]

if not token:
token = secrets.token_urlsafe(24)

start_dashboard_server(port, host, token)


if __name__ == "__main__":
main()
Loading
Loading