diff --git a/README.md b/README.md new file mode 100644 index 0000000..9acfb7a --- /dev/null +++ b/README.md @@ -0,0 +1,207 @@ +# Wilcon Client Agent + +**Version:** 0.3.3 +**Letzte Aktualisierung:** 2025-06-29 + +## 🛍 ProjektĂŒberblick + +Wilcon ist ein modularer Systemagent zur Überwachung von: + +- **System- und Hardwareinformationen** +- **Netzwerkdaten** +- **Laufenden Prozessen** +- **BenutzeraktivitĂ€t (inkl. Steuerung via CLI)** + +Die Daten werden ĂŒber eine REST-API bereitgestellt und in einem modernen Web-Dashboard dargestellt. ZusĂ€tzlich ist ein optionales CLI-Tool zur Benutzerkontrolle integriert (`Safe Kiddo Agent`). + +--- + +## 📁 Projektstruktur + +```text +. +├── main.py # (Legacy) einfacher Info-Endpunkt +├── wilcon.py # FastAPI-Server mit API-Key-Schutz & CORS fĂŒr 192.168.13.0/24 +├── sysinfo.py # System- und Hardwareinfos (CPU, RAM, Disk, GPU, User) +├── netinf.py # Netzwerk- und Verbindungserfassung +├── proginfo.py # ProzessĂŒberwachung mit Laufzeiten, CPU/RAM +├── user_control_agent.py # CLI-Tool zur Benutzersteuerung +├── index.html # Web-Dashboard (Frontend) +├── style.css # Design fĂŒr das Frontend +├── requirements.txt # AbhĂ€ngigkeiten (bereinigt, optional erweiterbar) +├── install_service.sh # Installer fĂŒr systemd (nicht mitgeliefert) +├── deploy.sh # Optionales Deploy-Skript (nicht mitgeliefert) +├── safe_kiddo_user_control.sh # Hilfsskript fĂŒr SafeKiddo CLI-Tool +└── tests/ # Testverzeichnis mit pytest-Skripten +``` + +--- + +## 🚀 Installation + +### Voraussetzungen + +- Python 3.9+ +- Linux empfohlen (getestet unter Ubuntu) +- Root-Rechte fĂŒr `user_control_agent.py` + +### Schritt 1 – Projekt klonen + +```bash +git clone https://dein-git-repo/wilcon-agent.git +cd wilcon-agent +``` + +### Schritt 2 – Virtuelle Umgebung (optional, empfohlen) + +```bash +python3 -m venv venv +source venv/bin/activate +``` + +### Schritt 3 – AbhĂ€ngigkeiten installieren + +```bash +pip install -r requirements.txt +``` + +#### Optional: + +Falls GPU-Informationen oder erweiterte CPU-/OS-Erkennung benötigt werden: + +```bash +pip install gputil py-cpuinfo distro +``` + +--- + +## ▶ Anwendung starten + +```bash +export WILCON_API_KEY=test123 +uvicorn wilcon:app --host 0.0.0.0 --port 8080 +``` + +Rufe dann das Webinterface auf unter: +[http://localhost:5500/index.html](http://localhost:5500/index.html) +(**Tipp:** Live Server in VS Code verwenden) + +**Hinweis:** Alle API-Endpunkte erfordern den HTTP-Header `x-api-key`: + +```bash +curl -H "x-api-key: test123" http://localhost:8080/sysinfo +``` + +--- + +## 🌐 API-Endpunkte + +| Endpunkt | Beschreibung | +| ----------- | --------------------------------------- | +| `/sysinfo` | Hardware- und OS-Informationen | +| `/netinfo` | Netzwerk-Interfaces, IPs, Gateways, DNS | +| `/procinfo` | Laufende Prozesse mit CPU, RAM & Zeit | + +--- + +## 💡 Web-Dashboard + +Das HTML-Dashboard bietet: + +- Übersicht ĂŒber Systemdaten +- Scrollbare und filterbare Tabellen +- Sortierbare Spalten (Prozesse, Netzwerk) +- GPU- und Benutzerinformationen + +--- + +## 🔐 Safe Kiddo CLI (user_control_agent.py) + +Ein Werkzeug zur lokalen Benutzerkontrolle, das u. a. folgende Funktionen bietet: + +- **Benutzer sperren/entsperren** +- **Countdown mit Sound & Benachrichtigung** +- **Shutdown nach Ablauf** + +### Beispiel: + +```bash +sudo python3 user_control_agent.py testuser disable --countdown --sound --time 30 --debug +``` + +> Hinweis: `notify-send`, `paplay` o. À. mĂŒssen installiert sein. + +--- + +## 💠 Entwicklerhinweise + +### Logging + +Jedes Modul loggt separat: + +- `~/hardware_info.log` +- `~/netinf.log` +- `~/process_monitor.log` +- `~/safe_kiddo_user_control.log` + +### Debug-Modus + +Alle CLI-Module und das Backend unterstĂŒtzen `--debug`. + +### Hinweis zu `hardware.py` + +Die Datei `hardware.py` war eine frĂŒhe Version von `sysinfo.py` und wurde durch letztere vollstĂ€ndig ersetzt. Sie kann aus dem Projekt entfernt werden. + +### Erweiterungsideen + +- Authentifizierung (JWT, API-Key ✅) +- Langzeit-Logging (Prometheus, SQLite) +- Dockerisierung +- Web-Control fĂŒr Safe Kiddo +- IP-basierte Netzwerkfilterung via `allow_origin_regex` ✅ + +--- + +## đŸ§Ș Tests & QualitĂ€tssicherung + +Das Projekt nutzt [pytest](https://pytest.org) fĂŒr automatisierte Tests. + +### Struktur + +```text +tests/ +├── test_sysinfo.py # Tests fĂŒr System- und Hardwaredaten +├── test_netinf.py # Tests fĂŒr Netzwerkfunktionen +├── test_proginfo.py # Tests fĂŒr Prozessinformationen +└── test_api.py # Tests fĂŒr die FastAPI-Endpunkte +``` + +### Beispiel-Test + +```python +from sysinfo import get_hardware_info + +def test_sysinfo_contains_required_keys(): + data = get_hardware_info() + assert "cpu" in data + assert "ram" in data +``` + +### AusfĂŒhrung + +```bash +pytest -v +``` + +--- + +## đŸ§‘â€đŸ’» Lizenz + +Dieses Projekt ist derzeit privat. FĂŒr Open-Source-Freigabe bitte Lizenz ergĂ€nzen. + +--- + +## 📞 Kontakt + +Erstellt von \[Dein Name] +Kontakt: \[[email@example.com](mailto:email@example.com)] diff --git a/agent_mvp.zip b/agent_mvp.zip new file mode 100644 index 0000000..73c1da2 Binary files /dev/null and b/agent_mvp.zip differ diff --git a/hardware.py b/hardware.py deleted file mode 100644 index 28953bd..0000000 --- a/hardware.py +++ /dev/null @@ -1,179 +0,0 @@ -#sysinfo.py -#Version : 0.1.0 -import os -import sys -import subprocess -import cpuinfo -import json -import psutil -import platform -import distro -import socket -import logging -import argparse -from datetime import datetime - -try: - import GPUtil - gpu_available = True -except ImportError: - gpu_available = False - -# Farben fĂŒr Debug-Logging -class LogColors: - INFO = "\033[94m" - DEBUG = "\033[92m" - WARNING = "\033[93m" - ERROR = "\033[91m" - RESET = "\033[0m" - -# Logging einrichten -LOG_FILE = os.path.expanduser("~/hardware_info.log") -logging.basicConfig( - filename=LOG_FILE, - level=logging.INFO, - format="%(asctime)s - %(levelname)s - %(message)s" -) - -def log_message(level, message): - """Loggt Nachrichten mit Farben fĂŒr die CLI, falls Debug aktiv ist.""" - if level == "info": - logging.info(message) - if args.debug: - print(f"{LogColors.INFO}[INFO]{LogColors.RESET} {message}") - elif level == "debug": - logging.debug(message) - if args.debug: - print(f"{LogColors.DEBUG}[DEBUG]{LogColors.RESET} {message}") - elif level == "warning": - logging.warning(message) - if args.debug: - print(f"{LogColors.WARNING}[WARNING]{LogColors.RESET} {message}") - elif level == "error": - logging.error(message) - if args.debug: - print(f"{LogColors.ERROR}[ERROR]{LogColors.RESET} {message}") - -def get_hardware_info(): - """Sammelt die relevanten Hardware- und Systeminformationen.""" - hardware_info = {} - - # Betriebssystem - log_message("info", "Sammle Betriebssystem-Daten...") - try: - os_info = { - "name": platform.system(), - "distribution": distro.name(pretty=True) - } - log_message("info", f"OS: {os_info['name']} - {os_info['distribution']}") - except Exception as e: - os_info = {} - log_message("warning", f"Konnte Betriebssystem-Daten nicht ermitteln: {str(e)}") - hardware_info["os"] = os_info - - # CPU - log_message("info", "Sammle CPU-Daten...") - try: - cpu_info = { - "model": cpuinfo.get_cpu_info().get('brand_raw', 'Unbekannt'), - "cores": psutil.cpu_count(logical=False), - "threads": psutil.cpu_count(logical=True), - "usage": psutil.cpu_percent(interval=1) - } - log_message("info", f"CPU: {cpu_info['model']} - {cpu_info['cores']} Cores, {cpu_info['threads']} Threads - {cpu_info['usage']}% Auslastung") - except Exception as e: - cpu_info = {} - log_message("warning", f"Konnte CPU-Daten nicht ermitteln: {str(e)}") - hardware_info["cpu"] = cpu_info - - # RAM - log_message("info", "Sammle RAM-Daten...") - try: - ram_info = { - "total": round(psutil.virtual_memory().total / (1024**3), 2), - "used": round(psutil.virtual_memory().used / (1024**3), 2), - "available": round(psutil.virtual_memory().available / (1024**3), 2) - } - log_message("info", f"RAM: {ram_info['total']}GB gesamt, {ram_info['used']}GB genutzt, {ram_info['available']}GB verfĂŒgbar") - except Exception as e: - ram_info = {} - log_message("warning", f"Konnte RAM-Daten nicht ermitteln: {str(e)}") - hardware_info["ram"] = ram_info - - # Festplatte - log_message("info", "Sammle Festplattendaten...") - try: - disk_info = { - "total": round(psutil.disk_usage('/').total / (1024**3), 2), - "used": round(psutil.disk_usage('/').used / (1024**3), 2), - "free": round(psutil.disk_usage('/').free / (1024**3), 2) - } - log_message("info", f"Disk: {disk_info['total']}GB gesamt, {disk_info['used']}GB genutzt, {disk_info['free']}GB frei") - except Exception as e: - disk_info = {} - log_message("warning", f"Konnte Festplatten-Daten nicht ermitteln: {str(e)}") - hardware_info["disk"] = disk_info - - log_message("info", "Sammle Netzwerkinformationen...") - try: - network_info = { - "hostname": socket.gethostname(), - "ip_address": socket.gethostbyname(socket.gethostname()) - } - log_message("info", f"Netzwerk: Hostname {network_info['hostname']}, IP {network_info['ip_address']}") - except Exception as e: - network_info = {} - log_message("warning", f"Konnte Netzwerkinformationen nicht ermitteln: {str(e)}") - hardware_info["network"] = network_info - - log_message("info", "Sammle Benutzerinformationen...") - try: - user_info = { - "logged_in": [user.name for user in psutil.users()], - "active_user": os.getlogin() - } - log_message("info", f"Benutzer: {', '.join(user_info['logged_in'])}, Aktiver User: {user_info['active_user']}") - except Exception as e: - user_info = {} - log_message("warning", f"Konnte Benutzerinformationen nicht ermitteln: {str(e)}") - hardware_info["users"] = user_info - - log_message("info", "Sammle dedizierte GPU-Daten...") - gpu_info = [] - try: - if gpu_available: - gpus = GPUtil.getGPUs() - for gpu in gpus: - gpu_info.append({ - "name": gpu.name, - "memory_total": f"{gpu.memoryTotal}MB", - "memory_used": f"{gpu.memoryUsed}MB", - "memory_free": f"{gpu.memoryFree}MB", - "load": f"{gpu.load * 100:.1f}%" - }) - log_message("info", f"Gefundene GPUs: {len(gpus)}") - else: - log_message("warning", "GPUtil nicht installiert oder keine dedizierte GPU vorhanden.") - except Exception as e: - log_message("warning", f"Konnte GPU-Daten nicht ermitteln: {str(e)}") - hardware_info["gpu"] = gpu_info - - log_message("info", "Hardware-Informationen erfolgreich erfasst.") - return hardware_info - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Ermittelt Hardware- und Systeminformationen.") - parser.add_argument("--debug", action="store_true", help="Aktiviert Debug-Logging auf der CLI.") - parser.add_argument("--json", type=str, help="Speichert die Ausgabe in einer JSON-Datei.") - args = parser.parse_args() - - # Hardware-Informationen abrufen - hardware_info = get_hardware_info() - - if args.json: - try: - with open(args.json, "w") as json_file: - json.dump(hardware_info, json_file, indent=4) - log_message("info", f"Hardware-Infos in JSON gespeichert: {args.json}") - except Exception as e: - log_message("error", f"Fehler beim Speichern der JSON-Datei: {str(e)}") diff --git a/requirements.txt b/requirements.txt index e85892b..d3c6296 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,42 +1,8 @@ -annotated-types==0.7.0 -anyio==4.9.0 -certifi==2025.6.15 -click==8.2.1 -dnspython==2.7.0 -email_validator==2.2.0 -exceptiongroup==1.3.0 fastapi==0.115.13 -fastapi-cli==0.0.7 -h11==0.16.0 -httpcore==1.0.9 -httptools==0.6.4 -httpx==0.28.1 -idna==3.10 -itsdangerous==2.2.0 -Jinja2==3.1.6 -markdown-it-py==3.0.0 -MarkupSafe==3.0.2 -mdurl==0.1.2 -orjson==3.10.18 -psutil==7.0.0 -pydantic==2.11.7 -pydantic-extra-types==2.10.5 -pydantic-settings==2.9.1 -pydantic_core==2.33.2 -Pygments==2.19.1 -python-dotenv==1.1.0 -python-multipart==0.0.20 -PyYAML==6.0.2 -rich==14.0.0 -rich-toolkit==0.14.7 -shellingham==1.5.4 -sniffio==1.3.1 -starlette==0.46.2 -typer==0.16.0 -typing-inspection==0.4.1 -typing_extensions==4.14.0 -ujson==5.10.0 uvicorn==0.34.3 -uvloop==0.21.0 -watchfiles==1.1.0 -websockets==15.0.1 +psutil==7.0.0 +httpx==0.28.1 +pytest>=7.0.0 +GPUtil # FĂŒr GPU-Infos +cpuinfo # FĂŒr exakte CPU-Namen +distro # FĂŒr Distributionserkennung diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 0000000..f5f1b22 --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1,19 @@ +from fastapi.testclient import TestClient +from wilcon import app + +client = TestClient(app) + +def test_sysinfo_endpoint(): + response = client.get("/sysinfo") + assert response.status_code == 200 + assert "cpu" in response.json() + +def test_netinfo_endpoint(): + response = client.get("/netinfo") + assert response.status_code == 200 + assert "ip_address" in response.json() + +def test_procinfo_endpoint(): + response = client.get("/procinfo") + assert response.status_code == 200 + assert isinstance(response.json(), list) diff --git a/tests/test_netinf.py b/tests/test_netinf.py new file mode 100644 index 0000000..1c2f2fe --- /dev/null +++ b/tests/test_netinf.py @@ -0,0 +1,8 @@ +from netinf import get_network_info + +def test_network_info_contains_expected_keys(): + data = get_network_info(debug=False) + assert isinstance(data, dict) + + for key in ["hostname", "ip_address", "interfaces", "gateway", "dns_servers", "connections"]: + assert key in data diff --git a/tests/test_proginfo.py b/tests/test_proginfo.py new file mode 100644 index 0000000..57da4e2 --- /dev/null +++ b/tests/test_proginfo.py @@ -0,0 +1,9 @@ +from proginfo import get_process_info + +def test_process_info_returns_list(): + data = get_process_info() + assert isinstance(data, list) + if data: # Teste nur, wenn Prozesse vorhanden sind + proc = data[0] + for key in ["pid", "name", "user", "cpu", "memory", "status", "start_time", "runtime_seconds"]: + assert key in proc diff --git a/tests/test_sysinfo.py b/tests/test_sysinfo.py new file mode 100644 index 0000000..bc500b9 --- /dev/null +++ b/tests/test_sysinfo.py @@ -0,0 +1,15 @@ +from sysinfo import get_hardware_info + +def test_sysinfo_contains_required_keys(): + data = get_hardware_info() + assert isinstance(data, dict) + + for key in ["cpu", "ram", "disk", "os", "network_basic", "user_accounts", "gpu"]: + assert key in data + +def test_cpu_info_structure(): + cpu = get_hardware_info()["cpu"] + assert "model" in cpu + assert "cores" in cpu + assert "threads" in cpu + assert "usage" in cpu diff --git a/wilcon.py b/wilcon.py index 6642f06..90596de 100644 --- a/wilcon.py +++ b/wilcon.py @@ -1,7 +1,8 @@ # wilcon.py -# version 0.3.1 (aktualisiert) +# Version 0.3.4 – mit API-Key-Schutz + CORS fĂŒr 192.168.13.0/24 -from fastapi import FastAPI +import os +from fastapi import FastAPI, Header, HTTPException, Depends from fastapi.middleware.cors import CORSMiddleware from sysinfo import get_hardware_info, set_debug_mode as set_sysinfo_debug_mode from netinf import get_network_info, set_debug_mode as set_netinf_debug_mode @@ -10,46 +11,39 @@ from proginfo import get_process_info, set_debug_mode as set_proginfo_debug_mode app = FastAPI( title="Wilcon Client Agent", description="API fĂŒr Client-System-, Netzwerk- und Prozessinformationen.", - version="0.3.1" + version="0.3.4" ) -# CORS-Konfiguration fĂŒr wilcon.py (auf Port 8080) -origins = [ - "http://localhost:5500", # FĂŒr Live Server in VS Code - "http://127.0.0.1:5500", # FĂŒr Live Server in VS Code - "http://localhost:8080", # Wenn du spĂ€ter das Frontend direkt ĂŒber Uvicorn ausliefern willst - "http://127.0.0.1:8080", - # FĂŒge hier die IP-Adresse oder den Hostnamen deines Client-Rechners hinzu, - # wenn du von einem anderen GerĂ€t im Netzwerk zugreifen möchtest, z.B. "http://192.168.1.100:5500" -] +# --- API-Key-Schutz --- +API_KEY = os.getenv("WILCON_API_KEY", "test123") # In .env oder als Umgebungsvariable setzen +def verify_api_key(x_api_key: str = Header(...)): + if x_api_key != API_KEY: + raise HTTPException(status_code=403, detail="Zugriff verweigert") + +# --- CORS-Konfiguration fĂŒr gesamtes 192.168.13.0/24-Netzwerk --- app.add_middleware( CORSMiddleware, - allow_origins=origins, + allow_origin_regex=r"http://192\.168\.13\.\d{1,3}(:\d+)?", allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) -# Optional: Setze den Debug-Modus fĂŒr die importierten Module. -# Dies wird die farbige CLI-Ausgabe in dem Terminal aktivieren, in dem uvicorn lĂ€uft. -# NĂŒtzlich fĂŒr die Entwicklung. -# set_sysinfo_debug_mode(True) -# set_netinf_debug_mode(True) -# set_proginfo_debug_mode(True) - - +# --- Endpunkte --- @app.get("/sysinfo") -async def get_sysinfo_endpoint(): # Umbenannt, um Konflikt mit importierter Funktion zu vermeiden - """API-Endpunkt fĂŒr Hardware- und Systeminformationen""" +async def get_sysinfo_endpoint(_: str = Depends(verify_api_key)): return get_hardware_info() @app.get("/netinfo") -async def get_netinfo_endpoint(): # Umbenannt - """API-Endpunkt fĂŒr Netzwerkinformationen""" +async def get_netinfo_endpoint(_: str = Depends(verify_api_key)): return get_network_info() @app.get("/procinfo") -async def get_procinfo_endpoint(): # Umbenannt - """API-Endpunkt fĂŒr Prozessinformationen""" - return get_process_info() \ No newline at end of file +async def get_procinfo_endpoint(_: str = Depends(verify_api_key)): + return get_process_info() + +# Optional Debug-Modus einschalten +# set_sysinfo_debug_mode(True) +# set_netinf_debug_mode(True) +# set_proginfo_debug_mode(True)