Skip to content

Attack history filters and dropdowns miss rows stored with the legacy identifier layout #4282

Open

Description

Describe the bug

Attack rows written before the atomic attack identifier grew its nested structure keep the attack class name and converter lists at $.children.attack instead of $.children.attack_technique.children.attack. The SDK displays these rows correctly (get_attack_strategy_identifier falls back to the older layout) and the analytics queries resolve the same fallback, but the attack history filters and the options endpoints only read the nested layout, so for these rows:

  • filtering GET /api/attacks by the attack type shown in the list returns nothing,
  • GET /api/attacks/attack-options omits the type, so the dropdown cannot even produce the value,
  • has_converters=true drops rows whose older layout records converters, and converter_types cannot find them.

Steps/Code to Reproduce

import asyncio, uuid
from datetime import UTC, datetime
from pyrit.memory.memory_models import AttackResultEntry
from pyrit.memory.sqlite_memory import SQLiteMemory

async def main():
    memory = SQLiteMemory(db_path=":memory:", silent=True)
    await memory.initialize_async()
    legacy = {
        "class_name": "AtomicAttack",
        "children": {"attack": {
            "class_name": "LegacyAttack",
            "children": {"request_converters": [{"class_name": "LegacyRequest"}]},
        }},
    }
    async with memory._get_async_engine().begin() as connection:
        await connection.execute(AttackResultEntry.__table__.insert().values(
            id=uuid.UUID(int=1), conversation_id=str(uuid.UUID(int=2)),
            objective="legacy objective", outcome="undetermined",
            timestamp=datetime(2026, 1, 1, tzinfo=UTC),
            atomic_attack_identifier=legacy, atomic_attack_identifier_hash=None))
    rows = await memory.get_attack_results_async()
    print("displayed type:", rows[0].get_attack_strategy_identifier().class_name)   # LegacyAttack
    print("filter by it:", len(await memory.get_attack_results_async(attack_classes=["LegacyAttack"])))
    print("options:", await memory.get_unique_attack_class_names_async())
    print("has converters:", len(await memory.get_attack_results_async(has_converters=True)))

asyncio.run(main())

Expected Results

The displayed type filters its own row, appears in the options list, and the row's recorded converter keeps it in has_converters=true, matching what analytics already does for the same stored shape.

Actual Results

The filter returns 0 rows, the options list is empty, and has_converters=true returns 0 rows even though the row records a request converter.

Screenshots

Not applicable.

Versions

OS: macOS
Python version: 3.13
PyRIT version: main at 165bdbd

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions