#!/usr/bin/env python3
"""
Lab 3 - Device posture check using osquery.

Runs two osquery queries against the local host via osqueryi --json,
parses the output, and emits an access verdict:
  ALLOW   - disk encrypted AND OS meets minimum version floor
  DENY    - one or both conditions not met; route to remediation

Prerequisites:
  osquery >= 5.23.0 installed and osqueryi on PATH.
  macOS / Linux (Windows: swap disk_encryption for bitlocker_info).

Usage:
  python3 posture-check.py [--os-floor MAJOR]

Default OS floor is 13 (macOS Ventura) or 22 (Ubuntu 22).
"""

import json
import subprocess
import sys
import argparse
from typing import Any

OSQUERYI = "osqueryi"

QUERY_DISK = "SELECT name, encrypted, encryption_status FROM disk_encryption;"
QUERY_OS   = "SELECT name, version, major, minor, patch, platform FROM os_version;"


def run_query(query: str) -> list[dict[str, Any]]:
    """Run an osquery SQL query and return the parsed JSON result."""
    result = subprocess.run(
        [OSQUERYI, "--json", query],
        capture_output=True,
        text=True,
        timeout=10,
    )
    if result.returncode != 0:
        print(f"[ERROR] osqueryi failed: {result.stderr.strip()}", file=sys.stderr)
        sys.exit(2)
    return json.loads(result.stdout)


def check_disk_encryption(rows: list[dict]) -> tuple[bool, str]:
    """Return (encrypted, detail).  True only if ALL volumes report encrypted=1."""
    if not rows:
        return False, "no disk_encryption rows returned"
    unencrypted = [r["name"] for r in rows if str(r.get("encrypted", "0")) != "1"]
    if unencrypted:
        return False, f"unencrypted volume(s): {', '.join(unencrypted)}"
    return True, f"{len(rows)} volume(s) encrypted"


def check_os_version(rows: list[dict], floor: int) -> tuple[bool, str]:
    """Return (meets_floor, detail).  True only if major >= floor."""
    if not rows:
        return False, "no os_version rows returned"
    row = rows[0]
    major = int(row.get("major", 0))
    platform = row.get("platform", "unknown")
    version  = row.get("version", "?")
    if major >= floor:
        return True, f"{platform} {version} (major={major}) >= floor {floor}"
    return False, f"{platform} {version} (major={major}) < floor {floor}"


def main() -> None:
    parser = argparse.ArgumentParser(description="osquery posture check (Lab 3)")
    parser.add_argument("--os-floor", type=int, default=13,
                        help="Minimum OS major version (default: 13)")
    args = parser.parse_args()

    print("[*] Querying disk encryption ...")
    disk_rows = run_query(QUERY_DISK)
    disk_ok, disk_detail = check_disk_encryption(disk_rows)
    print(f"    {'OK' if disk_ok else 'FAIL'}: {disk_detail}")

    print("[*] Querying OS version ...")
    os_rows = run_query(QUERY_OS)
    os_ok, os_detail = check_os_version(os_rows, args.os_floor)
    print(f"    {'OK' if os_ok else 'FAIL'}: {os_detail}")

    print()
    if disk_ok and os_ok:
        print("VERDICT: ALLOW")
        print("  Both posture conditions met - forward the request.")
    else:
        print("VERDICT: DENY - route to remediation")
        if not disk_ok:
            print("  ACTION: enable disk encryption (FileVault / LUKS / BitLocker)")
        if not os_ok:
            print(f"  ACTION: upgrade OS to major version >= {args.os_floor}")
        sys.exit(1)


if __name__ == "__main__":
    main()
