mirror of
https://github.com/usestrix/strix.git
synced 2026-08-21 18:52:47 +02:00
Argument parser: - Delete ``strix/tools/argument_parser.py`` and its tests. The SDK validates and types tool arguments via Pydantic before they hit our wrappers, and the in-container tool server receives JSON-typed kwargs over the wire. The string-coercion belt-and-suspenders is no longer pulling its weight. XML → JSON / typed structures: - ``create_vulnerability_report``: ``cvss_breakdown`` is now a ``dict[str, str]`` of the 8 metrics; ``code_locations`` is a ``list[dict]``. No more XML parsing in the tool or the renderer. - ``check_duplicate``: the dedup judge now emits a single JSON object instead of an ``<dedupe_result>`` block. Strict JSON parser handles optional code-fence wrappers. - ``agent_finish``: completion report posted to the parent inbox is a JSON object (``kind``, ``from``, ``agent_id``, ``success``, ``summary``, ``findings``, ``recommendations``) rather than a hand-rolled ``<agent_completion_report>`` XML envelope. - ``create_agent``: identity preamble + inherited-context markers are plain bracketed labels rather than ``<agent_delegation>`` / ``<inherited_context_from_parent>`` envelopes. - ``inject_messages_filter``: peer messages get a ``[Message from agent <id> | type=... | priority=...]`` header line instead of an ``<inter_agent_message>`` envelope. - Crash + system-warning messages: bracketed labels, no XML. - System prompt: the inter-agent block now describes the new header format and drops the "never echo XML envelope" rule. - ``strix/llm/utils.py``: deleted. ``clean_content`` collapsed into a one-line blank-line normalizer in the agent-message renderer (the XML envelope scrub had nothing left to scrub). Tests updated to match the new shapes.
200 lines
6.3 KiB
Python
200 lines
6.3 KiB
Python
import json
|
|
import logging
|
|
from typing import Any
|
|
|
|
import litellm
|
|
|
|
from strix.config.config import resolve_llm_config
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
DEDUPE_SYSTEM_PROMPT = """You are an expert vulnerability report deduplication judge.
|
|
Your task is to determine if a candidate vulnerability report describes the SAME vulnerability
|
|
as any existing report.
|
|
|
|
CRITICAL DEDUPLICATION RULES:
|
|
|
|
1. SAME VULNERABILITY means:
|
|
- Same root cause (e.g., "missing input validation" not just "SQL injection")
|
|
- Same affected component/endpoint/file (exact match or clear overlap)
|
|
- Same exploitation method or attack vector
|
|
- Would be fixed by the same code change/patch
|
|
|
|
2. NOT DUPLICATES if:
|
|
- Different endpoints even with same vulnerability type (e.g., SQLi in /login vs /search)
|
|
- Different parameters in same endpoint (e.g., XSS in 'name' vs 'comment' field)
|
|
- Different root causes (e.g., stored XSS vs reflected XSS in same field)
|
|
- Different severity levels due to different impact
|
|
- One is authenticated, other is unauthenticated
|
|
|
|
3. ARE DUPLICATES even if:
|
|
- Titles are worded differently
|
|
- Descriptions have different level of detail
|
|
- PoC uses different payloads but exploits same issue
|
|
- One report is more thorough than another
|
|
- Minor variations in technical analysis
|
|
|
|
COMPARISON GUIDELINES:
|
|
- Focus on the technical root cause, not surface-level similarities
|
|
- Same vulnerability type (SQLi, XSS) doesn't mean duplicate - location matters
|
|
- Consider the fix: would fixing one also fix the other?
|
|
- When uncertain, lean towards NOT duplicate
|
|
|
|
FIELDS TO ANALYZE:
|
|
- title, description: General vulnerability info
|
|
- target, endpoint, method: Exact location of vulnerability
|
|
- technical_analysis: Root cause details
|
|
- poc_description: How it's exploited
|
|
- impact: What damage it can cause
|
|
|
|
Respond with a single JSON object and nothing else:
|
|
|
|
{
|
|
"is_duplicate": true,
|
|
"duplicate_id": "vuln-0001",
|
|
"confidence": 0.95,
|
|
"reason": "Both reports describe SQL injection in /api/login via the username parameter"
|
|
}
|
|
|
|
Or, if not a duplicate:
|
|
|
|
{
|
|
"is_duplicate": false,
|
|
"duplicate_id": "",
|
|
"confidence": 0.90,
|
|
"reason": "Different endpoints: candidate is /api/search, existing is /api/login"
|
|
}
|
|
|
|
Rules:
|
|
- ``is_duplicate`` is a boolean.
|
|
- ``duplicate_id`` is the exact id from existing reports, or "" if not a duplicate.
|
|
- ``confidence`` is a number between 0 and 1.
|
|
- ``reason`` is a specific explanation mentioning endpoint/parameter/root cause.
|
|
- Output ONLY the JSON object — no surrounding prose, no code fences."""
|
|
|
|
|
|
def _prepare_report_for_comparison(report: dict[str, Any]) -> dict[str, Any]:
|
|
relevant_fields = [
|
|
"id",
|
|
"title",
|
|
"description",
|
|
"impact",
|
|
"target",
|
|
"technical_analysis",
|
|
"poc_description",
|
|
"endpoint",
|
|
"method",
|
|
]
|
|
|
|
cleaned = {}
|
|
for field in relevant_fields:
|
|
if report.get(field):
|
|
value = report[field]
|
|
if isinstance(value, str) and len(value) > 8000:
|
|
value = value[:8000] + "...[truncated]"
|
|
cleaned[field] = value
|
|
|
|
return cleaned
|
|
|
|
|
|
def _parse_dedupe_response(content: str) -> dict[str, Any]:
|
|
text = content.strip()
|
|
if text.startswith("```"):
|
|
text = text.strip("`")
|
|
if text.lower().startswith("json"):
|
|
text = text[4:]
|
|
text = text.strip()
|
|
start = text.find("{")
|
|
end = text.rfind("}")
|
|
if start == -1 or end == -1 or end <= start:
|
|
raise ValueError(f"No JSON object found in dedupe response: {content[:500]}")
|
|
parsed = json.loads(text[start : end + 1])
|
|
|
|
duplicate_id = str(parsed.get("duplicate_id") or "")[:64]
|
|
reason = str(parsed.get("reason") or "")[:500]
|
|
try:
|
|
confidence = float(parsed.get("confidence", 0.0))
|
|
except (TypeError, ValueError):
|
|
confidence = 0.0
|
|
|
|
return {
|
|
"is_duplicate": bool(parsed.get("is_duplicate", False)),
|
|
"duplicate_id": duplicate_id,
|
|
"confidence": confidence,
|
|
"reason": reason,
|
|
}
|
|
|
|
|
|
def check_duplicate(
|
|
candidate: dict[str, Any], existing_reports: list[dict[str, Any]]
|
|
) -> dict[str, Any]:
|
|
if not existing_reports:
|
|
return {
|
|
"is_duplicate": False,
|
|
"duplicate_id": "",
|
|
"confidence": 1.0,
|
|
"reason": "No existing reports to compare against",
|
|
}
|
|
|
|
try:
|
|
candidate_cleaned = _prepare_report_for_comparison(candidate)
|
|
existing_cleaned = [_prepare_report_for_comparison(r) for r in existing_reports]
|
|
|
|
comparison_data = {"candidate": candidate_cleaned, "existing_reports": existing_cleaned}
|
|
|
|
model_name, api_key, api_base = resolve_llm_config()
|
|
litellm_model: str | None = model_name
|
|
|
|
messages = [
|
|
{"role": "system", "content": DEDUPE_SYSTEM_PROMPT},
|
|
{
|
|
"role": "user",
|
|
"content": (
|
|
f"Compare this candidate vulnerability against existing reports:\n\n"
|
|
f"{json.dumps(comparison_data, indent=2)}\n\n"
|
|
f"Respond with ONLY the JSON object described in the system prompt."
|
|
),
|
|
},
|
|
]
|
|
|
|
completion_kwargs: dict[str, Any] = {
|
|
"model": litellm_model,
|
|
"messages": messages,
|
|
"timeout": 120,
|
|
}
|
|
if api_key:
|
|
completion_kwargs["api_key"] = api_key
|
|
if api_base:
|
|
completion_kwargs["api_base"] = api_base
|
|
|
|
response = litellm.completion(**completion_kwargs)
|
|
|
|
content = response.choices[0].message.content
|
|
if not content:
|
|
return {
|
|
"is_duplicate": False,
|
|
"duplicate_id": "",
|
|
"confidence": 0.0,
|
|
"reason": "Empty response from LLM",
|
|
}
|
|
|
|
result = _parse_dedupe_response(content)
|
|
|
|
logger.info(
|
|
f"Deduplication check: is_duplicate={result['is_duplicate']}, "
|
|
f"confidence={result['confidence']}, reason={result['reason'][:100]}"
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.exception("Error during vulnerability deduplication check")
|
|
return {
|
|
"is_duplicate": False,
|
|
"duplicate_id": "",
|
|
"confidence": 0.0,
|
|
"reason": f"Deduplication check failed: {e}",
|
|
"error": str(e),
|
|
}
|
|
else:
|
|
return result
|