From 55f6d0816cc829b92034a8f5eac4e33a26d798e0 Mon Sep 17 00:00:00 2001 From: stephan Date: Sun, 29 Jun 2025 08:33:19 +0200 Subject: [PATCH] sicherheitsupdate --- README.md | 207 +++++++++++++++++++++++++++++++++++++++++ agent_mvp.zip | Bin 0 -> 30826 bytes hardware.py | 179 ----------------------------------- requirements.txt | 46 ++------- tests/test_api.py | 19 ++++ tests/test_netinf.py | 8 ++ tests/test_proginfo.py | 9 ++ tests/test_sysinfo.py | 15 +++ wilcon.py | 50 +++++----- 9 files changed, 286 insertions(+), 247 deletions(-) create mode 100644 README.md create mode 100644 agent_mvp.zip delete mode 100644 hardware.py create mode 100644 tests/test_api.py create mode 100644 tests/test_netinf.py create mode 100644 tests/test_proginfo.py create mode 100644 tests/test_sysinfo.py 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 0000000000000000000000000000000000000000..73c1da2b399bc87dd2201bb91fbd44f2e1e3924c GIT binary patch literal 30826 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.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)