Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions strix/report/state.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import asyncio
import json
import logging
import re
Expand Down Expand Up @@ -210,6 +211,9 @@ def __init__(self, run_name: str | None = None):
self.end_time: str | None = None

self.vulnerability_reports: list[dict[str, Any]] = []
# Held while a new finding is checked against the stored ones and then added,
# so two agents filing the same finding at once cannot both pass the check.
self.dedupe_lock = asyncio.Lock()
self.final_scan_result: str | None = None

self.scan_results: dict[str, Any] | None = None
Expand Down
127 changes: 67 additions & 60 deletions strix/tools/reporting/tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -872,7 +872,6 @@ async def _do_create(

from strix.report.dedupe import check_duplicate

existing = report_state.get_existing_vulnerabilities()
candidate = {
"title": title,
"description": description,
Expand Down Expand Up @@ -913,30 +912,35 @@ async def _do_create(
"http_exchange_ids": normalized_http_exchange_ids,
}

dedupe = await check_duplicate(candidate, existing)
if dedupe.get("is_duplicate"):
duplicate_id = str(dedupe.get("duplicate_id") or "")
duplicate_title = next(
(r.get("title", "Unknown") for r in existing if r.get("id") == duplicate_id),
"",
# The duplicate check awaits an LLM call. Without a lock, two agents filing
# the same finding at once each compare against a list the other has not
# joined yet and both store it, so read, check and store as one step.
async with report_state.dedupe_lock:
existing = report_state.get_existing_vulnerabilities()
dedupe = await check_duplicate(candidate, existing)
if dedupe.get("is_duplicate"):
duplicate_id = str(dedupe.get("duplicate_id") or "")
duplicate_title = next(
(r.get("title", "Unknown") for r in existing if r.get("id") == duplicate_id),
"",
)
return {
"success": False,
"error": (
f"Potential duplicate of '{duplicate_title}' "
f"(id={duplicate_id[:8]}...) — do not re-report the same vulnerability"
),
"duplicate_of": duplicate_id,
"duplicate_title": duplicate_title,
"confidence": dedupe.get("confidence", 0.0),
"reason": dedupe.get("reason", ""),
}

report_id = report_state.add_vulnerability_report(
**report_fields,
agent_id=agent_id if isinstance(agent_id, str) else None,
agent_name=agent_name if isinstance(agent_name, str) else None,
)
return {
"success": False,
"error": (
f"Potential duplicate of '{duplicate_title}' "
f"(id={duplicate_id[:8]}...) — do not re-report the same vulnerability"
),
"duplicate_of": duplicate_id,
"duplicate_title": duplicate_title,
"confidence": dedupe.get("confidence", 0.0),
"reason": dedupe.get("reason", ""),
}

report_id = report_state.add_vulnerability_report(
**report_fields,
agent_id=agent_id if isinstance(agent_id, str) else None,
agent_name=agent_name if isinstance(agent_name, str) else None,
)
except Exception as e:
logger.exception("create_vulnerability_report persistence failed")
return {
Expand Down Expand Up @@ -2024,7 +2028,6 @@ async def _do_create_dependency( # noqa: PLR0912, PLR0915

from strix.report.dedupe import check_duplicate

existing = report_state.get_existing_vulnerabilities()
candidate = {
"title": title,
"description": description,
Expand All @@ -2033,41 +2036,45 @@ async def _do_create_dependency( # noqa: PLR0912, PLR0915
"dependency_metadata": dependency_metadata,
"technical_analysis": technical_analysis,
}
dedupe = await check_duplicate(candidate, existing)
if dedupe.get("is_duplicate"):
duplicate_id = dedupe.get("duplicate_id", "")
return {
"success": False,
"error": (
f"Potential duplicate (id={duplicate_id[:8]}...) — "
"do not re-report the same dependency finding"
),
"duplicate_of": duplicate_id,
"confidence": dedupe.get("confidence", 0.0),
"reason": dedupe.get("reason", ""),
}

report_id = report_state.add_vulnerability_report(
title=title,
description=description,
severity=severity,
impact=impact,
target=target,
technical_analysis=technical_analysis,
remediation_steps=remediation_steps,
evidence=evidence,
assumptions=assumptions,
fix_effort=fix_effort,
cvss=cvss_score if advisory_cvss is not None else None,
cve=parsed_cve,
cwe=cwe,
code_locations=parsed_locations,
fix_verification=fix_verification,
finding_class="dependency_cve",
dependency_metadata=dependency_metadata,
agent_id=agent_id if isinstance(agent_id, str) else None,
agent_name=agent_name if isinstance(agent_name, str) else None,
)
# A same-CVE report without package metadata can send dependency
# deduplication through the model too, so it shares the filing lock.
async with report_state.dedupe_lock:
existing = report_state.get_existing_vulnerabilities()
dedupe = await check_duplicate(candidate, existing)
if dedupe.get("is_duplicate"):
duplicate_id = dedupe.get("duplicate_id", "")
return {
"success": False,
"error": (
f"Potential duplicate (id={duplicate_id[:8]}...) — "
"do not re-report the same dependency finding"
),
"duplicate_of": duplicate_id,
"confidence": dedupe.get("confidence", 0.0),
"reason": dedupe.get("reason", ""),
}

report_id = report_state.add_vulnerability_report(
title=title,
description=description,
severity=severity,
impact=impact,
target=target,
technical_analysis=technical_analysis,
remediation_steps=remediation_steps,
evidence=evidence,
assumptions=assumptions,
fix_effort=fix_effort,
cvss=cvss_score if advisory_cvss is not None else None,
cve=parsed_cve,
cwe=cwe,
code_locations=parsed_locations,
fix_verification=fix_verification,
finding_class="dependency_cve",
dependency_metadata=dependency_metadata,
agent_id=agent_id if isinstance(agent_id, str) else None,
agent_name=agent_name if isinstance(agent_name, str) else None,
)
except Exception as e:
logger.exception("create_dependency_report persistence failed")
return {
Expand Down
Loading