209 lines
6.9 KiB
Python
209 lines
6.9 KiB
Python
import os
|
|
import sys
|
|
import json
|
|
import socket
|
|
import struct
|
|
import argparse
|
|
import warnings
|
|
from datetime import datetime, timezone, timedelta
|
|
|
|
# Suppress cryptography / pgpy deprecation notices
|
|
warnings.filterwarnings("ignore")
|
|
|
|
import pgpy
|
|
|
|
try:
|
|
import win32evtlog
|
|
except ImportError:
|
|
win32evtlog = None
|
|
|
|
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()
|
|
user_dns_domain = os.environ.get("USERDNSDOMAIN")
|
|
if user_dns_domain and user_dns_domain.lower() != hostname.lower():
|
|
return f"{hostname}.{user_dns_domain.lower()}"
|
|
|
|
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_windows_logs(hours: int = 6):
|
|
"""
|
|
Scans the Windows Application Event Log backwards for events within the window.
|
|
Edge Thinness & Noise Stripping: INFO and DEBUG events are dropped at the source.
|
|
"""
|
|
if win32evtlog is None:
|
|
print("[!] pywin32 is not installed or not running on Windows. Returning mock/empty candidate list.")
|
|
return []
|
|
|
|
server = "localhost"
|
|
log_type = "Application"
|
|
flags = win32evtlog.EVENTLOG_BACKWARDS_READ | win32evtlog.EVENTLOG_SEQUENTIAL_READ
|
|
|
|
try:
|
|
hand = win32evtlog.OpenEventLog(server, log_type)
|
|
except Exception as e:
|
|
print(f"[!] Error opening Windows event log: {e}")
|
|
return []
|
|
|
|
logs = []
|
|
cutoff_time = datetime.now() - timedelta(hours=hours)
|
|
machine_id = get_machine_identifier()
|
|
|
|
sev_map = {
|
|
1: "CRITICAL",
|
|
2: "ERROR",
|
|
3: "WARNING"
|
|
}
|
|
|
|
while True:
|
|
events = win32evtlog.ReadEventLog(hand, flags, 0)
|
|
if not events:
|
|
break
|
|
|
|
for event in events:
|
|
if event.TimeGenerated < cutoff_time:
|
|
break
|
|
|
|
# Drop conversational or informational noise (INFO=4, etc.) at source
|
|
# Only retain Critical (1), Error (2), and Warning (3)
|
|
if event.EventType in sev_map:
|
|
msg = " ".join(event.StringInserts) if event.StringInserts else "Event Log Entry"
|
|
logs.append({
|
|
"server": machine_id,
|
|
"os_type": "windows",
|
|
"signature": event.SourceName or "Windows-Event",
|
|
"severity": sev_map[event.EventType],
|
|
"message": msg[:2048] # Limit message length
|
|
})
|
|
|
|
if events[-1].TimeGenerated < cutoff_time:
|
|
break
|
|
|
|
win32evtlog.CloseEventLog(hand)
|
|
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()
|
|
|
|
# Load and verify server public key
|
|
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()
|
|
|
|
# Prepare zero-state candidate batch
|
|
payload = {
|
|
"server": machine_id,
|
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
"logs": logs
|
|
}
|
|
payload_json = json.dumps(payload)
|
|
|
|
# Encrypt payload with server's encryption-only key
|
|
pgp_msg = pgpy.PGPMessage.new(payload_json)
|
|
encrypted_msg = pub_key.encrypt(pgp_msg)
|
|
encrypted_armored = str(encrypted_msg)
|
|
|
|
# Envelope with socket authentication header
|
|
envelope = {
|
|
"auth_token": auth_token,
|
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
"encrypted_payload": encrypted_armored
|
|
}
|
|
envelope_bytes = json.dumps(envelope).encode("utf-8")
|
|
|
|
# Connect over TCP socket and transmit with 4-byte length prefix framing
|
|
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))
|
|
|
|
# Send frame: length (4 bytes big-endian) + envelope
|
|
frame = struct.pack(">I", len(envelope_bytes)) + envelope_bytes
|
|
sock.sendall(frame)
|
|
|
|
# Receive response length
|
|
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 Windows 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 event logs")
|
|
args = parser.parse_args()
|
|
|
|
try:
|
|
config = load_config(args.config)
|
|
except Exception as e:
|
|
print(f"[!] Configuration error: {e}")
|
|
sys.exit(1)
|
|
|
|
print(f"[*] Scanning Windows Application event log for candidate anomalies (last {args.hours} hours)...")
|
|
candidate_logs = get_recent_windows_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() |