feat: ethical AVR pipeline — semantic extractor, harness factory, chain analyzer, disclosure vault, Security Research tab
9452af2 verified | """ | |
| Rhodawk AI — Semantic Logic Extractor (Ethical Research Mode) | |
| ============================================================= | |
| STATIC ANALYSIS ONLY — no code is executed by this module. | |
| Maps the application's trust state machine across files to identify | |
| "Assumption Gaps" — points where developer intent diverges from actual | |
| code behaviour. All output is JSON for human review. | |
| Orchestrated by Nous Hermes 3 via OpenRouter. | |
| """ | |
| from __future__ import annotations | |
| import glob | |
| import json | |
| import os | |
| import re | |
| import time | |
| from pathlib import Path | |
| from typing import Optional | |
| import requests | |
| OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY", "") | |
| HERMES_MODEL = os.getenv( | |
| "RHODAWK_RESEARCH_MODEL", | |
| "nousresearch/hermes-3-llama-3.1-405b:free", | |
| ) | |
| _PRIORITY_KEYWORDS = [ | |
| "auth", "token", "session", "permission", "privilege", "trust", | |
| "validate", "sanitize", "parse", "decode", "deserialize", "marshal", | |
| "memory", "alloc", "buffer", "exec", "eval", "inject", "sign", "verify", | |
| "secret", "password", "credential", "acl", "role", "scope", "grant", | |
| ] | |
| _SKIP_DIRS = {".git", "vendor", "node_modules", "__pycache__", ".tox", "dist", "build"} | |
| def _hermes(system: str, user: str, max_tokens: int = 4096) -> str: | |
| """Call Nous Hermes 3 via OpenRouter.""" | |
| headers = { | |
| "Authorization": f"Bearer {OPENROUTER_API_KEY}", | |
| "Content-Type": "application/json", | |
| "HTTP-Referer": "https://rhodawk.ai", | |
| "X-Title": "Rhodawk Ethical Security Research", | |
| } | |
| payload = { | |
| "model": HERMES_MODEL, | |
| "messages": [ | |
| {"role": "system", "content": system}, | |
| {"role": "user", "content": user}, | |
| ], | |
| "max_tokens": max_tokens, | |
| "temperature": 0.1, | |
| } | |
| resp = requests.post( | |
| "https://openrouter.ai/api/v1/chat/completions", | |
| headers=headers, json=payload, timeout=120, | |
| ) | |
| resp.raise_for_status() | |
| return resp.json()["choices"][0]["message"]["content"] | |
| def _find_relevant_files(repo_dir: str, language: str) -> list[str]: | |
| lang_patterns: dict[str, list[str]] = { | |
| "python": ["**/*.py"], | |
| "javascript": ["**/*.js", "**/*.mjs"], | |
| "typescript": ["**/*.ts"], | |
| "go": ["**/*.go"], | |
| "java": ["**/*.java"], | |
| "rust": ["**/*.rs"], | |
| "c": ["**/*.c", "**/*.h"], | |
| "cpp": ["**/*.cpp", "**/*.hpp", "**/*.h"], | |
| "ruby": ["**/*.rb"], | |
| } | |
| patterns = lang_patterns.get(language.lower(), ["**/*.py"]) | |
| all_files: list[str] = [] | |
| for pat in patterns: | |
| for path in glob.glob(os.path.join(repo_dir, pat), recursive=True): | |
| rel = os.path.relpath(path, repo_dir) | |
| if any(s in rel for s in _SKIP_DIRS): | |
| continue | |
| all_files.append(rel) | |
| priority = [f for f in all_files if any(kw in f.lower() for kw in _PRIORITY_KEYWORDS)] | |
| rest = [f for f in all_files if f not in priority] | |
| return (priority + rest)[:30] | |
| def _read_file_head(repo_dir: str, rel_path: str, max_lines: int = 200) -> str: | |
| try: | |
| text = Path(os.path.join(repo_dir, rel_path)).read_text( | |
| encoding="utf-8", errors="replace" | |
| ) | |
| return "\n".join(text.splitlines()[:max_lines]) | |
| except Exception: | |
| return "" | |
| def _extract_json(text: str) -> dict: | |
| match = re.search(r"\{[\s\S]*\}", text) | |
| if match: | |
| try: | |
| return json.loads(match.group()) | |
| except json.JSONDecodeError: | |
| pass | |
| return {} | |
| def extract_trust_boundaries(repo_dir: str, file_paths: list[str]) -> dict: | |
| """ | |
| Static analysis pass: asks Hermes to map trust states and find assumption gaps. | |
| Returns a JSON-serialisable state machine graph. | |
| """ | |
| snippets = [] | |
| for rel in file_paths[:20]: | |
| head = _read_file_head(repo_dir, rel) | |
| if head: | |
| snippets.append(f"=== {rel} ===\n{head}") | |
| combined = "\n\n".join(snippets)[:14000] | |
| system = ( | |
| "You are a senior security researcher conducting responsible vulnerability research. " | |
| "You perform STATIC analysis only — you never execute code. " | |
| "Your goal is to map trust state machines and identify assumption gaps where developer " | |
| "intent diverges from actual code behaviour. " | |
| "Output valid JSON only — no prose outside the JSON block." | |
| ) | |
| user = f"""Analyse the source code below and output a trust state machine graph. | |
| Identify: | |
| 1. Where data enters the system (UNTRUSTED) | |
| 2. Validation / sanitisation steps (TRANSITION) | |
| 3. Where data is treated as trusted (TRUSTED) | |
| 4. ASSUMPTION GAPS — points where the code assumes safety without sufficient proof | |
| Return ONLY this JSON structure: | |
| {{ | |
| "language": "detected language", | |
| "trust_states": [ | |
| {{"id": "s1", "name": "...", "type": "UNTRUSTED|TRANSITION|TRUSTED", | |
| "files": ["file.py"], "description": "..."}} | |
| ], | |
| "transitions": [ | |
| {{"from": "s1", "to": "s2", "condition": "...", "file": "file.py", "line_hint": "..."}} | |
| ], | |
| "assumption_gaps": [ | |
| {{ | |
| "id": "gap_001", | |
| "severity_hypothesis": "P1|P2|P3", | |
| "file": "relative/path.py", | |
| "line_hint": "function name or ~line number", | |
| "description": "What the developer assumed vs what can actually reach this point", | |
| "untrusted_input": "What untrusted data reaches here", | |
| "bypassed_check": "What validation is missing or insufficient", | |
| "potential_impact": "Theoretical worst-case consequence", | |
| "confidence": "HIGH|MEDIUM|LOW", | |
| "requires_human_verification": true | |
| }} | |
| ] | |
| }} | |
| SOURCE CODE: | |
| {combined}""" | |
| try: | |
| raw = _hermes(system, user) | |
| result = _extract_json(raw) | |
| if result: | |
| return result | |
| except Exception as e: | |
| return {"error": str(e), "assumption_gaps": []} | |
| return {"assumption_gaps": []} | |
| def run_semantic_extraction(repo_dir: str, language: str = "python") -> dict: | |
| """ | |
| Main entry point for the semantic analysis pipeline. | |
| Pure static analysis — no code is executed. | |
| Returns a dict containing: | |
| - trust_states | |
| - transitions | |
| - assumption_gaps (each tagged requires_human_verification=True) | |
| - analyzed_files | |
| - status: always "PENDING_HUMAN_REVIEW" | |
| """ | |
| relevant_files = _find_relevant_files(repo_dir, language) | |
| if not relevant_files: | |
| return { | |
| "error": "No source files found for the detected language.", | |
| "assumption_gaps": [], | |
| "status": "PENDING_HUMAN_REVIEW", | |
| } | |
| result = extract_trust_boundaries(repo_dir, relevant_files) | |
| result["analyzed_files"] = relevant_files | |
| result["repo_dir"] = repo_dir | |
| result["language"] = language | |
| result["extracted_at"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) | |
| result["status"] = "PENDING_HUMAN_REVIEW" | |
| for gap in result.get("assumption_gaps", []): | |
| gap["requires_human_verification"] = True | |
| return result | |