#!/home/jevans/audit_reports/okta_system_logs/.venv/bin/python
import os
import re
import sys
import requests
from datetime import datetime, timedelta, timezone
import csv
import json
import pytz

# ---------------- .env loading ----------------
_ENV_LINE_RE = re.compile(r'^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)\s*$')

def _strip_quotes(val: str) -> str:
    val = val.strip()
    if len(val) >= 2 and (val[0] == val[-1]) and val[0] in ("'", '"'):
        return val[1:-1]
    return val

def load_env():
    """Load KEY=VALUE pairs from .env in script dir or cwd."""
    script_dir = os.path.dirname(os.path.abspath(__file__))
    candidates = [os.path.join(script_dir, ".env"), os.path.join(os.getcwd(), ".env")]
    for path in candidates:
        if os.path.exists(path):
            with open(path, "r", encoding="utf-8") as f:
                for raw in f:
                    line = raw.strip()
                    if not line or line.startswith("#"):
                        continue
                    m = _ENV_LINE_RE.match(line)
                    if not m:
                        continue
                    key, val = m.group(1), _strip_quotes(m.group(2))
                    if key and key not in os.environ:
                        os.environ[key] = val

load_env()

# ---------------- Config ----------------
OKTA_DOMAIN = os.getenv("OKTA_DOMAIN", "gallaudet.okta.com")
API_TOKEN   = os.getenv("OKTA_API_TOKEN")
LOGS_URL    = f"https://{OKTA_DOMAIN}/api/v1/logs"
USER_URL    = f"https://{OKTA_DOMAIN}/api/v1/users"

if not API_TOKEN:
    sys.stderr.write("ERROR: Missing OKTA_API_TOKEN in .env\n")
    sys.exit(1)

HEADERS = {
    "Authorization": f"SSWS {API_TOKEN}",
    "Accept": "application/json"
}

FIELD_ORDER = [
    "published","published_EST","actor.displayName","client.ipAddress",
    "client.geographicalContext.city","client.geographicalContext.state",
    "client.geographicalContext.country","client.geographicalContext.postalCode",
    "displayMessage","target.displayName","target.detailEntry",
    "eventType","outcome.result","outcome.reason",
    "client.userAgent.rawUserAgent","client.userAgent.os","client.userAgent.browser",
    "client.device","securityContext.asOrg","securityContext.isp","securityContext.domain",
]

# ---------------- Functions ----------------
def fetch_user_by_email(email):
    params = {"filter": f'profile.email eq "{email}"'}
    response = requests.get(USER_URL, headers=HEADERS, params=params)
    response.raise_for_status()
    users = response.json()
    return users[0] if users else None

def flatten_user_info(user):
    profile = user.get("profile", {})
    status = user.get("status", "")
    created = user.get("created", "")
    last_login = user.get("lastLogin", "")
    credentials = user.get("credentials", {})
    provider = credentials.get("provider", {}).get("type", "")
    return {
        "id": user.get("id", ""),
        "status": status,
        "created": created,
        "lastLogin": last_login,
        "login": profile.get("login", ""),
        "email": profile.get("email", ""),
        "firstName": profile.get("firstName", ""),
        "lastName": profile.get("lastName", ""),
        "displayName": profile.get("displayName", ""),
        "title": profile.get("title", ""),
        "department": profile.get("department", ""),
        "provider": provider,
    }

def print_user_info(user_flat):
    print("\nUser Information")
    print("-" * 60)
    for k, v in user_flat.items():
        print(f"{k}: {v}")
    print("-" * 60 + "\n")

def fetch_user_factors(user_id):
    url = f"{USER_URL}/{user_id}/factors"
    response = requests.get(url, headers=HEADERS)
    response.raise_for_status()
    return response.json()

def print_user_factors(factors):
    print("\nUser Enrolled Factors (MFA):")
    print("-" * 60)
    filtered = [f for f in factors if f.get('factorType') in ('sms', 'push')]
    if not filtered:
        print("No SMS or Push factors enrolled.")
    else:
        for f in filtered:
            factor_type = f.get('factorType')
            provider = f.get('provider')
            status = f.get('status')
            profile = f.get('profile', {})
            print(f"Type: {factor_type}, Provider: {provider}, Status: {status}")
            if profile:
                for k, v in profile.items():
                    if factor_type == 'push' and k == 'keys':
                        continue
                    print(f"  {k}: {v}")
            print()
    print("-" * 60)

def fetch_logs(actor_name, limit=100, since=None, until=None):
    filter_str = f'actor.displayName eq "{actor_name}"'
    params = {"limit": limit, "sortOrder": "DESCENDING", "filter": filter_str}
    if since:
        params["since"] = since
    if until:
        params["until"] = until

    logs = []
    url = LOGS_URL
    while True:
        response = requests.get(url, headers=HEADERS, params=params)
        response.raise_for_status()
        data = response.json()
        logs.extend(data)
        links = response.links
        if "next" in links:
            url = links["next"]["url"]
            params = {}
        else:
            break
        if len(logs) >= limit:
            break
    return logs[:limit]

def utc_to_est(utc_ts):
    if not utc_ts:
        return ""
    try:
        if utc_ts.endswith("Z"):
            utc_ts = utc_ts[:-1] + "+00:00"
        dt_utc = datetime.fromisoformat(utc_ts)
        eastern = pytz.timezone("US/Eastern")
        dt_est = dt_utc.astimezone(eastern)
        return dt_est.strftime("%Y-%m-%d %H:%M:%S %Z")
    except Exception as e:
        return f"Error: {e}"

def flatten_log_event(event):
    actor = event.get("actor") or {}
    client = event.get("client") or {}
    user_agent = client.get("userAgent") or {}
    geo = client.get("geographicalContext") or {}
    outcome = event.get("outcome") or {}
    security = event.get("securityContext") or {}
    targets = event.get("target") or []
    published = event.get("published", "")
    published_est = utc_to_est(published)
    target_display_names = ", ".join([t.get("displayName", "") for t in targets])
    target_details = []
    for t in targets:
        if t.get("detailEntry"):
            target_details.append(json.dumps(t["detailEntry"], ensure_ascii=False))
        else:
            target_details.append("")
    target_detail_entries = " | ".join(target_details)
    return {
        "published": published,"published_EST": published_est,
        "actor.displayName": actor.get("displayName", ""),
        "client.ipAddress": client.get("ipAddress", ""),
        "client.geographicalContext.city": geo.get("city", ""),
        "client.geographicalContext.state": geo.get("state", ""),
        "client.geographicalContext.country": geo.get("country", ""),
        "client.geographicalContext.postalCode": geo.get("postalCode", ""),
        "displayMessage": event.get("displayMessage", ""),
        "target.displayName": target_display_names,
        "target.detailEntry": target_detail_entries,
        "eventType": event.get("eventType", ""),
        "outcome.result": outcome.get("result", ""),
        "outcome.reason": outcome.get("reason", ""),
        "client.userAgent.rawUserAgent": user_agent.get("rawUserAgent", ""),
        "client.userAgent.os": user_agent.get("os", ""),
        "client.userAgent.browser": user_agent.get("browser", ""),
        "client.device": client.get("device", ""),
        "securityContext.asOrg": security.get("asOrg", ""),
        "securityContext.isp": security.get("isp", ""),
        "securityContext.domain": security.get("domain", ""),
    }

def print_log_event(event):
    published = event.get("published", "N/A")
    published_est = utc_to_est(published)
    print(f"published: {published}")
    print(f"published_EST: {published_est}")
    actor = event.get("actor") or {}
    print(f"actor.displayName: {actor.get('displayName', 'N/A')}")
    client = event.get("client") or {}
    print(f"client.ipAddress: {client.get('ipAddress', 'N/A')}")
    geo = client.get("geographicalContext") or {}
    print(f"client.geographicalContext.city: {geo.get('city', 'N/A')}")
    print(f"client.geographicalContext.state: {geo.get('state', 'N/A')}")
    print(f"client.geographicalContext.country: {geo.get('country', 'N/A')}")
    print(f"client.geographicalContext.postalCode: {geo.get('postalCode', 'N/A')}")
    print(f"displayMessage: {event.get('displayMessage', 'N/A')}")
    targets = event.get("target") or []
    print(f"target.displayName: {', '.join([t.get('displayName', '') for t in targets])}")
    print(f"target.detailEntry: {' | '.join([json.dumps(t['detailEntry'], ensure_ascii=False) if t.get('detailEntry') else '' for t in targets])}")
    print(f"eventType: {event.get('eventType', 'N/A')}")
    outcome = event.get("outcome") or {}
    print(f"outcome.result: {outcome.get('result', 'N/A')}")
    print(f"outcome.reason: {outcome.get('reason', 'N/A')}")
    user_agent = client.get("userAgent") or {}
    print(f"client.userAgent.rawUserAgent: {user_agent.get('rawUserAgent', 'N/A')}")
    print(f"client.userAgent.os: {user_agent.get('os', 'N/A')}")
    print(f"client.userAgent.browser: {user_agent.get('browser', 'N/A')}")
    print(f"client.device: {client.get('device', 'N/A')}")
    security = event.get("securityContext") or {}
    print(f"securityContext.asOrg: {security.get('asOrg', 'N/A')}")
    print(f"securityContext.isp: {security.get('isp', 'N/A')}")
    print(f"securityContext.domain: {security.get('domain', 'N/A')}")
    print("-" * 60)

def print_mfa_enrollments_and_removals(logs):
    print("\nRecent MFA Enrollment and Removal Events (Last 90 Days):")
    print("-" * 60)
    found = False
    for event in logs:
        etype = event.get("eventType", "")
        if etype in ["user.mfa.factor.activate", "user.mfa.factor.deactivate"]:
            found = True
            published = event.get("published", "")
            published_est = utc_to_est(published)
            actor = event.get("actor", {}).get("displayName", "")
            display_message = event.get("displayMessage", "")
            target_names = ", ".join([t.get("displayName", "") for t in event.get("target", [])])
            print(f"{published_est} | {etype.replace('user.mfa.factor.','').capitalize()} | By: {actor} | Target: {target_names}")
            print(f"  {display_message}\n")
    if not found:
        print("No recent MFA enrollment/removal events found.")
    print("-" * 60)

def print_failed_and_suspicious_events(logs):
    print("\nFailed Logins and Suspicious Events (Last 90 Days):")
    print("-" * 60)
    suspicious_types = [
        "user.authentication.failed","user.account.lock","user.authentication.sso.failed",
        "user.mfa.verification.failed","security.policy.violation","user.session.impersonation.initiate"
    ]
    found = False
    for event in logs:
        etype = event.get("eventType", "")
        if etype in suspicious_types:
            found = True
            published = event.get("published", "")
            published_est = utc_to_est(published)
            actor = event.get("actor", {}).get("displayName", "")
            display_message = event.get("displayMessage", "")
            client = event.get("client", {}) or {}
            ip = client.get("ipAddress", "")
            geo = client.get("geographicalContext", {}) or {}
            city = geo.get("city", "")
            country = geo.get("country", "")
            user_agent = (client.get("userAgent", {}) or {}).get("rawUserAgent", "")
            print(f"{published_est} | {etype} | By: {actor} | IP: {ip} | {city}, {country}")
            print(f"  UA: {user_agent}")
            print(f"  {display_message}\n")
    if not found:
        print("No failed logins or suspicious events found.")
    print("-" * 60)

def sanitize_filename(s):
    return re.sub(r'[^A-Za-z0-9]+', '_', s.strip()) or "output"

# ---------------- Main ----------------
def main():
    username = input("Enter the user's name (e.g., jared.evans): ").strip()
    email = username if '@' in username else f"{username}@gallaudet.edu"
    print("Retrieving user info from Okta...")
    user_obj = fetch_user_by_email(email)
    if not user_obj:
        print(f"No user found for email '{email}'. Exiting.")
        sys.exit(1)

    user_flat = flatten_user_info(user_obj)
    display_name = user_obj.get("profile", {}).get("displayName", "")
    if not display_name:
        print("Could not find displayName for this user. Exiting.")
        sys.exit(1)

    # Prompt for user-selected system log window
    while True:
        try:
            limit = int(input("How many results do you want to return? (1-1000): ").strip())
            if 1 <= limit <= 1000:
                break
            print("Please enter a number between 1 and 1000.")
        except ValueError:
            print("Please enter a valid integer.")

    while True:
        try:
            days = int(input("How many days back do you want to search for general events? (1-90): ").strip())
            if 1 <= days <= 90:
                break
            print("Please enter a number between 1 and 90.")
        except ValueError:
            print("Please enter a valid integer.")

    until_time = datetime.now(timezone.utc)
    since_time = until_time - timedelta(days=days)
    since_iso = since_time.isoformat().replace("+00:00", "Z")
    until_iso = until_time.isoformat().replace("+00:00", "Z")
    print(f"Fetching logs from {since_iso} to {until_iso} for actor.displayName eq \"{display_name}\"...")
    logs = fetch_logs(actor_name=display_name, limit=limit, since=since_iso, until=until_iso)
    print(f"Retrieved {len(logs)} log events:\n")

    flat_rows = []
    for event in logs:
        print_log_event(event)
        flat_rows.append(flatten_log_event(event))

    # Always fetch 90 days for special events
    special_until_time = datetime.now(timezone.utc)
    special_since_time = special_until_time - timedelta(days=90)
    logs_90d = fetch_logs(actor_name=display_name, limit=1000,
                          since=special_since_time.isoformat().replace("+00:00", "Z"),
                          until=special_until_time.isoformat().replace("+00:00", "Z"))

    print_user_info(user_flat)
    user_id = user_obj["id"]
    factors = fetch_user_factors(user_id)
    print_user_factors(factors)
    print_mfa_enrollments_and_removals(logs_90d)
    print_failed_and_suspicious_events(logs_90d)

    if logs:
        safe_actor = sanitize_filename(display_name)
        filename = f"{safe_actor}_results.csv"
        with open(filename, "w", newline='', encoding="utf-8") as f:
            writer = csv.DictWriter(f, fieldnames=FIELD_ORDER, extrasaction="ignore")
            writer.writeheader()
            for row in flat_rows:
                writer.writerow(row)
        print(f"\nResults saved to: {filename}")

if __name__ == "__main__":
    main()
