Rhodawk Agent commited on
Commit
59dc700
Β·
1 Parent(s): c664730

Fix: build error + ACTS consensus + ChainAnalyzer + rate limit + timer + venv path\n\n Build error fix:\n - requirements.txt: Remove atheris (requires Clang+libFuzzer, unavailable in HF Spaces)\n - fuzzing_engine.py: Graceful atheris fallback, detects availability at runtime,\n falls back to Hypothesis automatically\n - Dockerfile: Renamed builder stage to base, runtime inherits directly, eliminates\n broken reference to nonexistent 'builder' stage name in COPY --from\n\n ACTS Consensus bug (hermes_orchestrator.py):\n - _run_acts_consensus now calls all 3 models individually and concurrently,\n passing all 3 raw verdicts to compute_acts() so disagreement penalty works\n - Previously merged result collapsed to 1-item list (agreement_factor always 1.0)\n\n ChainAnalyzerTool in Hermes registry (hermes_orchestrator.py):\n - Added ChainAnalyzerTool with name=chain_analysis registered in _TOOL_REGISTRY\n - Hermes can now autonomously synthesize exploit chains without human UI click\n - _HERMES_SYSTEM prompt updated to document the new tool\n\n LLM rate limit backoff (hermes_orchestrator.py):\n - _hermes_llm_call retries on 429 with delays 15s/30s/60s\n - Single 429 no longer aborts the entire research session\n\n Timer consolidation (app.py):\n - get_combined_refresh() returns all 8 live-update outputs at once\n - 3 concurrent gr.Timer.tick() SSE streams collapsed to single tick\n - Reduces HF Space connection pressure under multiple simultaneous users\n\n Venv path fix (app.py):\n - execute_approved_harness uses VENV_DIR constant, not hardcoded string\n - Creates venv on-demand if missing so standalone Security Research tab works

Browse files
Files changed (5) hide show
  1. Dockerfile +10 -23
  2. app.py +0 -0
  3. fuzzing_engine.py +51 -12
  4. hermes_orchestrator.py +98 -16
  5. requirements.txt +2 -1
Dockerfile CHANGED
@@ -1,5 +1,5 @@
1
  # Stage 1: Builder
2
- FROM python:3.12-slim AS builder
3
 
4
  ENV DEBIAN_FRONTEND=noninteractive \
5
  PYTHONUNBUFFERED=1 \
@@ -16,33 +16,26 @@ COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
16
  WORKDIR /build
17
  COPY requirements.txt .
18
 
19
- # FIX: only build wheels here β€” do NOT also run uv pip install --system.
20
- # The previous double-install (uv pip install --system AND pip wheel) was
21
- # redundant and could produce conflicting bytecode in the builder layer.
22
- RUN pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt mcp-server-fetch
23
 
24
 
25
- # Stage 2: Runtime
26
- FROM python:3.12-slim AS runtime
27
 
28
  LABEL org.opencontainers.image.title="Rhodawk AI DevSecOps Engine"
29
 
30
- # FIX: UV_PYTHON now points to python3 (always present in python:3.12-slim)
31
- # rather than /usr/local/bin/python which may lack the executable in some
32
- # HuggingFace Space runtime snapshots. UV_PYTHON_PREFERENCE=system tells uv
33
- # to skip its managed-toolchain download and use the container Python directly.
34
- ENV DEBIAN_FRONTEND=noninteractive \
35
- PYTHONUNBUFFERED=1 \
36
- GRADIO_SERVER_NAME=0.0.0.0 \
37
  GRADIO_SERVER_PORT=7860 \
38
  HOME=/home/rhodawk \
39
  PATH="/home/rhodawk/.local/bin:/usr/local/bin:$PATH" \
40
- UV_LINK_MODE=copy \
41
  UV_PYTHON_PREFERENCE=system \
42
  UV_PYTHON=/usr/local/bin/python3
43
 
44
  RUN apt-get update && \
45
- apt-get install -y --no-install-recommends git curl ca-certificates nodejs npm && \
46
  rm -rf /var/lib/apt/lists/*
47
 
48
  RUN npm install -g --quiet @modelcontextprotocol/server-github
@@ -55,16 +48,10 @@ RUN id -u 1000 >/dev/null 2>&1 && (userdel -r $(id -un 1000) || true) || true &&
55
  # even before the application calls os.makedirs() at runtime.
56
  RUN mkdir -p /data /app && chmod 777 /data && chown -R rhodawk:rhodawk /app
57
 
58
- WORKDIR /app
59
-
60
- # Copy pre-built wheels from builder
61
- COPY --from=builder /wheels /wheels
62
-
63
  # Copy the uv executable from the official image
64
  COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
65
 
66
- RUN pip install --no-cache-dir /wheels/* && rm -rf /wheels
67
-
68
  USER rhodawk
69
 
70
  # Copy the source code
 
1
  # Stage 1: Builder
2
+ FROM python:3.12-slim AS base
3
 
4
  ENV DEBIAN_FRONTEND=noninteractive \
5
  PYTHONUNBUFFERED=1 \
 
16
  WORKDIR /build
17
  COPY requirements.txt .
18
 
19
+ # Install dependencies directly β€” avoids wheel-build failures for packages
20
+ # that require special compile-time tooling (e.g. atheris/libFuzzer).
21
+ # atheris has been removed from requirements.txt; Hypothesis is the fallback.
22
+ RUN pip install --no-cache-dir -r requirements.txt mcp-server-fetch
23
 
24
 
25
+ # Stage 2: Runtime β€” inherits installed packages from base
26
+ FROM base AS runtime
27
 
28
  LABEL org.opencontainers.image.title="Rhodawk AI DevSecOps Engine"
29
 
30
+ ENV GRADIO_SERVER_NAME=0.0.0.0 \
 
 
 
 
 
 
31
  GRADIO_SERVER_PORT=7860 \
32
  HOME=/home/rhodawk \
33
  PATH="/home/rhodawk/.local/bin:/usr/local/bin:$PATH" \
 
34
  UV_PYTHON_PREFERENCE=system \
35
  UV_PYTHON=/usr/local/bin/python3
36
 
37
  RUN apt-get update && \
38
+ apt-get install -y --no-install-recommends nodejs npm && \
39
  rm -rf /var/lib/apt/lists/*
40
 
41
  RUN npm install -g --quiet @modelcontextprotocol/server-github
 
48
  # even before the application calls os.makedirs() at runtime.
49
  RUN mkdir -p /data /app && chmod 777 /data && chown -R rhodawk:rhodawk /app
50
 
 
 
 
 
 
51
  # Copy the uv executable from the official image
52
  COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
53
 
54
+ WORKDIR /app
 
55
  USER rhodawk
56
 
57
  # Copy the source code
app.py CHANGED
The diff for this file is too large to render. See raw diff
 
fuzzing_engine.py CHANGED
@@ -152,21 +152,47 @@ def _extract_code_block(text: str) -> str:
152
  def _fallback_harness(target: str, language: str) -> str:
153
  """Generic fallback harness when LLM is unavailable."""
154
  if language == "python":
 
 
 
 
155
  return f"""
156
- import atheris
157
  import sys
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
 
159
- @atheris.instrument_func
160
- def fuzz_target(data):
161
- fdp = atheris.FuzzedDataProvider(data)
162
- try:
163
- val = fdp.ConsumeUnicodeNoSurrogates(128)
164
- # TODO: call {target}(val)
165
- except Exception:
166
- pass
167
 
168
- atheris.Setup(sys.argv, fuzz_target)
169
- atheris.Fuzz()
 
 
 
 
 
 
 
 
170
  """
171
  return f"# Fallback harness for {target} ({language})\n# Manual harness required\n"
172
 
@@ -331,7 +357,20 @@ def run_fuzzing_campaign(
331
 
332
  start = time.time()
333
  if language == "python" and "atheris" in harness_code:
334
- crashes = _run_python_atheris(harness_code, duration_s)
 
 
 
 
 
 
 
 
 
 
 
 
 
335
  else:
336
  crashes = _run_hypothesis(repo_dir, target, harness_code, duration_s)
337
 
 
152
  def _fallback_harness(target: str, language: str) -> str:
153
  """Generic fallback harness when LLM is unavailable."""
154
  if language == "python":
155
+ # FIX (Build Error): atheris requires Clang + libFuzzer at compile time and
156
+ # fails to build on HuggingFace Spaces. The fallback harness now uses
157
+ # hypothesis which is available everywhere, matching the Hypothesis
158
+ # fallback already in use when atheris is unavailable at runtime.
159
  return f"""
 
160
  import sys
161
+ try:
162
+ import atheris
163
+ _ATHERIS_AVAILABLE = True
164
+ except ImportError:
165
+ _ATHERIS_AVAILABLE = False
166
+
167
+ if _ATHERIS_AVAILABLE:
168
+ import sys
169
+
170
+ @atheris.instrument_func
171
+ def fuzz_target(data):
172
+ fdp = atheris.FuzzedDataProvider(data)
173
+ try:
174
+ val = fdp.ConsumeUnicodeNoSurrogates(128)
175
+ # TODO: call {target}(val)
176
+ except Exception:
177
+ pass
178
 
179
+ atheris.Setup(sys.argv, fuzz_target)
180
+ atheris.Fuzz()
181
+ else:
182
+ # Hypothesis-based fallback when atheris/libFuzzer is unavailable
183
+ from hypothesis import given, settings, HealthCheck
184
+ from hypothesis import strategies as st
 
 
185
 
186
+ @given(st.text(max_size=128))
187
+ @settings(max_examples=500, suppress_health_check=list(HealthCheck))
188
+ def fuzz_target(val):
189
+ try:
190
+ # TODO: call {target}(val)
191
+ pass
192
+ except Exception:
193
+ pass
194
+
195
+ fuzz_target()
196
  """
197
  return f"# Fallback harness for {target} ({language})\n# Manual harness required\n"
198
 
 
357
 
358
  start = time.time()
359
  if language == "python" and "atheris" in harness_code:
360
+ # FIX (Build Error): atheris may be unavailable; fall back to Hypothesis.
361
+ try:
362
+ import importlib.util
363
+ _atheris_available = importlib.util.find_spec("atheris") is not None
364
+ except Exception:
365
+ _atheris_available = False
366
+ if _atheris_available:
367
+ crashes = _run_python_atheris(harness_code, duration_s)
368
+ else:
369
+ # Rewrite the harness to use Hypothesis if atheris is not installed
370
+ harness_code = harness_code.replace(
371
+ "import atheris", "# atheris unavailable β€” using Hypothesis fallback"
372
+ )
373
+ crashes = _run_hypothesis(repo_dir, target, harness_code, duration_s)
374
  else:
375
  crashes = _run_hypothesis(repo_dir, target, harness_code, duration_s)
376
 
hermes_orchestrator.py CHANGED
@@ -210,10 +210,30 @@ class SSECTool(HermesTool):
210
  return run_ssec_scan(repo_dir, focus_files=focus_files)
211
 
212
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
  _TOOL_REGISTRY: dict[str, HermesTool] = {
214
  t.name: t() for t in [
215
  ReconTool, TaintTool, SymbolicTool, FuzzTool,
216
  ExploitTool, CVETool, CommitWatchTool, SSECTool,
 
217
  ]
218
  }
219
 
@@ -326,6 +346,7 @@ You have access to these tools:
326
  - cve_intel: Query historical CVEs for similar patterns
327
  - commit_watch: Find silent security patches in commit history
328
  - ssec_scan: Semantic similarity to known exploit patterns
 
329
 
330
  For each target, produce a research plan and execute it step by step.
331
  When you find something, rate its severity honestly. Never hallucinate findings.
@@ -352,7 +373,14 @@ Or to signal completion:
352
  """
353
 
354
 
 
 
 
355
  def _hermes_llm_call(messages: list[dict], model: str = None, timeout: int = 120) -> dict:
 
 
 
 
356
  if not OPENROUTER_API_KEY:
357
  return {"done": True, "summary": "OPENROUTER_API_KEY not set"}
358
 
@@ -370,17 +398,34 @@ def _hermes_llm_call(messages: list[dict], model: str = None, timeout: int = 120
370
  "max_tokens": 2048,
371
  "response_format": {"type": "json_object"},
372
  }
373
- try:
374
- resp = requests.post(
375
- f"{OPENROUTER_BASE}/chat/completions",
376
- headers=headers, json=payload, timeout=timeout,
377
- )
378
- resp.raise_for_status()
379
- content = resp.json()["choices"][0]["message"]["content"]
380
- return json.loads(content)
381
- except Exception as e:
382
- hermes_log(f"LLM call failed: {e}", "WARN")
383
- return {"done": True, "summary": f"LLM error: {e}"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
384
 
385
 
386
  # ──────────────────────────────────────────────────────────────
@@ -528,8 +573,17 @@ def run_hermes_research(
528
 
529
 
530
  def _run_acts_consensus(session: HermesSession):
531
- """Run multi-model adversarial consensus on each finding to compute ACTS score."""
532
- from adversarial_reviewer import _call_concurrent_consensus
 
 
 
 
 
 
 
 
 
533
 
534
  CONSENSUS_MODELS = [
535
  "deepseek/deepseek-r1:free",
@@ -551,12 +605,40 @@ def _run_acts_consensus(session: HermesSession):
551
  "Is this a real, exploitable vulnerability? Respond as a hostile security reviewer."
552
  )
553
  try:
554
- result, _ = _call_concurrent_consensus(prompt)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
555
  verdicts = [
556
- {"verdict": result.get("verdict", "UNCERTAIN"), "confidence": result.get("confidence", 0.5)}
 
 
 
 
557
  ]
558
  finding.acts_score = compute_acts(verdicts)
559
- hermes_log(f"ACTS score for {finding.finding_id}: {finding.acts_score}", "ACTS")
 
 
 
 
560
  except Exception as e:
561
  hermes_log(f"ACTS consensus failed for {finding.finding_id}: {e}", "WARN")
562
  finding.acts_score = finding.confidence
 
210
  return run_ssec_scan(repo_dir, focus_files=focus_files)
211
 
212
 
213
+ class ChainAnalyzerTool(HermesTool):
214
+ name = "chain_analysis"
215
+ description = (
216
+ "Synthesize stored primitive findings into higher-severity exploit chains. "
217
+ "Call after at least 2 primitives are recorded. Returns THEORETICAL proposals "
218
+ "tagged PENDING_HUMAN_REVIEW β€” no chain is executed automatically."
219
+ )
220
+
221
+ def run(self, repo_dir: str, repo: str = "", **kwargs) -> dict:
222
+ hermes_log(f"Chain analysis β†’ {repo or repo_dir}", "EXPLOIT")
223
+ from chain_analyzer import analyze_chains, get_all_primitives
224
+ target = repo or repo_dir
225
+ primitives = get_all_primitives(repo=target)
226
+ if len(primitives) < 2:
227
+ return {"chains": [], "note": f"Only {len(primitives)} primitive(s) stored β€” need β‰₯2 to chain"}
228
+ chains = analyze_chains(repo=target)
229
+ return {"chains": chains, "primitive_count": len(primitives)}
230
+
231
+
232
  _TOOL_REGISTRY: dict[str, HermesTool] = {
233
  t.name: t() for t in [
234
  ReconTool, TaintTool, SymbolicTool, FuzzTool,
235
  ExploitTool, CVETool, CommitWatchTool, SSECTool,
236
+ ChainAnalyzerTool,
237
  ]
238
  }
239
 
 
346
  - cve_intel: Query historical CVEs for similar patterns
347
  - commit_watch: Find silent security patches in commit history
348
  - ssec_scan: Semantic similarity to known exploit patterns
349
+ - chain_analysis: Synthesize stored primitive findings into exploit chains (call after β‰₯2 findings)
350
 
351
  For each target, produce a research plan and execute it step by step.
352
  When you find something, rate its severity honestly. Never hallucinate findings.
 
373
  """
374
 
375
 
376
+ _RATE_LIMIT_BACKOFF_DELAYS = [15, 30, 60] # seconds β€” exponential backoff for 429s
377
+
378
+
379
  def _hermes_llm_call(messages: list[dict], model: str = None, timeout: int = 120) -> dict:
380
+ """
381
+ Call the Hermes LLM with exponential backoff on rate-limit (429) responses.
382
+ Three retries before giving up β€” prevents a single 429 from aborting a session.
383
+ """
384
  if not OPENROUTER_API_KEY:
385
  return {"done": True, "summary": "OPENROUTER_API_KEY not set"}
386
 
 
398
  "max_tokens": 2048,
399
  "response_format": {"type": "json_object"},
400
  }
401
+
402
+ last_error: Exception | None = None
403
+ for attempt, backoff in enumerate([0] + _RATE_LIMIT_BACKOFF_DELAYS):
404
+ if backoff:
405
+ hermes_log(f"Rate limit hit β€” waiting {backoff}s before retry {attempt}/{len(_RATE_LIMIT_BACKOFF_DELAYS)}", "WARN")
406
+ time.sleep(backoff)
407
+ try:
408
+ resp = requests.post(
409
+ f"{OPENROUTER_BASE}/chat/completions",
410
+ headers=headers, json=payload, timeout=timeout,
411
+ )
412
+ if resp.status_code == 429:
413
+ last_error = Exception(f"HTTP 429 rate limit (attempt {attempt + 1})")
414
+ hermes_log(str(last_error), "WARN")
415
+ continue
416
+ resp.raise_for_status()
417
+ content = resp.json()["choices"][0]["message"]["content"]
418
+ return json.loads(content)
419
+ except Exception as e:
420
+ if "429" in str(e):
421
+ last_error = e
422
+ hermes_log(f"Rate limit exception: {e}", "WARN")
423
+ continue
424
+ hermes_log(f"LLM call failed: {e}", "WARN")
425
+ return {"done": True, "summary": f"LLM error: {e}"}
426
+
427
+ hermes_log(f"LLM call exhausted all retries. Last error: {last_error}", "WARN")
428
+ return {"done": True, "summary": f"LLM rate limit β€” all retries exhausted: {last_error}"}
429
 
430
 
431
  # ──────────────────────────────────────────────────────────────
 
573
 
574
 
575
  def _run_acts_consensus(session: HermesSession):
576
+ """
577
+ Run multi-model adversarial consensus on each finding to compute ACTS score.
578
+
579
+ FIX (ACTS Bug): Previously _call_concurrent_consensus returned a single merged
580
+ result, so compute_acts() always received a 1-item list with agreement_factor=1.0
581
+ β€” completely bypassing the disagreement penalty. Now we call each consensus model
582
+ individually so all 3 raw verdicts are passed to compute_acts(), enabling the full
583
+ Bayesian disagreement weighting to work as designed.
584
+ """
585
+ import concurrent.futures
586
+ from adversarial_reviewer import _call_single_model, ADVERSARY_SYSTEM_PROMPT
587
 
588
  CONSENSUS_MODELS = [
589
  "deepseek/deepseek-r1:free",
 
605
  "Is this a real, exploitable vulnerability? Respond as a hostile security reviewer."
606
  )
607
  try:
608
+ # Call all 3 models concurrently and collect individual raw verdicts.
609
+ # This is required so compute_acts() receives the full disagreement signal
610
+ # rather than a pre-merged single verdict (which collapses agreement_factor to 1.0).
611
+ individual_results: list[dict] = []
612
+ with concurrent.futures.ThreadPoolExecutor(max_workers=len(CONSENSUS_MODELS)) as ex:
613
+ futures = {
614
+ ex.submit(_call_single_model, model, prompt): model
615
+ for model in CONSENSUS_MODELS
616
+ }
617
+ for future in concurrent.futures.as_completed(futures, timeout=90):
618
+ try:
619
+ result_dict, _ = future.result()
620
+ if result_dict is not None:
621
+ individual_results.append(result_dict)
622
+ except Exception:
623
+ pass
624
+
625
+ if not individual_results:
626
+ raise RuntimeError("All ACTS consensus models failed")
627
+
628
+ # Build one verdict entry per model response so compute_acts sees N items.
629
  verdicts = [
630
+ {
631
+ "verdict": r.get("verdict", "UNCERTAIN"),
632
+ "confidence": float(r.get("confidence", 0.5)),
633
+ }
634
+ for r in individual_results
635
  ]
636
  finding.acts_score = compute_acts(verdicts)
637
+ hermes_log(
638
+ f"ACTS score for {finding.finding_id}: {finding.acts_score} "
639
+ f"({len(verdicts)} model verdicts: {[v['verdict'] for v in verdicts]})",
640
+ "ACTS",
641
+ )
642
  except Exception as e:
643
  hermes_log(f"ACTS consensus failed for {finding.finding_id}: {e}", "WARN")
644
  finding.acts_score = finding.confidence
requirements.txt CHANGED
@@ -24,7 +24,8 @@ z3-solver>=4.12.0
24
  qdrant-client>=1.9.0
25
  transformers>=4.40.0
26
  torch>=2.2.0
27
- atheris>=2.3.0
 
28
  angr>=9.2.0
29
  networkx>=3.0
30
  defusedxml>=0.7.1
 
24
  qdrant-client>=1.9.0
25
  transformers>=4.40.0
26
  torch>=2.2.0
27
+ # atheris removed: requires Clang + libFuzzer at compile time which is unavailable
28
+ # on HuggingFace Space Docker images. Fuzzing falls back to Hypothesis automatically.
29
  angr>=9.2.0
30
  networkx>=3.0
31
  defusedxml>=0.7.1