fix(guard): scope online-research gate to the exploited hypothesis

- check_command now accepts hypothesis_id (schema + handler + args)
- Exploit-phase research gate checks only the named hypothesis when
  provided; otherwise all candidates (previous behavior)
- Error message names the remediation tool: violin_record_hypothesis
  id=H-00N cve_research=... exploit_research=...
- Fixes silent bypass: normalized id comparison (002 vs 2) so an
  unresearched hypothesis is actually blocked
- New test: named-hypothesis passes, unresearched H-002 blocked,
  all-candidates mode still enforces full rows
This commit is contained in:
Violin
2026-08-12 18:52:42 +01:00
parent 46bad5c1a8
commit 723e349291
4 changed files with 103 additions and 6 deletions
+25 -6
View File
@@ -58,6 +58,7 @@ class CheckCommandArgs:
target: str | None = None
session_id: str | None = None
account_sync: bool = True
hypothesis_id: str | None = None
@dataclass
@@ -530,10 +531,26 @@ def check_hypothesis_freshness(
Phase.PRIVESC,
Phase.FLAGS,
}:
researched = [h for h in relevant if h.cve_research.strip() and h.exploit_research.strip()]
if not researched:
# Online research is required before exploit execution: the engagement
# brief may have no exploit path, so the first action for any real
# exploit is to look for prior work. When the command names a specific
# hypothesis, only that hypothesis must carry research rows; otherwise
# every candidate hypothesis must. 'no results' / 'not applicable' /
# 'source unavailable' are valid truthful outcomes.
if norm_hyp_id is not None:
research_targets = [
h
for h in relevant
if (h.id.strip().upper().removeprefix("H-").lstrip("0") or "0") == norm_hyp_id
]
else:
research_targets = relevant
researched = [h for h in research_targets if h.cve_research.strip() and h.exploit_research.strip()]
if len(researched) < len(research_targets):
missing = []
for h in relevant:
for h in research_targets:
if h.cve_research.strip() and h.exploit_research.strip():
continue
fields = []
if not h.cve_research.strip():
fields.append("CVE Research")
@@ -543,8 +560,10 @@ def check_hypothesis_freshness(
result.add_error(
"online research must be attempted and recorded before exploit execution; "
+ "; ".join(missing)
+ ". Record each query/source and outcome; 'no results', 'not applicable', "
"or 'source unavailable' are valid outcomes when truthful."
+ ". Record each query/source/outcome via violin_record_hypothesis "
"id=H-00N cve_research='...' exploit_research='...''no results', "
"'not applicable', or 'source unavailable' are valid outcomes when "
"truthful."
)
return result
@@ -746,7 +765,7 @@ def check_command(args: CheckCommandArgs) -> CheckResult:
phase,
args.command,
args.target,
hypothesis_id=active_task_hyp_id,
hypothesis_id=args.hypothesis_id or active_task_hyp_id,
match_command_target=not research_primary,
)
result.errors.extend(hyp_result.errors)
+1
View File
@@ -92,6 +92,7 @@ def _check_command_internal(a) -> cmd_module.CheckResult:
scope=a.get("scope", ""),
target=a.get("target"),
session_id=a.get("session_id"),
hypothesis_id=a.get("hypothesis_id"),
)
)
try:
+7
View File
@@ -30,6 +30,13 @@ class CheckCommandArgsModel(BaseModel):
),
)
session_id: str = ""
hypothesis_id: str = Field(
"",
description=(
"Optional hypothesis the command targets (H-00N). When provided, exploit-phase "
"guards (e.g. online-research requirement) check only this hypothesis."
),
)
class RecordPttArgsModel(BaseModel):