From 325f66a75b34682e3d68fe27a2363413361291f3 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Wed, 3 Dec 2025 21:30:05 +0000 Subject: [PATCH] Add automatic update checking with SHA256 hash comparison MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements simple auto-update checking as requested in issue #21: - Fetches latest script version from GitHub raw - Compares SHA256 hashes of local vs remote versions - Shows alert notification when new version is detected - Includes direct link to releases page for downloading - Checks once per day to avoid excessive network calls - Gracefully handles network failures without false alerts 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Nick Sweeting --- security-growler.30s.py | 91 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/security-growler.30s.py b/security-growler.30s.py index a1c7f90..4d0a7fe 100755 --- a/security-growler.30s.py +++ b/security-growler.30s.py @@ -35,6 +35,7 @@ import json import asyncio import subprocess +import hashlib from datetime import datetime, timedelta from pathlib import Path from typing import Optional, List, Dict, Tuple, Any @@ -148,6 +149,93 @@ def log_event(event_type: str, title: str, body: str) -> None: pass +# ============================================================================= +# Auto-Update Checking +# ============================================================================= + +def compute_file_sha256(filepath: str) -> Optional[str]: + """Compute SHA256 hash of a file.""" + try: + sha256_hash = hashlib.sha256() + with open(filepath, "rb") as f: + # Read in chunks to handle large files + for byte_block in iter(lambda: f.read(4096), b""): + sha256_hash.update(byte_block) + return sha256_hash.hexdigest() + except (IOError, OSError): + return None + + +def fetch_remote_script_hash() -> Optional[str]: + """Fetch the latest version of this script from GitHub and compute its hash.""" + github_raw_url = "https://github.com/pirate/security-growler/raw/master/security-growler.30s.py" + + try: + # Use curl to fetch the remote script + result = subprocess.run( + ["curl", "--max-time", "10", "--silent", "--location", github_raw_url], + capture_output=True, + timeout=15 + ) + + if result.returncode != 0: + return None + + # Compute hash of remote content + remote_content = result.stdout + if not remote_content: + return None + + sha256_hash = hashlib.sha256() + sha256_hash.update(remote_content) + return sha256_hash.hexdigest() + + except (subprocess.TimeoutExpired, subprocess.SubprocessError): + return None + + +def check_for_updates(state: Dict[str, Any]) -> List[Tuple[str, str, str]]: + """Check if a new version is available on GitHub.""" + events = [] + + # Only check once per day + last_update_check = state.get("last_update_check") + if last_update_check: + try: + last_check_time = datetime.fromisoformat(last_update_check) + if datetime.now() - last_check_time < timedelta(days=1): + # Skip check, too soon + return events + except (ValueError, TypeError): + pass + + # Get path to this script + script_path = os.path.abspath(__file__) + + # Compute local hash + local_hash = compute_file_sha256(script_path) + if not local_hash: + return events + + # Fetch and compute remote hash + remote_hash = fetch_remote_script_hash() + if not remote_hash: + # Failed to fetch, but don't alert + return events + + # Compare hashes + if local_hash != remote_hash: + title = "UPDATE AVAILABLE" + body = "New version of Security Growler detected" + download_url = "https://github.com/pirate/security-growler/releases" + events.append(("alert", title, f"{body} - {download_url}")) + + # Update last check time + state["last_update_check"] = datetime.now().isoformat() + + return events + + # ============================================================================= # Notifications # ============================================================================= @@ -1359,6 +1447,9 @@ def collect_all_events(state: Dict[str, Any]) -> List[Tuple[str, str, str]]: # ARP spoofing detection all_events.extend(parse_arp_spoof_events(state)) + # Auto-update checking + all_events.extend(check_for_updates(state)) + return all_events