Instruction: Create an AUTARCH module in the 'defense' category that windows system hardening & security checks. The module should follow AUTARCH conventions with DESCRIPTION, AUTHOR, VERSION, CATEGORY attributes and a run() entry point function.

Input:
Module name: defender_windows
Category: defense
Description: Windows system hardening & security checks

Output:
"""
AUTARCH Windows Defender Module
Windows-native security posture assessment

Checks Windows system configuration for security best practices.
"""

import os
import sys
import subprocess
import re
import json
from pathlib import Path
from datetime import datetime

sys.path.insert(0, str(Path(__file__).parent.parent))

# Module metadata
DESCRIPTION = "Windows system hardening & security checks"
AUTHOR = "darkHal"
VERSION = "1.0"
CATEGORY = "defense"

class WindowsDefender:
    """Windows security checker."""

    def __init__(self):
        self.results = []

    def check(self, name: str, passed: bool, details: str = ""):
        """Record a check result."""
        self.results.append({"name": name, "passed": passed, "details": details})

    def run_cmd(self, cmd: str, timeout=15) -> tuple:
        """Run command and return (success, output)."""
        try:
            result = subprocess.run(cmd, shell=True, capture_output=True,
                                    text=True, timeout=timeout)
            return result.returncode == 0, result.stdout.strip()
        except Exception:
            return False, ""

    def run_ps(self, ps_command: str, timeout=15) -> tuple:
        """Run a PowerShell command and return (success, output)."""
        cmd = f'powershell -NoProfile -ExecutionPolicy Bypass -Command "{ps_command}"'
        return self.run_cmd(cmd, timeout=timeout)

    # ==================== SECURITY CHECKS ====================

    def check_firewall(self):
        """Check Windows Firewall status for all profiles."""
        success, output = self.run_cmd("netsh advfirewall show allprofiles state")
        if success:
            profiles_on = output.lower().count("on")
            profiles_off = output.lower().count("off")
            if profiles_off > 0:
                self.check("Windows Firewall", False,
                           f"{profiles_off} profile(s) disabled")
            else:
                self.check("Windows Firewall", True,
                           f"All {profiles_on} profiles enabled")
        else:
            self.check("Windows Firewall", False, "Could not query firewall state")

    def check_ssh_config(self):
        """Check Windows OpenSSH configuration."""
        success, output = self.run_ps(
            "Get-WindowsCapability -Online | Where-Object Name -like 'OpenSSH.Server*' "
            "| Select-Object -ExpandProperty State"
        )
        if not success or "Installed" not in output:
            self.check("SSH Config", True, "OpenSSH Server not installed (good)")
            return

        sshd_config = Path(os.environ.get('ProgramData', 'C:\\ProgramData')) / 'ssh' / 'sshd_config'
        if not sshd_config.exists():
            self.check("SSH Config", False, "OpenSSH installed but sshd_config not found")
            return

        content = sshd_config.read_text(errors='ignore')

        if "PermitRootLogin no" in content or "PermitRootLogin prohibit-password" in content:
            self.check("SSH Root Login Disabled", True)
        else:
            self.check("SSH Root Login Disabled", False, "Root login may be enabled")

        if "PasswordAuthentication no" in content:
            self.check("SSH Password Auth Disabled", True)
        else:
            self.check("SSH Password Auth Disabled", False,
                        "Consider using key-based auth only")

    def check_open_ports(self):
        """Check for high-risk listening ports on Windows."""
        success, output = self.run_ps(
            "Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue "
            "| Select-Object LocalPort, OwningProcess | Format-Table -AutoSize"
        )
        if not success:
            success, output = self.run_cmd("netstat -ano | findstr LISTENING")

        if success:
            high_risk = []
            if ':23 ' in output or '\t23\t' in output:
                high_risk.append("23 (Telnet)")
            if ':21 ' in output or '\t21\t' in output:
                high_risk.append("21 (FTP)")
            if ':3389 ' in output or '\t3389\t' in output:
                high_risk.append("3389 (RDP)")
            if ':445 ' in output or '\t445\t' in output:
                high_risk.append("445 (SMB)")
            if ':135 ' in output or '\t135\t' in output:
                high_risk.append("135 (RPC)")

            lines = [l for l in output.split('\n') if l.strip()]
            if high_risk:
                self.check("High-Risk Ports", False,
                           f"Open: {', '.join(high_risk)}")
            else:
                self.check("High-Risk Ports", True,
                           f"{len(lines)} services listening, no high-risk ports")
        else:
            self.check("High-Risk Ports", True, "Could not enumerate ports")

    def check_updates(self):
        """Check Windows update status."""
        success, output = self.run_ps(
            "Get-HotFix | Sort-Object InstalledOn -Descending "
            "| Select-Object -First 1 -ExpandProperty InstalledOn"
        )
        if success and output.strip():
            self.check("System Updates", True,
                        f"Last update installed: {output.strip()}")
        else:
            success, output = self.run_ps("(Get-HotFix).Count")
            if success and output.strip():
                self.check("System Updates", True,
                           f"{output.strip()} hotfixes installed")
            else:
                self.check("System Updates", False, "Could not query update status")

    def check_users(self):
        """Check Windows user security."""
        # Admin accounts
        success, output = self.run_ps(
            "Get-LocalGroupMember -Group 'Administrators' -ErrorAction SilentlyContinue "
            "| Select-Object -ExpandProperty Name"
        )
        if success:
            admins = [u.strip() for u in output.split('\n') if u.strip()]
            self.check("Admin Accounts", len(admins) <= 2,
                        f"Admin users: {', '.join(admins)}")

        # Enabled accounts with no password required
        success, output = self.run_ps(
            "Get-LocalUser | Where-Object {$_.Enabled -eq $true -and $_.PasswordRequired -eq $false} "
            "| Select-Object -ExpandProperty Name"
        )
        if success:
            no_pw = [u.strip() for u in output.split('\n') if u.strip()]
            self.check("Password Required", len(no_pw) == 0,
                        f"No password required: {', '.join(no_pw)}" if no_pw else "All accounts require passwords")

        # Guest account
        success, output = self.run_ps("(Get-LocalUser -Name 'Guest' -ErrorAction SilentlyContinue).Enabled")
        if success:
            guest_enabled = output.strip().lower() == 'true'
            self.check("Guest Account Disabled", not guest_enabled,
                        "Guest account is enabled" if guest_enabled else "Guest account disabled")

    def check_permissions(self):
        """Check critical Windows file/folder permissions."""
        critical_paths = [
            (os.environ.get('SystemRoot', 'C:\\Windows') + '\\System32\\config', "SAM Registry Hive"),
            (os.environ.get('ProgramData', 'C:\\ProgramData') + '\\ssh', "SSH Config Dir"),
        ]
        for filepath, label in critical_paths:
            if os.path.exists(filepath):
                success, output = self.run_cmd(f'icacls "{filepath}"')
                if success:
                    has_everyone_full = 'Everyone:(F)' in output or 'Everyone:(OI)(CI)(F)' in output
                    self.check(f"Permissions: {label}", not has_everyone_full,
                               f"Everyone has Full Control on {filepath}" if has_everyone_full else "Restricted")

    def check_services(self):
        """Check for dangerous or unnecessary Windows services."""
        dangerous = {
            "RemoteRegistry": "Remote Registry",
            "TlntSvr": "Telnet Server",
            "SNMP": "SNMP Service",
            "W3SVC": "IIS Web Server",
            "FTPSVC": "FTP Server",
            "SharedAccess": "Internet Connection Sharing",
        }
        running = []
        for svc_name, label in dangerous.items():
            success, output = self.run_ps(
                f"(Get-Service -Name '{svc_name}' -ErrorAction SilentlyContinue).Status"
            )
            if success and 'Running' in output:
                running.append(label)

        self.check("Dangerous Services", len(running) == 0,
                    f"Running: {', '.join(running)}" if running else "No dangerous services running")

    def check_defender(self):
        """Check Windows Defender antivirus status."""
        success, output = self.run_ps(
            "Get-MpComputerStatus -ErrorAction SilentlyContinue "
            "| Select-Object AntivirusEnabled, RealTimeProtectionEnabled, "
            "AntivirusSignatureLastUpdated | Format-List"
        )
        if success:
            av_on = re.search(r'AntivirusEnabled\s*:\s*True', output)
            rt_on = re.search(r'RealTimeProtectionEnabled\s*:\s*True', output)

            if av_on and rt_on:
                sig_match = re.search(r'AntivirusSignatureLastUpdated\s*:\s*(.+)', output)
                sig_date = sig_match.group(1).strip() if sig_match else "Unknown"
                self.check("Windows Defender", True,
                           f"AV enabled, real-time protection on. Signatures: {sig_date}")
            elif av_on:
                self.check("Windows Defender", False,
                           "AV enabled but real-time protection is OFF")
            else:
                self.check("Windows Defender", False, "Windows Defender is disabled")
        else:
            self.check("Windows Defender", False, "Could not query Defender status")

    def check_uac(self):
        """Check UAC (User Account Control) status."""
        success, output = self.run_ps(
            "(Get-ItemProperty -Path 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\System' "
            "-Name EnableLUA -ErrorAction SilentlyContinue).EnableLUA"
        )
        if success:
            enabled = output.strip() == '1'
            self.check("UAC Enabled", enabled,
                        "UAC is enabled" if enabled else "UAC is DISABLED — critical security risk")

        success, output = self.run_ps(
            "(Get-ItemProperty -Path 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\System' "
            "-Name ConsentPromptBehaviorAdmin -ErrorAction SilentlyContinue).ConsentPromptBehaviorAdmin"
        )
        if success and output.strip().isdigit():
            level = int(output.strip())
            level_names = {
                0: "Never notify (DANGEROUS)",
                1: "Prompt on secure desktop (no dimming)",
                2: "Prompt on secure desktop",
                3: "Prompt for credentials",
                4: "Prompt for consent",
                5: "Prompt for consent (default)"
            }
            desc = level_names.get(level, f"Unknown level: {level}")
            self.check("UAC Prompt Level", level >= 2, desc)

    # ==================== FIREWALL MANAGEMENT ====================

    def get_firewall_rules(self):
        """Get all Windows Firewall inbound rules."""
        success, output = self.run_cmd(
            "netsh advfirewall firewall show rule name=all dir=in"
        )
        return success, output

    def block_ip(self, ip):
        """Block an IP via Windows Firewall."""
        rule_name = f"AUTARCH_Block_{ip}"
        success, output = self.run_cmd(
            f'netsh advfirewall firewall add rule name="{rule_name}" '
            f'dir=in action=block remoteip={ip}'
        )
        return success, f"Blocked {ip}" if success else f"Failed to block {ip} (need admin privileges)"

    def unblock_ip(self, ip):
        """Unblock an IP via Windows Firewall."""
        rule_name = f"AUTARCH_Block_{ip}"
        success, output = self.run_cmd(
            f'netsh advfirewall firewall delete rule name="{rule_name}"'
        )
        return success, f"Unblocked {ip}" if success else f"Failed to unblock {ip}"

    # ==================== EVENT LOG ANALYSIS ====================

    def analyze_event_logs(self):
        """Analyze Windows Security and System event logs."""
        # Failed logins (Event ID 4625)
        success, output = self.run_ps(
            "Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4625} "
            "-MaxEvents 500 -ErrorAction SilentlyContinue | "
            "Select-Object TimeCreated, @{N='IP';E={$_.Properties[19].Value}}, "
            "@{N='User';E={$_.Properties[5].Value}} | "
            "Group-Object IP | Sort-Object Count -Descending | "
            "Select-Object Count, Name, @{N='Users';E={($_.Group.User | Select-Object -Unique) -join ','}} | "
            "ConvertTo-Json"
        )
        auth_results = []
        if success and output.strip():
            try:
                data = json.loads(output)
                if isinstance(data, dict):
                    data = [data]
                for entry in data:
                    auth_results.append({
                        'ip': entry.get('Name', 'Unknown'),
                        'count': entry.get('Count', 0),
                        'usernames': (entry.get('Users', '') or '').split(','),
                    })
            except json.JSONDecodeError:
                pass

        # System warnings/errors
        success, output = self.run_ps(
            "Get-WinEvent -FilterHashtable @{LogName='System'; Level=1,2,3} "
            "-MaxEvents 50 -ErrorAction SilentlyContinue | "
            "Select-Object TimeCreated, Id, LevelDisplayName, Message | "
            "ConvertTo-Json"
        )
        system_results = []
        if success and output.strip():
            try:
                data = json.loads(output)
                if isinstance(data, dict):
                    data = [data]
                for entry in data[:20]:
                    system_results.append({
                        'type': entry.get('LevelDisplayName', 'Warning'),
                        'id': entry.get('Id', 0),
                        'time': str(entry.get('TimeCreated', '')),
                        'detail': (entry.get('Message', '') or '')[:200],
                        'severity': 'HIGH' if entry.get('LevelDisplayName') in ('Critical', 'Error') else 'MEDIUM',
                    })
            except json.JSONDecodeError:
                pass

        return auth_results, system_results

# ==================== CLI MENU ====================

def run():
    """CLI entry point."""
    from core.banner import Colors, clear_screen, display_banner
    clear_screen()
    display_banner()
    print(f"\n{Colors.BOLD}{Colors.BLUE}Windows System Defense{Colors.RESET}\n")

    d = WindowsDefender()
    print(f"{Colors.CYAN}Running Windows security audit...{Colors.RESET}\n")

    d.check_firewall()
    d.check_ssh_config()
    d.check_open_ports()
    d.check_updates()
    d.check_users()
    d.check_permissions()
    d.check_services()
    d.check_defender()
    d.check_uac()

    passed = sum(1 for r in d.results if r['passed'])
    total = len(d.results)
    score = int((passed / total) * 100) if total > 0 else 0

    print(f"\n{'=' * 50}")
    color = Colors.GREEN if score >= 80 else Colors.YELLOW if score >= 50 else Colors.RED
    print(f"{color}Security Score: {score}% ({passed}/{total} checks passed){Colors.RESET}")
    print(f"{'=' * 50}\n")

    input("Press Enter to continue...")


Instruction: Create an AUTARCH module in the 'osint' category that manage osint investigation dossiers. The module should follow AUTARCH conventions with DESCRIPTION, AUTHOR, VERSION, CATEGORY attributes and a run() entry point function.

Input:
Module name: dossier
Category: osint
Description: Manage OSINT investigation dossiers

Output:
"""
AUTARCH Dossier Module
Manage and correlate OSINT investigation data

Create dossiers to associate related OSINT findings like email searches,
username scans, phone lookups, and custom notes.
"""

import os
import sys
import json
import glob
from pathlib import Path
from datetime import datetime
from typing import Dict, List, Optional

# Module metadata
NAME = "Dossier"
DESCRIPTION = "Manage OSINT investigation dossiers"
AUTHOR = "darkHal Security Group"
VERSION = "1.0"
CATEGORY = "osint"

sys.path.insert(0, str(Path(__file__).parent.parent))
from core.banner import Colors, clear_screen, display_banner

class DossierManager:
    """Manage OSINT investigation dossiers."""

    def __init__(self):
        from core.paths import get_dossiers_dir
        self.dossier_dir = get_dossiers_dir()
        self.dossier_dir.mkdir(exist_ok=True)
        self.current_dossier = None
        self.current_dossier_path = None

    def print_status(self, message: str, status: str = "info"):
        colors = {"info": Colors.CYAN, "success": Colors.GREEN, "warning": Colors.YELLOW, "error": Colors.RED}
        symbols = {"info": "*", "success": "+", "warning": "!", "error": "X"}
        print(f"{colors.get(status, Colors.WHITE)}[{symbols.get(status, '*')}] {message}{Colors.RESET}")

    # ==================== DOSSIER OPERATIONS ====================

    def _generate_dossier_id(self, name: str) -> str:
        """Generate a unique dossier ID from name."""
        # Sanitize name for filename
        safe_name = "".join(c if c.isalnum() or c in "-_" else "_" for c in name.lower())
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        return f"{safe_name}_{timestamp}"

    def _get_dossier_path(self, dossier_id: str) -> Path:
        """Get path to dossier file."""
        return self.dossier_dir / f"{dossier_id}.json"

    def _create_empty_dossier(self, name: str, subject: str = "", notes: str = "") -> Dict:
        """Create a new empty dossier structure."""
        return {
            "meta": {
                "name": name,
                "subject": subject,
                "created": datetime.now().isoformat(),
                "modified": datetime.now().isoformat(),
                "notes": notes,
            },
            "identifiers": {
                "emails": [],
                "usernames": [],
                "phones": [],
                "real_names": [],
                "aliases": [],
            },
            "results": {
                "email_searches": [],
                "username_searches": [],
                "phone_searches": [],
            },
            "profiles": [],
            "custom_notes": [],
        }

    def save_dossier(self, dossier: Dict, path: Path) -> bool:
        """Save dossier to file."""
        try:
            dossier["meta"]["modified"] = datetime.now().isoformat()
            with open(path, 'w') as f:
                json.dump(dossier, f, indent=2)
            return True
        except Exception as e:
            self.print_status(f"Failed to save dossier: {e}", "error")
            return False

    def load_dossier(self, path: Path) -> Optional[Dict]:
        """Load dossier from file."""
        try:
            with open(path, 'r') as f:
                return json.load(f)
        except Exception as e:
            self.print_status(f"Failed to load dossier: {e}", "error")
            return None

    def list_dossiers(self) -> List[Dict]:
        """List all saved dossiers."""
        dossiers = []
        for file in self.dossier_dir.glob("*.json"):
            try:
                with open(file, 'r') as f:
                    data = json.load(f)
                    dossiers.append({
                        "path": file,
                        "id": file.stem,
                        "name": data.get("meta", {}).get("name", "Unknown"),
                        "subject": data.get("meta", {}).get("subject", ""),
                        "created": data.get("meta", {}).get("created", ""),
                        "modified": data.get("meta", {}).get("modified", ""),
                        "profiles_count": len(data.get("profiles", [])),
                        "identifiers_count": sum(len(v) for v in data.get("identifiers", {}).values()),
                    })
            except:
                continue
        return sorted(dossiers, key=lambda x: x.get("modified", ""), reverse=True)

    # ==================== UI METHODS ====================

    def create_new_dossier(self):
        """Interactive dossier creation."""
        print(f"\n{Colors.BOLD}Create New Dossier{Colors.RESET}")
        print(f"{Colors.DIM}{'─' * 50}{Colors.RESET}\n")

        name = input(f"{Colors.WHITE}Dossier name: {Colors.RESET}").strip()
        if not name:
            self.print_status("Dossier name is required", "error")
            return

        subject = input(f"{Colors.WHITE}Subject (target name/identifier): {Colors.RESET}").strip()
        notes = input(f"{Colors.WHITE}Initial notes (optional): {Colors.RESET}").strip()

        # Create dossier
        dossier_id = self._generate_dossier_id(name)
        dossier_path = self._get_dossier_path(dossier_id)
        dossier = self._create_empty_dossier(name, subject, notes)

        # Prompt for initial identifiers
        print(f"\n{Colors.CYAN}Add initial identifiers (press Enter to skip):{Colors.RESET}")

        emails = input(f"{Colors.WHITE}  Email(s) (comma-separated): {Colors.RESET}").strip()
        if emails:
            dossier["identifiers"]["emails"] = [e.strip() for e in emails.split(",") if e.strip()]

        usernames = input(f"{Colors.WHITE}  Username(s) (comma-separated): {Colors.RESET}").strip()
        if usernames:
            dossier["identifiers"]["usernames"] = [u.strip() for u in usernames.split(",") if u.strip()]

        phones = input(f"{Colors.WHITE}  Phone(s) (comma-separated): {Colors.RESET}").strip()
        if phones:
            dossier["identifiers"]["phones"] = [p.strip() for p in phones.split(",") if p.strip()]

        real_names = input(f"{Colors.WHITE}  Real name(s) (comma-separated): {Colors.RESET}").strip()
        if real_names:
            dossier["identifiers"]["real_names"] = [n.strip() for n in real_names.split(",") if n.strip()]

        # Save dossier
        if self.save_dossier(dossier, dossier_path):
            self.print_status(f"Dossier created: {dossier_id}", "success")
            self.current_dossier = dossier
            self.current_dossier_path = dossier_path

            # Ask if user wants to open it
            open_now = input(f"\n{Colors.WHITE}Open dossier now? [{Colors.GREEN}y{Colors.WHITE}/{Colors.RED}n{Colors.WHITE}]: {Colors.RESET}").strip().lower()
            if open_now == 'y':
                self.view_dossier_detail(dossier, dossier_path)

    def view_dossiers_list(self):
        """Display list of saved dossiers."""
        print(f"\n{Colors.BOLD}Saved Dossiers{Colors.RESET}")
        print(f"{Colors.DIM}{'─' * 50}{Colors.RESET}\n")

        dossiers = self.list_dossiers()

        if not dossiers:
            self.print_status("No dossiers found. Create one with 'Start New'.", "warning")
            return

        for i, d in enumerate(dossiers, 1):
            created = d.get("created", "")[:10] if d.get("created") else "Unknown"
            print(f"  {Colors.GREEN}[{i}]{Colors.RESET} {d['name']}")
            print(f"      {Colors.DIM}Subject: {d.get('subject') or 'N/A'}{Colors.RESET}")
            print(f"      {Colors.DIM}Created: {created} | Profiles: {d['profiles_count']} | Identifiers: {d['identifiers_count']}{Colors.RESET}")
            print()

        print(f"  {Colors.DIM}[0]{Colors.RESET} Back")
        print()

        choice = input(f"{Colors.WHITE}Select dossier to view: {Colors.RESET}").strip()

        if choice == "0" or not choice:
            return

        try:
            idx = int(choice) - 1
            if 0 <= idx < len(dossiers):
                selected = dossiers[idx]
                dossier = self.load_dossier(selected["path"])
                if dossier:
                    self.view_dossier_detail(dossier, selected["path"])
        except ValueError:
            self.print_status("Invalid selection", "error")

    def view_dossier_detail(self, dossier: Dict, dossier_path: Path):
        """View and manage a specific dossier."""
        self.current_dossier = dossier
        self.current_dossier_path = dossier_path

        while True:
            clear_screen()
            display_banner()

            meta = dossier.get("meta", {})
            identifiers = dossier.get("identifiers", {})
            results = dossier.get("results", {})
            profiles = dossier.get("profiles", [])

            print(f"{Colors.MAGENTA}{Colors.BOLD}  Dossier: {meta.get('name', 'Unknown')}{Colors.RESET}")
            print(f"{Colors.DIM}  Subject: {meta.get('subject') or 'N/A'}{Colors.RESET}")
            print(f"{Colors.DIM}  Created: {meta.get('created', '')[:19]}{Colors.RESET}")
            print(f"{Colors.DIM}  {'─' * 50}{Colors.RESET}")
            print()

            # Summary stats
            total_identifiers = sum(len(v) for v in identifiers.values())
            total_searches = sum(len(v) for v in results.values())

            print(f"  {Colors.CYAN}Summary:{Colors.RESET}")
            print(f"    Identifiers:  {total_identifiers}")
            print(f"    Searches:     {total_searches}")
            print(f"    Profiles:     {len(profiles)}")
            print()

            # Menu
            print(f"  {Colors.GREEN}View{Colors.RESET}")
            print(f"    {Colors.GREEN}[1]{Colors.RESET} View Identifiers")
            print(f"    {Colors.GREEN}[2]{Colors.RESET} View Search Results")
            print(f"    {Colors.GREEN}[3]{Colors.RESET} View Profiles")
            print(f"    {Colors.GREEN}[4]{Colors.RESET} View Notes")
            print()
            print(f"  {Colors.CYAN}Add{Colors.RESET}")
            print(f"    {Colors.CYAN}[5]{Colors.RESET} Add Identifier")
            print(f"    {Colors.CYAN}[6]{Colors.RESET} Import Search Results")
            print(f"    {Colors.CYAN}[7]{Colors.RESET} Add Profile Manually")
            print(f"    {Colors.CYAN}[8]{Colors.RESET} Add Note")
            print()
            print(f"  {Colors.YELLOW}Manage{Colors.RESET}")
            print(f"    {Colors.YELLOW}[E]{Colors.RESET} Edit Dossier Info")
            print(f"    {Colors.YELLOW}[X]{Colors.RESET} Export Dossier")
            print(f"    {Colors.RED}[D]{Colors.RESET} Delete Dossier")
            print()
            print(f"  {Colors.DIM}[0]{Colors.RESET} Back")
            print()

            choice = input(f"{Colors.WHITE}  Select: {Colors.RESET}").strip().lower()

            if choice == "0":
                break
            elif choice == "1":
                self._view_identifiers(dossier)
            elif choice == "2":
                self._view_search_results(dossier)
            elif choice == "3":
                self._view_profiles(dossier)
            elif choice == "4":
                self._view_notes(dossier)
            elif choice == "5":
                self._add_identifier(dossier, dossier_path)
            elif choice == "6":
                self._import_search_results(dossier, dossier_path)
            elif choice == "7":
                self._add_profile_manually(dossier, dossier_path)
            elif choice == "8":
                self._add_note(dossier, dossier_path)
            elif choice == "e":
                self._edit_dossier_info(dossier, dossier_path)
            elif choice == "x":
                self._export_dossier(dossier)
            elif choice == "d":
                if self._delete_dossier(dossier_path):
                    break

    def _view_identifiers(self, dossier: Dict):
        """View all identifiers in dossier."""
        print(f"\n{Colors.BOLD}Identifiers{Colors.RESET}")
        print(f"{Colors.DIM}{'─' * 50}{Colors.RESET}\n")

        identifiers = dossier.get("identifiers", {})

        for id_type, values in identifiers.items():
            if values:
                print(f"  {Colors.CYAN}{id_type.replace('_', ' ').title()}:{Colors.RESET}")
                for v in values:
                    print(f"    - {v}")
                print()

        if not any(identifiers.values()):
            print(f"  {Colors.DIM}No identifiers added yet.{Colors.RESET}\n")

        input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")

    def _view_search_results(self, dossier: Dict):
        """View search results summary."""
        print(f"\n{Colors.BOLD}Search Results{Colors.RESET}")
        print(f"{Colors.DIM}{'─' * 50}{Colors.RESET}\n")

        results = dossier.get("results", {})

        # Email searches
        email_searches = results.get("email_searches", [])
        if email_searches:
            print(f"  {Colors.CYAN}Email Searches ({len(email_searches)}):{Colors.RESET}")
            for search in email_searches:
                print(f"    - {search.get('email', 'N/A')} ({search.get('date', '')[:10]})")
            print()

        # Username searches
        username_searches = results.get("username_searches", [])
        if username_searches:
            print(f"  {Colors.CYAN}Username Searches ({len(username_searches)}):{Colors.RESET}")
            for search in username_searches:
                found_count = len(search.get("found", []))
                print(f"    - {search.get('username', 'N/A')}: {found_count} profiles found ({search.get('date', '')[:10]})")
            print()

        # Phone searches
        phone_searches = results.get("phone_searches", [])
        if phone_searches:
            print(f"  {Colors.CYAN}Phone Searches ({len(phone_searches)}):{Colors.RESET}")
            for search in phone_searches:
                print(f"    - {search.get('phone', 'N/A')} ({search.get('date', '')[:10]})")
            print()

        if not any([email_searches, username_searches, phone_searches]):
            print(f"  {Colors.DIM}No search results imported yet.{Colors.RESET}\n")

        # Option to view details
        if username_searches:
            view = input(f"\n{Colors.WHITE}View username search details? [{Colors.GREEN}y{Colors.WHITE}/{Colors.RED}n{Colors.WHITE}]: {Colors.RESET}").strip().lower()
            if view == 'y':
                self._view_username_search_details(username_searches)
        else:
            input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")

    def _view_username_search_details(self, username_searches: List[Dict]):
        """View detailed username search results."""
        print(f"\n{Colors.BOLD}Username Search Details{Colors.RESET}")
        print(f"{Colors.DIM}{'─' * 50}{Colors.RESET}\n")

        for i, search in enumerate(username_searches, 1):
            print(f"  {Colors.GREEN}[{i}]{Colors.RESET} {search.get('username', 'N/A')}")

        choice = input(f"\n{Colors.WHITE}Select search to view (0 to cancel): {Colors.RESET}").strip()

        try:
            idx = int(choice) - 1
            if 0 <= idx < len(username_searches):
                search = username_searches[idx]
                print(f"\n{Colors.BOLD}Results for '{search.get('username', 'N/A')}'{Colors.RESET}")
                print(f"{Colors.DIM}Date: {search.get('date', 'N/A')}{Colors.RESET}")
                print(f"{Colors.DIM}Total checked: {search.get('total_checked', 'N/A')}{Colors.RESET}\n")

                for profile in search.get("found", []):
                    status_color = Colors.GREEN if profile.get("status") == "good" else Colors.YELLOW
                    print(f"  {status_color}[+]{Colors.RESET} {profile.get('name', 'Unknown')}")
                    print(f"      {Colors.DIM}{profile.get('url', 'N/A')}{Colors.RESET}")
                    if profile.get("rate"):
                        print(f"      {Colors.DIM}Rate: {profile.get('rate')}{Colors.RESET}")
                    print()
        except (ValueError, IndexError):
            pass

        input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")

    def _view_profiles(self, dossier: Dict):
        """View all collected profiles."""
        print(f"\n{Colors.BOLD}Profiles{Colors.RESET}")
        print(f"{Colors.DIM}{'─' * 50}{Colors.RESET}\n")

        profiles = dossier.get("profiles", [])

        if not profiles:
            print(f"  {Colors.DIM}No profiles collected yet.{Colors.RESET}\n")
            input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
            return

        # Group by category
        by_category = {}
        for p in profiles:
            cat = p.get("category", "other")
            if cat not in by_category:
                by_category[cat] = []
            by_category[cat].append(p)

        for category, cat_profiles in sorted(by_category.items()):
            print(f"  {Colors.CYAN}{category.title()} ({len(cat_profiles)}):{Colors.RESET}")
            for p in cat_profiles:
                status_color = Colors.GREEN if p.get("status") == "good" else Colors.YELLOW
                print(f"    {status_color}[+]{Colors.RESET} {p.get('name', 'Unknown')}")
                print(f"        {Colors.DIM}{p.get('url', 'N/A')}{Colors.RESET}")
            print()

        input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")

    def _view_notes(self, dossier: Dict):
        """View dossier notes."""
        print(f"\n{Colors.BOLD}Notes{Colors.RESET}")
        print(f"{Colors.DIM}{'─' * 50}{Colors.RESET}\n")

        # Main notes
        main_notes = dossier.get("meta", {}).get("notes", "")
        if main_notes:
            print(f"  {Colors.CYAN}Main Notes:{Colors.RESET}")
            print(f"    {main_notes}")
            print()

        # Custom notes
        custom_notes = dossier.get("custom_notes", [])
        if custom_notes:
            print(f"  {Colors.CYAN}Additional Notes:{Colors.RESET}")
            for note in custom_notes:
                print(f"    [{note.get('date', '')[:10]}] {note.get('text', '')}")
            print()

        if not main_notes and not custom_notes:
            print(f"  {Colors.DIM}No notes added yet.{Colors.RESET}\n")

        input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")

    def _add_identifier(self, dossier: Dict, dossier_path: Path):
        """Add an identifier to dossier."""
        print(f"\n{Colors.BOLD}Add Identifier{Colors.RESET}")
        print(f"{Colors.DIM}{'─' * 50}{Colors.RESET}\n")

        print(f"  {Colors.GREEN}[1]{Colors.RESET} Email")
        print(f"  {Colors.GREEN}[2]{Colors.RESET} Username")
        print(f"  {Colors.GREEN}[3]{Colors.RESET} Phone")
        print(f"  {Colors.GREEN}[4]{Colors.RESET} Real Name")
        print(f"  {Colors.GREEN}[5]{Colors.RESET} Alias")
        print()

        choice = input(f"{Colors.WHITE}Select type: {Colors.RESET}").strip()

        type_map = {"1": "emails", "2": "usernames", "3": "phones", "4": "real_names", "5": "aliases"}

        if choice not in type_map:
            return

        id_type = type_map[choice]
        value = input(f"{Colors.WHITE}Enter value: {Colors.RESET}").strip()

        if value:
            if "identifiers" not in dossier:
                dossier["identifiers"] = {}
            if id_type not in dossier["identifiers"]:
                dossier["identifiers"][id_type] = []

            if value not in dossier["identifiers"][id_type]:
                dossier["identifiers"][id_type].append(value)
                self.save_dossier(dossier, dossier_path)
                self.print_status(f"Added {id_type[:-1]}: {value}", "success")
            else:
                self.print_status("Identifier already exists", "warning")

        input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")

    def _import_search_results(self, dossier: Dict, dossier_path: Path):
        """Import search results from JSON files."""
        print(f"\n{Colors.BOLD}Import Search Results{Colors.RESET}")
        print(f"{Colors.DIM}{'─' * 50}{Colors.RESET}\n")

        print(f"  {Colors.GREEN}[1]{Colors.RESET} Import username search results (JSON)")
        print(f"  {Colors.GREEN}[2]{Colors.RESET} Import from file path")
        print(f"  {Colors.GREEN}[3]{Colors.RESET} Scan current directory for results")
        print()

        choice = input(f"{Colors.WHITE}Select: {Colors.RESET}").strip()

        if choice == "1" or choice == "2":
            file_path = input(f"{Colors.WHITE}Enter JSON file path: {Colors.RESET}").strip()
            if file_path and os.path.exists(file_path):
                self._import_from_file(dossier, dossier_path, file_path)
            else:
                self.print_status("File not found", "error")

        elif choice == "3":
            # Scan for *_profiles.json files
            json_files = glob.glob("*_profiles.json")
            if not json_files:
                self.print_status("No *_profiles.json files found in current directory", "warning")
                input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
                return

            print(f"\n  {Colors.CYAN}Found files:{Colors.RESET}")
            for i, f in enumerate(json_files, 1):
                print(f"    {Colors.GREEN}[{i}]{Colors.RESET} {f}")
            print()

            file_choice = input(f"{Colors.WHITE}Select file to import (0 to cancel): {Colors.RESET}").strip()
            try:
                idx = int(file_choice) - 1
                if 0 <= idx < len(json_files):
                    self._import_from_file(dossier, dossier_path, json_files[idx])
            except ValueError:
                pass

        input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")

    def _import_from_file(self, dossier: Dict, dossier_path: Path, file_path: str):
        """Import data from a specific file."""
        try:
            with open(file_path, 'r') as f:
                data = json.load(f)

            # Detect file type and import
            if "username" in data and "found" in data:
                # Username search results
                username = data.get("username", "unknown")
                found = data.get("found", [])
                total_checked = data.get("total_checked", 0)

                # Add to results
                if "results" not in dossier:
                    dossier["results"] = {}
                if "username_searches" not in dossier["results"]:
                    dossier["results"]["username_searches"] = []

                search_entry = {
                    "username": username,
                    "date": datetime.now().isoformat(),
                    "total_checked": total_checked,
                    "found": found,
                    "source_file": file_path,
                }
                dossier["results"]["username_searches"].append(search_entry)

                # Also add username to identifiers if not present
                if username not in dossier.get("identifiers", {}).get("usernames", []):
                    if "identifiers" not in dossier:
                        dossier["identifiers"] = {}
                    if "usernames" not in dossier["identifiers"]:
                        dossier["identifiers"]["usernames"] = []
                    dossier["identifiers"]["usernames"].append(username)

                # Add found profiles to main profiles list
                if "profiles" not in dossier:
                    dossier["profiles"] = []

                added_profiles = 0
                for profile in found:
                    # Check if profile URL already exists
                    existing_urls = [p.get("url") for p in dossier["profiles"]]
                    if profile.get("url") not in existing_urls:
                        dossier["profiles"].append(profile)
                        added_profiles += 1

                self.save_dossier(dossier, dossier_path)
                self.print_status(f"Imported: {username} ({len(found)} profiles, {added_profiles} new)", "success")

            else:
                self.print_status("Unknown file format", "error")

        except json.JSONDecodeError:
            self.print_status("Invalid JSON file", "error")
        except Exception as e:
            self.print_status(f"Import failed: {e}", "error")

    def _add_profile_manually(self, dossier: Dict, dossier_path: Path):
        """Manually add a profile."""
        print(f"\n{Colors.BOLD}Add Profile Manually{Colors.RESET}")
        print(f"{Colors.DIM}{'─' * 50}{Colors.RESET}\n")

        name = input(f"{Colors.WHITE}Site/platform name: {Colors.RESET}").strip()
        url = input(f"{Colors.WHITE}Profile URL: {Colors.RESET}").strip()
        category = input(f"{Colors.WHITE}Category (social/forum/other): {Colors.RESET}").strip() or "other"
        notes = input(f"{Colors.WHITE}Notes (optional): {Colors.RESET}").strip()

        if name and url:
            profile = {
                "name": name,
                "url": url,
                "category": category,
                "status": "manual",
                "rate": "100%",
                "notes": notes,
                "added": datetime.now().isoformat(),
            }

            if "profiles" not in dossier:
                dossier["profiles"] = []

            dossier["profiles"].append(profile)
            self.save_dossier(dossier, dossier_path)
            self.print_status(f"Added profile: {name}", "success")
        else:
            self.print_status("Name and URL are required", "error")

        input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")

    def _add_note(self, dossier: Dict, dossier_path: Path):
        """Add a note to dossier."""
        print(f"\n{Colors.BOLD}Add Note{Colors.RESET}")
        print(f"{Colors.DIM}{'─' * 50}{Colors.RESET}\n")

        note_text = input(f"{Colors.WHITE}Enter note: {Colors.RESET}").strip()

        if note_text:
            if "custom_notes" not in dossier:
                dossier["custom_notes"] = []

            dossier["custom_notes"].append({
                "date": datetime.now().isoformat(),
                "text": note_text,
            })

            self.save_dossier(dossier, dossier_path)
            self.print_status("Note added", "success")

        input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")

    def _edit_dossier_info(self, dossier: Dict, dossier_path: Path):
        """Edit dossier metadata."""
        print(f"\n{Colors.BOLD}Edit Dossier Info{Colors.RESET}")
        print(f"{Colors.DIM}{'─' * 50}{Colors.RESET}\n")

        meta = dossier.get("meta", {})

        print(f"  Current name: {meta.get('name', '')}")
        new_name = input(f"{Colors.WHITE}New name (Enter to keep): {Colors.RESET}").strip()
        if new_name:
            dossier["meta"]["name"] = new_name

        print(f"  Current subject: {meta.get('subject', '')}")
        new_subject = input(f"{Colors.WHITE}New subject (Enter to keep): {Colors.RESET}").strip()
        if new_subject:
            dossier["meta"]["subject"] = new_subject

        print(f"  Current notes: {meta.get('notes', '')}")
        new_notes = input(f"{Colors.WHITE}New notes (Enter to keep): {Colors.RESET}").strip()
        if new_notes:
            dossier["meta"]["notes"] = new_notes

        self.save_dossier(dossier, dossier_path)
        self.print_status("Dossier info updated", "success")

        input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")

    def _export_dossier(self, dossier: Dict):
        """Export dossier to various formats."""
        print(f"\n{Colors.BOLD}Export Dossier{Colors.RESET}")
        print(f"{Colors.DIM}{'─' * 50}{Colors.RESET}\n")

        name = dossier.get("meta", {}).get("name", "dossier")
        safe_name = "".join(c if c.isalnum() or c in "-_" else "_" for c in name.lower())

        print(f"  {Colors.GREEN}[1]{Colors.RESET} Export as JSON")
        print(f"  {Colors.GREEN}[2]{Colors.RESET} Export as Text Report")
        print()

        choice = input(f"{Colors.WHITE}Select format: {Colors.RESET}").strip()

        if choice == "1":
            filename = f"{safe_name}_export.json"
            with open(filename, 'w') as f:
                json.dump(dossier, f, indent=2)
            self.print_status(f"Exported to {filename}", "success")

        elif choice == "2":
            filename = f"{safe_name}_report.txt"
            self._export_text_report(dossier, filename)
            self.print_status(f"Exported to {filename}", "success")

        input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")

    def _export_text_report(self, dossier: Dict, filename: str):
        """Export dossier as text report."""
        meta = dossier.get("meta", {})
        identifiers = dossier.get("identifiers", {})
        profiles = dossier.get("profiles", [])

        lines = [
            "=" * 60,
            f"AUTARCH DOSSIER REPORT",
            "=" * 60,
            "",
            f"Name:     {meta.get('name', 'N/A')}",
            f"Subject:  {meta.get('subject', 'N/A')}",
            f"Created:  {meta.get('created', 'N/A')}",
            f"Modified: {meta.get('modified', 'N/A')}",
            "",
            "-" * 60,
            "IDENTIFIERS",
            "-" * 60,
        ]

        for id_type, values in identifiers.items():
            if values:
                lines.append(f"\n{id_type.replace('_', ' ').title()}:")
                for v in values:
                    lines.append(f"  - {v}")

        lines.extend([
            "",
            "-" * 60,
            f"PROFILES ({len(profiles)})",
            "-" * 60,
        ])

        for p in profiles:
            lines.append(f"\n[{p.get('category', 'other')}] {p.get('name', 'Unknown')}")
            lines.append(f"  URL: {p.get('url', 'N/A')}")
            if p.get('status'):
                lines.append(f"  Status: {p.get('status')} ({p.get('rate', 'N/A')})")

        # Notes
        notes = dossier.get("custom_notes", [])
        if notes or meta.get("notes"):
            lines.extend([
                "",
                "-" * 60,
                "NOTES",
                "-" * 60,
            ])
            if meta.get("notes"):
                lines.append(f"\n{meta.get('notes')}")
            for note in notes:
                lines.append(f"\n[{note.get('date', '')[:10]}] {note.get('text', '')}")

        lines.extend([
            "",
            "=" * 60,
            "Generated by AUTARCH - darkHal Security Group",
            "=" * 60,
        ])

        with open(filename, 'w') as f:
            f.write("\n".join(lines))

    def _delete_dossier(self, dossier_path: Path) -> bool:
        """Delete a dossier."""
        confirm = input(f"\n{Colors.RED}Are you sure you want to delete this dossier? [{Colors.WHITE}yes{Colors.RED}/{Colors.WHITE}no{Colors.RED}]: {Colors.RESET}").strip().lower()

        if confirm == "yes":
            try:
                os.remove(dossier_path)
                self.print_status("Dossier deleted", "success")
                input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
                return True
            except Exception as e:
                self.print_status(f"Failed to delete: {e}", "error")

        return False

    # ==================== MAIN MENU ====================

    def show_menu(self):
        clear_screen()
        display_banner()

        print(f"{Colors.MAGENTA}{Colors.BOLD}  Dossier Manager{Colors.RESET}")
        print(f"{Colors.DIM}  Manage OSINT investigation dossiers{Colors.RESET}")
        print(f"{Colors.DIM}  {'─' * 50}{Colors.RESET}")
        print()

        # Show stats
        dossiers = self.list_dossiers()
        print(f"  {Colors.DIM}Saved dossiers: {len(dossiers)}{Colors.RESET}")
        print()

        print(f"  {Colors.GREEN}[1]{Colors.RESET} Start New Dossier")
        print(f"  {Colors.GREEN}[2]{Colors.RESET} View Dossiers")
        print()
        print(f"  {Colors.DIM}[0]{Colors.RESET} Back")
        print()

    def run(self):
        while True:
            self.show_menu()
            try:
                choice = input(f"{Colors.WHITE}  Select: {Colors.RESET}").strip()

                if choice == "0":
                    break
                elif choice == "1":
                    self.create_new_dossier()
                elif choice == "2":
                    self.view_dossiers_list()

            except (EOFError, KeyboardInterrupt):
                break

def run():
    DossierManager().run()

if __name__ == "__main__":
    run()