# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Email: Georgescottfoley@proton.me
# Licensed under the MIT License - see LICENSE file for details

"""
Sysaudit Skill

System security auditing via native CLI tools: Lynis (CIS baseline),
nmap (port scanning), Trivy (container vulnerability scanning),
and Suricata (IDS alert parsing).

No additional Python dependencies — wraps installed system tools.

Prerequisites (installed via dnf/apt or profile script):
    lynis, nmap, trivy, suricata
"""

import json
import logging
import os
import shutil
import subprocess
from pathlib import Path

logger = logging.getLogger(__name__)

SUBPROCESS_TIMEOUT = 300  # 5 minutes for lynis
NMAP_TIMEOUT = 120
TRIVY_TIMEOUT = 180
SURICATA_LOG = Path(os.environ.get("SURICATA_EVE_LOG", "/var/log/suricata/eve.json"))


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _tool_available(name: str) -> bool:
    return shutil.which(name) is not None


def _run(cmd: list, timeout: int = SUBPROCESS_TIMEOUT) -> tuple[int, str, str]:
    """Run a subprocess, return (returncode, stdout, stderr)."""
    try:
        result = subprocess.run(
            cmd,
            capture_output=True,
            text=True,
            timeout=timeout,
        )
        return result.returncode, result.stdout, result.stderr
    except subprocess.TimeoutExpired:
        return -1, "", f"Command timed out after {timeout}s"
    except Exception as e:
        return -1, "", str(e)


# ---------------------------------------------------------------------------
# Tool handlers
# ---------------------------------------------------------------------------


def run_lynis_audit(data: dict) -> str:
    """Run a Lynis CIS security baseline audit on the local system."""
    if not _tool_available("lynis"):
        return (
            "lynis is not installed. Install it with: sudo dnf install lynis  "
            "(or sudo apt install lynis)"
        )

    profile = data.get("profile", "default")  # future: custom profiles
    logger.info("Starting Lynis audit (this may take 1-3 minutes)...")

    cmd = ["lynis", "audit", "system", "--quiet", "--no-colors", "--noplugins"]
    rc, stdout, stderr = _run(cmd, timeout=SUBPROCESS_TIMEOUT)

    if rc == -1:
        return f"Lynis timed out or failed: {stderr}"

    # Parse hardening index and suggestions
    lines = stdout.splitlines()
    hardening_index = ""
    suggestions: list[str] = []
    warnings: list[str] = []
    in_suggestions = False
    in_warnings = False

    for line in lines:
        line = line.strip()
        if "Hardening index" in line:
            hardening_index = line
        if line.startswith("* ") and in_suggestions:
            suggestions.append(line[2:])
        if line.startswith("! ") and in_warnings:
            warnings.append(line[2:])
        if "Suggestions" in line and "----" not in line:
            in_suggestions = True
        if "Warnings" in line and "----" not in line:
            in_suggestions = False
            in_warnings = True
        if "Follow-up" in line or "Lynis security" in line:
            in_suggestions = False
            in_warnings = False

    result_lines = ["Lynis Security Audit Results:"]
    if hardening_index:
        result_lines.append(f"  {hardening_index}")

    result_lines.append(f"\nWarnings ({len(warnings)}):")
    for w in warnings[:10]:
        result_lines.append(f"  ⚠  {w}")
    if len(warnings) > 10:
        result_lines.append(f"  ... and {len(warnings) - 10} more")

    result_lines.append(f"\nTop suggestions ({min(len(suggestions), 15)} of {len(suggestions)}):")
    for s in suggestions[:15]:
        result_lines.append(f"  → {s}")
    if len(suggestions) > 15:
        result_lines.append(f"  ... and {len(suggestions) - 15} more")

    if not warnings and not suggestions and not hardening_index:
        result_lines.append("\nAudit complete. Check the full log at /var/log/lynis.log")

    return "\n".join(result_lines)


def scan_network_ports(data: dict) -> str:
    """Scan a target host or subnet for open ports using nmap."""
    if not _tool_available("nmap"):
        return "nmap is not installed. Install it with: sudo dnf install nmap"

    target = data.get("target", "").strip()
    if not target:
        return "target is required (IP, hostname, or CIDR like 192.168.1.0/24)."

    scan_type = data.get("scan_type", "basic")
    options_map = {
        "basic": ["-sV", "--version-intensity", "2", "-T4"],
        "stealth": ["-sS", "-T2"],
        "full": ["-sV", "-sC", "-p-", "-T4"],
        "ping": ["-sn"],
        "udp": ["-sU", "-T4", "--top-ports", "20"],
    }
    extra_args = options_map.get(scan_type, options_map["basic"])
    cmd = ["nmap"] + extra_args + ["-oN", "-", target]

    logger.info("Running nmap: %s", " ".join(cmd))
    rc, stdout, stderr = _run(cmd, timeout=NMAP_TIMEOUT)

    if rc == -1:
        return f"nmap timed out or failed: {stderr}"

    if not stdout.strip():
        return f"nmap returned no output for target: {target}"

    # Trim to key lines only (skip verbose nmap header noise)
    lines = stdout.splitlines()
    relevant = []
    for line in lines:
        stripped = line.strip()
        if not stripped:
            continue
        if stripped.startswith("#"):
            continue
        relevant.append(stripped)

    summary = "\n".join(relevant[:80])
    if len(relevant) > 80:
        summary += f"\n... ({len(relevant) - 80} lines truncated)"

    return f"nmap scan of {target} ({scan_type}):\n\n{summary}"


def scan_container_vulnerabilities(data: dict) -> str:
    """Scan a container image for known CVEs using Trivy."""
    if not _tool_available("trivy"):
        return (
            "trivy is not installed. Install it with: "
            "sudo dnf install trivy  (or see https://trivy.dev/)"
        )

    image = data.get("image", "").strip()
    if not image:
        return "image is required (e.g. 'nginx:latest', 'my-app:1.0')."

    severity = data.get("severity", "CRITICAL,HIGH").upper()
    cmd = [
        "trivy",
        "image",
        "--severity",
        severity,
        "--no-progress",
        "--format",
        "json",
        image,
    ]

    logger.info("Running trivy scan on: %s", image)
    rc, stdout, stderr = _run(cmd, timeout=TRIVY_TIMEOUT)

    if rc == -1:
        return f"Trivy timed out or failed: {stderr}"

    try:
        report = json.loads(stdout)
    except (json.JSONDecodeError, ValueError):
        if stdout.strip():
            return f"Trivy output (raw):\n{stdout[:2000]}"
        return f"Trivy failed with exit code {rc}: {stderr[:500]}"

    results = report.get("Results", [])
    total_vulns: dict[str, int] = {}
    vuln_lines: list[str] = []

    for target_result in results:
        tgt = target_result.get("Target", "")
        vulns = target_result.get("Vulnerabilities") or []
        for v in vulns:
            sev = v.get("Severity", "UNKNOWN")
            total_vulns[sev] = total_vulns.get(sev, 0) + 1
            if len(vuln_lines) < 30:
                pkg = v.get("PkgName", "?")
                vid = v.get("VulnerabilityID", "?")
                installed = v.get("InstalledVersion", "?")
                fixed = v.get("FixedVersion", "-")
                title = v.get("Title", "")[:60]
                vuln_lines.append(f"  {sev:<8} {vid:<20} {pkg:<25} {installed} → {fixed}  {title}")

    summary_parts = [f"{sev}:{cnt}" for sev, cnt in sorted(total_vulns.items())]
    total_count = sum(total_vulns.values())

    lines = [
        f"Trivy scan of {image}:",
        f"  Total vulnerabilities: {total_count}  ({', '.join(summary_parts) if summary_parts else 'none'})",
        "",
    ]
    if vuln_lines:
        lines.append(f"Top findings (severity={severity}):")
        lines.extend(vuln_lines)
        if total_count > 30:
            lines.append(f"  ... and {total_count - 30} more")
    else:
        lines.append(f"No vulnerabilities found at severity {severity}.")

    return "\n".join(lines)


def get_suricata_alerts(data: dict) -> str:
    """Read recent Suricata IDS/IPS alerts from the EVE JSON log."""
    limit = min(int(data.get("limit", 20)), 100)

    if not SURICATA_LOG.exists():
        alt_paths = [
            Path("/var/log/suricata/eve.json"),
            Path("/var/log/suricata/fast.log"),
        ]
        found = next((p for p in alt_paths if p.exists()), None)
        if not found:
            return (
                "Suricata log not found at /var/log/suricata/eve.json. "
                "Ensure Suricata is installed and running: sudo systemctl status suricata"
            )
        log_path = found
    else:
        log_path = SURICATA_LOG

    try:
        # Read last N lines efficiently (tail-style)
        with open(log_path, "rb") as f:
            f.seek(0, 2)
            file_size = f.tell()
            chunk = min(file_size, 512 * 1024)  # read last 512KB
            f.seek(-chunk, 2)
            raw = f.read().decode("utf-8", errors="ignore")

        lines = [l.strip() for l in raw.splitlines() if l.strip()]

        # Parse EVE JSON for alert events only
        alerts = []
        for line in reversed(lines):
            if '"event_type":"alert"' not in line:
                continue
            try:
                event = json.loads(line)
                if event.get("event_type") == "alert":
                    alerts.append(event)
                    if len(alerts) >= limit:
                        break
            except (json.JSONDecodeError, ValueError):
                continue

        if not alerts:
            return f"No Suricata alerts found in {log_path}."

        # Count by signature
        sig_counts: dict[str, int] = {}
        for a in alerts:
            sig = a.get("alert", {}).get("signature", "unknown")
            sig_counts[sig] = sig_counts.get(sig, 0) + 1

        lines_out = [f"Recent Suricata alerts ({len(alerts)} shown):"]
        lines_out.append("")

        for alert in alerts[:limit]:
            ts = alert.get("timestamp", "")[:19]
            src_ip = alert.get("src_ip", "?")
            dst_ip = alert.get("dest_ip", "?")
            dst_port = alert.get("dest_port", "")
            al = alert.get("alert", {})
            sig = al.get("signature", "?")[:60]
            severity = al.get("severity", "?")
            proto = alert.get("proto", "")
            port_str = f":{dst_port}" if dst_port else ""
            lines_out.append(f"  [{ts}] sev={severity}  {src_ip} → {dst_ip}{port_str} ({proto})")
            lines_out.append(f"    {sig}")

        lines_out.append("")
        lines_out.append("Top signatures:")
        for sig, cnt in sorted(sig_counts.items(), key=lambda x: x[1], reverse=True)[:5]:
            lines_out.append(f"  {cnt:>4}x  {sig[:70]}")

        return "\n".join(lines_out)

    except (IOError, OSError, PermissionError) as e:
        return (
            f"Cannot read Suricata log at {log_path}: {e}\n"
            "Try running Familiar with appropriate permissions, or check: "
            "sudo setfacl -m u:familiar:r /var/log/suricata/eve.json"
        )


# ---------------------------------------------------------------------------
# Tool definitions
# ---------------------------------------------------------------------------

TOOLS = [
    {
        "name": "run_lynis_audit",
        "description": (
            "Run a Lynis CIS security baseline audit on the local system. "
            "Reports the hardening index score, security warnings, and prioritized suggestions. "
            "Takes 1-3 minutes to complete. Use this to assess overall security posture "
            "and get a prioritized remediation list. Requires lynis to be installed."
        ),
        "input_schema": {"type": "object", "properties": {}},
        "handler": run_lynis_audit,
        "category": "sysaudit",
    },
    {
        "name": "scan_network_ports",
        "description": (
            "Scan a target IP, hostname, or CIDR subnet for open ports and services using nmap. "
            "scan_type options: 'basic' (service detection, default), 'stealth' (SYN scan), "
            "'full' (all ports + scripts), 'ping' (host discovery only), 'udp' (top UDP ports). "
            "For subnets in RFC-1918 private space, no authorization is needed. "
            "External targets require confirm_osint_target() first."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "target": {
                    "type": "string",
                    "description": "IP, hostname, or CIDR (e.g. '192.168.1.0/24', '10.0.0.1')",
                },
                "scan_type": {
                    "type": "string",
                    "enum": ["basic", "stealth", "full", "ping", "udp"],
                    "description": "Scan type (default: basic)",
                    "default": "basic",
                },
            },
            "required": ["target"],
        },
        "handler": scan_network_ports,
        "category": "sysaudit",
    },
    {
        "name": "scan_container_vulnerabilities",
        "description": (
            "Scan a container image for known CVEs using Trivy. "
            "Reports vulnerability counts by severity (CRITICAL, HIGH, MEDIUM, LOW) "
            "with package names, CVE IDs, installed/fixed versions, and titles. "
            "Use this before deploying a new container or to audit running images. "
            "Requires trivy to be installed."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "image": {
                    "type": "string",
                    "description": "Container image name and tag (e.g. 'nginx:latest', 'myapp:1.0')",
                },
                "severity": {
                    "type": "string",
                    "description": "Comma-separated severity levels to include",
                    "default": "CRITICAL,HIGH",
                },
            },
            "required": ["image"],
        },
        "handler": scan_container_vulnerabilities,
        "category": "sysaudit",
    },
    {
        "name": "get_suricata_alerts",
        "description": (
            "Read recent Suricata IDS/IPS alerts from the EVE JSON log. "
            "Shows timestamp, source/destination IPs, protocol, severity, and signature name. "
            "Also summarizes the top triggered signatures for pattern analysis. "
            "Requires Suricata to be running and the eve.json log to be readable."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "limit": {
                    "type": "integer",
                    "description": "Max alerts to return (default 20)",
                    "default": 20,
                },
            },
        },
        "handler": get_suricata_alerts,
        "category": "sysaudit",
    },
]
