#!/usr/bin/env python3
"""Agent for auditing Linux endpoints against CIS Benchmark hardening controls."""

import argparse
import json
import os
import re
import shlex
import subprocess
from datetime import datetime, timezone


def run_cmd(cmd, timeout=10):
    """Run a command and return stdout."""
    try:
        # Strip shell stderr redirects since we use subprocess.DEVNULL
        clean_cmd = cmd.replace("2>/dev/null", "").strip()
        # Use shell only for commands with shell operators
        needs_shell = any(op in clean_cmd for op in ("|", ";", "&&", "||"))
        return subprocess.check_output(
            clean_cmd if needs_shell else shlex.split(clean_cmd),
            shell=needs_shell, text=True, errors="replace",
            timeout=timeout, stderr=subprocess.DEVNULL
        ).strip()
    except subprocess.SubprocessError:
        return ""


def check_filesystem_config():
    """CIS Section 1 — Filesystem Configuration."""
    findings = []
    cramfs = run_cmd("modprobe -n -v cramfs 2>/dev/null")
    if "install /bin/true" not in cramfs and "install /bin/false" not in cramfs:
        findings.append({"check": "1.1.1.1", "issue": "cramfs module not disabled", "severity": "LOW"})
    tmp_mount = run_cmd("findmnt /tmp")
    if not tmp_mount:
        findings.append({"check": "1.1.2", "issue": "/tmp not a separate partition", "severity": "MEDIUM"})
    elif "nodev" not in tmp_mount or "nosuid" not in tmp_mount:
        findings.append({"check": "1.1.3", "issue": "/tmp missing nodev/nosuid options", "severity": "MEDIUM"})
    return findings


def check_services():
    """CIS Section 2 — Services."""
    findings = []
    unnecessary = ["avahi-daemon", "cups", "dhcpd", "slapd", "nfs-server",
                    "rpcbind", "named", "vsftpd", "httpd", "dovecot", "smb", "squid"]
    for svc in unnecessary:
        status = run_cmd(f"systemctl is-enabled {svc} 2>/dev/null")
        if status == "enabled":
            findings.append({"check": "2.x", "issue": f"Unnecessary service enabled: {svc}", "severity": "MEDIUM"})
    return findings


def check_network_parameters():
    """CIS Section 3 — Network Parameters."""
    findings = []
    params = {
        "net.ipv4.ip_forward": ("0", "3.1.1", "IP forwarding enabled"),
        "net.ipv4.conf.all.send_redirects": ("0", "3.1.2", "ICMP redirects enabled"),
        "net.ipv4.conf.all.accept_source_route": ("0", "3.2.1", "Source routing accepted"),
        "net.ipv4.conf.all.accept_redirects": ("0", "3.2.2", "ICMP redirects accepted"),
        "net.ipv4.conf.all.log_martians": ("1", "3.2.4", "Martian logging disabled"),
        "net.ipv4.tcp_syncookies": ("1", "3.2.8", "SYN cookies disabled"),
    }
    for param, (expected, cis_id, desc) in params.items():
        value = run_cmd(f"sysctl -n {param} 2>/dev/null")
        if value != expected:
            findings.append({"check": cis_id, "issue": desc, "current": value, "expected": expected, "severity": "MEDIUM"})
    return findings


def check_access_auth():
    """CIS Section 5 — Access, Authentication, Authorization."""
    findings = []
    sshd_config = run_cmd("cat /etc/ssh/sshd_config 2>/dev/null")
    if "PermitRootLogin yes" in sshd_config:
        findings.append({"check": "5.2.10", "issue": "SSH root login permitted", "severity": "HIGH"})
    if "PasswordAuthentication yes" in sshd_config:
        findings.append({"check": "5.2.12", "issue": "SSH password authentication enabled", "severity": "MEDIUM"})
    if "Protocol 1" in sshd_config:
        findings.append({"check": "5.2.4", "issue": "SSH Protocol 1 enabled", "severity": "HIGH"})

    passwd_maxdays = run_cmd("grep PASS_MAX_DAYS /etc/login.defs 2>/dev/null")
    if passwd_maxdays:
        match = re.search(r'PASS_MAX_DAYS\s+(\d+)', passwd_maxdays)
        if match and int(match.group(1)) > 365:
            findings.append({"check": "5.4.1.1", "issue": f"Password max age: {match.group(1)} days", "severity": "MEDIUM"})
    return findings


def check_audit_logging():
    """CIS Section 4 — Logging and Auditing."""
    findings = []
    auditd = run_cmd("systemctl is-active auditd 2>/dev/null")
    if auditd != "active":
        findings.append({"check": "4.1.1", "issue": "auditd not active", "severity": "HIGH"})
    rsyslog = run_cmd("systemctl is-active rsyslog 2>/dev/null")
    if rsyslog != "active":
        findings.append({"check": "4.2.1", "issue": "rsyslog not active", "severity": "MEDIUM"})
    return findings


def check_file_permissions():
    """CIS Section 6 — System File Permissions."""
    findings = []
    critical_files = {
        "/etc/passwd": "644",
        "/etc/shadow": "000",
        "/etc/group": "644",
        "/etc/gshadow": "000",
    }
    for fpath, expected in critical_files.items():
        if os.path.isfile(fpath):
            mode = oct(os.stat(fpath).st_mode)[-3:]
            if mode > expected:
                findings.append({"check": "6.1.x", "issue": f"{fpath}: mode {mode} > {expected}", "severity": "MEDIUM"})
    return findings


def main():
    parser = argparse.ArgumentParser(
        description="Audit Linux endpoint against CIS Benchmark"
    )
    parser.add_argument("--output", "-o", help="Output JSON report")
    parser.add_argument("--verbose", "-v", action="store_true")
    args = parser.parse_args()

    print("[*] Linux CIS Benchmark Hardening Audit Agent")
    all_findings = []
    all_findings.extend(check_filesystem_config())
    all_findings.extend(check_services())
    all_findings.extend(check_network_parameters())
    all_findings.extend(check_access_auth())
    all_findings.extend(check_audit_logging())
    all_findings.extend(check_file_permissions())

    high = sum(1 for f in all_findings if f["severity"] == "HIGH")
    report = {
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "findings": all_findings,
        "total": len(all_findings),
        "high_severity": high,
        "compliance_score": max(0, 100 - len(all_findings) * 5),
    }
    print(f"[*] Findings: {len(all_findings)} (HIGH: {high})")

    if args.output:
        with open(args.output, "w") as f:
            json.dump(report, f, indent=2)
        print(f"[*] Report saved to {args.output}")
    else:
        print(json.dumps(report, indent=2))


if __name__ == "__main__":
    main()
