diff --git a/.github/workflows/actions.yml b/.github/workflows/actions.yml index f2d937d..d27fc10 100644 --- a/.github/workflows/actions.yml +++ b/.github/workflows/actions.yml @@ -4,10 +4,10 @@ on: push: branches: - main - pull_request: - types: [opened, synchronize] - branches: - - main + pull_request: + types: [opened, synchronize] + branches: + - main jobs: build: diff --git a/build_managed_app.py b/build_managed_app.py new file mode 100644 index 0000000..e273de2 --- /dev/null +++ b/build_managed_app.py @@ -0,0 +1,65 @@ +import os +import sys +import subprocess +import shutil + +# Security constants must match the Hub/Publisher +SALT = b"\x3c\x4f\x6d\x2a\x7a\x31\x5b\x44" +SENTINEL = b"\x73\x65\x63\x75\x72\x65\x76\x65\x72" # "securever" + +def build_app(app_id, source_file, version): + print(f"--- Building {app_id} v{version} ---") + + # 1. Use PyInstaller to create a single-file executable + try: + subprocess.check_call([ + sys.executable, "-m", "PyInstaller", + "--onefile", + "--clean", + "--name", "app_temp", + source_file + ]) + except subprocess.CalledProcessError as e: + print(f"PyInstaller failed: {e}") + return + + # 2. Get the compiled binary path + ext = ".exe" if sys.platform == "win32" else "" + compiled_path = os.path.join("dist", f"app_temp{ext}") + + # 3. Read the binary and append the Steganographic Footer + print("Injecting security footer...") + with open(compiled_path, "rb") as f: + binary_data = f.read() + + # Pad version to 8 bytes and XOR obfuscate + ver_bytes = version.encode().ljust(8, b"\x00") + obfuscated = bytes([b ^ SALT[i % len(SALT)] for i, b in enumerate(ver_bytes)]) + + final_binary = binary_data + obfuscated + SENTINEL + + # 4. Save to final destination + output_name = f"{app_id}_v{version}{ext}" + with open(output_name, "wb") as f: + f.write(final_binary) + + # Cleanup + shutil.rmtree("build", ignore_errors=True) + shutil.rmtree("dist", ignore_errors=True) + if os.path.exists("app_temp.spec"): os.remove("app_temp.spec") + + print(f"[SUCCESS] Compiled secure binary: {output_name}") + return output_name + +if __name__ == "__main__": + if not os.path.exists("managed_apps_sources/hello_app.py"): + print("Error: Source file not found.") + sys.exit(1) + + # Install dependencies for pyinstaller if needed + # build_app("hello_world", "managed_apps_sources/hello_app.py", "1.0.0") + print("\nUSE THIS SCRIPT TO COMPILE REAL APPS:") + print("Example: python build_managed_app.py") + + # Execute actual build for demonstration + build_app("hello_world", "managed_apps_sources/hello_app.py", "1.0.0") diff --git a/client/anti_tamper.py b/client/anti_tamper.py new file mode 100644 index 0000000..df2b324 --- /dev/null +++ b/client/anti_tamper.py @@ -0,0 +1,29 @@ +import sys +import os + +def check_environment(): + """ + Client Hardening: Proactive environment checks. + Detects if a debugger or analysis tool is attached to protect + the application's memory and cryptographic keys. + """ + + # 1. Check for Python Debuggers (sys.gettrace) + if sys.gettrace() is not None: + _self_destruct("Debugger detected (sys.gettrace)") + + # 2. Check for common analysis environment variables + analysis_vars = ["PYTHONINSPECT", "PYTHONDONTWRITEBYTECODE"] + for var in analysis_vars: + if var in os.environ: + # Note: Some IDEs use these, but in high-security production they are suspicious + pass + +def _self_destruct(reason): + """ + Gracefully terminates the application and logs the breach attempt. + """ + print(f"\n[🛡️ SECURITY SHIELD] CRITICAL BREACH: {reason}") + print("Application memory is being protected. Session terminated.\n") + # Using os._exit to bypass standard exception handlers for immediate kill + os._exit(1) diff --git a/client/app.py b/client/app.py index 30a3403..f948311 100644 --- a/client/app.py +++ b/client/app.py @@ -19,6 +19,8 @@ # Add project root to sys.path sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from anti_tamper import check_environment + def setup_logging(config): log_config = config.get("logging", {}) level = getattr(logging, log_config.get("level", "INFO")) @@ -37,12 +39,18 @@ class SecurityError(Exception): pass class UpdateClient: - def __init__(self, global_config, client_config, base_dir, anim_callback=None): + # Steganographic constants must match Publisher + SALT = b"\x3c\x4f\x6d\x2a\x7a\x31\x5b\x44" + SENTINEL = b"\x73\x65\x63\x75\x72\x65\x76\x65\x72" # "securever" + + def __init__(self, global_config, client_config, base_dir, ui_callback=None): self.global_config = global_config self.client_config = client_config self.base_dir = base_dir self.logger = logging.getLogger("UpdateHub") - self.anim_callback = anim_callback + self.ui_callback = ui_callback + self.pause_event = threading.Event() + self.manual_mode = False self.server_url = f"{global_config['server']['url']}/static" self.public_key_path = os.path.join(base_dir, "keys", "public_key.pem") @@ -59,47 +67,65 @@ def _load_public_key(self): return None with open(self.public_key_path, "rb") as key_file: return serialization.load_pem_public_key(key_file.read()) - except Exception as e: - self.logger.error(f"Failed to load public key: {e}") + except Exception: return None - def trigger_visual(self, phase, message=""): - if self.anim_callback: - self.anim_callback(phase, message) - time.sleep(0.5) + def trigger_visual(self, phase, message="", data=None): + if self.ui_callback: + self.ui_callback(phase, message, data) + + # Only pause if Manual Mode is enabled + if self.manual_mode and phase not in ["finish", "found"]: + self.pause_event.clear() + self.pause_event.wait() + else: + # Short aesthetic delay for automated mode + time.sleep(0.8) def calculate_sha256(self, filepath): sha256_hash = hashlib.sha256() with open(filepath, "rb") as f: - for byte_block in iter(lambda: f.read(4096), b""): + # 64KB chunks for optimal production performance + for byte_block in iter(lambda: f.read(65536), b""): sha256_hash.update(byte_block) return sha256_hash.hexdigest() def get_app_path(self, app_id): - return os.path.join(self.managed_apps_dir, app_id, "app.exe") + # Platform-specific extension + ext = ".exe" if sys.platform == "win32" else "" + return os.path.join(self.managed_apps_dir, app_id, f"app{ext}") def get_local_version(self, app_id): - version_file = os.path.join(self.managed_apps_dir, app_id, "version.json") - if not os.path.exists(version_file): + """ + Extracts the version from the steganographic binary footer. + The version is XOR-encoded and hidden behind a magic sentinel. + """ + target_exe = self.get_app_path(app_id) + if not os.path.exists(target_exe): return "0.0.0" - with open(version_file, "r") as f: - return json.load(f).get("version", "0.0.0") - - def save_local_version(self, app_id, version): - app_dir = os.path.join(self.managed_apps_dir, app_id) - if not os.path.exists(app_dir): os.makedirs(app_dir) - version_file = os.path.join(app_dir, "version.json") - with open(version_file, "w") as f: - json.dump({"version": version}, f) + try: + with open(target_exe, "rb") as f: + # Seek to end: [Obfuscated Ver (8 bytes)][Sentinel (9 bytes)] + f.seek(-17, os.SEEK_END) + footer = f.read(17) + + if footer.endswith(self.SENTINEL): + obfuscated = footer[:8] + # Reverse XOR using the same SALT + ver_bytes = bytes([b ^ self.SALT[i % len(self.SALT)] for i, b in enumerate(obfuscated)]) + return ver_bytes.decode(errors="ignore").strip("\x00") + return "unknown" + except Exception: + return "error" def check_app_update(self, app_id): network_cfg = self.client_config["client"]["network"] current_version = self.get_local_version(app_id) try: - self.trigger_visual("scan", f"Checking {app_id}...") + self.trigger_visual("scan", f"PHASE 1: SECURE CONNECTION\nFinding official update for {app_id}...") - # 1. Fetch Manifest + # 1. Fetch Metadata m_resp = requests.get(f"{self.server_url}/{app_id}/manifest.json", timeout=network_cfg["timeout"]) m_resp.raise_for_status() m_bytes = m_resp.content @@ -110,204 +136,345 @@ def check_app_update(self, app_id): self.trigger_visual("download_meta", "Signed metadata received.") - # 2. Cryptographic Verification - self.trigger_visual("verify", "Validating Ed25519 signature...") + # 2. Cryptographic Proof + self.trigger_visual("verify", "PHASE 2: AUTHENTICITY PROOF\nClick NEXT to verify Developer Signature...", + {"ALGORITHM": "Ed25519 (Elliptic Curve)", "TRUST_KEY": "Verified Local PEM", "SIGNATURE": sig_b64[:32] + "..."}) + signature = base64.b64decode(sig_b64) self.public_key.verify(signature, m_bytes) - self.trigger_visual("success", "Source AUTHENTIC.") + self.trigger_visual("success", "SOURCE VERIFIED: Trusted Developer confirmed.") - # 3. Logic Check + # 3. Defensive Rules manifest = json.loads(m_bytes.decode('utf-8')) metadata = manifest["metadata"] - valid_until = datetime.strptime(metadata["valid_until"], "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc) + # Expiry Check + valid_until_str = metadata["valid_until"] + valid_until = datetime.strptime(valid_until_str, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc) + self.trigger_visual("ttl", f"PHASE 3: FRESHNESS CHECK\nClick NEXT to check manifest expiration date...", + {"EXPIRES": valid_until_str, "CURRENT": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), "RESULT": "VALID (Not Expired)"}) + if datetime.now(timezone.utc) > valid_until: - raise SecurityError("Update manifest EXPIRED!") + raise SecurityError("SECURITY BREACH: Manifest is expired!") + # Version Check latest_version = metadata["latest_version"] + self.trigger_visual("version", f"Checking versions: Hub v{current_version} vs Cloud v{latest_version}", + {"HUB_VERSION": current_version, "CLOUD_VERSION": latest_version, "RULE": "Forward-Only Upgrade"}) + if latest_version <= current_version: return {"status": "up_to_date", "version": current_version} return {"status": "update_available", "manifest": manifest, "latest": latest_version} except InvalidSignature: - self.trigger_visual("breach", "SIGNATURE INVALID!") - raise SecurityError(f"Security Alert: Untrusted update for {app_id}!") + self.trigger_visual("breach", "SECURITY BREACH DETECTED: Tampered Source!") + raise SecurityError(f"CRITICAL: Signature invalid for {app_id}!") except Exception as e: self.trigger_visual("error", str(e)) raise e def apply_update(self, app_id, update_info): manifest = update_info["manifest"] - patch_info = manifest["assets"]["patches"][0] - app_dir = os.path.join(self.managed_apps_dir, app_id) - if not os.path.exists(app_dir): - os.makedirs(app_dir) # BUG FIX: Ensure app folder exists + if not os.path.exists(app_dir): os.makedirs(app_dir) target_exe = self.get_app_path(app_id) - temp_patch = os.path.join(app_dir, "temp.patch") temp_new_exe = os.path.join(app_dir, "app.new") - # 1. Ensure local base exists + # --- SMART RECOVERY LOGIC --- if not os.path.exists(target_exe): - self.trigger_visual("info", "No base version found. Creating initial binary.") - with open(target_exe, "wb") as f: - f.write(f"REAL BINARY DATA FOR {app_id} v{patch_info['from_version']}\n".encode() + os.urandom(1024)) - - # 2. Download Patch - self.trigger_visual("download_patch", "Fetching delta diff...") - p_resp = requests.get(patch_info["url"], timeout=10) - with open(temp_patch, "wb") as f: f.write(p_resp.content) - - # 3. Apply - self.trigger_visual("patch", "Reconstructing binary...") - bsdiff4.file_patch(target_exe, temp_new_exe, temp_patch) - - # 4. Final Integrity - self.trigger_visual("integrity", "Computing SHA-256 identity...") - if self.calculate_sha256(temp_new_exe) != patch_info["result_sha256_hash"]: - if os.path.exists(temp_patch): os.remove(temp_patch) + # APP IS MISSING: Download the Full Installer + self.trigger_visual("info", f"Fresh install detected for {app_id}.\nFetching FULL SECURE INSTALLER...") + full_info = manifest["assets"]["full_installer"] + + self.trigger_visual("download_patch", f"PHASE 4: FULL DOWNLOAD\nDownloading {full_info['file_name']}...") + resp = requests.get(full_info["url"], timeout=30) + resp.raise_for_status() + + with open(temp_new_exe, "wb") as f: + f.write(resp.content) + + expected_hash = full_info["sha256_hash"] + else: + # APP EXISTS: Use the optimized Delta Patch + patch_info = manifest["assets"]["patches"][0] + self.trigger_visual("download_patch", "PHASE 4: SECURE DISTRIBUTION\nClick NEXT to fetch binary delta diff...") + + p_resp = requests.get(patch_info["url"], timeout=10) + p_resp.raise_for_status() + patch_data = p_resp.content + + self.trigger_visual("patch", "PHASE 5: SECURE SURGERY\nClick NEXT to reconstruct binary in-memory...") + try: + with open(target_exe, "rb") as f: + old_data = f.read() + new_data = bsdiff4.patch(old_data, patch_data) + with open(temp_new_exe, "wb") as f: + f.write(new_data) + except Exception: + if os.path.exists(temp_new_exe): os.remove(temp_new_exe) + raise SecurityError("Patch failure: Source binary corrupted.") + + expected_hash = patch_info["result_sha256_hash"] + + # 4. Integrity check (Common for both Full and Patch) + self.trigger_visual("integrity", f"PHASE 6: INTEGRITY LOCK\nProving the final file is bit-perfect...", + {"expected": expected_hash[:24] + "..."}) + + computed_hash = self.calculate_sha256(temp_new_exe) + + self.trigger_visual("hash_compare", f"Comparing Digital Fingerprints:\nTarget: {expected_hash[:20]}...\nActual: {computed_hash[:20]}...", + {"actual": computed_hash[:24] + "..."}) + + if computed_hash != expected_hash: if os.path.exists(temp_new_exe): os.remove(temp_new_exe) - raise SecurityError("INTEGRITY CHECK FAILED!") + self.trigger_visual("breach", "INTEGRITY CORRUPTED!") + raise SecurityError("Fingerprint mismatch!") - # 5. Atomic Swap - self.trigger_visual("success", "Swapping files...") - if os.path.exists(target_exe): os.remove(target_exe) - os.rename(temp_new_exe, target_exe) - if os.path.exists(temp_patch): os.remove(temp_patch) + # Atomic Swap + self.trigger_visual("success", "INTEGRITY MATCHED. Final Step: Atomic File Swap.") + # os.replace is natively atomic and overwrites if target exists + os.replace(temp_new_exe, target_exe) - self.save_local_version(app_id, update_info["latest"]) - self.trigger_visual("finish", f"{app_id} Upgraded to {update_info['latest']}") + # --- LINUX COMPATIBILITY FIX --- + if sys.platform != "win32": + import stat + os.chmod(target_exe, os.stat(target_exe).st_mode | stat.S_IEXEC) + + self.trigger_visual("finish", f"MANAGEMENT COMPLETE: {app_id} is now v{update_info['latest']}") class SecureHubUI(ctk.CTk): def __init__(self, g_cfg, c_cfg, base_dir): super().__init__() self.client = UpdateClient(g_cfg, c_cfg, base_dir, self.on_security_event) - self.title("🛡️ SECURITY MANAGEMENT HUB") - self.geometry("950x750") + self.title("🛡️ SECURITY GUARDIAN MANAGEMENT HUB") + self.geometry("1200x950") ctk.set_appearance_mode("dark") + # ROOT GRID CONFIG self.grid_columnconfigure(0, weight=1) - self.grid_rowconfigure(2, weight=1) + self.grid_rowconfigure(0, weight=1) + + # MAIN SCROLLABLE CONTAINER + self.scroll_frame = ctk.CTkScrollableFrame(self, fg_color="transparent") + self.scroll_frame.grid(row=0, column=0, sticky="nsew") + self.scroll_frame.grid_columnconfigure(0, weight=1) + + # Fonts + self.title_font = ctk.CTkFont(family="Segoe UI", size=34, weight="bold") + self.status_font = ctk.CTkFont(family="Segoe UI", size=24, weight="bold") + self.code_font = ctk.CTkFont(family="Consolas", size=16) + self.desc_font = ctk.CTkFont(family="Segoe UI", size=18) + self.big_btn_font = ctk.CTkFont(family="Segoe UI", size=28, weight="bold") # Header - self.header = ctk.CTkLabel(self, text="ENTERPRISE SECURITY HUB", font=ctk.CTkFont(size=28, weight="bold"), text_color="#3498db") - self.header.grid(row=0, column=0, pady=20) + self.header_frame = ctk.CTkFrame(self.scroll_frame, fg_color="transparent") + self.header_frame.grid(row=0, column=0, pady=(30, 10), sticky="ew") + self.header_frame.grid_columnconfigure(0, weight=1) + + self.header = ctk.CTkLabel(self.header_frame, text="🛡️ SECURITY COMMAND CENTER", font=self.title_font, text_color="#3498db") + self.header.grid(row=0, column=0, padx=(150, 0)) + + self.demo_switch = ctk.CTkSwitch(self.header_frame, text="DEMO MODE (MANUAL)", font=self.desc_font, + command=self.toggle_demo_mode) + self.demo_switch.grid(row=0, column=1, padx=50) - # Visual Canvas - self.canvas = Canvas(self, width=800, height=200, bg="#1a1a1a", highlightthickness=0) + # Main Visualization Canvas + self.canvas = Canvas(self.scroll_frame, width=1050, height=260, bg="#111111", highlightthickness=0) self.canvas.grid(row=1, column=0, pady=10) self.setup_canvas() + # SPLIT-SCREEN CONTAINER + self.center_container = ctk.CTkFrame(self.scroll_frame, fg_color="transparent") + self.center_container.grid(row=2, column=0, padx=50, pady=10, sticky="nsew") + self.center_container.grid_columnconfigure(0, weight=1) # Left + self.center_container.grid_columnconfigure(1, weight=0) # Right + + # LEFT HALF: THE BLACKBOARD / COMMAND BOX (Fixed Height) + self.data_frame = ctk.CTkFrame(self.center_container, fg_color="#1a1a1a", border_width=2, border_color="#2c3e50", height=400) + self.data_frame.grid(row=0, column=0, padx=5, pady=5, sticky="nsew", columnspan=2) + self.data_frame.grid_propagate(False) + + self.step_label = ctk.CTkLabel(self.data_frame, text="System Standby. Awaiting Scan...", font=self.status_font, text_color="#bdc3c7") + self.step_label.pack(pady=(20, 5)) + + self.data_display = ctk.CTkLabel(self.data_frame, text="", font=self.code_font, text_color="#2ecc71") + self.data_display.pack(pady=10) + + # NEXT STEP BUTTON + self.btn_next = ctk.CTkButton(self.data_frame, text="NEXT STEP ➔", font=self.big_btn_font, + fg_color="#27ae60", hover_color="#2ecc71", height=60, width=300, + command=self.on_next_click) + + # RIGHT HALF: THE DEBUG / ANALYSIS BOX (Fixed Height) + self.verbose_frame = ctk.CTkFrame(self.center_container, fg_color="#1e272e", border_width=1, border_color="#34495e", height=400) + self.verbose_frame.grid_propagate(False) + + self.verbose_title = ctk.CTkLabel(self.verbose_frame, text="🔬 DETAILED SECURITY ANALYSIS", + font=ctk.CTkFont(size=16, weight="bold"), text_color="#3498db") + self.verbose_title.pack(pady=10) + + self.verbose_text = ctk.CTkTextbox(self.verbose_frame, font=self.code_font, + fg_color="#000000", text_color="#ecf0f1", wrap="word") + self.verbose_text.pack(padx=20, pady=(0, 20), fill="both", expand=True) + self.verbose_text.insert("0.0", "Awaiting security scan for deep analysis...") + self.verbose_text.configure(state="disabled") + # App List - self.app_frame = ctk.CTkScrollableFrame(self, label_text="MANAGED APPLICATIONS") - self.app_frame.grid(row=2, column=0, padx=40, pady=20, sticky="nsew") + self.app_frame = ctk.CTkScrollableFrame(self.scroll_frame, label_text="MANAGED ASSETS", height=300) + self.app_frame.grid(row=3, column=0, padx=50, pady=20, sticky="ew") self.app_frame.grid_columnconfigure(0, weight=1) self.managed_apps = { - "security_scanner": "Security Scanner", - "engine_monitor": "Engine Monitor" + "security_scanner": "Security Scanner", + "engine_monitor": "Engine Monitor", + "hello_world": "Hello World App" } - self.app_widgets = {} # Track buttons and bars + self.app_widgets = {} self.refresh_app_list() def setup_canvas(self): - self.canvas.create_line(150, 100, 650, 100, fill="#2c3e50", dash=(4,4)) - self.canvas.create_text(200, 100, text="☁️", font=("Arial", 60), fill="#3498db") - self.canvas.create_text(700, 100, text="🛡️", font=("Arial", 60), fill="#2ecc71") + # Connection Line + self.canvas.create_line(250, 120, 780, 120, fill="#2c3e50", width=4, dash=(10, 10)) - def refresh_app_list(self): - for widget in self.app_frame.winfo_children(): widget.destroy() - self.app_widgets = {} + # Cloud/Server (Left) + self.canvas.create_text(260, 120, text="☁️", font=("Arial", 70), fill="#3498db", tags="cloud") + self.canvas.create_text(200, 200, text="CLOUD SOURCE", font=("Arial", 14), fill="#3498db") - for app_id, name in self.managed_apps.items(): - version = self.client.get_local_version(app_id) - row = ctk.CTkFrame(self.app_frame) - row.pack(fill="x", pady=8, padx=10) - - ctk.CTkLabel(row, text=name, font=ctk.CTkFont(size=18, weight="bold"), width=200, anchor="w").pack(side="left", padx=20) - - v_label = ctk.CTkLabel(row, text=f"v{version}", font=("Consolas", 16), width=100) - v_label.pack(side="left") + # System (Right) + self.canvas.create_text(860, 120, text="🛡️", font=("Arial", 70), fill="#2ecc71", tags="shield") + self.canvas.create_text(800, 200, text="SECURE SYSTEM", font=("Arial", 14), fill="#2ecc71") + + + def toggle_demo_mode(self): + self.client.manual_mode = self.demo_switch.get() + if self.client.manual_mode: + # Prepare for split screen: force columns to be identical width + self.center_container.grid_columnconfigure(0, weight=1, uniform="demo_split") + self.center_container.grid_columnconfigure(1, weight=1, uniform="demo_split") + self.data_frame.grid(row=0, column=0, padx=5, pady=5, sticky="nsew", columnspan=1) + self.verbose_frame.grid(row=0, column=1, padx=5, pady=5, sticky="nsew") + else: + # Full Width: Single column + self.verbose_frame.grid_forget() + self.center_container.grid_columnconfigure(1, weight=0, uniform="") + self.center_container.grid_columnconfigure(0, weight=1, uniform="") + self.data_frame.grid(row=0, column=0, padx=5, pady=5, sticky="nsew", columnspan=2) - p_bar = ctk.CTkProgressBar(row, width=200) - p_bar.pack(side="left", padx=20) - p_bar.set(0) - - btn = ctk.CTkButton(row, text="SECURE UPDATE", command=lambda aid=app_id: self.start_update(aid)) - btn.pack(side="right", padx=20, pady=10) - - self.app_widgets[app_id] = { - "button": btn, - "progress": p_bar, - "version_label": v_label - } - - def on_security_event(self, phase, message): - if phase == "download_meta": self.animate_data("{ }", "#f1c40f") - if phase == "verify": self.animate_lock() - if phase == "breach": self.flash_red() - - def animate_data(self, char, color): - item = self.canvas.create_text(150, 100, text=char, font=("Consolas", 14), fill=color) + self.step_label.configure(text=f"DEMO MODE: {'ENABLED' if self.client.manual_mode else 'DISABLED'}") + + def on_next_click(self): + self.btn_next.pack_forget() + self.client.pause_event.set() + + def on_security_event(self, phase, message, data=None): + self.after(0, lambda: self.step_label.configure(text=message.upper())) + + # UI Layout Adjustment + if self.client.manual_mode and phase not in ["finish", "found"]: + self.after(0, lambda: self.verbose_frame.grid(row=0, column=1, padx=5, pady=5, sticky="nsew")) + self.after(0, lambda: self.data_frame.grid(row=0, column=0, padx=5, pady=5, sticky="nsew", columnspan=1)) + self.after(500, lambda: self.btn_next.pack(pady=20)) + else: + self.after(0, lambda: self.verbose_frame.grid_forget()) + self.after(0, lambda: self.data_frame.grid(row=0, column=0, padx=5, pady=5, sticky="nsew", columnspan=2)) + self.after(0, lambda: self.btn_next.pack_forget()) + + context = { + "scan": "Zero-Trust Handshake: Treating the remote server as untrusted until proven otherwise. Requesting signed metadata.", + "download_meta": "Manifest Received: Contains update instructions and file hashes. Parsing is deferred until signature is proven.", + "verify": "Ed25519 VERIFICATION: Using local Public Key to verify 'manifest.sig'. Proves developer identity and blocks MITM.", + "ttl": "REPLAY PROTECTION: Checking 'valid_until' timestamp. Blocks 'stale' manifests from being reused by hackers.", + "version": "DOWNGRADE PROTECTION: Prevents force-install of older versions by strictly enforcing forward-only logic.", + "download_patch": "DELTA DISTRIBUTION: Fetching bit-level binary patches to minimize data usage and attack surface.", + "patch": "SECURE SURGERY: 'bsdiff4' applying mathematical diffs to reconstruct binary in isolated buffer.", + "integrity": "INTEGRITY LOCK: Calculating SHA-256 Fingerprint. Proves patching was bit-perfect and zero corruption occurred.", + "hash_compare": "AUTHENTICITY MATCH: Final bit-by-bit comparison of signed fingerprint vs local calculation.", + "success": "Security checks finalized. Authentic binary safe for atomic deployment.", + "breach": "CRITICAL ALERT: Security barrier tripped. Hub terminated process to protect the system environment." + } + + if phase in context: self.after(0, lambda p=phase: self.update_verbose_panel(context[p])) + if data: + self.after(0, lambda: self.data_display.configure(text="\n".join([f"{k}: {v}" for k, v in data.items()]))) + else: + self.after(0, lambda: self.data_display.configure(text="")) + + # Visuals + if phase == "download_meta": [self.after(random.randint(0,400), lambda: self.animate_packet("{}", "#f1c40f")) for _ in range(8)] + if phase == "download_patch": [self.after(random.randint(0,800), lambda: self.animate_packet("01", "#3498db", 25)) for _ in range(20)] + if phase == "breach": self.after(0, self.flash_red) + + def update_verbose_panel(self, text): + self.verbose_text.configure(state="normal") + self.verbose_text.delete("0.0", "end") + self.verbose_text.insert("0.0", text) + self.verbose_text.configure(state="disabled") + + def animate_packet(self, char, color, speed=12): + item = self.canvas.create_text(250, 120, text=char, font=("Consolas", 14, "bold"), fill=color) def move(): coords = self.canvas.coords(item) - if coords and coords[0] < 650: - self.canvas.move(item, 20, random.randint(-2, 2)) + if coords and coords[0] < 800: + self.canvas.move(item, speed, random.randint(-3, 3)) self.after(20, move) else: self.canvas.delete(item) move() - def animate_lock(self): - lock = self.canvas.create_text(400, 100, text="🔒", font=("Arial", 40), fill="#f1c40f") - self.after(500, lambda: self.canvas.delete(lock)) - def flash_red(self): - orig = self.canvas.cget("bg") - for i in range(4): - self.after(i*200, lambda: self.canvas.configure(bg="#c0392b")) - self.after(i*200+100, lambda: self.canvas.configure(bg=orig)) + def c(col): self.canvas.configure(bg=col) + for i in range(6): self.after(i*200, lambda: c("#c0392b")); self.after(i*200+100, lambda: c("#111111")) + + def refresh_app_list(self): + for widget in self.app_frame.winfo_children(): widget.destroy() + self.app_widgets = {} + for app_id, name in self.managed_apps.items(): + version = self.client.get_local_version(app_id) + row = ctk.CTkFrame(self.app_frame, height=80) + row.pack(fill="x", pady=10, padx=10) + ctk.CTkLabel(row, text=name, font=self.status_font, width=250, anchor="w").pack(side="left", padx=30) + v_label = ctk.CTkLabel(row, text=f"VER: {version}", font=self.code_font, width=120) + v_label.pack(side="left") + p_bar = ctk.CTkProgressBar(row, width=300, height=12) + p_bar.pack(side="left", padx=30); p_bar.set(0) + btn = ctk.CTkButton(row, text="SECURE SCAN", font=self.status_font, width=180, height=45, + command=lambda aid=app_id: self.start_update(aid)) + btn.pack(side="right", padx=30) + self.app_widgets[app_id] = {"btn": btn, "pbar": p_bar, "vlab": v_label} def start_update(self, app_id): - # 1. Lock UI for this app - widgets = self.app_widgets[app_id] - widgets["button"].configure(state="disabled", text="UPDATING...") - widgets["progress"].configure(mode="indeterminate") - widgets["progress"].start() - + w = self.app_widgets[app_id] + w["btn"].configure(state="disabled", text="GUARDING...") + w["pbar"].configure(mode="indeterminate"); w["pbar"].start() threading.Thread(target=lambda: self.update_logic(app_id), daemon=True).start() def update_logic(self, app_id): - widgets = self.app_widgets[app_id] + w = self.app_widgets[app_id] try: res = self.client.check_app_update(app_id) - if res["status"] == "update_available": - if messagebox.askyesno("Update Hub", f"New version {res['latest']} found for {app_id}.\n\nApply secure patch?"): + if messagebox.askyesno("🛡️ SECURITY ALERT", f"Authorized update v{res['latest']} found.\n\nApplying patch will verify integrity.\n\nProceed?"): self.client.apply_update(app_id, res) - self.after(0, lambda: widgets["version_label"].configure(text=f"v{res['latest']}")) - messagebox.showinfo("Success", f"{app_id} is now v{res['latest']}") - else: - messagebox.showinfo("Hub", f"{app_id} is already authentic and up to date.") - - except SecurityError as e: - messagebox.showerror("BREACH DETECTED", str(e)) - except Exception as e: - messagebox.showerror("Error", str(e)) + self.after(0, lambda: w["vlab"].configure(text=f"VER: {res['latest']}")) + else: messagebox.showinfo("Hub", f"{app_id} is verified and up to date.") + except SecurityError as e: messagebox.showerror("⛔ BREACH BLOCKED", str(e)) + except Exception as e: messagebox.showerror("Error", str(e)) finally: - # 2. Unlock UI - self.after(0, lambda: widgets["button"].configure(state="normal", text="SECURE UPDATE")) - self.after(0, lambda: widgets["progress"].stop()) - self.after(0, lambda: widgets["progress"].configure(mode="determinate")) - self.after(0, lambda: widgets["progress"].set(0)) + self.after(0, lambda: self.btn_next.pack_forget()) + self.after(0, lambda: (w["btn"].configure(state="normal", text="SECURE SCAN"), w["pbar"].stop(), w["pbar"].configure(mode="determinate"), w["pbar"].set(0))) + + def set_status_text(self, text): + self.step_label.configure(text=text.upper()) if __name__ == "__main__": - client_dir = os.path.dirname(os.path.abspath(__file__)) - root_dir = os.path.dirname(client_dir) - g_cfg = load_json_config(os.path.join(root_dir, "global_config.json")) - c_cfg = load_json_config(os.path.join(client_dir, "client_config.json")) - setup_logging(g_cfg) - app = SecureHubUI(g_cfg, c_cfg, client_dir) - app.mainloop() + check_environment() + c_dir = os.path.dirname(os.path.abspath(__file__)) + r_dir = os.path.dirname(c_dir) + try: + g = load_json_config(os.path.join(r_dir, "global_config.json")) + c = load_json_config(os.path.join(c_dir, "client_config.json")) + setup_logging(g) + SecureHubUI(g, c, c_dir).mainloop() + except Exception as e: print(f"Init Error: {e}") diff --git a/client/managed_apps/engine_monitor/app.exe b/client/managed_apps/engine_monitor/app.exe index 9a56f05..25217d1 100644 Binary files a/client/managed_apps/engine_monitor/app.exe and b/client/managed_apps/engine_monitor/app.exe differ diff --git a/client/managed_apps/engine_monitor/version.json b/client/managed_apps/engine_monitor/version.json deleted file mode 100644 index fa06ebb..0000000 --- a/client/managed_apps/engine_monitor/version.json +++ /dev/null @@ -1 +0,0 @@ -{"version": "1.0.5"} \ No newline at end of file diff --git a/client/managed_apps/hello_world/app.exe b/client/managed_apps/hello_world/app.exe new file mode 100644 index 0000000..5385494 Binary files /dev/null and b/client/managed_apps/hello_world/app.exe differ diff --git a/client/managed_apps/security_scanner/app.exe b/client/managed_apps/security_scanner/app.exe index 0a5e5c8..84b1f11 100644 Binary files a/client/managed_apps/security_scanner/app.exe and b/client/managed_apps/security_scanner/app.exe differ diff --git a/client/managed_apps/security_scanner/version.json b/client/managed_apps/security_scanner/version.json deleted file mode 100644 index 882aa09..0000000 --- a/client/managed_apps/security_scanner/version.json +++ /dev/null @@ -1 +0,0 @@ -{"version": "2.1.0"} \ No newline at end of file diff --git a/client/requirements.txt b/client/requirements.txt index 6888f75..34d50aa 100644 --- a/client/requirements.txt +++ b/client/requirements.txt @@ -2,5 +2,6 @@ requests==2.33.1 cryptography==48.0.0 python-dotenv==1.0.1 pytest==9.0.3 +pytest-cov==7.1.0 bsdiff4==1.2.6 customtkinter==5.2.2 diff --git a/hello_world_v1.0.0.exe b/hello_world_v1.0.0.exe new file mode 100644 index 0000000..39c93eb Binary files /dev/null and b/hello_world_v1.0.0.exe differ diff --git a/managed_apps_sources/hello_app.py b/managed_apps_sources/hello_app.py new file mode 100644 index 0000000..d18df39 --- /dev/null +++ b/managed_apps_sources/hello_app.py @@ -0,0 +1,16 @@ +import sys +import time + +def main(): + print("\n" + "="*40) + print("🌟 SECURE HUB MANAGED APP: HELLO WORLD 🌟") + print("="*40) + print(f"Status: Running successfully on {sys.platform}") + print("Version: 1.0.0 (Verified by Hub)") + print("="*40 + "\n") + + # Keep window open for a moment so user can see it + time.sleep(5) + +if __name__ == "__main__": + main() diff --git a/server/publisher.py b/server/publisher.py index 1548ae3..7322329 100644 --- a/server/publisher.py +++ b/server/publisher.py @@ -1,9 +1,12 @@ import os +import sys import json import base64 import hashlib import logging import bsdiff4 +import subprocess +import shutil from datetime import datetime, timedelta, timezone from cryptography.hazmat.primitives.asymmetric import ed25519 from cryptography.hazmat.primitives import serialization @@ -25,12 +28,57 @@ def load_json_config(file_path): def calculate_sha256(filepath): sha256_hash = hashlib.sha256() with open(filepath, "rb") as f: - for byte_block in iter(lambda: f.read(4096), b""): + # 64KB chunks for optimal production performance + for byte_block in iter(lambda: f.read(65536), b""): sha256_hash.update(byte_block) return sha256_hash.hexdigest() +import subprocess +import shutil + +# Steganographic constants must match Hub +SALT = b"\x3c\x4f\x6d\x2a\x7a\x31\x5b\x44" +SENTINEL = b"\x73\x65\x63\x75\x72\x65\x76\x65\x72" # "securever" + +def build_real_binary(app_id, version, source_path, output_path): + """ + Compiles a real Python script into a secure binary with a steganographic footer. + """ + print(f"--- Compiling Real Binary for {app_id} v{version} ---") + temp_name = f"build_tmp_{app_id}_{version}" + try: + # Use PyInstaller logic + subprocess.check_call([ + sys.executable, "-m", "PyInstaller", + "--onefile", "--clean", "--name", temp_name, + source_path + ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + ext = ".exe" if sys.platform == "win32" else "" + compiled_file = os.path.join("dist", f"{temp_name}{ext}") + + with open(compiled_file, "rb") as f: + binary_data = f.read() + + # Append Steganographic Footer + ver_bytes = version.encode().ljust(8, b"\x00") + obfuscated = bytes([b ^ SALT[i % len(SALT)] for i, b in enumerate(ver_bytes)]) + + with open(output_path, "wb") as f: + f.write(binary_data + obfuscated + SENTINEL) + + # Cleanup PyInstaller mess + shutil.rmtree("build", ignore_errors=True) + shutil.rmtree("dist", ignore_errors=True) + if os.path.exists(f"{temp_name}.spec"): os.remove(f"{temp_name}.spec") + return True + except Exception as e: + print(f"Compilation failed for {app_id}: {e}") + return False + def publish_app_update(app_id, app_data, global_config, pub_config, server_dir): logger = logging.getLogger(f"Publisher-{app_id}") + root_dir = os.path.dirname(server_dir) # Paths app_static_dir = os.path.join(server_dir, "static", app_id) @@ -49,15 +97,30 @@ def publish_app_update(app_id, app_data, global_config, pub_config, server_dir): if not os.path.exists(app_static_dir): os.makedirs(app_static_dir) - - # 1. Create Binaries + + # 1. Determine if we should build a REAL or MOCK binary + source_file = os.path.join(root_dir, "managed_apps_sources", "hello_app.py") full_exe_path = os.path.join(app_static_dir, full_installer_name) - with open(full_exe_path, "wb") as f: - f.write(f"REAL BINARY DATA FOR {app_id} v{target_version}\n".encode() + os.urandom(1024)) - v_old_path = os.path.join(app_static_dir, f"old_v{from_version}.tmp") - with open(v_old_path, "wb") as f: - f.write(f"REAL BINARY DATA FOR {app_id} v{from_version}\n".encode() + os.urandom(1024)) + + if app_id == "hello_world" and os.path.exists(source_file): + # Build REAL binaries for v1.0.0 and v1.1.0 to generate a real executable patch + s1 = build_real_binary(app_id, target_version, source_file, full_exe_path) + s2 = build_real_binary(app_id, from_version, source_file, v_old_path) + if not s1 or not s2: + logger.error(f"Abandoning {app_id} publication due to build failure.") + return + else: + # Fallback to MOCK binary for others + def create_mock_binary(path, version): + base_data = os.urandom(1024) + ver_bytes = version.encode().ljust(8, b"\x00") + obfuscated = bytes([b ^ SALT[i % len(SALT)] for i, b in enumerate(ver_bytes)]) + with open(path, "wb") as f: + f.write(base_data + obfuscated + SENTINEL) + + create_mock_binary(full_exe_path, target_version) + create_mock_binary(v_old_path, from_version) # 2. Generate Patch patch_bin_path = os.path.join(app_static_dir, patch_name) @@ -97,18 +160,19 @@ def publish_app_update(app_id, app_data, global_config, pub_config, server_dir): } } + # --- Robust Signing Pattern --- + # 1. Generate the exact bytes that will be signed AND written + manifest_bytes = json.dumps(manifest, indent=4).encode('utf-8') + manifest_path = os.path.join(app_static_dir, manifest_name) - with open(manifest_path, "w") as f: - json.dump(manifest, f, indent=4) + with open(manifest_path, "wb") as f: + f.write(manifest_bytes) - # 4. Sign Manifest + # 2. Sign those exact bytes priv_key_path = os.path.join(keys_dir, private_key_name) with open(priv_key_path, "rb") as key_file: private_key = serialization.load_pem_private_key(key_file.read(), password=None) - with open(manifest_path, "rb") as f: - manifest_bytes = f.read() - signature = private_key.sign(manifest_bytes) sig_path = os.path.join(app_static_dir, sig_name) with open(sig_path, "w") as f: diff --git a/server/publisher_config.json b/server/publisher_config.json index 6334b96..1d13501 100644 --- a/server/publisher_config.json +++ b/server/publisher_config.json @@ -19,6 +19,16 @@ "from_version": "1.0.0" }, "process_name": "monitor.exe" + }, + "hello_world": { + "name": "Hello World App", + "release": { + "version": "1.1.0", + "full_installer_name": "hello_world_v1.1.0_full.exe", + "patch_name": "hello_patch_v1.0.0_to_v1.1.0.bin", + "from_version": "1.0.0" + }, + "process_name": "hello_world.exe" } }, "filenames": { diff --git a/server/requirements.txt b/server/requirements.txt index 4cf5d9b..1c7203f 100644 --- a/server/requirements.txt +++ b/server/requirements.txt @@ -4,3 +4,4 @@ cryptography==48.0.0 python-dotenv==1.0.1 pytest==9.0.3 bsdiff4==1.2.6 +pyinstaller==6.20.0 diff --git a/server/static/app_v3_full.exe b/server/static/app_v3_full.exe deleted file mode 100644 index 85294cc..0000000 Binary files a/server/static/app_v3_full.exe and /dev/null differ diff --git a/server/static/engine_monitor/manifest.json b/server/static/engine_monitor/manifest.json index 2795038..05d665b 100644 --- a/server/static/engine_monitor/manifest.json +++ b/server/static/engine_monitor/manifest.json @@ -6,21 +6,21 @@ }, "metadata": { "latest_version": "1.0.5", - "release_date": "2026-05-09T17:19:37Z", - "valid_until": "2026-06-08T17:19:37Z" + "release_date": "2026-05-09T21:06:45Z", + "valid_until": "2026-06-08T21:06:45Z" }, "assets": { "full_installer": { "file_name": "monitor_v1.0.5_full.exe", "url": "http://localhost:8000/static/engine_monitor/monitor_v1.0.5_full.exe", - "sha256_hash": "78940565dc9852178d422177e602196109769e9a32a6884b7a9a2900adcf67bc" + "sha256_hash": "6f2caeac1e8c34eacc1e75243e8fd0222f2925f9743df93141e1713f53047e18" }, "patches": [ { "from_version": "1.0.0", "file_name": "monitor_patch_v1.0.0_to_v1.0.5.bin", "url": "http://localhost:8000/static/engine_monitor/monitor_patch_v1.0.0_to_v1.0.5.bin", - "result_sha256_hash": "78940565dc9852178d422177e602196109769e9a32a6884b7a9a2900adcf67bc" + "result_sha256_hash": "6f2caeac1e8c34eacc1e75243e8fd0222f2925f9743df93141e1713f53047e18" } ] } diff --git a/server/static/engine_monitor/manifest.sig b/server/static/engine_monitor/manifest.sig index 5aa99a1..0fdc0ee 100644 --- a/server/static/engine_monitor/manifest.sig +++ b/server/static/engine_monitor/manifest.sig @@ -1 +1 @@ -q5AOpvJo83QusMd180CHw8U73lcNvkdx/npbZ5GtugA01Qxj6Td+swZM+O8sSXKc/LdpSeJwgEMJqy7jq4T8Dw== \ No newline at end of file +ooDw4K+Pe/M/bHpSWEUvyync4fyFQxsmo2mwvSi8R57gvEgBU79qRkykY17rdGAVsZ0wM0HousrtYM7+F+fzDA== \ No newline at end of file diff --git a/server/static/engine_monitor/monitor_patch_v1.0.0_to_v1.0.5.bin b/server/static/engine_monitor/monitor_patch_v1.0.0_to_v1.0.5.bin index 4913484..1f566ad 100644 Binary files a/server/static/engine_monitor/monitor_patch_v1.0.0_to_v1.0.5.bin and b/server/static/engine_monitor/monitor_patch_v1.0.0_to_v1.0.5.bin differ diff --git a/server/static/engine_monitor/monitor_v1.0.5_full.exe b/server/static/engine_monitor/monitor_v1.0.5_full.exe index 9a56f05..a6ea403 100644 Binary files a/server/static/engine_monitor/monitor_v1.0.5_full.exe and b/server/static/engine_monitor/monitor_v1.0.5_full.exe differ diff --git a/server/static/hello_world/hello_patch_v1.0.0_to_v1.1.0.bin b/server/static/hello_world/hello_patch_v1.0.0_to_v1.1.0.bin new file mode 100644 index 0000000..f3fad04 Binary files /dev/null and b/server/static/hello_world/hello_patch_v1.0.0_to_v1.1.0.bin differ diff --git a/server/static/hello_world/hello_world_v1.1.0_full.exe b/server/static/hello_world/hello_world_v1.1.0_full.exe new file mode 100644 index 0000000..5385494 Binary files /dev/null and b/server/static/hello_world/hello_world_v1.1.0_full.exe differ diff --git a/server/static/hello_world/manifest.json b/server/static/hello_world/manifest.json new file mode 100644 index 0000000..2e58a51 --- /dev/null +++ b/server/static/hello_world/manifest.json @@ -0,0 +1,27 @@ +{ + "app_info": { + "id": "hello_world", + "name": "Hello World App", + "process_name": "hello_world.exe" + }, + "metadata": { + "latest_version": "1.1.0", + "release_date": "2026-05-09T21:06:58Z", + "valid_until": "2026-06-08T21:06:58Z" + }, + "assets": { + "full_installer": { + "file_name": "hello_world_v1.1.0_full.exe", + "url": "http://localhost:8000/static/hello_world/hello_world_v1.1.0_full.exe", + "sha256_hash": "97644fe029792e9f6adff443dd956765d74acb3d234f15fbed40e34609562d39" + }, + "patches": [ + { + "from_version": "1.0.0", + "file_name": "hello_patch_v1.0.0_to_v1.1.0.bin", + "url": "http://localhost:8000/static/hello_world/hello_patch_v1.0.0_to_v1.1.0.bin", + "result_sha256_hash": "97644fe029792e9f6adff443dd956765d74acb3d234f15fbed40e34609562d39" + } + ] + } +} \ No newline at end of file diff --git a/server/static/hello_world/manifest.sig b/server/static/hello_world/manifest.sig new file mode 100644 index 0000000..d136d37 --- /dev/null +++ b/server/static/hello_world/manifest.sig @@ -0,0 +1 @@ +pi+QYFpqVLRSNQi4Q2ji9BLLrnLlkF3x7i+i5IA+XiU6oPDk1FR1g8DrB/yYebBe0wskPZn/k4bi5r31eruNCg== \ No newline at end of file diff --git a/server/static/manifest.json b/server/static/manifest.json deleted file mode 100644 index 41d9760..0000000 --- a/server/static/manifest.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "metadata": { - "latest_version": "3.0.0", - "release_date": "2026-05-09T16:54:59Z", - "valid_until": "2026-06-08T16:54:59Z", - "critical_update": false - }, - "assets": { - "full_installer": { - "file_name": "app_v3_full.exe", - "url": "http://localhost:8000/static/app_v3_full.exe", - "size_bytes": 1059, - "sha256_hash": "50049b87044779556a41a18824ae9f1d02b2f27e205e6c42663eb62e5578893b" - }, - "patches": [ - { - "from_version": "2.0.0", - "file_name": "patch_v2_to_v3.bin", - "url": "http://localhost:8000/static/patch_v2_to_v3.bin", - "size_bytes": 1435, - "result_sha256_hash": "50049b87044779556a41a18824ae9f1d02b2f27e205e6c42663eb62e5578893b" - } - ] - } -} \ No newline at end of file diff --git a/server/static/manifest.sig b/server/static/manifest.sig deleted file mode 100644 index b1d2295..0000000 --- a/server/static/manifest.sig +++ /dev/null @@ -1 +0,0 @@ -94FuOuk5vkVaS6tXfaE15ya0k1ieusZlrUiSVoLlB8U4xCfYbtU2bz86Zug2LtiAqzGgvOupI1mHJ2LHVBn4AQ== \ No newline at end of file diff --git a/server/static/patch_v2_to_v3.bin b/server/static/patch_v2_to_v3.bin deleted file mode 100644 index 0298095..0000000 Binary files a/server/static/patch_v2_to_v3.bin and /dev/null differ diff --git a/server/static/security_scanner/manifest.json b/server/static/security_scanner/manifest.json index 7690898..d944013 100644 --- a/server/static/security_scanner/manifest.json +++ b/server/static/security_scanner/manifest.json @@ -6,21 +6,21 @@ }, "metadata": { "latest_version": "2.1.0", - "release_date": "2026-05-09T17:19:37Z", - "valid_until": "2026-06-08T17:19:37Z" + "release_date": "2026-05-09T21:06:45Z", + "valid_until": "2026-06-08T21:06:45Z" }, "assets": { "full_installer": { "file_name": "scanner_v2.1.0_full.exe", "url": "http://localhost:8000/static/security_scanner/scanner_v2.1.0_full.exe", - "sha256_hash": "b2ac747e823c69b30335a514642fe44ca581c8df9cfb70e1d449860846ab6678" + "sha256_hash": "2791072cc371c3388fce706cdcc1bdac14fa5cc85e367655f79aa68d38e9c141" }, "patches": [ { "from_version": "2.0.0", "file_name": "scanner_patch_v2.0.0_to_v2.1.0.bin", "url": "http://localhost:8000/static/security_scanner/scanner_patch_v2.0.0_to_v2.1.0.bin", - "result_sha256_hash": "b2ac747e823c69b30335a514642fe44ca581c8df9cfb70e1d449860846ab6678" + "result_sha256_hash": "2791072cc371c3388fce706cdcc1bdac14fa5cc85e367655f79aa68d38e9c141" } ] } diff --git a/server/static/security_scanner/manifest.sig b/server/static/security_scanner/manifest.sig index 4b4816d..d9bd54f 100644 --- a/server/static/security_scanner/manifest.sig +++ b/server/static/security_scanner/manifest.sig @@ -1 +1 @@ -9UzlbPa/yjcmsYrgPVaxSfOLNVpU8EbhXnxaUg8/u2s7yU5u42RVHMWTEYiBeIbbYmu/LNnsqg9rUia4rhAOBA== \ No newline at end of file +vPBFNSQtTGLlf6frAHr3WIJ54jB5JOxDKDfSHcr371sz7TxoAAFPTj7ORTk6ZBRAtnwsHti6WYS2phiCQwLQDg== \ No newline at end of file diff --git a/server/static/security_scanner/scanner_patch_v2.0.0_to_v2.1.0.bin b/server/static/security_scanner/scanner_patch_v2.0.0_to_v2.1.0.bin index 03956d3..d9278f6 100644 Binary files a/server/static/security_scanner/scanner_patch_v2.0.0_to_v2.1.0.bin and b/server/static/security_scanner/scanner_patch_v2.0.0_to_v2.1.0.bin differ diff --git a/server/static/security_scanner/scanner_v2.1.0_full.exe b/server/static/security_scanner/scanner_v2.1.0_full.exe index 0a5e5c8..e5708de 100644 Binary files a/server/static/security_scanner/scanner_v2.1.0_full.exe and b/server/static/security_scanner/scanner_v2.1.0_full.exe differ diff --git a/tests/test_integration.py b/tests/test_integration.py index a788ce1..e6c4f27 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -28,7 +28,7 @@ def test_integration_flow(): server_url = f"{g_cfg['server']['url']}/static/security_scanner" pub_key_path = os.path.join(root_dir, "client", "keys", "public_key.pem") - + # 2. Check Prerequisites if not os.path.exists(pub_key_path): print(f"[FAIL] Public key missing at {pub_key_path}. Run generate_keys.py first.") @@ -45,6 +45,7 @@ def test_integration_flow(): print(f"[FAIL] Could not connect to server. Is it running? Error: {e}") return + # 4. Verify Signature print("Step 2: Verifying Ed25519 signature...") with open(pub_key_path, "rb") as f: