CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 1m14s
146 lines
5.5 KiB
Python
146 lines
5.5 KiB
Python
"""
|
|
LOGAR Linux Edge Log Forwarder
|
|
Collects Linux systemd journalctl / syslog entries and securely streams them
|
|
to the central LOGAR Hub via mutual TLS (mTLS 1.3) or OpenPGP envelope.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import time
|
|
import argparse
|
|
import warnings
|
|
from typing import Optional
|
|
|
|
# Ensure script directory and repo root are in sys.path
|
|
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
if SCRIPT_DIR not in sys.path:
|
|
sys.path.insert(0, SCRIPT_DIR)
|
|
REPO_ROOT = os.path.abspath(os.path.join(SCRIPT_DIR, ".."))
|
|
if REPO_ROOT not in sys.path:
|
|
sys.path.insert(0, REPO_ROOT)
|
|
|
|
# Suppress cryptography / pgpy deprecation notices
|
|
warnings.filterwarnings("ignore")
|
|
|
|
import pgpy
|
|
|
|
from log_collector import (
|
|
get_machine_identifier,
|
|
is_path_writable,
|
|
get_default_data_dir,
|
|
resolve_writable_path,
|
|
get_state_path,
|
|
load_state,
|
|
save_state,
|
|
commit_state,
|
|
LinuxLogCollector,
|
|
BaseLogCollector
|
|
)
|
|
from client_transport import (
|
|
is_cert_expiring_soon,
|
|
enroll_client_if_needed,
|
|
get_tls_socket,
|
|
load_config,
|
|
ensure_enrolled as transport_ensure_enrolled,
|
|
send_encrypted_logs_over_socket as transport_send_encrypted_logs,
|
|
check_and_apply_auto_update
|
|
)
|
|
|
|
CONFIG_FILE_NAME = "client_config.json"
|
|
STATE_FILE_NAME = "client_state.json"
|
|
|
|
|
|
def get_recent_linux_logs(hours: int = 24, state: Optional[dict] = None) -> list:
|
|
"""
|
|
Collects info, warnings, and errors from systemd journalctl over the lookback window.
|
|
Edge Filtering: Retains INFO, WARNING, and ERROR. Strips DEBUG noise.
|
|
State Tracking: Skips events older than lookback window and already sent cursors.
|
|
"""
|
|
collector = LinuxLogCollector(state=state)
|
|
return collector.collect_logs(hours=hours)
|
|
|
|
|
|
def send_encrypted_logs_over_socket(config: dict, logs: list):
|
|
"""Streams logs to the LOGAR hub over mTLS or encrypted TCP envelope."""
|
|
return transport_send_encrypted_logs(config=config, logs=logs, os_type="linux")
|
|
|
|
|
|
def ensure_enrolled(config: dict, machine_id: str) -> bool:
|
|
"""Proactively ensures client certificate enrollment with the Hub if enrollment_secret is available."""
|
|
return transport_ensure_enrolled(config=config, machine_id=machine_id, os_type="linux")
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="LOGAR Linux Edge Log Forwarder with State Tracking")
|
|
parser.add_argument("--config", default=CONFIG_FILE_NAME, help="Path to client_config.json")
|
|
parser.add_argument("--hours", type=int, default=24, help="Lookback window in hours for journalctl logs (default: 24)")
|
|
parser.add_argument("--state-file", default=None, help="Path to state tracking file (default: client_state.json next to config)")
|
|
parser.add_argument("--no-state", action="store_true", help="Disable state tracking and send all events matching lookback window")
|
|
parser.add_argument("--interval", type=int, default=0, help="Continuous polling interval in seconds (0 = run once and exit, >0 = service loop)")
|
|
args = parser.parse_args()
|
|
|
|
try:
|
|
config = load_config(args.config)
|
|
except Exception as e:
|
|
print(f"[!] Configuration error: {e}")
|
|
sys.exit(1)
|
|
|
|
state_path = get_state_path(args.config, args.state_file)
|
|
state = None if args.no_state else load_state(state_path)
|
|
|
|
machine_id = config.get("client_id") or get_machine_identifier()
|
|
print(f"[*] Edge Forwarder Node: {machine_id}")
|
|
if state and ("last_cursor" in state or "last_timestamp_us" in state):
|
|
print(f"[*] State tracking active: resuming after previous cursor/timestamp (state file: {state_path})")
|
|
elif not args.no_state:
|
|
print(f"[*] State tracking initialized (state file: {state_path})")
|
|
|
|
# Proactively ensure dynamic PKI certificate enrollment with Server Hub
|
|
ensure_enrolled(config, machine_id)
|
|
|
|
collector = LinuxLogCollector(state=state, machine_id=machine_id)
|
|
|
|
def run_forwarder_cycle():
|
|
# Continuously verify/renew PKI enrollment if server is licensed & online
|
|
ensure_enrolled(config, machine_id)
|
|
|
|
# Periodically check server for client auto-updates
|
|
try:
|
|
check_and_apply_auto_update(config=config, machine_id=machine_id, os_type="linux")
|
|
except Exception as update_err:
|
|
pass
|
|
|
|
print(f"[*] Scanning Linux journalctl for unsent entries (last {args.hours} hours)...")
|
|
candidate_logs = collector.collect_logs(hours=args.hours)
|
|
print(f"[*] Found {len(candidate_logs)} unsent candidate entries (INFO to ERROR, entries > {args.hours}h and already-sent skipped).")
|
|
|
|
if not candidate_logs:
|
|
print("[*] No new unsent events to transmit.")
|
|
if state is not None:
|
|
commit_state(state, state_path)
|
|
return
|
|
|
|
try:
|
|
resp = send_encrypted_logs_over_socket(config, candidate_logs)
|
|
if state is not None and resp and resp.get("status") == "success":
|
|
commit_state(state, state_path)
|
|
print(f"[+] State successfully committed to {state_path}")
|
|
except Exception as e:
|
|
print(f"[!] Failed to stream logs to server: {e}")
|
|
if not args.interval:
|
|
sys.exit(1)
|
|
|
|
if args.interval and args.interval > 0:
|
|
print(f"[*] Starting continuous forwarder service loop (Interval: {args.interval}s)...")
|
|
while True:
|
|
try:
|
|
run_forwarder_cycle()
|
|
except Exception as e:
|
|
print(f"[!] Error in forwarder cycle: {e}")
|
|
time.sleep(args.interval)
|
|
else:
|
|
run_forwarder_cycle()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main() |