fix: address P0/P1/P2 audit findings — security, reliability, CI, docs
P0 - Critical: - config.py: add _mask() credential-redaction helper, SETTINGS_SCHEMA_VERSION, plaintext-storage warning; corrupt settings.json backed up before defaults - gost_util.py: pin SHA256 for gost_3.2.6_windows_amd64.zip; verify before extraction; _add_defender_exclusion now logs warning on failure - firewall.py: add emergency_disengage() for atexit/signal use - service.py: register fw_emergency_disengage via atexit + SIGTERM/SIGINT; stop() calls emergency_disengage if thread hangs past 15s timeout - app.py: wrap main() in top-level except with CTk error dialog + log - LICENSE: add MIT license file P1 - Important: - app.py: switch to RotatingFileHandler (5 MB / 3 backups) - config.py: settings_version + migrate(); save_settings() writes .bak before overwrite; _is_safe_https_url() strips RFC-1918 sources/ip_check_url - service.py: threading.Lock on _settings; _signal_handler; graceful stop - requirements.txt: pin exact versions; add cryptography==48.0.0 - .gitignore: add settings.json, credential JSON files, screenshot noise - tray.py, dns_leak.py, gost_util.py: replace bare except:pass with logging - CHANGELOG.md: document all session changes P2 - Nice to have: - .github/workflows/test.yml: CI on Python 3.10/3.11/3.12 windows-latest - run.py: --version / -V flag - docs/OPERATOR_RUNBOOK.md: emergency disengage, proxy leak, GOST, settings Tests: 47/47 passed (python -m unittest discover -s tests -v) Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import io
|
||||
import logging
|
||||
import shutil
|
||||
@@ -19,6 +20,11 @@ GOST_RELEASE_ZIP = (
|
||||
"gost_3.2.6_windows_amd64.zip"
|
||||
)
|
||||
|
||||
# Pinned SHA256 of the release zip. Update this constant when bumping GOST_RELEASE_ZIP.
|
||||
# Verified 2026-05-21 against https://github.com/go-gost/gost/releases/download/v3.2.6/
|
||||
GOST_RELEASE_ZIP_SHA256 = "32f4edf3d94b622e67f1979f6f5de82dac62abc0977772cf96215dd199ef7e7b"
|
||||
|
||||
|
||||
|
||||
def _add_defender_exclusion(path: Path) -> None:
|
||||
"""Add Windows Defender exclusion so GOST is not quarantined."""
|
||||
@@ -35,8 +41,21 @@ def _add_defender_exclusion(path: Path) -> None:
|
||||
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
||||
)
|
||||
log.info("Defender exclusion added for %s", path.parent)
|
||||
except Exception:
|
||||
pass # non-fatal
|
||||
except Exception as exc:
|
||||
log.warning("Defender exclusion failed (non-fatal): %s", exc)
|
||||
|
||||
|
||||
def _verify_zip_sha256(data: bytes, expected: str) -> None:
|
||||
"""Raise RuntimeError if SHA256 of *data* does not match *expected* (hex)."""
|
||||
actual = hashlib.sha256(data).hexdigest().lower()
|
||||
if actual != expected.lower():
|
||||
raise RuntimeError(
|
||||
f"GOST zip SHA256 mismatch — possible supply-chain attack!\n"
|
||||
f" expected: {expected.lower()}\n"
|
||||
f" actual: {actual}\n"
|
||||
"Delete the downloaded zip and retry, or update GOST_RELEASE_ZIP_SHA256."
|
||||
)
|
||||
log.info("GOST zip SHA256 OK: %s", actual)
|
||||
|
||||
|
||||
def ensure_gost(target: Path | None = None) -> Path:
|
||||
@@ -53,6 +72,8 @@ def ensure_gost(target: Path | None = None) -> Path:
|
||||
data = r.content
|
||||
if len(data) < 64 or data[:2] != b"PK":
|
||||
raise RuntimeError("Downloaded GOST zip looks invalid (not a zip).")
|
||||
# Integrity check before extraction
|
||||
_verify_zip_sha256(data, GOST_RELEASE_ZIP_SHA256)
|
||||
with zipfile.ZipFile(io.BytesIO(data), "r") as z:
|
||||
names = [n for n in z.namelist() if n.lower().endswith("gost.exe")]
|
||||
if not names:
|
||||
@@ -122,8 +143,9 @@ def terminate_process(proc: subprocess.Popen | None) -> None:
|
||||
try:
|
||||
proc.terminate()
|
||||
proc.wait(timeout=5)
|
||||
except Exception:
|
||||
except Exception as exc:
|
||||
log.debug("GOST terminate failed (%s) — escalating to kill.", exc)
|
||||
try:
|
||||
proc.kill()
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as kill_exc:
|
||||
log.warning("GOST kill also failed: %s", kill_exc)
|
||||
|
||||
Reference in New Issue
Block a user