#!/usr/bin/env python3
PK     §#]u)”)Æ  Æ     Linux_Client.pyimport os
import sys
import json
import socket
import struct
import argparse
import subprocess
import warnings
from datetime import datetime, timezone

# Suppress cryptography / pgpy deprecation notices
warnings.filterwarnings("ignore")

import pgpy

CONFIG_FILE_NAME = "client_config.json"


def load_config(config_path: str = CONFIG_FILE_NAME):
    if not os.path.exists(config_path):
        raise FileNotFoundError(
            f"Client configuration file not found at: {config_path}\n"
            f"Generate one from the server using: python Server.py --create-client-config --client-out {config_path}"
        )
    with open(config_path, "r", encoding="utf-8") as f:
        return json.load(f)


def get_machine_identifier() -> str:
    """
    Returns the hostname of the machine sending the logs,
    and appends the network/DNS domain if available.
    """
    fqdn = socket.getfqdn()
    if fqdn and "." in fqdn and not fqdn.startswith("localhost"):
        return fqdn

    hostname = socket.gethostname()

    try:
        if os.path.exists("/etc/resolv.conf"):
            with open("/etc/resolv.conf", "r", encoding="utf-8") as f:
                for line in f:
                    parts = line.strip().split()
                    if parts and parts[0] in ["domain", "search"] and len(parts) > 1:
                        domain = parts[1]
                        if domain and not domain.startswith("."):
                            return f"{hostname}.{domain}"
    except Exception:
        pass

    try:
        host_ip = socket.gethostbyname(hostname)
        canonical_name = socket.gethostbyaddr(host_ip)[0]
        if canonical_name and "." in canonical_name and not canonical_name.startswith("localhost"):
            return canonical_name
    except Exception:
        pass

    return hostname


def get_recent_linux_logs(hours: int = 6):
    """
    Collects warnings and errors from systemd journalctl over the lookback window.
    Edge Thinness: Drops INFO and DEBUG entries at the source.
    """
    cmd = ["journalctl", "--since", f"{hours} hours ago", "-p", "warning", "--output=json"]
    try:
        result = subprocess.run(cmd, capture_output=True, text=True, check=False)
    except FileNotFoundError:
        print("[!] journalctl command not found. Ensure this script runs on a systemd-enabled Linux system.")
        return []
    except Exception as e:
        print(f"[!] Error running journalctl: {e}")
        return []

    logs = []
    machine_id = get_machine_identifier()

    for line in result.stdout.splitlines():
        line_str = line.strip()
        if not line_str:
            continue
        try:
            entry = json.loads(line_str)
            priority = str(entry.get("PRIORITY", "4"))
            if int(priority) > 4:
                continue

            sev = "WARNING" if priority == "4" else "ERROR"
            logs.append({
                "server": machine_id,
                "os_type": "linux",
                "signature": entry.get("SYSLOG_IDENTIFIER", "unknown"),
                "severity": sev,
                "message": entry.get("MESSAGE", "")[:2048]
            })
        except (json.JSONDecodeError, ValueError):
            continue

    return logs


def send_encrypted_logs_over_socket(config: dict, logs: list):
    """
    Encrypts the payload using the server's OpenPGP public key and streams
    over an authenticated TCP socket. Zero local state is maintained on the client.
    """
    server_host = config["server_host"]
    server_port = int(config["server_port"])
    auth_token = config["auth_token"]
    pub_key_armored = config["server_public_key"]
    expected_fp = config.get("server_fingerprint", "").replace(" ", "").upper()

    pub_key, _ = pgpy.PGPKey.from_blob(pub_key_armored)
    actual_fp = str(pub_key.fingerprint).replace(" ", "").upper()
    if expected_fp and actual_fp != expected_fp:
        raise ValueError(
            f"Server fingerprint mismatch! Expected {expected_fp}, but key has {actual_fp}."
        )

    machine_id = get_machine_identifier()

    payload = {
        "server": machine_id,
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "logs": logs
    }
    payload_json = json.dumps(payload)

    pgp_msg = pgpy.PGPMessage.new(payload_json)
    encrypted_msg = pub_key.encrypt(pgp_msg)
    encrypted_armored = str(encrypted_msg)

    envelope = {
        "auth_token": auth_token,
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "encrypted_payload": encrypted_armored
    }
    envelope_bytes = json.dumps(envelope).encode("utf-8")

    print(f"[*] Connecting to LOGAR server at {server_host}:{server_port} over secure TCP socket...")
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
        sock.settimeout(15.0)
        sock.connect((server_host, server_port))

        frame = struct.pack(">I", len(envelope_bytes)) + envelope_bytes
        sock.sendall(frame)

        resp_len_bytes = sock.recv(4)
        if not resp_len_bytes:
            raise ConnectionError("Server closed connection without response.")

        resp_len = struct.unpack(">I", resp_len_bytes)[0]
        resp_bytes = bytearray()
        while len(resp_bytes) < resp_len:
            chunk = sock.recv(min(4096, resp_len - len(resp_bytes)))
            if not chunk:
                break
            resp_bytes.extend(chunk)

        response = json.loads(resp_bytes.decode("utf-8"))
        print(f"[+] Server response: {response}")
        return response


def main():
    parser = argparse.ArgumentParser(description="LOGAR Linux Edge Log Forwarder (Zero State)")
    parser.add_argument("--config", default=CONFIG_FILE_NAME, help="Path to client_config.json")
    parser.add_argument("--hours", type=int, default=6, help="Lookback window in hours for journalctl logs")
    args = parser.parse_args()

    try:
        config = load_config(args.config)
    except Exception as e:
        print(f"[!] Configuration error: {e}")
        sys.exit(1)

    machine_id = get_machine_identifier()
    print(f"[*] Edge Forwarder Node: {machine_id}")
    print(f"[*] Scanning Linux journalctl for candidate anomalies (last {args.hours} hours)...")
    candidate_logs = get_recent_linux_logs(hours=args.hours)
    print(f"[*] Found {len(candidate_logs)} candidate anomalies (noise stripped at source).")

    try:
        send_encrypted_logs_over_socket(config, candidate_logs)
    except Exception as e:
        print(f"[!] Failed to stream logs to server: {e}")
        sys.exit(1)


if __name__ == "__main__":
    main()
PK     L§#]Ò³ý‘@   @      __main__.py# -*- coding: utf-8 -*-
import Linux_Client
Linux_Client.main()
PK      §#]u)”)Æ  Æ             ¶   Linux_Client.pyPK      L§#]Ò³ý‘@   @              €
  __main__.pyPK      v   s    