feat: ethical AVR pipeline — semantic extractor, harness factory, chain analyzer, disclosure vault, Security Research tab
Browse files- SECURITY_RESEARCH_PLAYBOOK.md +199 -0
- app.py +325 -0
- chain_analyzer.py +285 -0
- disclosure_vault.py +365 -0
- harness_factory.py +220 -0
- semantic_extractor.py +212 -0
SECURITY_RESEARCH_PLAYBOOK.md
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Rhodawk AI — Ethical Autonomous Vulnerability Research (AVR)
|
| 2 |
+
## Operator & Investor Playbook v1.0
|
| 3 |
+
|
| 4 |
+
> **"The next generation of security tooling does not find known CVEs.
|
| 5 |
+
> It finds the assumptions that developers got wrong — before attackers do."**
|
| 6 |
+
|
| 7 |
+
---
|
| 8 |
+
|
| 9 |
+
## The Problem: Why Existing Security Tools Are Obsolete Against Tier-1 Targets
|
| 10 |
+
|
| 11 |
+
The global security tooling market ($20B+ and growing) is flooded with scanners that search for patterns they already know about. They find Log4Shell *after* it has been CVE-assigned. They find SQL injection via regex. They find secret keys with keyword matching.
|
| 12 |
+
|
| 13 |
+
Against Tier-1 targets — Linux kernel, Kubernetes, V8, OpenSSL, gRPC — none of this works. The vulnerabilities that command $500K+ bug bounty payouts and change the course of software history are **logic flaws**. They are invisible to pattern-matching tools because they have never occurred before.
|
| 14 |
+
|
| 15 |
+
The root cause of every novel critical vulnerability is a single cognitive failure: **a developer made an assumption that was true in their mental model but false at the boundary of another component's execution context.**
|
| 16 |
+
|
| 17 |
+
---
|
| 18 |
+
|
| 19 |
+
## The Solution: Rhodawk AVR — Semantic Logic Reversal
|
| 20 |
+
|
| 21 |
+
Rhodawk's Ethical Autonomous Vulnerability Research (AVR) module reconstructs the developer's mental model from their code, then systematically proves where that model breaks.
|
| 22 |
+
|
| 23 |
+
This is not fuzzing. This is not SAST. This is a reasoning engine.
|
| 24 |
+
|
| 25 |
+
---
|
| 26 |
+
|
| 27 |
+
## Architecture: The Five-Stage Ethical Pipeline
|
| 28 |
+
|
| 29 |
+
Every stage is designed around a single principle: **the human operator is the final authority at every decision point**. The AI accelerates discovery. The human ensures responsibility.
|
| 30 |
+
|
| 31 |
+
---
|
| 32 |
+
|
| 33 |
+
### Stage 1 — Semantic Reversal Engine
|
| 34 |
+
|
| 35 |
+
**What it does:**
|
| 36 |
+
Ingests the target repository alongside any available RFCs, API documentation, or architecture markdown. Instructs Nous Hermes 3 (405B via OpenRouter) to construct a JSON graph of the application's **trust state machine** — the precise path that data travels from "completely untrusted external input" to "fully trusted internal state".
|
| 37 |
+
|
| 38 |
+
**What it finds:**
|
| 39 |
+
The "Assumption Gap" — the exact line of code where a developer assumed a variable was safe, but the state machine graph proves an edge case can deliver an untrusted value to that point.
|
| 40 |
+
|
| 41 |
+
**Ethical constraint:**
|
| 42 |
+
This stage is **pure static analysis**. No code is executed. The repository is cloned locally and read. No network calls are made from within the analysis. Every finding is tagged `requires_human_verification: true`.
|
| 43 |
+
|
| 44 |
+
**Output:**
|
| 45 |
+
A structured JSON state machine graph with scored assumption gaps, ready for operator review.
|
| 46 |
+
|
| 47 |
+
---
|
| 48 |
+
|
| 49 |
+
### Stage 2 — Dynamic Harness Compiler
|
| 50 |
+
|
| 51 |
+
**What it does:**
|
| 52 |
+
For each operator-reviewed assumption gap, Hermes generates a **minimal Python proof-of-concept harness** that exercises the specific code path identified in Stage 1.
|
| 53 |
+
|
| 54 |
+
The harness speaks the application's own protocol (correct JSON structure, valid OAuth flows, proper gRPC framing) to reach the deep logic — then introduces precisely the edge-case input that the assumption gap predicts will bypass the validation.
|
| 55 |
+
|
| 56 |
+
**What it is not:**
|
| 57 |
+
This is not a weaponised exploit. The harness is PoC-grade: it demonstrates whether the gap is triggerable in a controlled local environment. It cannot be repurposed for remote attacks without substantial additional development by a human actor.
|
| 58 |
+
|
| 59 |
+
**Ethical constraint:**
|
| 60 |
+
The harness is displayed to the operator in full before any execution occurs. The operator must read the code and click **"I have reviewed this code"** to proceed. The harness runs in an isolated local sandbox with:
|
| 61 |
+
- All secrets and API keys stripped from the environment
|
| 62 |
+
- No outbound network connections permitted
|
| 63 |
+
- 30-second hard timeout
|
| 64 |
+
- Execution against a locally cloned copy of the codebase only
|
| 65 |
+
|
| 66 |
+
**Output:**
|
| 67 |
+
`TRIGGERED: True / False` — a boolean result that either validates or falsifies the assumption gap hypothesis.
|
| 68 |
+
|
| 69 |
+
---
|
| 70 |
+
|
| 71 |
+
### Stage 3 — Vulnerability Chain Synthesiser
|
| 72 |
+
|
| 73 |
+
**What it does:**
|
| 74 |
+
Advanced vulnerabilities are rarely a single bug. A P5 memory leak combined with a P4 timing discrepancy might chain into a P1 privilege escalation that neither primitive reveals alone.
|
| 75 |
+
|
| 76 |
+
Hermes maintains a local SQLite database (`chain_memory.sqlite`) of all primitive findings. When sufficient primitives are stored for a target, it reasons about logical chains — the sequence of steps an attacker would need to take to elevate a collection of low-severity primitives into a critical exploit.
|
| 77 |
+
|
| 78 |
+
**Ethical constraint:**
|
| 79 |
+
All chain proposals are **theoretical documents** tagged `PENDING_HUMAN_REVIEW`. No chain is executed automatically. The operator reads each proposed chain, assesses its plausibility, and approves or rejects it before any further action is taken.
|
| 80 |
+
|
| 81 |
+
**Output:**
|
| 82 |
+
A structured chain proposal document with severity rating, required conditions, and human verification checklist.
|
| 83 |
+
|
| 84 |
+
---
|
| 85 |
+
|
| 86 |
+
### Stage 4 — Isolated Execution Chamber
|
| 87 |
+
|
| 88 |
+
**What it does:**
|
| 89 |
+
Executes operator-approved harnesses against locally cloned repository code. Returns a concrete boolean result: did the harness trigger the hypothesised behaviour?
|
| 90 |
+
|
| 91 |
+
The chamber enforces:
|
| 92 |
+
- **Offline execution** — the target codebase runs without network access
|
| 93 |
+
- **Secret isolation** — all credentials removed from the subprocess environment
|
| 94 |
+
- **Time boxing** — hard 30-second limit per harness
|
| 95 |
+
- **No persistence** — harness temp files deleted after execution
|
| 96 |
+
|
| 97 |
+
**What it does not do:**
|
| 98 |
+
It does not execute against live production systems. It does not attempt to achieve real privilege escalation in a production environment. It does not exfiltrate data. The "detonation" is entirely simulated against local code.
|
| 99 |
+
|
| 100 |
+
**Ethical constraint:**
|
| 101 |
+
This stage is only reachable after the operator has passed through Stage 2's explicit human approval gate.
|
| 102 |
+
|
| 103 |
+
**Output:**
|
| 104 |
+
Execution result stored as a primitive finding and surfaced in the Rhodawk dashboard.
|
| 105 |
+
|
| 106 |
+
---
|
| 107 |
+
|
| 108 |
+
### Stage 5 — Air-Gapped Disclosure Vault
|
| 109 |
+
|
| 110 |
+
**What it does:**
|
| 111 |
+
Compiles all findings — semantic graph, assumption gap description, harness PoC, chain analysis, execution result — into a structured responsible disclosure dossier.
|
| 112 |
+
|
| 113 |
+
The dossier is stored locally in encrypted-at-rest format. Nothing leaves the system until the operator takes explicit action.
|
| 114 |
+
|
| 115 |
+
**The Disclosure Lifecycle:**
|
| 116 |
+
|
| 117 |
+
| Stage | Actor | Action |
|
| 118 |
+
|---|---|---|
|
| 119 |
+
| DRAFT | System | Dossier compiled, stored locally |
|
| 120 |
+
| PENDING | **Human Operator** | Reads full dossier, independently verifies |
|
| 121 |
+
| APPROVED | **Human Operator** | Clicks Approve, enters their name (audit record) |
|
| 122 |
+
| DISCLOSED | **Human Operator** | Sends prepared message via maintainer's security channel |
|
| 123 |
+
| COORDINATED | Maintainer | Acknowledges, begins remediation |
|
| 124 |
+
| PUBLIC | Both | Coordinated public disclosure after 90-day window |
|
| 125 |
+
|
| 126 |
+
**Ethical constraint:**
|
| 127 |
+
The system never sends anything to an external party. After approval, it generates a disclosure message that the operator sends manually via the maintainer's existing security policy (SECURITY.md, HackerOne, Bugcrowd, direct email). The 90-day responsible disclosure clock is tracked and surfaced in the dashboard.
|
| 128 |
+
|
| 129 |
+
**GitHub API lockout:**
|
| 130 |
+
When AVR mode is active, all outbound GitHub API write access is severed. The system is a research instrument, not an autonomous actor.
|
| 131 |
+
|
| 132 |
+
---
|
| 133 |
+
|
| 134 |
+
## The Adversarial Reviewer (Multi-Model Validation)
|
| 135 |
+
|
| 136 |
+
Before any finding advances past Stage 2, the Rhodawk Multi-Model Adversarial Reviewer evaluates the logical soundness of the assumption gap. Three models (Qwen, Gemma, Mistral) independently assess whether the proposed gap is:
|
| 137 |
+
|
| 138 |
+
- Logically coherent given the state machine graph
|
| 139 |
+
- Supported by the actual source code evidence
|
| 140 |
+
- Not a hallucination or pattern-match false positive
|
| 141 |
+
|
| 142 |
+
A 2-of-3 consensus is required to advance. This eliminates low-quality findings before they consume expensive harness generation and sandbox compute — and prevents operators from being overwhelmed with noise.
|
| 143 |
+
|
| 144 |
+
---
|
| 145 |
+
|
| 146 |
+
## Market Opportunity
|
| 147 |
+
|
| 148 |
+
| Segment | TAM | Rhodawk Position |
|
| 149 |
+
|---|---|---|
|
| 150 |
+
| Application security testing | $8.3B (2025) | AI-native SAST replacement |
|
| 151 |
+
| Bug bounty & VDP platforms | $1.2B | Autonomous discovery layer |
|
| 152 |
+
| Penetration testing services | $4.5B | Augmented researcher tooling |
|
| 153 |
+
| Threat intelligence platforms | $6.7B | Novel zero-day feed |
|
| 154 |
+
|
| 155 |
+
**The inflection point:** Every major tech company (Google, Meta, Apple, Microsoft) now pays six-figure sums for Tier-1 zero-days through internal and external bug bounty programmes. The bottleneck is not payouts — it is the scarcity of researchers capable of finding these vulnerabilities.
|
| 156 |
+
|
| 157 |
+
Rhodawk does not replace security researchers. It gives them an AI-powered Tier-1 co-researcher that works 24/7, never forgets a primitive finding, and can hold the entire state machine of a 500,000-line codebase in context simultaneously.
|
| 158 |
+
|
| 159 |
+
---
|
| 160 |
+
|
| 161 |
+
## Why Ethical Design Is the Moat
|
| 162 |
+
|
| 163 |
+
Automated exploitation tooling is a commodity. Nation-state actors have had it for decades. The defensible market position is not in building another offensive tool — it is in building the first platform that makes elite vulnerability research **auditable, reproducible, and responsible at scale**.
|
| 164 |
+
|
| 165 |
+
Every action in Rhodawk AVR is logged with a SHA-256 JSONL audit trail. Every human approval is recorded with the operator's name and timestamp. Every disclosure follows the industry-standard 90-day coordinated timeline.
|
| 166 |
+
|
| 167 |
+
This is not a constraint on capability. It is the capability. Enterprises, governments, and bug bounty programmes will pay premium rates for a platform whose output is defensible in front of a board of directors, a regulator, or a court.
|
| 168 |
+
|
| 169 |
+
---
|
| 170 |
+
|
| 171 |
+
## Technical Stack
|
| 172 |
+
|
| 173 |
+
| Component | Technology | Role |
|
| 174 |
+
|---|---|---|
|
| 175 |
+
| Orchestrator LLM | Nous Hermes 3 405B (OpenRouter) | Semantic reasoning, chain synthesis |
|
| 176 |
+
| Adversarial Reviewer | Qwen + Gemma + Mistral (2/3 consensus) | Hallucination elimination |
|
| 177 |
+
| Static Analysis | Custom `semantic_extractor.py` | Trust state machine extraction |
|
| 178 |
+
| Harness Generation | `harness_factory.py` + Hermes | PoC code synthesis |
|
| 179 |
+
| Chain Memory | SQLite (`chain_memory.sqlite`) | Longitudinal primitive storage |
|
| 180 |
+
| Disclosure Vault | SQLite + local filesystem | Dossier lifecycle management |
|
| 181 |
+
| Sandbox Runtime | subprocess + env isolation | Time-limited local execution |
|
| 182 |
+
| Audit Trail | SHA-256 JSONL chain | SOC 2 / ISO 27001 evidence |
|
| 183 |
+
| UI | Gradio (HuggingFace Spaces) | Human operator dashboard |
|
| 184 |
+
|
| 185 |
+
---
|
| 186 |
+
|
| 187 |
+
## Operational Principles (Non-Negotiable)
|
| 188 |
+
|
| 189 |
+
1. **No automated disclosure.** The human operator is the final authority.
|
| 190 |
+
2. **No live system testing.** All PoC execution is against locally cloned code.
|
| 191 |
+
3. **No credential use.** Secrets are stripped from all sandbox environments.
|
| 192 |
+
4. **90-day timeline.** All disclosures follow coordinated responsible disclosure.
|
| 193 |
+
5. **Open-source targets only.** Only repositories with established security policies.
|
| 194 |
+
6. **Full audit log.** Every action, approval, and disclosure is recorded.
|
| 195 |
+
|
| 196 |
+
---
|
| 197 |
+
|
| 198 |
+
*Rhodawk AI — Building the infrastructure for responsible AI-native security research*
|
| 199 |
+
*Contact: security-research@rhodawk.ai*
|
app.py
CHANGED
|
@@ -1109,6 +1109,190 @@ def reset_queue():
|
|
| 1109 |
return "✅ Job queue cleared."
|
| 1110 |
|
| 1111 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1112 |
# ──────────────────────────────────────────────────────────────
|
| 1113 |
# GRADIO ENTERPRISE DASHBOARD
|
| 1114 |
# ──────────────────────────────────────────────────────────────
|
|
@@ -1394,6 +1578,147 @@ GET /webhook/queue — Current job status (JSON)
|
|
| 1394 |
- Public leaderboard (`public_leaderboard.py`) — real numbers, real PRs, no fake metrics
|
| 1395 |
""")
|
| 1396 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1397 |
# ── AUTO-REFRESH ────────────────────────────────────────────
|
| 1398 |
timer = gr.Timer(3)
|
| 1399 |
timer.tick(get_live_logs, outputs=live_logs)
|
|
|
|
| 1109 |
return "✅ Job queue cleared."
|
| 1110 |
|
| 1111 |
|
| 1112 |
+
# ──────────────────────────────────────────────────────────────
|
| 1113 |
+
# ETHICAL SECURITY RESEARCH PIPELINE
|
| 1114 |
+
# Human approval gate at every stage — nothing disclosed automatically
|
| 1115 |
+
# ──────────────────────────────────────────────────────────────
|
| 1116 |
+
|
| 1117 |
+
def _research_clone(repo: str) -> str:
|
| 1118 |
+
"""Clone repo to a local research directory (read-only analysis)."""
|
| 1119 |
+
repo_dir = f"/tmp/research_{repo.replace('/', '_')}"
|
| 1120 |
+
if not os.path.exists(repo_dir):
|
| 1121 |
+
ui_log(f"Cloning {repo} for static analysis...", "INFO")
|
| 1122 |
+
Repo.clone_from(f"https://github.com/{repo}.git", repo_dir)
|
| 1123 |
+
return repo_dir
|
| 1124 |
+
|
| 1125 |
+
|
| 1126 |
+
def run_semantic_analysis(repo_input: str) -> tuple[str, str]:
|
| 1127 |
+
"""Pure static analysis — no code executed."""
|
| 1128 |
+
repo = (repo_input or "").strip()
|
| 1129 |
+
if not repo or "/" not in repo or len(repo.split("/")) != 2:
|
| 1130 |
+
return "❌ Use format: owner/repo", ""
|
| 1131 |
+
try:
|
| 1132 |
+
from semantic_extractor import run_semantic_extraction
|
| 1133 |
+
from language_runtime import RuntimeFactory
|
| 1134 |
+
repo_dir = _research_clone(repo)
|
| 1135 |
+
runtime = RuntimeFactory.for_repo(repo_dir)
|
| 1136 |
+
result = run_semantic_extraction(repo_dir, runtime.language)
|
| 1137 |
+
gaps = result.get("assumption_gaps", [])
|
| 1138 |
+
summary = (
|
| 1139 |
+
f"Static analysis complete.\n"
|
| 1140 |
+
f"Language: {result.get('language', 'unknown')}\n"
|
| 1141 |
+
f"Files analysed: {len(result.get('analyzed_files', []))}\n"
|
| 1142 |
+
f"Assumption gaps found: {len(gaps)}\n\n"
|
| 1143 |
+
f"All findings tagged requires_human_verification=true."
|
| 1144 |
+
)
|
| 1145 |
+
ui_log(f"Semantic analysis: {repo} → {len(gaps)} gap(s)", "INFO")
|
| 1146 |
+
return summary, json.dumps(result, indent=2)[:10000]
|
| 1147 |
+
except Exception as e:
|
| 1148 |
+
return f"Analysis failed: {e}", ""
|
| 1149 |
+
|
| 1150 |
+
|
| 1151 |
+
def generate_harness_for_review(gap_json: str, repo_input: str) -> str:
|
| 1152 |
+
"""Generate PoC harness for operator review — NOT executed here."""
|
| 1153 |
+
try:
|
| 1154 |
+
from harness_factory import generate_poc_harness
|
| 1155 |
+
gap = json.loads(gap_json)
|
| 1156 |
+
repo_dir = f"/tmp/research_{repo_input.strip().replace('/', '_')}"
|
| 1157 |
+
result = generate_poc_harness(gap, repo_dir)
|
| 1158 |
+
if "error" in result:
|
| 1159 |
+
return f"Generation failed: {result['error']}"
|
| 1160 |
+
return (
|
| 1161 |
+
f"Status: {result['status']}\n"
|
| 1162 |
+
f"Gap ID: {result['gap_id']}\n\n"
|
| 1163 |
+
f"--- REVIEW THIS CODE BEFORE APPROVING EXECUTION ---\n\n"
|
| 1164 |
+
f"{result['harness_code']}"
|
| 1165 |
+
)
|
| 1166 |
+
except Exception as e:
|
| 1167 |
+
return f"Error: {e}"
|
| 1168 |
+
|
| 1169 |
+
|
| 1170 |
+
def execute_approved_harness(harness_code: str, repo_input: str, venv_path: str) -> str:
|
| 1171 |
+
"""
|
| 1172 |
+
Sandbox execution — only after operator reads and approves harness.
|
| 1173 |
+
No network access, secrets stripped, 30 s timeout.
|
| 1174 |
+
"""
|
| 1175 |
+
if not harness_code.strip():
|
| 1176 |
+
return "❌ No harness code provided."
|
| 1177 |
+
try:
|
| 1178 |
+
from harness_factory import run_harness_in_sandbox
|
| 1179 |
+
repo_dir = f"/tmp/research_{repo_input.strip().replace('/', '_')}"
|
| 1180 |
+
r = run_harness_in_sandbox(harness_code, repo_dir, venv_path.strip() or "/data/target_venv")
|
| 1181 |
+
status = "⚠️ GAP TRIGGERED in sandbox" if r.get("triggered") else "✅ Not triggered"
|
| 1182 |
+
return (
|
| 1183 |
+
f"{status}\n\n"
|
| 1184 |
+
f"Exit code : {r.get('exit_code', 'N/A')}\n"
|
| 1185 |
+
f"Timed out : {r.get('timed_out', False)}\n\n"
|
| 1186 |
+
f"Stdout:\n{r.get('stdout', '')}\n\n"
|
| 1187 |
+
f"Stderr:\n{r.get('stderr', '')}"
|
| 1188 |
+
)
|
| 1189 |
+
except Exception as e:
|
| 1190 |
+
return f"Sandbox error: {e}"
|
| 1191 |
+
|
| 1192 |
+
|
| 1193 |
+
def store_primitive_finding(
|
| 1194 |
+
repo_input: str, gap_id: str, severity: str,
|
| 1195 |
+
description: str, triggered_str: str, sandbox_output: str,
|
| 1196 |
+
) -> str:
|
| 1197 |
+
try:
|
| 1198 |
+
from chain_analyzer import store_primitive
|
| 1199 |
+
triggered = "TRIGGERED: True" in (sandbox_output or "")
|
| 1200 |
+
fid = store_primitive(
|
| 1201 |
+
repo=repo_input.strip(), gap_id=gap_id.strip(),
|
| 1202 |
+
severity=severity.strip(), description=description.strip(),
|
| 1203 |
+
triggered=triggered, confidence="MEDIUM",
|
| 1204 |
+
harness_result={"stdout": sandbox_output},
|
| 1205 |
+
)
|
| 1206 |
+
return f"✅ Primitive stored with ID: {fid}"
|
| 1207 |
+
except Exception as e:
|
| 1208 |
+
return f"Error: {e}"
|
| 1209 |
+
|
| 1210 |
+
|
| 1211 |
+
def run_chain_analysis(repo_input: str) -> str:
|
| 1212 |
+
try:
|
| 1213 |
+
from chain_analyzer import analyze_chains, get_pending_chains
|
| 1214 |
+
repo = repo_input.strip()
|
| 1215 |
+
chains = analyze_chains(repo) if repo else []
|
| 1216 |
+
pending = get_pending_chains(repo if repo else None)
|
| 1217 |
+
if not pending:
|
| 1218 |
+
return "No chains identified yet. Store primitive findings first."
|
| 1219 |
+
lines = []
|
| 1220 |
+
for c in pending:
|
| 1221 |
+
lines.append(
|
| 1222 |
+
f"[{c['id']}] {c['severity']} | Confidence: {c['confidence']}\n"
|
| 1223 |
+
f" Repo: {c['repo']}\n"
|
| 1224 |
+
f" {c['description'][:200]}\n"
|
| 1225 |
+
f" Status: {c['status']}"
|
| 1226 |
+
)
|
| 1227 |
+
return f"Proposed chains (PENDING HUMAN REVIEW):\n\n" + "\n\n".join(lines)
|
| 1228 |
+
except Exception as e:
|
| 1229 |
+
return f"Error: {e}"
|
| 1230 |
+
|
| 1231 |
+
|
| 1232 |
+
def get_vault_display() -> str:
|
| 1233 |
+
try:
|
| 1234 |
+
from disclosure_vault import get_all_disclosures
|
| 1235 |
+
items = get_all_disclosures()
|
| 1236 |
+
if not items:
|
| 1237 |
+
return "No disclosures yet."
|
| 1238 |
+
lines = []
|
| 1239 |
+
for d in items:
|
| 1240 |
+
lines.append(
|
| 1241 |
+
f"[{d['id']}] {d['severity']} — {d['repo']}\n"
|
| 1242 |
+
f" Title : {d['title'][:70]}\n"
|
| 1243 |
+
f" Status: {d['status']} | Days remaining: {d['days_remaining']}\n"
|
| 1244 |
+
f" Bug Bounty: {d.get('bug_bounty_program','N/A')}"
|
| 1245 |
+
)
|
| 1246 |
+
return "\n\n".join(lines)
|
| 1247 |
+
except Exception as e:
|
| 1248 |
+
return f"Error: {e}"
|
| 1249 |
+
|
| 1250 |
+
|
| 1251 |
+
def read_dossier_fn(disclosure_id: str) -> str:
|
| 1252 |
+
try:
|
| 1253 |
+
from disclosure_vault import read_dossier
|
| 1254 |
+
return read_dossier(disclosure_id.strip())
|
| 1255 |
+
except Exception as e:
|
| 1256 |
+
return f"Error: {e}"
|
| 1257 |
+
|
| 1258 |
+
|
| 1259 |
+
def compile_dossier_fn(
|
| 1260 |
+
repo_input: str, gap_json: str, harness_result: str, bug_bounty: str,
|
| 1261 |
+
) -> str:
|
| 1262 |
+
try:
|
| 1263 |
+
from disclosure_vault import compile_dossier
|
| 1264 |
+
gap = json.loads(gap_json) if gap_json.strip() else {}
|
| 1265 |
+
did = compile_dossier(
|
| 1266 |
+
repo=repo_input.strip(),
|
| 1267 |
+
semantic_graph={},
|
| 1268 |
+
assumption_gap=gap,
|
| 1269 |
+
harness_result={"stdout": harness_result, "triggered": "TRIGGERED: True" in harness_result},
|
| 1270 |
+
bug_bounty_program=bug_bounty.strip(),
|
| 1271 |
+
)
|
| 1272 |
+
return f"✅ Dossier compiled. Disclosure ID: {did}\nStatus: DRAFT — awaiting human approval."
|
| 1273 |
+
except Exception as e:
|
| 1274 |
+
return f"Error: {e}"
|
| 1275 |
+
|
| 1276 |
+
|
| 1277 |
+
def approve_and_prepare_msg(disclosure_id: str, approver: str) -> str:
|
| 1278 |
+
try:
|
| 1279 |
+
from disclosure_vault import approve_disclosure, prepare_disclosure_message
|
| 1280 |
+
approve_disclosure(disclosure_id.strip(), approver.strip() or "operator")
|
| 1281 |
+
msg = prepare_disclosure_message(disclosure_id.strip())
|
| 1282 |
+
return f"✅ Approved by: {approver}\n\n--- DISCLOSURE MESSAGE (send manually) ---\n\n{msg}"
|
| 1283 |
+
except Exception as e:
|
| 1284 |
+
return f"Error: {e}"
|
| 1285 |
+
|
| 1286 |
+
|
| 1287 |
+
def reject_disclosure_fn(disclosure_id: str) -> str:
|
| 1288 |
+
try:
|
| 1289 |
+
from disclosure_vault import reject_disclosure
|
| 1290 |
+
reject_disclosure(disclosure_id.strip())
|
| 1291 |
+
return f"✅ Disclosure {disclosure_id.strip()} rejected and archived."
|
| 1292 |
+
except Exception as e:
|
| 1293 |
+
return f"Error: {e}"
|
| 1294 |
+
|
| 1295 |
+
|
| 1296 |
# ──────────────────────────────────────────────────────────────
|
| 1297 |
# GRADIO ENTERPRISE DASHBOARD
|
| 1298 |
# ──────────────────────────────────────────────────────────────
|
|
|
|
| 1578 |
- Public leaderboard (`public_leaderboard.py`) — real numbers, real PRs, no fake metrics
|
| 1579 |
""")
|
| 1580 |
|
| 1581 |
+
# ── TAB 11: ETHICAL SECURITY RESEARCH ────────────────────
|
| 1582 |
+
with gr.Tab("🔬 Security Research"):
|
| 1583 |
+
gr.Markdown("""
|
| 1584 |
+
### Ethical Security Research Pipeline
|
| 1585 |
+
|
| 1586 |
+
Static analysis → Human review → Responsible disclosure
|
| 1587 |
+
|
| 1588 |
+
**Every stage requires explicit operator approval. Nothing is disclosed automatically.**
|
| 1589 |
+
All PoC testing is local and sandboxed. No live systems are attacked.
|
| 1590 |
+
""")
|
| 1591 |
+
|
| 1592 |
+
with gr.Tabs():
|
| 1593 |
+
|
| 1594 |
+
# Step 1 — Semantic Analysis
|
| 1595 |
+
with gr.Tab("1. Semantic Analysis"):
|
| 1596 |
+
gr.Markdown(
|
| 1597 |
+
"**Static analysis only — no code executed.** "
|
| 1598 |
+
"Hermes maps the repo's trust state machine and identifies assumption gaps."
|
| 1599 |
+
)
|
| 1600 |
+
sr_repo = gr.Textbox(
|
| 1601 |
+
label="Open-source repository (owner/repo)",
|
| 1602 |
+
placeholder="e.g. psf/requests or pallets/flask",
|
| 1603 |
+
)
|
| 1604 |
+
sr_analyze_btn = gr.Button("🔍 Run Semantic Analysis", variant="primary")
|
| 1605 |
+
sr_summary = gr.Textbox(label="Summary", interactive=False, lines=6)
|
| 1606 |
+
sr_graph = gr.TextArea(label="State Machine Graph + Assumption Gaps (JSON)", lines=22, interactive=False)
|
| 1607 |
+
sr_analyze_btn.click(
|
| 1608 |
+
run_semantic_analysis,
|
| 1609 |
+
inputs=sr_repo,
|
| 1610 |
+
outputs=[sr_summary, sr_graph],
|
| 1611 |
+
)
|
| 1612 |
+
|
| 1613 |
+
# Step 2 — Harness Generation
|
| 1614 |
+
with gr.Tab("2. Generate PoC (Review Only)"):
|
| 1615 |
+
gr.Markdown(
|
| 1616 |
+
"Paste a single assumption gap JSON from Step 1. "
|
| 1617 |
+
"Hermes generates a minimal PoC harness **for your review**. "
|
| 1618 |
+
"The harness is NOT executed here."
|
| 1619 |
+
)
|
| 1620 |
+
sr_gap_input = gr.TextArea(label="Assumption Gap JSON", lines=10)
|
| 1621 |
+
sr_repo2 = gr.Textbox(label="Repository (owner/repo)")
|
| 1622 |
+
sr_gen_btn = gr.Button("⚙️ Generate Harness for Review", variant="secondary")
|
| 1623 |
+
sr_harness = gr.TextArea(
|
| 1624 |
+
label="Generated Harness — READ CAREFULLY BEFORE PROCEEDING",
|
| 1625 |
+
lines=22, interactive=True,
|
| 1626 |
+
)
|
| 1627 |
+
sr_gen_btn.click(
|
| 1628 |
+
generate_harness_for_review,
|
| 1629 |
+
inputs=[sr_gap_input, sr_repo2],
|
| 1630 |
+
outputs=sr_harness,
|
| 1631 |
+
)
|
| 1632 |
+
|
| 1633 |
+
# Step 3 — Sandbox Execution
|
| 1634 |
+
with gr.Tab("3. Sandbox Execution (Operator Approved)"):
|
| 1635 |
+
gr.Markdown("""
|
| 1636 |
+
**By clicking Execute you confirm:**
|
| 1637 |
+
- You have read every line of the harness above
|
| 1638 |
+
- You authorise local sandbox execution only
|
| 1639 |
+
- No network connections will be made
|
| 1640 |
+
- Execution is time-limited to 30 seconds
|
| 1641 |
+
""")
|
| 1642 |
+
sr_exec_code = gr.TextArea(label="Harness Code (reviewed by operator)", lines=15)
|
| 1643 |
+
with gr.Row():
|
| 1644 |
+
sr_exec_repo = gr.Textbox(label="Repository (owner/repo)", scale=3)
|
| 1645 |
+
sr_exec_venv = gr.Textbox(label="Venv path", value="/data/target_venv", scale=2)
|
| 1646 |
+
sr_exec_btn = gr.Button("🚀 Execute in Sandbox (I have reviewed this code)", variant="primary")
|
| 1647 |
+
sr_exec_out = gr.TextArea(label="Sandbox Result", lines=12, interactive=False)
|
| 1648 |
+
sr_exec_btn.click(
|
| 1649 |
+
execute_approved_harness,
|
| 1650 |
+
inputs=[sr_exec_code, sr_exec_repo, sr_exec_venv],
|
| 1651 |
+
outputs=sr_exec_out,
|
| 1652 |
+
)
|
| 1653 |
+
|
| 1654 |
+
# Step 4 — Store & Chain Analysis
|
| 1655 |
+
with gr.Tab("4. Chain Analysis"):
|
| 1656 |
+
gr.Markdown(
|
| 1657 |
+
"Store primitive findings, then ask Hermes to propose theoretical chains. "
|
| 1658 |
+
"All chain proposals are tagged PENDING_HUMAN_REVIEW."
|
| 1659 |
+
)
|
| 1660 |
+
with gr.Row():
|
| 1661 |
+
sr_prim_repo = gr.Textbox(label="Repository", scale=2)
|
| 1662 |
+
sr_prim_gapid = gr.Textbox(label="Gap ID", scale=1)
|
| 1663 |
+
sr_prim_sev = gr.Textbox(label="Severity", value="P2", scale=1)
|
| 1664 |
+
sr_prim_desc = gr.Textbox(label="Description", lines=2)
|
| 1665 |
+
sr_prim_sandbox = gr.TextArea(label="Sandbox Output (from Step 3)", lines=5)
|
| 1666 |
+
sr_store_btn = gr.Button("💾 Store Primitive Finding", variant="secondary")
|
| 1667 |
+
sr_store_out = gr.Textbox(label="", interactive=False)
|
| 1668 |
+
sr_store_btn.click(
|
| 1669 |
+
store_primitive_finding,
|
| 1670 |
+
inputs=[sr_prim_repo, sr_prim_gapid, sr_prim_sev, sr_prim_desc, sr_prim_gapid, sr_prim_sandbox],
|
| 1671 |
+
outputs=sr_store_out,
|
| 1672 |
+
)
|
| 1673 |
+
gr.HTML("<hr/>")
|
| 1674 |
+
sr_chain_repo = gr.Textbox(label="Repository for chain analysis (leave blank for all)")
|
| 1675 |
+
sr_chain_btn = gr.Button("🔗 Analyse Chains", variant="secondary")
|
| 1676 |
+
sr_chain_out = gr.TextArea(label="Proposed Chains (PENDING HUMAN REVIEW)", lines=14, interactive=False)
|
| 1677 |
+
sr_chain_btn.click(run_chain_analysis, inputs=sr_chain_repo, outputs=sr_chain_out)
|
| 1678 |
+
|
| 1679 |
+
# Step 5 — Disclosure Vault
|
| 1680 |
+
with gr.Tab("5. Disclosure Vault"):
|
| 1681 |
+
gr.Markdown("""
|
| 1682 |
+
**Human approval is mandatory before any disclosure is sent.**
|
| 1683 |
+
Approved disclosures generate a message you send manually via the maintainer's security policy.
|
| 1684 |
+
""")
|
| 1685 |
+
with gr.Row():
|
| 1686 |
+
sr_vault_repo = gr.Textbox(label="Repository (owner/repo)", scale=3)
|
| 1687 |
+
sr_vault_bounty = gr.Textbox(label="Bug bounty programme URL", scale=2)
|
| 1688 |
+
sr_vault_gap = gr.TextArea(label="Assumption Gap JSON", lines=6)
|
| 1689 |
+
sr_vault_poc = gr.TextArea(label="Sandbox output from Step 3", lines=4)
|
| 1690 |
+
sr_compile_btn = gr.Button("📋 Compile Disclosure Dossier", variant="secondary")
|
| 1691 |
+
sr_compile_out = gr.Textbox(label="", interactive=False)
|
| 1692 |
+
sr_compile_btn.click(
|
| 1693 |
+
compile_dossier_fn,
|
| 1694 |
+
inputs=[sr_vault_repo, sr_vault_gap, sr_vault_poc, sr_vault_bounty],
|
| 1695 |
+
outputs=sr_compile_out,
|
| 1696 |
+
)
|
| 1697 |
+
|
| 1698 |
+
gr.HTML("<hr/>")
|
| 1699 |
+
gr.Button("🔄 Refresh Vault", variant="secondary").click(
|
| 1700 |
+
get_vault_display, outputs=gr.TextArea(label="All Disclosures", lines=10, interactive=False)
|
| 1701 |
+
)
|
| 1702 |
+
|
| 1703 |
+
gr.HTML("<hr/>")
|
| 1704 |
+
sr_did = gr.Textbox(label="Disclosure ID")
|
| 1705 |
+
sr_read_btn = gr.Button("📄 Read Full Dossier", variant="secondary")
|
| 1706 |
+
sr_dossier = gr.TextArea(label="Dossier (read before approving)", lines=24, interactive=False)
|
| 1707 |
+
sr_read_btn.click(read_dossier_fn, inputs=sr_did, outputs=sr_dossier)
|
| 1708 |
+
|
| 1709 |
+
gr.HTML("<hr/>")
|
| 1710 |
+
sr_approver = gr.Textbox(label="Your name (approval record)")
|
| 1711 |
+
with gr.Row():
|
| 1712 |
+
sr_approve_btn = gr.Button("✅ Approve & Prepare Disclosure Message", variant="primary")
|
| 1713 |
+
sr_reject_btn = gr.Button("❌ Reject & Archive", variant="secondary")
|
| 1714 |
+
sr_approval_out = gr.TextArea(label="Result / Disclosure Message (send manually)", lines=14, interactive=False)
|
| 1715 |
+
sr_approve_btn.click(
|
| 1716 |
+
approve_and_prepare_msg,
|
| 1717 |
+
inputs=[sr_did, sr_approver],
|
| 1718 |
+
outputs=sr_approval_out,
|
| 1719 |
+
)
|
| 1720 |
+
sr_reject_btn.click(reject_disclosure_fn, inputs=sr_did, outputs=sr_approval_out)
|
| 1721 |
+
|
| 1722 |
# ── AUTO-REFRESH ────────────────────────────────────────────
|
| 1723 |
timer = gr.Timer(3)
|
| 1724 |
timer.tick(get_live_logs, outputs=live_logs)
|
chain_analyzer.py
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Rhodawk AI — Vulnerability Chain Analyzer
|
| 3 |
+
==========================================
|
| 4 |
+
Documents how primitive findings (individual assumption gaps + PoC results)
|
| 5 |
+
might combine into higher-severity chains.
|
| 6 |
+
|
| 7 |
+
ETHICAL CONSTRAINTS:
|
| 8 |
+
- Chains are THEORETICAL proposals documented for human review
|
| 9 |
+
- No chain is automatically executed
|
| 10 |
+
- All chain proposals are stored with status PENDING_HUMAN_REVIEW
|
| 11 |
+
- Human operator must approve or reject every chain before any further action
|
| 12 |
+
|
| 13 |
+
Orchestrated by Nous Hermes 3 via OpenRouter.
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
from __future__ import annotations
|
| 17 |
+
|
| 18 |
+
import hashlib
|
| 19 |
+
import json
|
| 20 |
+
import os
|
| 21 |
+
import re
|
| 22 |
+
import sqlite3
|
| 23 |
+
import time
|
| 24 |
+
from typing import Optional
|
| 25 |
+
|
| 26 |
+
import requests
|
| 27 |
+
|
| 28 |
+
OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY", "")
|
| 29 |
+
HERMES_MODEL = os.getenv(
|
| 30 |
+
"RHODAWK_RESEARCH_MODEL",
|
| 31 |
+
"nousresearch/hermes-3-llama-3.1-405b:free",
|
| 32 |
+
)
|
| 33 |
+
CHAIN_DB = os.getenv("RHODAWK_CHAIN_DB", "/data/chain_memory.sqlite")
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def _init_db() -> None:
|
| 37 |
+
os.makedirs(os.path.dirname(CHAIN_DB), exist_ok=True)
|
| 38 |
+
conn = sqlite3.connect(CHAIN_DB)
|
| 39 |
+
conn.executescript("""
|
| 40 |
+
CREATE TABLE IF NOT EXISTS primitive_findings (
|
| 41 |
+
id TEXT PRIMARY KEY,
|
| 42 |
+
repo TEXT NOT NULL,
|
| 43 |
+
gap_id TEXT NOT NULL,
|
| 44 |
+
severity TEXT,
|
| 45 |
+
description TEXT,
|
| 46 |
+
triggered INTEGER DEFAULT 0,
|
| 47 |
+
confidence TEXT DEFAULT 'UNKNOWN',
|
| 48 |
+
created_at REAL,
|
| 49 |
+
harness_out TEXT
|
| 50 |
+
);
|
| 51 |
+
CREATE TABLE IF NOT EXISTS chains (
|
| 52 |
+
id TEXT PRIMARY KEY,
|
| 53 |
+
repo TEXT NOT NULL,
|
| 54 |
+
primitive_ids TEXT NOT NULL,
|
| 55 |
+
description TEXT,
|
| 56 |
+
chained_severity TEXT,
|
| 57 |
+
confidence TEXT,
|
| 58 |
+
conditions TEXT,
|
| 59 |
+
theoretical_impact TEXT,
|
| 60 |
+
human_notes TEXT,
|
| 61 |
+
status TEXT DEFAULT 'PENDING_HUMAN_REVIEW',
|
| 62 |
+
created_at REAL,
|
| 63 |
+
human_approved INTEGER DEFAULT 0,
|
| 64 |
+
human_reviewer TEXT,
|
| 65 |
+
reviewed_at REAL
|
| 66 |
+
);
|
| 67 |
+
""")
|
| 68 |
+
conn.commit()
|
| 69 |
+
conn.close()
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def store_primitive(
|
| 73 |
+
repo: str,
|
| 74 |
+
gap_id: str,
|
| 75 |
+
severity: str,
|
| 76 |
+
description: str,
|
| 77 |
+
triggered: bool,
|
| 78 |
+
confidence: str = "UNKNOWN",
|
| 79 |
+
harness_result: Optional[dict] = None,
|
| 80 |
+
) -> str:
|
| 81 |
+
"""Persist a primitive finding from the harness execution."""
|
| 82 |
+
_init_db()
|
| 83 |
+
finding_id = hashlib.sha256(
|
| 84 |
+
f"{repo}:{gap_id}:{time.time()}".encode()
|
| 85 |
+
).hexdigest()[:16]
|
| 86 |
+
conn = sqlite3.connect(CHAIN_DB)
|
| 87 |
+
conn.execute(
|
| 88 |
+
"""INSERT OR REPLACE INTO primitive_findings
|
| 89 |
+
(id, repo, gap_id, severity, description, triggered, confidence, created_at, harness_out)
|
| 90 |
+
VALUES (?,?,?,?,?,?,?,?,?)""",
|
| 91 |
+
(
|
| 92 |
+
finding_id, repo, gap_id, severity, description,
|
| 93 |
+
1 if triggered else 0, confidence, time.time(),
|
| 94 |
+
json.dumps(harness_result or {}),
|
| 95 |
+
),
|
| 96 |
+
)
|
| 97 |
+
conn.commit()
|
| 98 |
+
conn.close()
|
| 99 |
+
return finding_id
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def analyze_chains(repo: str) -> list[dict]:
|
| 103 |
+
"""
|
| 104 |
+
Ask Hermes to propose vulnerability chains from stored primitives.
|
| 105 |
+
|
| 106 |
+
Returns THEORETICAL proposals — all tagged PENDING_HUMAN_REVIEW.
|
| 107 |
+
Nothing is executed automatically.
|
| 108 |
+
"""
|
| 109 |
+
_init_db()
|
| 110 |
+
conn = sqlite3.connect(CHAIN_DB)
|
| 111 |
+
rows = conn.execute(
|
| 112 |
+
"""SELECT id, gap_id, severity, description, triggered, confidence
|
| 113 |
+
FROM primitive_findings WHERE repo = ?
|
| 114 |
+
ORDER BY created_at DESC""",
|
| 115 |
+
(repo,),
|
| 116 |
+
).fetchall()
|
| 117 |
+
conn.close()
|
| 118 |
+
|
| 119 |
+
if len(rows) < 2:
|
| 120 |
+
return []
|
| 121 |
+
|
| 122 |
+
primitives_text = "\n".join(
|
| 123 |
+
f"- [{r[0]}] Gap: {r[1]} | Sev: {r[2]} | Triggered: {bool(r[4])} "
|
| 124 |
+
f"| Confidence: {r[5]} | {r[3][:120]}"
|
| 125 |
+
for r in rows
|
| 126 |
+
)
|
| 127 |
+
|
| 128 |
+
headers = {
|
| 129 |
+
"Authorization": f"Bearer {OPENROUTER_API_KEY}",
|
| 130 |
+
"Content-Type": "application/json",
|
| 131 |
+
"HTTP-Referer": "https://rhodawk.ai",
|
| 132 |
+
}
|
| 133 |
+
payload = {
|
| 134 |
+
"model": HERMES_MODEL,
|
| 135 |
+
"messages": [
|
| 136 |
+
{
|
| 137 |
+
"role": "system",
|
| 138 |
+
"content": (
|
| 139 |
+
"You are a senior security researcher conducting responsible vulnerability research. "
|
| 140 |
+
"Analyse primitive findings and propose THEORETICAL vulnerability chains for human review. "
|
| 141 |
+
"Be rigorous and conservative — only propose chains that are logically sound based on "
|
| 142 |
+
"the available evidence. Mark any speculation clearly. Output valid JSON only."
|
| 143 |
+
),
|
| 144 |
+
},
|
| 145 |
+
{
|
| 146 |
+
"role": "user",
|
| 147 |
+
"content": (
|
| 148 |
+
f"Analyse these primitive findings from {repo} and identify plausible chains.\n\n"
|
| 149 |
+
f"PRIMITIVES:\n{primitives_text}\n\n"
|
| 150 |
+
"Output ONLY this JSON:\n"
|
| 151 |
+
'{"chains": [{'
|
| 152 |
+
'"primitive_ids": ["id1","id2"],'
|
| 153 |
+
'"description": "Step-by-step logical chain",'
|
| 154 |
+
'"chained_severity": "P1|P2|P3",'
|
| 155 |
+
'"confidence": "HIGH|MEDIUM|LOW",'
|
| 156 |
+
'"required_conditions": ["condition1"],'
|
| 157 |
+
'"theoretical_impact": "What an attacker could theoretically achieve",'
|
| 158 |
+
'"human_verification_needed": "What a human researcher must manually verify before treating this as real"'
|
| 159 |
+
"}]}"
|
| 160 |
+
),
|
| 161 |
+
},
|
| 162 |
+
],
|
| 163 |
+
"max_tokens": 2048,
|
| 164 |
+
"temperature": 0.1,
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
try:
|
| 168 |
+
resp = requests.post(
|
| 169 |
+
"https://openrouter.ai/api/v1/chat/completions",
|
| 170 |
+
headers=headers, json=payload, timeout=120,
|
| 171 |
+
)
|
| 172 |
+
resp.raise_for_status()
|
| 173 |
+
raw = resp.json()["choices"][0]["message"]["content"]
|
| 174 |
+
|
| 175 |
+
match = re.search(r"\{[\s\S]*\}", raw)
|
| 176 |
+
if not match:
|
| 177 |
+
return []
|
| 178 |
+
|
| 179 |
+
data = json.loads(match.group())
|
| 180 |
+
chains = data.get("chains", [])
|
| 181 |
+
|
| 182 |
+
conn = sqlite3.connect(CHAIN_DB)
|
| 183 |
+
for chain in chains:
|
| 184 |
+
chain_id = hashlib.sha256(
|
| 185 |
+
f"{repo}:{':'.join(chain.get('primitive_ids', []))}:{time.time()}".encode()
|
| 186 |
+
).hexdigest()[:16]
|
| 187 |
+
chain["id"] = chain_id
|
| 188 |
+
conn.execute(
|
| 189 |
+
"""INSERT OR IGNORE INTO chains
|
| 190 |
+
(id, repo, primitive_ids, description, chained_severity, confidence,
|
| 191 |
+
conditions, theoretical_impact, status, created_at)
|
| 192 |
+
VALUES (?,?,?,?,?,?,?,?,'PENDING_HUMAN_REVIEW',?)""",
|
| 193 |
+
(
|
| 194 |
+
chain_id, repo,
|
| 195 |
+
json.dumps(chain.get("primitive_ids", [])),
|
| 196 |
+
chain.get("description", ""),
|
| 197 |
+
chain.get("chained_severity", "P3"),
|
| 198 |
+
chain.get("confidence", "LOW"),
|
| 199 |
+
json.dumps(chain.get("required_conditions", [])),
|
| 200 |
+
chain.get("theoretical_impact", ""),
|
| 201 |
+
time.time(),
|
| 202 |
+
),
|
| 203 |
+
)
|
| 204 |
+
conn.commit()
|
| 205 |
+
conn.close()
|
| 206 |
+
return chains
|
| 207 |
+
|
| 208 |
+
except Exception as e:
|
| 209 |
+
return [{"error": str(e)}]
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
def get_pending_chains(repo: Optional[str] = None) -> list[dict]:
|
| 213 |
+
_init_db()
|
| 214 |
+
conn = sqlite3.connect(CHAIN_DB)
|
| 215 |
+
if repo:
|
| 216 |
+
rows = conn.execute(
|
| 217 |
+
"""SELECT id, repo, description, chained_severity, confidence, status, created_at
|
| 218 |
+
FROM chains WHERE repo=? AND status='PENDING_HUMAN_REVIEW'
|
| 219 |
+
ORDER BY created_at DESC""",
|
| 220 |
+
(repo,),
|
| 221 |
+
).fetchall()
|
| 222 |
+
else:
|
| 223 |
+
rows = conn.execute(
|
| 224 |
+
"""SELECT id, repo, description, chained_severity, confidence, status, created_at
|
| 225 |
+
FROM chains WHERE status='PENDING_HUMAN_REVIEW'
|
| 226 |
+
ORDER BY created_at DESC""",
|
| 227 |
+
).fetchall()
|
| 228 |
+
conn.close()
|
| 229 |
+
return [
|
| 230 |
+
{
|
| 231 |
+
"id": r[0], "repo": r[1], "description": r[2],
|
| 232 |
+
"severity": r[3], "confidence": r[4], "status": r[5], "created_at": r[6],
|
| 233 |
+
}
|
| 234 |
+
for r in rows
|
| 235 |
+
]
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
def get_all_primitives(repo: Optional[str] = None) -> list[dict]:
|
| 239 |
+
_init_db()
|
| 240 |
+
conn = sqlite3.connect(CHAIN_DB)
|
| 241 |
+
if repo:
|
| 242 |
+
rows = conn.execute(
|
| 243 |
+
"SELECT id, repo, gap_id, severity, description, triggered, confidence, created_at "
|
| 244 |
+
"FROM primitive_findings WHERE repo=? ORDER BY created_at DESC",
|
| 245 |
+
(repo,),
|
| 246 |
+
).fetchall()
|
| 247 |
+
else:
|
| 248 |
+
rows = conn.execute(
|
| 249 |
+
"SELECT id, repo, gap_id, severity, description, triggered, confidence, created_at "
|
| 250 |
+
"FROM primitive_findings ORDER BY created_at DESC",
|
| 251 |
+
).fetchall()
|
| 252 |
+
conn.close()
|
| 253 |
+
return [
|
| 254 |
+
{
|
| 255 |
+
"id": r[0], "repo": r[1], "gap_id": r[2], "severity": r[3],
|
| 256 |
+
"description": r[4], "triggered": bool(r[5]),
|
| 257 |
+
"confidence": r[6], "created_at": r[7],
|
| 258 |
+
}
|
| 259 |
+
for r in rows
|
| 260 |
+
]
|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
def approve_chain(chain_id: str, reviewer: str) -> bool:
|
| 264 |
+
_init_db()
|
| 265 |
+
conn = sqlite3.connect(CHAIN_DB)
|
| 266 |
+
conn.execute(
|
| 267 |
+
"UPDATE chains SET status='HUMAN_APPROVED', human_approved=1, "
|
| 268 |
+
"human_reviewer=?, reviewed_at=? WHERE id=?",
|
| 269 |
+
(reviewer, time.time(), chain_id),
|
| 270 |
+
)
|
| 271 |
+
conn.commit()
|
| 272 |
+
conn.close()
|
| 273 |
+
return True
|
| 274 |
+
|
| 275 |
+
|
| 276 |
+
def reject_chain(chain_id: str, reviewer: str) -> bool:
|
| 277 |
+
_init_db()
|
| 278 |
+
conn = sqlite3.connect(CHAIN_DB)
|
| 279 |
+
conn.execute(
|
| 280 |
+
"UPDATE chains SET status='HUMAN_REJECTED', human_reviewer=?, reviewed_at=? WHERE id=?",
|
| 281 |
+
(reviewer, time.time(), chain_id),
|
| 282 |
+
)
|
| 283 |
+
conn.commit()
|
| 284 |
+
conn.close()
|
| 285 |
+
return True
|
disclosure_vault.py
ADDED
|
@@ -0,0 +1,365 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Rhodawk AI — Responsible Disclosure Vault
|
| 3 |
+
==========================================
|
| 4 |
+
Manages the complete responsible disclosure lifecycle with a mandatory
|
| 5 |
+
human approval gate at every stage.
|
| 6 |
+
|
| 7 |
+
DISCLOSURE POLICY (non-negotiable):
|
| 8 |
+
1. ALL findings start as DRAFT — nothing is shared externally
|
| 9 |
+
2. Human operator must read the full dossier and click Approve
|
| 10 |
+
3. After approval, the system generates a disclosure message —
|
| 11 |
+
the operator sends it via the maintainer's own security channel
|
| 12 |
+
4. Standard 90-day responsible disclosure timeline is tracked
|
| 13 |
+
5. Bug bounty submissions are prepared for human submission —
|
| 14 |
+
never automated
|
| 15 |
+
6. No GitHub API writes in AVR mode
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
from __future__ import annotations
|
| 19 |
+
|
| 20 |
+
import hashlib
|
| 21 |
+
import json
|
| 22 |
+
import os
|
| 23 |
+
import sqlite3
|
| 24 |
+
import time
|
| 25 |
+
from pathlib import Path
|
| 26 |
+
from typing import Optional
|
| 27 |
+
|
| 28 |
+
VAULT_DB = os.getenv("RHODAWK_VAULT_DB", "/data/disclosure_vault.sqlite")
|
| 29 |
+
VAULT_DIR = os.getenv("RHODAWK_VAULT_DIR", "/data/vault")
|
| 30 |
+
DISCLOSURE_DAYS = int(os.getenv("RHODAWK_DISCLOSURE_DAYS", "90"))
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def _init_db() -> None:
|
| 34 |
+
os.makedirs(os.path.dirname(VAULT_DB), exist_ok=True)
|
| 35 |
+
os.makedirs(VAULT_DIR, exist_ok=True)
|
| 36 |
+
conn = sqlite3.connect(VAULT_DB)
|
| 37 |
+
conn.executescript("""
|
| 38 |
+
CREATE TABLE IF NOT EXISTS disclosures (
|
| 39 |
+
id TEXT PRIMARY KEY,
|
| 40 |
+
repo TEXT NOT NULL,
|
| 41 |
+
severity TEXT NOT NULL,
|
| 42 |
+
title TEXT NOT NULL,
|
| 43 |
+
status TEXT DEFAULT 'DRAFT',
|
| 44 |
+
created_at REAL,
|
| 45 |
+
human_approved INTEGER DEFAULT 0,
|
| 46 |
+
approved_by TEXT,
|
| 47 |
+
approved_at REAL,
|
| 48 |
+
disclosed_at REAL,
|
| 49 |
+
deadline_at REAL,
|
| 50 |
+
dossier_path TEXT,
|
| 51 |
+
bug_bounty_program TEXT,
|
| 52 |
+
maintainer_contact TEXT
|
| 53 |
+
);
|
| 54 |
+
""")
|
| 55 |
+
conn.commit()
|
| 56 |
+
conn.close()
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def compile_dossier(
|
| 60 |
+
repo: str,
|
| 61 |
+
semantic_graph: dict,
|
| 62 |
+
assumption_gap: dict,
|
| 63 |
+
harness_result: dict,
|
| 64 |
+
chain_analysis: Optional[list] = None,
|
| 65 |
+
bug_bounty_program: str = "",
|
| 66 |
+
maintainer_contact: str = "",
|
| 67 |
+
) -> str:
|
| 68 |
+
"""
|
| 69 |
+
Compile a structured responsible disclosure dossier.
|
| 70 |
+
Stored locally — NOT sent anywhere until a human operator approves.
|
| 71 |
+
Returns the disclosure ID.
|
| 72 |
+
"""
|
| 73 |
+
_init_db()
|
| 74 |
+
|
| 75 |
+
disclosure_id = hashlib.sha256(
|
| 76 |
+
f"{repo}:{assumption_gap.get('id','')}:{time.time()}".encode()
|
| 77 |
+
).hexdigest()[:16]
|
| 78 |
+
|
| 79 |
+
deadline_ts = time.time() + (DISCLOSURE_DAYS * 86400)
|
| 80 |
+
severity = assumption_gap.get("severity_hypothesis", "P3")
|
| 81 |
+
gap_desc = assumption_gap.get("description", "N/A")[:120]
|
| 82 |
+
|
| 83 |
+
triggered_str = str(harness_result.get("triggered", "Not tested"))
|
| 84 |
+
poc_output = harness_result.get("stdout", "N/A")[:1500]
|
| 85 |
+
chain_block = (
|
| 86 |
+
json.dumps(chain_analysis, indent=2)
|
| 87 |
+
if chain_analysis
|
| 88 |
+
else "No chains identified."
|
| 89 |
+
)
|
| 90 |
+
|
| 91 |
+
trust_states = json.dumps(semantic_graph.get("trust_states", []), indent=2)[:2000]
|
| 92 |
+
transitions = json.dumps(semantic_graph.get("transitions", []), indent=2)[:1000]
|
| 93 |
+
|
| 94 |
+
dossier = f"""# Responsible Disclosure Report — {disclosure_id}
|
| 95 |
+
|
| 96 |
+
> **STATUS: DRAFT — PENDING HUMAN OPERATOR REVIEW**
|
| 97 |
+
> This report has NOT been sent to any maintainer or bug bounty programme.
|
| 98 |
+
> No live system has been tested or attacked.
|
| 99 |
+
|
| 100 |
+
---
|
| 101 |
+
|
| 102 |
+
| Field | Value |
|
| 103 |
+
|---|---|
|
| 104 |
+
| **Disclosure ID** | `{disclosure_id}` |
|
| 105 |
+
| **Repository** | `{repo}` |
|
| 106 |
+
| **Severity Hypothesis** | **{severity}** |
|
| 107 |
+
| **Disclosure Deadline** | {time.strftime('%Y-%m-%d', time.localtime(deadline_ts))} ({DISCLOSURE_DAYS}-day standard) |
|
| 108 |
+
| **Bug Bounty Programme** | {bug_bounty_program or "Not specified"} |
|
| 109 |
+
| **Maintainer Contact** | {maintainer_contact or "See SECURITY.md"} |
|
| 110 |
+
| **Created** | {time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())} |
|
| 111 |
+
|
| 112 |
+
---
|
| 113 |
+
|
| 114 |
+
## ⚠️ Operator Action Required
|
| 115 |
+
|
| 116 |
+
Before this disclosure proceeds, you must:
|
| 117 |
+
|
| 118 |
+
- [ ] Read and understand the full dossier
|
| 119 |
+
- [ ] Independently verify the assumption gap description is accurate
|
| 120 |
+
- [ ] Confirm the PoC result matches what is claimed
|
| 121 |
+
- [ ] Verify the target repo has a responsible disclosure policy or bug bounty programme
|
| 122 |
+
- [ ] Click **Approve** in the Rhodawk Security Research dashboard
|
| 123 |
+
|
| 124 |
+
---
|
| 125 |
+
|
| 126 |
+
## 1. Executive Summary
|
| 127 |
+
|
| 128 |
+
**Finding:** {assumption_gap.get('description', 'N/A')}
|
| 129 |
+
|
| 130 |
+
**File:** `{assumption_gap.get('file', 'N/A')}`
|
| 131 |
+
**Location:** `{assumption_gap.get('line_hint', 'N/A')}`
|
| 132 |
+
**Confidence:** {assumption_gap.get('confidence', 'UNKNOWN')}
|
| 133 |
+
|
| 134 |
+
---
|
| 135 |
+
|
| 136 |
+
## 2. State Machine Analysis
|
| 137 |
+
|
| 138 |
+
**Untrusted Input Path:**
|
| 139 |
+
{assumption_gap.get('untrusted_input', 'N/A')}
|
| 140 |
+
|
| 141 |
+
**Bypassed or Insufficient Check:**
|
| 142 |
+
{assumption_gap.get('bypassed_check', 'N/A')}
|
| 143 |
+
|
| 144 |
+
**Theoretical Impact:**
|
| 145 |
+
{assumption_gap.get('potential_impact', 'N/A')}
|
| 146 |
+
|
| 147 |
+
### Trust States Identified
|
| 148 |
+
|
| 149 |
+
```json
|
| 150 |
+
{trust_states}
|
| 151 |
+
```
|
| 152 |
+
|
| 153 |
+
### State Transitions
|
| 154 |
+
|
| 155 |
+
```json
|
| 156 |
+
{transitions}
|
| 157 |
+
```
|
| 158 |
+
|
| 159 |
+
---
|
| 160 |
+
|
| 161 |
+
## 3. Proof of Concept (Local Sandbox Only)
|
| 162 |
+
|
| 163 |
+
> All PoC testing was performed against a locally cloned copy of the repository.
|
| 164 |
+
> No live/production system was accessed.
|
| 165 |
+
|
| 166 |
+
**Gap Triggered in Sandbox:** {triggered_str}
|
| 167 |
+
**Exit Code:** {harness_result.get('exit_code', 'N/A')}
|
| 168 |
+
**Timed Out:** {harness_result.get('timed_out', False)}
|
| 169 |
+
|
| 170 |
+
**Sandbox Output:**
|
| 171 |
+
```
|
| 172 |
+
{poc_output}
|
| 173 |
+
```
|
| 174 |
+
|
| 175 |
+
---
|
| 176 |
+
|
| 177 |
+
## 4. Vulnerability Chain Analysis (Theoretical)
|
| 178 |
+
|
| 179 |
+
{chain_block}
|
| 180 |
+
|
| 181 |
+
> All chain proposals above are theoretical and require independent human verification.
|
| 182 |
+
|
| 183 |
+
---
|
| 184 |
+
|
| 185 |
+
## 5. Responsible Disclosure Next Steps
|
| 186 |
+
|
| 187 |
+
1. **Operator** reviews and verifies this dossier
|
| 188 |
+
2. **Operator** approves disclosure via Rhodawk dashboard
|
| 189 |
+
3. **Operator** contacts maintainer via their `SECURITY.md` / `security@` policy
|
| 190 |
+
4. Submit to bug bounty programme if applicable: `{bug_bounty_program or 'N/A'}`
|
| 191 |
+
5. Allow **{DISCLOSURE_DAYS} days** for maintainer to produce a fix
|
| 192 |
+
6. Coordinate public disclosure date with maintainer
|
| 193 |
+
|
| 194 |
+
---
|
| 195 |
+
|
| 196 |
+
*Generated by Rhodawk AI Ethical Security Research Platform*
|
| 197 |
+
*All findings require human verification and explicit approval before disclosure*
|
| 198 |
+
*No automated exploitation of live systems is performed*
|
| 199 |
+
"""
|
| 200 |
+
|
| 201 |
+
dossier_path = os.path.join(VAULT_DIR, f"{disclosure_id}.md")
|
| 202 |
+
Path(dossier_path).write_text(dossier, encoding="utf-8")
|
| 203 |
+
|
| 204 |
+
conn = sqlite3.connect(VAULT_DB)
|
| 205 |
+
conn.execute(
|
| 206 |
+
"""INSERT INTO disclosures
|
| 207 |
+
(id, repo, severity, title, status, created_at, deadline_at,
|
| 208 |
+
dossier_path, bug_bounty_program, maintainer_contact)
|
| 209 |
+
VALUES (?,?,?,?, 'DRAFT',?,?, ?,?,?)""",
|
| 210 |
+
(
|
| 211 |
+
disclosure_id, repo, severity,
|
| 212 |
+
f"{severity} — {gap_desc}",
|
| 213 |
+
time.time(), deadline_ts,
|
| 214 |
+
dossier_path, bug_bounty_program, maintainer_contact,
|
| 215 |
+
),
|
| 216 |
+
)
|
| 217 |
+
conn.commit()
|
| 218 |
+
conn.close()
|
| 219 |
+
|
| 220 |
+
return disclosure_id
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
def get_pending_disclosures() -> list[dict]:
|
| 224 |
+
_init_db()
|
| 225 |
+
conn = sqlite3.connect(VAULT_DB)
|
| 226 |
+
rows = conn.execute(
|
| 227 |
+
"""SELECT id, repo, severity, title, status, created_at, deadline_at, bug_bounty_program
|
| 228 |
+
FROM disclosures WHERE status IN ('DRAFT','HUMAN_APPROVED')
|
| 229 |
+
ORDER BY created_at DESC"""
|
| 230 |
+
).fetchall()
|
| 231 |
+
conn.close()
|
| 232 |
+
now = time.time()
|
| 233 |
+
return [
|
| 234 |
+
{
|
| 235 |
+
"id": r[0], "repo": r[1], "severity": r[2], "title": r[3],
|
| 236 |
+
"status": r[4], "created_at": r[5],
|
| 237 |
+
"days_remaining": max(0, int((r[6] - now) / 86400)) if r[6] else DISCLOSURE_DAYS,
|
| 238 |
+
"bug_bounty_program": r[7] or "N/A",
|
| 239 |
+
}
|
| 240 |
+
for r in rows
|
| 241 |
+
]
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
def get_all_disclosures() -> list[dict]:
|
| 245 |
+
_init_db()
|
| 246 |
+
conn = sqlite3.connect(VAULT_DB)
|
| 247 |
+
rows = conn.execute(
|
| 248 |
+
"SELECT id, repo, severity, title, status, created_at, deadline_at, bug_bounty_program "
|
| 249 |
+
"FROM disclosures ORDER BY created_at DESC"
|
| 250 |
+
).fetchall()
|
| 251 |
+
conn.close()
|
| 252 |
+
now = time.time()
|
| 253 |
+
return [
|
| 254 |
+
{
|
| 255 |
+
"id": r[0], "repo": r[1], "severity": r[2], "title": r[3],
|
| 256 |
+
"status": r[4], "created_at": r[5],
|
| 257 |
+
"days_remaining": max(0, int((r[6] - now) / 86400)) if r[6] else DISCLOSURE_DAYS,
|
| 258 |
+
"bug_bounty_program": r[7] or "N/A",
|
| 259 |
+
}
|
| 260 |
+
for r in rows
|
| 261 |
+
]
|
| 262 |
+
|
| 263 |
+
|
| 264 |
+
def read_dossier(disclosure_id: str) -> str:
|
| 265 |
+
_init_db()
|
| 266 |
+
conn = sqlite3.connect(VAULT_DB)
|
| 267 |
+
row = conn.execute(
|
| 268 |
+
"SELECT dossier_path FROM disclosures WHERE id=?", (disclosure_id,)
|
| 269 |
+
).fetchone()
|
| 270 |
+
conn.close()
|
| 271 |
+
if not row or not row[0]:
|
| 272 |
+
return f"Dossier not found for ID: {disclosure_id}"
|
| 273 |
+
try:
|
| 274 |
+
return Path(row[0]).read_text(encoding="utf-8")
|
| 275 |
+
except Exception as e:
|
| 276 |
+
return f"Error reading dossier: {e}"
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
def approve_disclosure(disclosure_id: str, approved_by: str) -> bool:
|
| 280 |
+
"""Human operator explicitly approves a finding for disclosure."""
|
| 281 |
+
_init_db()
|
| 282 |
+
conn = sqlite3.connect(VAULT_DB)
|
| 283 |
+
conn.execute(
|
| 284 |
+
"UPDATE disclosures SET status='HUMAN_APPROVED', human_approved=1, "
|
| 285 |
+
"approved_by=?, approved_at=? WHERE id=?",
|
| 286 |
+
(approved_by, time.time(), disclosure_id),
|
| 287 |
+
)
|
| 288 |
+
conn.commit()
|
| 289 |
+
conn.close()
|
| 290 |
+
return True
|
| 291 |
+
|
| 292 |
+
|
| 293 |
+
def reject_disclosure(disclosure_id: str, reason: str = "") -> bool:
|
| 294 |
+
"""Human operator rejects / archives a finding."""
|
| 295 |
+
_init_db()
|
| 296 |
+
conn = sqlite3.connect(VAULT_DB)
|
| 297 |
+
conn.execute(
|
| 298 |
+
"UPDATE disclosures SET status='REJECTED' WHERE id=?",
|
| 299 |
+
(disclosure_id,),
|
| 300 |
+
)
|
| 301 |
+
conn.commit()
|
| 302 |
+
conn.close()
|
| 303 |
+
return True
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
def prepare_disclosure_message(disclosure_id: str) -> str:
|
| 307 |
+
"""
|
| 308 |
+
After human approval, generate the message for the operator to send
|
| 309 |
+
to the maintainer via THEIR preferred channel (SECURITY.md / email / HackerOne).
|
| 310 |
+
|
| 311 |
+
The operator sends this manually — it is never automated.
|
| 312 |
+
"""
|
| 313 |
+
_init_db()
|
| 314 |
+
conn = sqlite3.connect(VAULT_DB)
|
| 315 |
+
row = conn.execute(
|
| 316 |
+
"SELECT repo, severity, title, bug_bounty_program, human_approved, approved_by "
|
| 317 |
+
"FROM disclosures WHERE id=?",
|
| 318 |
+
(disclosure_id,),
|
| 319 |
+
).fetchone()
|
| 320 |
+
conn.close()
|
| 321 |
+
|
| 322 |
+
if not row:
|
| 323 |
+
return "Disclosure not found."
|
| 324 |
+
if not row[4]:
|
| 325 |
+
return "ERROR: Human approval is required before generating a disclosure message."
|
| 326 |
+
|
| 327 |
+
repo, severity, title, bounty, _, approved_by = row
|
| 328 |
+
|
| 329 |
+
msg = f"""Subject: Responsible Disclosure — {severity} Finding in {repo}
|
| 330 |
+
|
| 331 |
+
Hello {repo.split('/')[0]} security team,
|
| 332 |
+
|
| 333 |
+
I am reaching out as part of responsible security research conducted through the Rhodawk AI ethical research platform.
|
| 334 |
+
|
| 335 |
+
We have identified a potential {severity}-severity security finding in `{repo}`.
|
| 336 |
+
|
| 337 |
+
**Finding:** {title}
|
| 338 |
+
|
| 339 |
+
**Disclosure ID:** `{disclosure_id}`
|
| 340 |
+
**Disclosure Deadline:** {DISCLOSURE_DAYS} days from today (industry standard)
|
| 341 |
+
**Bug Bounty Programme:** {bounty or "N/A"}
|
| 342 |
+
|
| 343 |
+
We have prepared a full technical dossier including:
|
| 344 |
+
- State machine analysis
|
| 345 |
+
- Proof-of-concept (local sandbox only — no live systems tested)
|
| 346 |
+
- Theoretical vulnerability chain analysis
|
| 347 |
+
|
| 348 |
+
We would like to coordinate disclosure privately before any public disclosure.
|
| 349 |
+
Please let us know your preferred communication channel and we will share the full dossier.
|
| 350 |
+
|
| 351 |
+
This report was reviewed and approved for disclosure by a human researcher before being sent.
|
| 352 |
+
|
| 353 |
+
Respectfully,
|
| 354 |
+
Rhodawk AI Security Research Team
|
| 355 |
+
"""
|
| 356 |
+
|
| 357 |
+
conn = sqlite3.connect(VAULT_DB)
|
| 358 |
+
conn.execute(
|
| 359 |
+
"UPDATE disclosures SET status='DISCLOSED', disclosed_at=? WHERE id=?",
|
| 360 |
+
(time.time(), disclosure_id),
|
| 361 |
+
)
|
| 362 |
+
conn.commit()
|
| 363 |
+
conn.close()
|
| 364 |
+
|
| 365 |
+
return msg
|
harness_factory.py
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Rhodawk AI — Ethical PoC Harness Factory
|
| 3 |
+
=========================================
|
| 4 |
+
Generates minimal proof-of-concept test harnesses that exercise identified
|
| 5 |
+
assumption gaps LOCALLY in an isolated sandbox.
|
| 6 |
+
|
| 7 |
+
ETHICAL CONSTRAINTS (hard-coded, not configurable):
|
| 8 |
+
- Generated harnesses target only locally cloned source code
|
| 9 |
+
- No network calls from within generated harnesses
|
| 10 |
+
- Execution is time-limited (default 30 s)
|
| 11 |
+
- All secrets stripped from sandbox environment
|
| 12 |
+
- Harness code is shown to operator BEFORE execution — never auto-run
|
| 13 |
+
- Output is PoC-grade only: demonstrates behaviour, not weaponised
|
| 14 |
+
|
| 15 |
+
Orchestrated by Nous Hermes 3 via OpenRouter.
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
from __future__ import annotations
|
| 19 |
+
|
| 20 |
+
import os
|
| 21 |
+
import re
|
| 22 |
+
import subprocess
|
| 23 |
+
import tempfile
|
| 24 |
+
import time
|
| 25 |
+
from pathlib import Path
|
| 26 |
+
from typing import Optional
|
| 27 |
+
|
| 28 |
+
import requests
|
| 29 |
+
|
| 30 |
+
OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY", "")
|
| 31 |
+
HERMES_MODEL = os.getenv(
|
| 32 |
+
"RHODAWK_RESEARCH_MODEL",
|
| 33 |
+
"nousresearch/hermes-3-llama-3.1-405b:free",
|
| 34 |
+
)
|
| 35 |
+
HARNESS_TIMEOUT = int(os.getenv("RHODAWK_HARNESS_TIMEOUT", "30"))
|
| 36 |
+
|
| 37 |
+
_SECRETS = [
|
| 38 |
+
"OPENROUTER_API_KEY", "GITHUB_TOKEN", "GITHUB_PERSONAL_ACCESS_TOKEN",
|
| 39 |
+
"TELEGRAM_BOT_TOKEN", "SLACK_WEBHOOK_URL", "RHODAWK_WEBHOOK_SECRET",
|
| 40 |
+
]
|
| 41 |
+
|
| 42 |
+
_HARNESS_PREAMBLE = """\
|
| 43 |
+
# ETHICAL POC — FOR RESPONSIBLE DISCLOSURE ONLY
|
| 44 |
+
# Generated by Rhodawk AI Ethical Security Research Platform
|
| 45 |
+
# This harness tests local code only — no network calls, no privilege escalation
|
| 46 |
+
# REVIEW CAREFULLY before authorising sandbox execution
|
| 47 |
+
"""
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def _hermes(system: str, user: str) -> str:
|
| 51 |
+
headers = {
|
| 52 |
+
"Authorization": f"Bearer {OPENROUTER_API_KEY}",
|
| 53 |
+
"Content-Type": "application/json",
|
| 54 |
+
"HTTP-Referer": "https://rhodawk.ai",
|
| 55 |
+
}
|
| 56 |
+
payload = {
|
| 57 |
+
"model": HERMES_MODEL,
|
| 58 |
+
"messages": [
|
| 59 |
+
{"role": "system", "content": system},
|
| 60 |
+
{"role": "user", "content": user},
|
| 61 |
+
],
|
| 62 |
+
"max_tokens": 4096,
|
| 63 |
+
"temperature": 0.1,
|
| 64 |
+
}
|
| 65 |
+
resp = requests.post(
|
| 66 |
+
"https://openrouter.ai/api/v1/chat/completions",
|
| 67 |
+
headers=headers, json=payload, timeout=120,
|
| 68 |
+
)
|
| 69 |
+
resp.raise_for_status()
|
| 70 |
+
return resp.json()["choices"][0]["message"]["content"]
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def generate_poc_harness(assumption_gap: dict, repo_dir: str) -> dict:
|
| 74 |
+
"""
|
| 75 |
+
Ask Hermes to generate a minimal PoC harness for a specific assumption gap.
|
| 76 |
+
|
| 77 |
+
The harness is returned for HUMAN REVIEW — it is NOT executed here.
|
| 78 |
+
Status is always 'PENDING_HUMAN_REVIEW' until the operator explicitly
|
| 79 |
+
calls run_harness_in_sandbox().
|
| 80 |
+
"""
|
| 81 |
+
gap_id = assumption_gap.get("id", "unknown")
|
| 82 |
+
gap_file = assumption_gap.get("file", "unknown")
|
| 83 |
+
gap_desc = assumption_gap.get("description", "")
|
| 84 |
+
gap_input = assumption_gap.get("untrusted_input", "")
|
| 85 |
+
gap_check = assumption_gap.get("bypassed_check", "")
|
| 86 |
+
gap_impact = assumption_gap.get("potential_impact", "")
|
| 87 |
+
|
| 88 |
+
src_snippet = ""
|
| 89 |
+
if gap_file and gap_file != "unknown":
|
| 90 |
+
full_path = os.path.join(repo_dir, gap_file)
|
| 91 |
+
if os.path.exists(full_path):
|
| 92 |
+
try:
|
| 93 |
+
lines = Path(full_path).read_text(
|
| 94 |
+
encoding="utf-8", errors="replace"
|
| 95 |
+
).splitlines()[:120]
|
| 96 |
+
src_snippet = "\n".join(lines)
|
| 97 |
+
except Exception:
|
| 98 |
+
pass
|
| 99 |
+
|
| 100 |
+
system = (
|
| 101 |
+
"You are a responsible security researcher generating a minimal proof-of-concept. "
|
| 102 |
+
"RULES:\n"
|
| 103 |
+
"1. The harness imports and tests LOCAL code only — absolutely no network calls, "
|
| 104 |
+
" no socket connections, no subprocess calls to external tools.\n"
|
| 105 |
+
"2. It must run in under 30 seconds.\n"
|
| 106 |
+
"3. Last line must print exactly: TRIGGERED: True OR TRIGGERED: False\n"
|
| 107 |
+
"4. It must NOT write files outside /tmp, escalate privileges, or exfiltrate data.\n"
|
| 108 |
+
"5. PoC-grade only — demonstrates the behaviour, is not a weapon.\n"
|
| 109 |
+
"6. Begin with the ethical preamble comment block shown in the user message."
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
user = (
|
| 113 |
+
f"Generate a minimal Python PoC harness for this assumption gap.\n\n"
|
| 114 |
+
f"Gap ID: {gap_id}\n"
|
| 115 |
+
f"File: {gap_file}\n"
|
| 116 |
+
f"Description: {gap_desc}\n"
|
| 117 |
+
f"Untrusted input path: {gap_input}\n"
|
| 118 |
+
f"Bypassed check: {gap_check}\n"
|
| 119 |
+
f"Theoretical impact: {gap_impact}\n"
|
| 120 |
+
f"Repo path: {repo_dir}\n\n"
|
| 121 |
+
f"Relevant source snippet:\n```\n{src_snippet[:3000]}\n```\n\n"
|
| 122 |
+
f"Start the file with this exact preamble:\n{_HARNESS_PREAMBLE}\n"
|
| 123 |
+
f"Output ONLY the Python harness code — no explanation, no markdown fences."
|
| 124 |
+
)
|
| 125 |
+
|
| 126 |
+
try:
|
| 127 |
+
raw = _hermes(system, user)
|
| 128 |
+
code = re.sub(r"^```(?:python)?\n?", "", raw.strip(), flags=re.MULTILINE)
|
| 129 |
+
code = re.sub(r"\n?```$", "", code.strip())
|
| 130 |
+
|
| 131 |
+
if _HARNESS_PREAMBLE.strip().splitlines()[0] not in code:
|
| 132 |
+
code = _HARNESS_PREAMBLE + "\n" + code
|
| 133 |
+
|
| 134 |
+
return {
|
| 135 |
+
"gap_id": gap_id,
|
| 136 |
+
"file": gap_file,
|
| 137 |
+
"harness_code": code,
|
| 138 |
+
"status": "PENDING_HUMAN_REVIEW",
|
| 139 |
+
"executed": False,
|
| 140 |
+
"result": None,
|
| 141 |
+
"generated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
| 142 |
+
}
|
| 143 |
+
except Exception as e:
|
| 144 |
+
return {
|
| 145 |
+
"gap_id": gap_id,
|
| 146 |
+
"error": str(e),
|
| 147 |
+
"status": "GENERATION_FAILED",
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def run_harness_in_sandbox(
|
| 152 |
+
harness_code: str,
|
| 153 |
+
repo_dir: str,
|
| 154 |
+
venv_dir: str = "/data/target_venv",
|
| 155 |
+
) -> dict:
|
| 156 |
+
"""
|
| 157 |
+
Execute a HUMAN-REVIEWED harness in an isolated local sandbox.
|
| 158 |
+
|
| 159 |
+
This function must only be called after the operator has:
|
| 160 |
+
1. Read the harness code
|
| 161 |
+
2. Clicked "I have reviewed this code" in the UI
|
| 162 |
+
|
| 163 |
+
No network access, secrets stripped, time-limited.
|
| 164 |
+
"""
|
| 165 |
+
if not harness_code.strip():
|
| 166 |
+
return {"error": "Empty harness code.", "triggered": False}
|
| 167 |
+
|
| 168 |
+
python_bin = (
|
| 169 |
+
os.path.join(venv_dir, "bin", "python")
|
| 170 |
+
if venv_dir and os.path.exists(os.path.join(venv_dir, "bin", "python"))
|
| 171 |
+
else "python3"
|
| 172 |
+
)
|
| 173 |
+
|
| 174 |
+
fd, harness_path = tempfile.mkstemp(
|
| 175 |
+
prefix="rhodawk_poc_", suffix=".py", dir="/tmp"
|
| 176 |
+
)
|
| 177 |
+
try:
|
| 178 |
+
with os.fdopen(fd, "w") as f:
|
| 179 |
+
f.write(harness_code)
|
| 180 |
+
|
| 181 |
+
env = os.environ.copy()
|
| 182 |
+
for secret in _SECRETS:
|
| 183 |
+
env.pop(secret, None)
|
| 184 |
+
env["PYTHONPATH"] = repo_dir
|
| 185 |
+
env["PYTHONDONTWRITEBYTECODE"] = "1"
|
| 186 |
+
|
| 187 |
+
proc = subprocess.run(
|
| 188 |
+
[python_bin, harness_path],
|
| 189 |
+
capture_output=True,
|
| 190 |
+
text=True,
|
| 191 |
+
timeout=HARNESS_TIMEOUT,
|
| 192 |
+
cwd=repo_dir,
|
| 193 |
+
env=env,
|
| 194 |
+
)
|
| 195 |
+
|
| 196 |
+
stdout = proc.stdout[:3000]
|
| 197 |
+
triggered = "TRIGGERED: True" in stdout
|
| 198 |
+
|
| 199 |
+
return {
|
| 200 |
+
"exit_code": proc.returncode,
|
| 201 |
+
"stdout": stdout,
|
| 202 |
+
"stderr": proc.stderr[:1000],
|
| 203 |
+
"triggered": triggered,
|
| 204 |
+
"timed_out": False,
|
| 205 |
+
"executed_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
except subprocess.TimeoutExpired:
|
| 209 |
+
return {
|
| 210 |
+
"triggered": False,
|
| 211 |
+
"timed_out": True,
|
| 212 |
+
"error": f"Harness timed out after {HARNESS_TIMEOUT}s — gap may not be triggerable.",
|
| 213 |
+
}
|
| 214 |
+
except Exception as e:
|
| 215 |
+
return {"triggered": False, "error": str(e)}
|
| 216 |
+
finally:
|
| 217 |
+
try:
|
| 218 |
+
os.unlink(harness_path)
|
| 219 |
+
except OSError:
|
| 220 |
+
pass
|
semantic_extractor.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Rhodawk AI — Semantic Logic Extractor (Ethical Research Mode)
|
| 3 |
+
=============================================================
|
| 4 |
+
STATIC ANALYSIS ONLY — no code is executed by this module.
|
| 5 |
+
|
| 6 |
+
Maps the application's trust state machine across files to identify
|
| 7 |
+
"Assumption Gaps" — points where developer intent diverges from actual
|
| 8 |
+
code behaviour. All output is JSON for human review.
|
| 9 |
+
|
| 10 |
+
Orchestrated by Nous Hermes 3 via OpenRouter.
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
|
| 15 |
+
import glob
|
| 16 |
+
import json
|
| 17 |
+
import os
|
| 18 |
+
import re
|
| 19 |
+
import time
|
| 20 |
+
from pathlib import Path
|
| 21 |
+
from typing import Optional
|
| 22 |
+
|
| 23 |
+
import requests
|
| 24 |
+
|
| 25 |
+
OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY", "")
|
| 26 |
+
HERMES_MODEL = os.getenv(
|
| 27 |
+
"RHODAWK_RESEARCH_MODEL",
|
| 28 |
+
"nousresearch/hermes-3-llama-3.1-405b:free",
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
_PRIORITY_KEYWORDS = [
|
| 32 |
+
"auth", "token", "session", "permission", "privilege", "trust",
|
| 33 |
+
"validate", "sanitize", "parse", "decode", "deserialize", "marshal",
|
| 34 |
+
"memory", "alloc", "buffer", "exec", "eval", "inject", "sign", "verify",
|
| 35 |
+
"secret", "password", "credential", "acl", "role", "scope", "grant",
|
| 36 |
+
]
|
| 37 |
+
|
| 38 |
+
_SKIP_DIRS = {".git", "vendor", "node_modules", "__pycache__", ".tox", "dist", "build"}
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def _hermes(system: str, user: str, max_tokens: int = 4096) -> str:
|
| 42 |
+
"""Call Nous Hermes 3 via OpenRouter."""
|
| 43 |
+
headers = {
|
| 44 |
+
"Authorization": f"Bearer {OPENROUTER_API_KEY}",
|
| 45 |
+
"Content-Type": "application/json",
|
| 46 |
+
"HTTP-Referer": "https://rhodawk.ai",
|
| 47 |
+
"X-Title": "Rhodawk Ethical Security Research",
|
| 48 |
+
}
|
| 49 |
+
payload = {
|
| 50 |
+
"model": HERMES_MODEL,
|
| 51 |
+
"messages": [
|
| 52 |
+
{"role": "system", "content": system},
|
| 53 |
+
{"role": "user", "content": user},
|
| 54 |
+
],
|
| 55 |
+
"max_tokens": max_tokens,
|
| 56 |
+
"temperature": 0.1,
|
| 57 |
+
}
|
| 58 |
+
resp = requests.post(
|
| 59 |
+
"https://openrouter.ai/api/v1/chat/completions",
|
| 60 |
+
headers=headers, json=payload, timeout=120,
|
| 61 |
+
)
|
| 62 |
+
resp.raise_for_status()
|
| 63 |
+
return resp.json()["choices"][0]["message"]["content"]
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def _find_relevant_files(repo_dir: str, language: str) -> list[str]:
|
| 67 |
+
lang_patterns: dict[str, list[str]] = {
|
| 68 |
+
"python": ["**/*.py"],
|
| 69 |
+
"javascript": ["**/*.js", "**/*.mjs"],
|
| 70 |
+
"typescript": ["**/*.ts"],
|
| 71 |
+
"go": ["**/*.go"],
|
| 72 |
+
"java": ["**/*.java"],
|
| 73 |
+
"rust": ["**/*.rs"],
|
| 74 |
+
"c": ["**/*.c", "**/*.h"],
|
| 75 |
+
"cpp": ["**/*.cpp", "**/*.hpp", "**/*.h"],
|
| 76 |
+
"ruby": ["**/*.rb"],
|
| 77 |
+
}
|
| 78 |
+
patterns = lang_patterns.get(language.lower(), ["**/*.py"])
|
| 79 |
+
|
| 80 |
+
all_files: list[str] = []
|
| 81 |
+
for pat in patterns:
|
| 82 |
+
for path in glob.glob(os.path.join(repo_dir, pat), recursive=True):
|
| 83 |
+
rel = os.path.relpath(path, repo_dir)
|
| 84 |
+
if any(s in rel for s in _SKIP_DIRS):
|
| 85 |
+
continue
|
| 86 |
+
all_files.append(rel)
|
| 87 |
+
|
| 88 |
+
priority = [f for f in all_files if any(kw in f.lower() for kw in _PRIORITY_KEYWORDS)]
|
| 89 |
+
rest = [f for f in all_files if f not in priority]
|
| 90 |
+
return (priority + rest)[:30]
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def _read_file_head(repo_dir: str, rel_path: str, max_lines: int = 200) -> str:
|
| 94 |
+
try:
|
| 95 |
+
text = Path(os.path.join(repo_dir, rel_path)).read_text(
|
| 96 |
+
encoding="utf-8", errors="replace"
|
| 97 |
+
)
|
| 98 |
+
return "\n".join(text.splitlines()[:max_lines])
|
| 99 |
+
except Exception:
|
| 100 |
+
return ""
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def _extract_json(text: str) -> dict:
|
| 104 |
+
match = re.search(r"\{[\s\S]*\}", text)
|
| 105 |
+
if match:
|
| 106 |
+
try:
|
| 107 |
+
return json.loads(match.group())
|
| 108 |
+
except json.JSONDecodeError:
|
| 109 |
+
pass
|
| 110 |
+
return {}
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def extract_trust_boundaries(repo_dir: str, file_paths: list[str]) -> dict:
|
| 114 |
+
"""
|
| 115 |
+
Static analysis pass: asks Hermes to map trust states and find assumption gaps.
|
| 116 |
+
Returns a JSON-serialisable state machine graph.
|
| 117 |
+
"""
|
| 118 |
+
snippets = []
|
| 119 |
+
for rel in file_paths[:20]:
|
| 120 |
+
head = _read_file_head(repo_dir, rel)
|
| 121 |
+
if head:
|
| 122 |
+
snippets.append(f"=== {rel} ===\n{head}")
|
| 123 |
+
|
| 124 |
+
combined = "\n\n".join(snippets)[:14000]
|
| 125 |
+
|
| 126 |
+
system = (
|
| 127 |
+
"You are a senior security researcher conducting responsible vulnerability research. "
|
| 128 |
+
"You perform STATIC analysis only — you never execute code. "
|
| 129 |
+
"Your goal is to map trust state machines and identify assumption gaps where developer "
|
| 130 |
+
"intent diverges from actual code behaviour. "
|
| 131 |
+
"Output valid JSON only — no prose outside the JSON block."
|
| 132 |
+
)
|
| 133 |
+
|
| 134 |
+
user = f"""Analyse the source code below and output a trust state machine graph.
|
| 135 |
+
|
| 136 |
+
Identify:
|
| 137 |
+
1. Where data enters the system (UNTRUSTED)
|
| 138 |
+
2. Validation / sanitisation steps (TRANSITION)
|
| 139 |
+
3. Where data is treated as trusted (TRUSTED)
|
| 140 |
+
4. ASSUMPTION GAPS — points where the code assumes safety without sufficient proof
|
| 141 |
+
|
| 142 |
+
Return ONLY this JSON structure:
|
| 143 |
+
{{
|
| 144 |
+
"language": "detected language",
|
| 145 |
+
"trust_states": [
|
| 146 |
+
{{"id": "s1", "name": "...", "type": "UNTRUSTED|TRANSITION|TRUSTED",
|
| 147 |
+
"files": ["file.py"], "description": "..."}}
|
| 148 |
+
],
|
| 149 |
+
"transitions": [
|
| 150 |
+
{{"from": "s1", "to": "s2", "condition": "...", "file": "file.py", "line_hint": "..."}}
|
| 151 |
+
],
|
| 152 |
+
"assumption_gaps": [
|
| 153 |
+
{{
|
| 154 |
+
"id": "gap_001",
|
| 155 |
+
"severity_hypothesis": "P1|P2|P3",
|
| 156 |
+
"file": "relative/path.py",
|
| 157 |
+
"line_hint": "function name or ~line number",
|
| 158 |
+
"description": "What the developer assumed vs what can actually reach this point",
|
| 159 |
+
"untrusted_input": "What untrusted data reaches here",
|
| 160 |
+
"bypassed_check": "What validation is missing or insufficient",
|
| 161 |
+
"potential_impact": "Theoretical worst-case consequence",
|
| 162 |
+
"confidence": "HIGH|MEDIUM|LOW",
|
| 163 |
+
"requires_human_verification": true
|
| 164 |
+
}}
|
| 165 |
+
]
|
| 166 |
+
}}
|
| 167 |
+
|
| 168 |
+
SOURCE CODE:
|
| 169 |
+
{combined}"""
|
| 170 |
+
|
| 171 |
+
try:
|
| 172 |
+
raw = _hermes(system, user)
|
| 173 |
+
result = _extract_json(raw)
|
| 174 |
+
if result:
|
| 175 |
+
return result
|
| 176 |
+
except Exception as e:
|
| 177 |
+
return {"error": str(e), "assumption_gaps": []}
|
| 178 |
+
|
| 179 |
+
return {"assumption_gaps": []}
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
def run_semantic_extraction(repo_dir: str, language: str = "python") -> dict:
|
| 183 |
+
"""
|
| 184 |
+
Main entry point for the semantic analysis pipeline.
|
| 185 |
+
Pure static analysis — no code is executed.
|
| 186 |
+
|
| 187 |
+
Returns a dict containing:
|
| 188 |
+
- trust_states
|
| 189 |
+
- transitions
|
| 190 |
+
- assumption_gaps (each tagged requires_human_verification=True)
|
| 191 |
+
- analyzed_files
|
| 192 |
+
- status: always "PENDING_HUMAN_REVIEW"
|
| 193 |
+
"""
|
| 194 |
+
relevant_files = _find_relevant_files(repo_dir, language)
|
| 195 |
+
if not relevant_files:
|
| 196 |
+
return {
|
| 197 |
+
"error": "No source files found for the detected language.",
|
| 198 |
+
"assumption_gaps": [],
|
| 199 |
+
"status": "PENDING_HUMAN_REVIEW",
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
result = extract_trust_boundaries(repo_dir, relevant_files)
|
| 203 |
+
result["analyzed_files"] = relevant_files
|
| 204 |
+
result["repo_dir"] = repo_dir
|
| 205 |
+
result["language"] = language
|
| 206 |
+
result["extracted_at"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
| 207 |
+
result["status"] = "PENDING_HUMAN_REVIEW"
|
| 208 |
+
|
| 209 |
+
for gap in result.get("assumption_gaps", []):
|
| 210 |
+
gap["requires_human_verification"] = True
|
| 211 |
+
|
| 212 |
+
return result
|