mirror of
https://github.com/usestrix/strix.git
synced 2026-10-16 04:28:23 +00:00
fix(agents): prevent lost subagent findings and warn on unfiled reports
In multi-agent pentests (e.g. Issue #1439 against DVWA), subagents frequently discover exploitable vulnerabilities and supply them as narrative strings in agent_finish(findings=[...]) without first calling create_vulnerability_report. Because parent agents strictly rely on filed_report_ids from authoritative state and avoid refiling from narrative text, these findings are silently dropped, causing scans of vulnerable targets to complete with 0 findings. 1. In child_initial_input, explicitly instruct subagents to file confirmed vulnerabilities via create_vulnerability_report before calling agent_finish, clarifying that passing findings to agent_finish does not persist reports. 2. In agent_finish, when findings is non-empty but filed_report_ids is empty, attach an explicit warning to the tool response payload and the rendered completion report delivered to the parent inbox. 3. Add regression tests in test_agent_graph_coordination.py and test_inputs.py.
This commit is contained in:
parent
62b496430d
commit
b60224b662
4 changed files with 69 additions and 13 deletions
|
|
@ -364,8 +364,10 @@ def child_initial_input(
|
|||
)
|
||||
parts.append(
|
||||
f"You are agent {name} ({child_id}); your parent is {parent_id}. "
|
||||
"Maintain your own identity. Call agent_finish when your task "
|
||||
"is complete.",
|
||||
"Maintain your own identity. File any confirmed vulnerabilities using "
|
||||
"create_vulnerability_report (or create_dependency_report) BEFORE calling "
|
||||
"agent_finish — passing findings to agent_finish does not file reports. "
|
||||
"Call agent_finish when your task is complete.",
|
||||
)
|
||||
parts.append(task)
|
||||
return [{"role": "user", "content": "\n\n".join(parts)}]
|
||||
|
|
|
|||
|
|
@ -101,6 +101,12 @@ def _render_completion_report(
|
|||
lines.extend(_render_filed_report(r) for r in filed_reports)
|
||||
else:
|
||||
lines.append("- (none)")
|
||||
if findings:
|
||||
lines.append(
|
||||
" [WARNING: This agent reported narrative findings above but filed 0 "
|
||||
"authoritative vulnerability reports. Unreported findings are not included "
|
||||
"in scan results or SARIF unless filed via create_vulnerability_report.]"
|
||||
)
|
||||
lines.append("")
|
||||
lines.append("Open items (unresolved, need follow-up):")
|
||||
if open_items:
|
||||
|
|
@ -693,6 +699,7 @@ async def agent_finish(
|
|||
|
||||
filed_reports = _filed_reports_by(me)
|
||||
filed_report_ids = [str(r.get("id")) for r in filed_reports]
|
||||
unfiled_findings = bool((findings or []) and not filed_report_ids)
|
||||
|
||||
parent_notified = False
|
||||
if report_to_parent and await coordinator.claim_parent_notice(me):
|
||||
|
|
@ -718,6 +725,7 @@ async def agent_finish(
|
|||
"type": "completion",
|
||||
"priority": "high",
|
||||
"filed_report_ids": filed_report_ids,
|
||||
"unfiled_findings": unfiled_findings,
|
||||
},
|
||||
)
|
||||
parent_notified = True
|
||||
|
|
@ -735,19 +743,33 @@ async def agent_finish(
|
|||
len(filed_report_ids),
|
||||
parent_notified,
|
||||
)
|
||||
if unfiled_findings:
|
||||
logger.warning(
|
||||
"agent_finish: %s reported %d narrative finding(s) but filed 0 vulnerability reports",
|
||||
me,
|
||||
len(findings or []),
|
||||
)
|
||||
|
||||
response_payload: dict[str, Any] = {
|
||||
"success": True,
|
||||
"agent_completed": True,
|
||||
"parent_notified": parent_notified,
|
||||
"agent_id": me,
|
||||
"summary": result_summary,
|
||||
"filed_report_ids": filed_report_ids,
|
||||
"findings_count": len(findings or []),
|
||||
"open_items_count": len(open_items or []),
|
||||
"has_recommendations": bool(final_recommendations),
|
||||
}
|
||||
if unfiled_findings:
|
||||
response_payload["warning"] = (
|
||||
f"Reported {len(findings or [])} finding(s) but filed 0 vulnerability reports. "
|
||||
"Narrative findings in agent_finish do not register vulnerabilities in the scan "
|
||||
"report. Verified findings must be filed via create_vulnerability_report."
|
||||
)
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"agent_completed": True,
|
||||
"parent_notified": parent_notified,
|
||||
"agent_id": me,
|
||||
"summary": result_summary,
|
||||
"filed_report_ids": filed_report_ids,
|
||||
"findings_count": len(findings or []),
|
||||
"open_items_count": len(open_items or []),
|
||||
"has_recommendations": bool(final_recommendations),
|
||||
},
|
||||
response_payload,
|
||||
ensure_ascii=False,
|
||||
default=str,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -256,3 +256,33 @@ async def test_agent_finish_without_report_state_still_completes() -> None:
|
|||
|
||||
assert result["success"] is True
|
||||
assert result["filed_report_ids"] == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.usefixtures("report_state")
|
||||
async def test_agent_finish_warns_when_findings_reported_without_filing() -> None:
|
||||
coordinator = await _graph(interactive=False)
|
||||
|
||||
result = await _call(
|
||||
agent_finish,
|
||||
coordinator,
|
||||
"child",
|
||||
{
|
||||
"result_summary": "discovered injection",
|
||||
"findings": ["SQL injection on /login.php"],
|
||||
},
|
||||
parent_id="root",
|
||||
)
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["filed_report_ids"] == []
|
||||
assert "warning" in result
|
||||
assert "Reported 1 finding(s) but filed 0 vulnerability reports" in result["warning"]
|
||||
|
||||
delivered = coordinator.runtimes["root"].mailbox
|
||||
assert len(delivered) == 1
|
||||
assert delivered[0]["unfiled_findings"] is True
|
||||
body = delivered[0]["content"]
|
||||
assert "authoritative vulnerability reports" in body
|
||||
assert "create_vulnerability_report" in body
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ def test_child_initial_input_single_message_without_history() -> None:
|
|||
content = result[0]["content"]
|
||||
assert "agent scout (agent-2)" in content
|
||||
assert "Audit the login flow." in content
|
||||
assert "create_vulnerability_report" in content
|
||||
assert "Inherited context" not in content
|
||||
|
||||
|
||||
|
|
@ -49,6 +50,7 @@ def test_child_initial_input_single_message_with_history() -> None:
|
|||
assert "previous work" in content
|
||||
assert "agent scout (agent-2)" in content
|
||||
assert "Audit the login flow." in content
|
||||
assert "create_vulnerability_report" in content
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue