Scenario progress double-counts an attack result after its timestamp is bumped
Maintainers usually reply within 2 days
Assessment
- Difficulty
- 3/5
- Estimated time
- Half a day
- Newbie friendliness
- 25/100
Research direction
Start in ScenarioProgressReadModel._hydrate_new_deltas_async, where each page is appended with entry.deltas.extend(page) and nothing is deduplicated by attack_result_id. Then read get_scenario_attack_result_deltas_async to see how the keyset cursor over (timestamp, id) re-emits bumped rows. A re-emitted delta should update the cached copy of that attempt, not add a second one. Linked PR #3184 is already open against this, so check it before starting.
Written by the indexing model from the issue text.
Description
Describe the bug
The scenario progress read model double-counts an attack result whose row timestamp is later bumped.
get_scenario_attack_result_deltas_async pages rows in ascending keyset order over (timestamp, id), so any row whose timestamp moves forward compares greater than the stored cursor and is emitted again. That happens today: AttackService.update_attack_async and remove_human_score_async write "timestamp": datetime.now(UTC) on every edit, and promote_attack_conversation rewrites timestamps too.
ScenarioProgressReadModel._hydrate_new_deltas_async handles each page with entry.deltas.extend(page) and no dedup by attack_result_id, so the re-emitted row is appended as a second copy of the same attempt. The snapshot's results then contains the same attack twice, which inflates per-unit attempt counts and retry pressure for the lifetime of the cache entry.
Steps/Code to Reproduce
- Poll
GET /scenarios/{id}/progressonce so the read model caches the run's deltas and cursor. - Edit any attack result that belongs to that scenario (for example
PUT /attacks/{attack_result_id}), which bumps the row's timestamp. - Poll the scenario progress endpoint again.
The storage feed re-emits the edited row (keyset timestamp > cursor), and the read model appends it again.
Minimal reproduction against the read model:
memory.get_scenario_attack_result_deltas_async = AsyncMock(
side_effect=[([delta], False), ([bumped_delta_with_same_id], False)]
)
read_model = ScenarioProgressReadModel(memory=memory)
first = await read_model.get_snapshot_async(scenario_result_id="run", ...)
second = await read_model.get_snapshot_async(scenario_result_id="run", ...)
# second.results holds the same attack_result_id twice
Expected Results
A re-emitted delta updates the already cached copy of that attempt instead of adding a second one.
Actual Results
The same attack result id appears twice in the snapshot results, inflating attempt counts.
Screenshots
N/A
Versions
OS: macOS
Python version: 3.14
PyRIT version: installed from main (66c41fa)
- Dominant language
- Python
- Stars
- 4.6k
- Forks
- 944
- Avg merge
- 2d 16h
- Merged PRs (30d)
- 350
Getting set up
Starts the project's dev container in your browser, under your own GitHub account.
- No Dockerfile or Docker Compose file
- Has a pull request template
- No contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from microsoft/PyRIT
-
BUG: SQLInjectionOutputScorer misses standard inline comment bypasses ('-- and ' OR 1=1--)Possibly taken A pull request linked to this issue is open or already merged. Open
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
microsoft/PyRIT#3204 · 1 comment ·
Maintainers usually reply within 2 days
-
BUG: label queries only return the final conversation of an attackPossibly taken @u7k4rs6 claimed this 1 day ago. Open
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
microsoft/PyRIT#3095 · 4 comments · 1 assignee ·
Maintainers usually reply within 2 days
-
BUG Configuration keeps runtime-status errors after polling recoversPossibly taken @joeyvictorino claimed this 4 days ago. OpenBug: triage GUI help wanted
Difficulty 2/5 1-3 hours Newbie friendliness 86/100
microsoft/PyRIT#2868 · 4 comments · 1 assignee ·
Maintainers usually reply within 2 days
-
BUG: HTTPTarget sends conflicting Content-Length and Transfer-Encoding headersPossibly taken @Kaap10 claimed this 9 days ago. OpenBug: triage
Difficulty 2/5 1-3 hours Newbie friendliness 35/100
microsoft/PyRIT#3202 · 1 comment ·
Maintainers usually reply within 2 days
-
Difficulty 3/5 Half a day Newbie friendliness 25/100
Maintainers usually reply within 2 days
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 62/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
WingedGuardian/GENesis-AGI#3375 ·
Maintainers usually reply within 1 day
-
curriculum documentation quality
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
githubnext/gh-aw-workshop#4601 ·
Maintainers usually reply within 1 day
-
approved Good First Issue scope: docs
Difficulty 1/5 Under an hour Newbie friendliness 85/100
hiero-ledger/hiero-sdk-python#2730 · 3 comments ·
Maintainers usually reply within 1 day