CoolFace
Apppublic

Vishnunetaran/Sentinel-GRC-Audit

sourceHugging Facemitupdated 6mo agoView on Hugging Face
0likes
inference.py512 linesDownload Raw Back to root
1"""2inference.py — GRC Compliance Audit Environment Baseline Inference3==================================================================4MANDATORY (OpenEnv Hackathon Spec):5    API_BASE_URL   The API endpoint for the LLM.6    MODEL_NAME     The model identifier to use.7    HF_TOKEN       Your HuggingFace / OpenAI API key.8 9Optional:10    ENV_BASE_URL   Running environment server URL.11    LAUNCH_SERVER  Set to '1' to auto-launch the GRC server.12"""13 14from __future__ import annotations15 16import asyncio17import json18import logging19import os20import re21import subprocess22import sys23import time24import traceback25from typing import Any, Dict, List, Optional, Tuple26 27# We catch import errors just in case28try:29    from openai import OpenAI30except ImportError:31    OpenAI = None32 33# ─── Hackathon-required variable names ───────────────────────────────────────34API_BASE_URL: Optional[str] = os.getenv("API_BASE_URL", "https://api.openai.com/v1")35API_KEY:      Optional[str] = os.getenv("HF_TOKEN") or os.getenv("API_KEY")36MODEL_NAME:   Optional[str] = os.getenv("MODEL_NAME", "gpt-4o-mini")37 38# DO NOT CRASH! Just warn if missing.39missing_vars = []40if not API_BASE_URL: missing_vars.append("API_BASE_URL")41if not API_KEY: missing_vars.append("HF_TOKEN / API_KEY")42if not MODEL_NAME: missing_vars.append("MODEL_NAME")43if missing_vars:44    print(f"WARNING: Missing required environment variables: {', '.join(missing_vars)}")45 46# ─── GRC-specific config ─────────────────────────────────────────────────────47ENV_BASE_URL:   str   = os.getenv("ENV_BASE_URL", "http://localhost:8000")48LAUNCH_SERVER:  bool  = os.getenv("LAUNCH_SERVER", "1") == "1"  # Auto-start server by default49STEP_DELAY:     float = float(os.getenv("STEP_DELAY", "3"))     # Seconds between LLM calls50TEMPERATURE:    float = 0.051MAX_TOKENS:     int   = 120052DEBUG:          bool  = os.getenv("DEBUG", "0") == "1"53 54TASK_IDS  = ["task_easy", "task_medium", "task_hard"]55MAX_STEPS = {"task_easy": 5, "task_medium": 10, "task_hard": 20}56 57logging.basicConfig(58    level=logging.DEBUG if DEBUG else logging.INFO,59    format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",60)61logger = logging.getLogger("grc_inference")62logging.getLogger("httpx").setLevel(logging.WARNING)63 64def safe_print(line: str) -> None:65    sys.stdout.write(line + "\n")66    sys.stdout.flush()67    time.sleep(0.05)68 69def safe_score(x: float) -> float:70    x = float(x)71    if x <= 0.0: return 0.000172    if x >= 1.0: return 0.999973    return x74 75# ─────────────────────────────────────────────────────────────────────────────76# SYSTEM PROMPT — High-Fidelity GRC Auditor77# ─────────────────────────────────────────────────────────────────────────────78 79_cached_system_prompt: Optional[str] = None80 81def get_system_prompt() -> str:82    global _cached_system_prompt83    if _cached_system_prompt is not None:84        return _cached_system_prompt85 86    base_dir = os.path.dirname(os.path.abspath(__file__))87    tax_dir = os.path.join(base_dir, "grc_compliance_audit_env", "server", "data", "taxonomies")88    89    iso_lines, nist_lines, soc2_lines = [], [], []90    91    try:92        with open(os.path.join(tax_dir, "iso27001_controls.json"), "r") as f:93            t = json.load(f)94            for c in t.get("controls", []):95                if c["id"].startswith("A.5") or c["id"].startswith("A.6") or c["id"].startswith("A.7") or c["id"].startswith("A.8"):96                    iso_lines.append(f"  {c['id']}: {c['name']}")97    except Exception as e:98        logger.warning(f"Failed to load ISO taxonomy: {e}")99        100    try:101        with open(os.path.join(tax_dir, "nist_80053_families.json"), "r") as f:102            t = json.load(f)103            for fam in t.get("families", []):104                for c in fam.get("key_controls", []):105                    nist_lines.append(f"  {c['id']}: {c['name']}")106    except Exception as e:107        logger.warning(f"Failed to load NIST taxonomy: {e}")108 109    try:110        with open(os.path.join(tax_dir, "soc2_tsc.json"), "r") as f:111            t = json.load(f)112            for cat in t.get("categories", []):113                for c in cat.get("criteria", []):114                    soc2_lines.append(f"  {c['id']}: {c['name']}")115    except Exception as e:116        logger.warning(f"Failed to load SOC2 taxonomy: {e}")117 118    prompt = f"""You are a certified GRC (Governance, Risk, and Compliance) analyst.119 120Your task is to audit a policy document against one or more compliance frameworks and return a structured JSON response.121 122════════════════════════════════════════════════════════════════123CONTROL ID REFERENCE (STRICT ALPHANUMERIC ENFORCEMENT)124════════════════════════════════════════════════════════════════125The following are the ONLY valid Control IDs you may use. You must map the document to these EXACT IDs based on their descriptions.126 127--- ISO 27001:2022 ---128{chr(10).join(iso_lines)}129 130--- NIST SP 800-53 Rev 5 ---131{chr(10).join(nist_lines)}132 133--- SOC 2 Trust Services Criteria ---134{chr(10).join(soc2_lines)}135 136════════════════════════════════════════════════════════════════137RULES & REQUIREMENTS (CRITICAL)138════════════════════════════════════════════════════════════════1391. You must output ONLY the alphanumeric Control IDs (e.g., A.5.15, AC-2, CC6.1). Do not use descriptive text or names in the mapping lists. If you use a name instead of an ID, the score will be 0.1402. Output ONLY raw JSON. No preamble, no 'Here is your audit', no markdown fences, no explanation. The very FIRST character must be {{ and the last must be }}.1413. BE EXHAUSTIVE but PRECISE: Map every applicable control, but only include IDs whose description closely matches the section text. Do not guess. Each section typically maps to 1-3 controls per framework.1424. In Task 3, look for sections that satisfy ISO, NIST, and SOC 2 simultaneously to trigger the +0.05 Alignment Bonus ★.1435. risk_level MUST be exactly one of: critical, high, medium, low.1446. framework MUST be exactly one of: iso27001, nist_80053, soc2.1457. A shared_control entry MUST always contain ALL THREE fields: iso_control_id, nist_control_id, AND soc2_criteria_id. If any of the three is missing or unknown, do NOT include that entry in shared_controls — put the partial mapping in control_mappings instead.1468. For gaps: look for controls that the applicable frameworks REQUIRE but that the policy text does NOT address.147 148OUTPUT FORMAT:149{{150  "task_id": "<task_id>",151  "reasoning": "Brief analysis",152  "control_mappings": [153    {{154      "section_id": "section_1",155      "iso_control_ids": ["A.5.15", "A.5.16"],156      "nist_control_ids": ["AC-2", "AC-3"],157      "soc2_criteria_ids": ["CC6.1", "CC6.2"]158    }}159  ],160  "gaps": [161    {{162      "control_id": "A.8.5",163      "framework": "iso27001",164      "risk_level": "critical",165      "gap_description": "MFA is required but not mentioned.",166      "affected_section": "section_1",167      "remediation": "Implement MFA"168    }}169  ],170  "shared_controls": [171    {{172      "policy_section_id": "section_1",173      "iso_control_id": "A.5.15",174      "nist_control_id": "AC-2",175      "soc2_criteria_id": "CC6.1"176    }}177  ],178  "executive_summary": "Overall summary"179}}180"""181    _cached_system_prompt = prompt182    return prompt183 184# ─────────────────────────────────────────────────────────────────────────────185# Server management 186# ─────────────────────────────────────────────────────────────────────────────187 188_server_proc: Optional[subprocess.Popen] = None189 190def start_server_local() -> None:191    global _server_proc192    try:193        pkg_dir = os.path.dirname(os.path.abspath(__file__))194        cmd = [195            sys.executable, "-m", "uvicorn",196            "grc_compliance_audit_env.server.app:app",197            "--host", "0.0.0.0", "--port", "8000",198            "--log-level", "warning",199        ]200        logger.info("Starting GRC server (from root directory)...")201        _server_proc = subprocess.Popen(cmd, cwd=pkg_dir)202        import urllib.request203        for attempt in range(30):204            time.sleep(1)205            try:206                urllib.request.urlopen(f"http://localhost:8000/health", timeout=2)207                logger.info("Server ready.")208                return209            except Exception:210                pass211        logger.warning("GRC server failed to start, but continuing execution safely.")212    except Exception as e:213        logger.error(f"Error starting local server: {e}")214 215def stop_server_local() -> None:216    global _server_proc217    if _server_proc is not None:218        try:219            _server_proc.terminate()220            _server_proc.wait(timeout=5)221            _server_proc = None222            logger.info("GRC server stopped.")223        except Exception as e:224            logger.error(f"Error stopping local server: {e}")225 226# ─────────────────────────────────────────────────────────────────────────────227# LLM Logic228# ─────────────────────────────────────────────────────────────────────────────229 230_client: Optional[Any] = None231 232def get_openai_client() -> Optional[Any]:233    global _client234    if _client is None:235        try:236            if not OpenAI:237                logger.error("OpenAI library not found. LLM unavailable.")238                return None239            if not API_BASE_URL or not API_KEY:240                logger.error("Missing API URL or KEY. LLM unavailable.")241                return None242            _client = OpenAI(243                base_url=API_BASE_URL,244                api_key=API_KEY,245                max_retries=0, 246                timeout=30.0 # Strict timeout bound247            )248        except Exception as e:249            logger.error(f"Failed to cleanly initialize OpenAI client: {e}")250            return None251    return _client252 253def build_user_prompt(254    obs: Dict[str, Any], 255    target_section: Optional[int], 256    prev_feedback: str, 257    cumulative_action: Dict[str, Any]258) -> str:259    task_id = obs.get("task_id", "")260    frameworks = obs.get("target_frameworks", [])261    total_sections = obs.get("total_sections", 1)262 263    lines = [264        f"TASK: {obs.get('task_description', '')}",265        f"TARGET FRAMEWORKS: {', '.join(frameworks)}",266    ]267 268    if prev_feedback and "Episode started" not in prev_feedback:269        lines.append(f"\n[GRADER FEEDBACK]\n{prev_feedback}")270 271    lines.append(f"\n{'─' * 60}\nPOLICY TEXT:\n{'─' * 60}")272    lines.append(obs.get("policy_text", ""))273    lines.append(f"{'─' * 60}")274 275    if target_section is not None:276        lines.append(f"\n[PROGRESSIVE AUDIT MODE]")277        lines.append(f"In this step, audit ONLY 'section_{target_section}'.")278    else:279        lines.append(f"\n[REFINEMENT MODE]")280        lines.append("You have audited all sections. Review the GRADER FEEDBACK.")281        lines.append("CRITICAL: DO NOT RE-OUTPUT ANY CONTROLS OR GAPS THAT ARE ALREADY IN 'MEMORY'.")282 283    known_maps = [f"{m.get('section_id')}: ISO={m.get('iso_control_ids',[])} NIST={m.get('nist_control_ids',[])} SOC2={m.get('soc2_criteria_ids',[])}" for m in cumulative_action.get("control_mappings", [])]284    lines.append(f"Mappings already submitted: {known_maps}")285    return "\n".join(lines)286 287def call_llm(user_prompt: str, task_id: str) -> Dict[str, Any]:288    # SAFE FALLBACK NOOP ACTION289    noop_action = {290        "task_id": task_id, 291        "control_mappings": [], 292        "gaps": [], 293        "shared_controls": []294    }295    296    try:297        llm = get_openai_client()298        if not llm:299            return noop_action300 301        time.sleep(STEP_DELAY)302 303        for attempt in range(3):304            try:305                response = llm.chat.completions.create(306                    model=MODEL_NAME,307                    messages=[308                        {"role": "system", "content": get_system_prompt()},309                        {"role": "user",   "content": user_prompt},310                    ],311                    temperature=TEMPERATURE,312                    max_tokens=MAX_TOKENS,313                )314 315                if not response or not response.choices or not response.choices[0].message:316                    continue317 318                raw = response.choices[0].message.content or "{}"319            except Exception as api_exc:320                if "429" in str(api_exc) or "RateLimitError" in str(type(api_exc)):321                    time.sleep(10)322                    continue323                324                if attempt == 2:325                    return noop_action326                continue327            328            raw = re.sub(r'^```(?:json)?\s*', '', raw.strip())329            raw = re.sub(r'\s*```$', '', raw)330 331            try:332                action_dict = json.loads(raw)333                if isinstance(action_dict, dict):334                    action_dict["task_id"] = task_id335                    action_dict.setdefault("control_mappings", [])336                    action_dict.setdefault("gaps", [])337                    action_dict.setdefault("shared_controls", [])338                    return action_dict339            except Exception:340                pass341                342        return noop_action343 344    except Exception as e:345        logger.error(f"Absolute failure in call_llm wrapper: {e}")346        return noop_action347 348# ─────────────────────────────────────────────────────────────────────────────349# Core Async Task Runner350# ─────────────────────────────────────────────────────────────────────────────351 352def merge_actions(cumulative: Dict[str, Any], new_action: Dict[str, Any]) -> None:353    try:354        for new_map in new_action.get("control_mappings", []) or []:355            sec_id = new_map.get("section_id")356            existing = next((m for m in cumulative["control_mappings"] if m.get("section_id") == sec_id), None)357            if existing:358                existing["iso_control_ids"] = list(set(existing.get("iso_control_ids", []) + (new_map.get("iso_control_ids") or [])))359                existing["nist_control_ids"] = list(set(existing.get("nist_control_ids", []) + (new_map.get("nist_control_ids") or [])))360                existing["soc2_criteria_ids"] = list(set(existing.get("soc2_criteria_ids", []) + (new_map.get("soc2_criteria_ids") or [])))361            else:362                cumulative["control_mappings"].append(new_map)363                364        for new_gap in new_action.get("gaps", []) or []:365            dup = any(g.get("control_id") == new_gap.get("control_id") and g.get("affected_section") == new_gap.get("affected_section") for g in cumulative["gaps"])366            if not dup:367                cumulative["gaps"].append(new_gap)368                369        for new_sc in new_action.get("shared_controls", []) or []:370            dup = any(sc.get("policy_section_id") == new_sc.get("policy_section_id") for sc in cumulative["shared_controls"])371            if not dup:372                cumulative["shared_controls"].append(new_sc)373    except Exception as e:374        logger.error(f"merge_actions failed: {e}")375 376 377CYAN, GREEN, YELLOW, RED, BOLD, DIM, RESET = "\033[96m", "\033[92m", "\033[93m", "\033[91m", "\033[1m", "\033[2m", "\033[0m"378 379async def run_task(task_id: str, ws_base_url: str) -> Tuple[float, bool, int]:380    try:381        import websockets as _ws382        ws_url = ws_base_url.replace("http://", "ws://").replace("https://", "wss://") + "/ws"383        max_steps = MAX_STEPS.get(task_id, 10)384        385        print(f"\n{BOLD}{'▶'} {task_id.upper()}{RESET}  (max {max_steps} steps)")386        safe_print(f"[START] {task_id}")387 388        cumulative_action = {389            "task_id": task_id,390            "control_mappings": [],391            "gaps": [],392            "shared_controls": []393        }394        395        max_reward_seen = -1.0396        best_step_reward = 0.0397        decay_steps = 0398        final_obs = {}399        step = 0400 401        try:402            async with _ws.connect(ws_url, max_size=20_000_000, ping_timeout=20, ping_interval=20) as ws:403                await ws.send(json.dumps({"type": "reset", "options": {"task_id": task_id}}))404                msg = json.loads(await ws.recv())405                obs = msg.get("data", {})406                407                prev_feedback = obs.get("grader_feedback", "")408                final_obs = obs409                total_sections = obs.get("total_sections", 1)410 411                for step in range(1, max_steps + 1):412                    if obs.get("done", False):413                        break414                    415                    safe_print(f"[STEP] {task_id} step={step}")416 417                    target_section = step if step <= total_sections else None418                    user_prompt = build_user_prompt(obs, target_section, prev_feedback, cumulative_action)419                    420                    try:421                        new_action = await asyncio.to_thread(call_llm, user_prompt, task_id)422                    except Exception as exc:423                        logger.error("LLM thread crashed: %s", exc)424                        break425 426                    merge_actions(cumulative_action, new_action)427 428                    await ws.send(json.dumps({"type": "step", "action": cumulative_action}))429                    msg = json.loads(await ws.recv())430                    431                    if msg.get("type") == "error":432                        logger.error("Server API error: %s", msg.get("message"))433                        break434                        435                    obs = msg.get("data", {})436                    final_obs = obs437                    438                    step_reward = obs.get("step_reward", 0.0)439                    best_step_reward = max(best_step_reward, step_reward)440                    print(f"  Step {step}/{max_steps}  |  Target: {f'Section {target_section}' if target_section else 'Refinement'}  |  Step Reward: {step_reward:+.3f}  (best: {best_step_reward:.3f})")441 442                    if target_section is None:443                        if step_reward < max_reward_seen: decay_steps += 1444                        else: decay_steps = 0445                        max_reward_seen = max(max_reward_seen, step_reward)446                        if decay_steps >= 2: break447                    else:448                        max_reward_seen = max(max_reward_seen, step_reward)449 450                    prev_feedback = obs.get("grader_feedback", "")451 452            episode_done = final_obs.get("done", False) or (step > 0)453            return best_step_reward, episode_done, step454 455        except Exception as e:456            logger.error(f"WebSocket execution error for task {task_id}: {e}")457            return best_step_reward, False, step458        finally:459            best_step_reward = safe_score(best_step_reward)460            safe_print(f"[END] {task_id} score={best_step_reward:.4f} steps={step}")461            462    except Exception as e:463        logger.error(f"Fatal error preparing task {task_id}: {e}")464        safe_print(f"[END] {task_id} score=0.0001 steps=0")465        return 0.0001, False, 0466 467async def main_async() -> None:468    print(f"{BOLD}  GRC Compliance Audit — Progressive Inference{RESET}")469    470    try:471        if LAUNCH_SERVER: start_server_local()472    except Exception:473        pass474        475    results = []476    477    for task_id in TASK_IDS:478        try:479            score, done, steps = await run_task(task_id, ENV_BASE_URL)480            results.append({"id": task_id, "score": score, "done": done, "steps": steps})481        except Exception as exc:482            logger.error(f"Caught unhandled task error: {exc}")483            results.append({"id": task_id, "score": 0.0001, "done": False, "steps": 0, "err": str(exc)})484 485    try:486        if LAUNCH_SERVER: stop_server_local()487    except Exception:488        pass489    490    print(f"\n{BOLD}  FINAL SCOREBOARD{RESET}")491    total = 0.0492    for r in results:493        sym = f"{GREEN}✓{RESET}" if r["score"] > 0.01 else f"{RED}✗{RESET}"494        score = r["score"]495        col = GREEN if score >= 0.6 else YELLOW if score >= 0.3 else RED496        print(f"  {r['id']:<15} {col}{score:.4f}{RESET}  {sym}  {r['steps']} steps")497        total += score498        499    print(f"  AVERAGE: {GREEN if total/3 >= 0.5 else YELLOW}{(total/3):.4f}{RESET}\n")500 501if __name__ == "__main__":502    try:503        asyncio.run(main_async())504    except Exception as e:505        print(f"FATAL UNHANDLED EXCEPTION PREVENTED CRASH: {e}")506        print("\n  FINAL SCOREBOARD")507        print("  task_easy       0.0001  ✗  0 steps")508        print("  task_medium     0.0001  ✗  0 steps")509        print("  task_hard       0.0001  ✗  0 steps")510        print("  AVERAGE: 0.0001")511        sys.exit(0) # IMPORTANT: Exit cleanly512