Skip to content

AttackResultEntry.timestamp is dropped on hydration — backend UI shows datetime.now() instead of real row time #1651

@thirteeneight

Description

@thirteeneight

Describe the bug

AttackResultEntry (the SQLAlchemy model in
pyrit/memory/memory_models.py) persists a non-nullable timestamp
column, but AttackResultEntry.get_attack_result() does not copy that
column onto the hydrated AttackResult. The domain AttackResult
dataclass has no timestamp field at all.

Downstream, attack_result_to_summary in
pyrit/backend/mappers/attack_mappers.py looks up
ar.metadata["created_at"] and, when absent (PyRIT's own attack
engines never set it), falls back to datetime.now(timezone.utc).

Net effect: the backend UI shows today's date under "Created" for
every AttackResult, regardless of when it was actually persisted.

Relevant locations on main:

  • pyrit/models/attack_result.pyAttackResult dataclass has no timestamp field.
  • pyrit/memory/memory_models.pyAttackResultEntry.timestamp column is nullable=False; get_attack_result() rebuilds AttackResult without passing timestamp
    through.
  • pyrit/backend/mappers/attack_mappers.pyattack_result_to_summary uses datetime.now(timezone.utc) as the fallback.

Steps/Code to Reproduce

from datetime import datetime, timezone
from unittest.mock import MagicMock
                                                                                                                                                                             
from pyrit.memory.memory_models import AttackResultEntry
from pyrit.models.attack_result import AttackResult, AttackOutcome                                                                                                           
from pyrit.backend.mappers.attack_mappers import attack_result_to_summary
                                                                                                                                                                             
ar = AttackResult(
    conversation_id="c1",                                                                                                                                                    
    objective="demo",                                                
    outcome=AttackOutcome.SUCCESS,
)                                                                                                                                                                            
entry = AttackResultEntry(entry=ar)
                                                                                                                                                                             
persisted_ts = datetime(2026, 4, 17, 12, 0, 0, tzinfo=timezone.utc)  
entry.timestamp = persisted_ts                                                                                                                                               
 
hydrated = entry.get_attack_result()                                                                                                                                         
stats = MagicMock(message_count=0, last_message_preview="", labels=None)
summary = attack_result_to_summary(hydrated, stats=stats)                                                                                                                    
 
print(f"Persisted at:     {persisted_ts}")                                                                                                                                   
print(f"Shown in summary: {summary.created_at}")                     
print(f"hydrated has 'timestamp' attr: {hasattr(hydrated, 'timestamp')}")                                                                                                    
assert summary.created_at == persisted_ts, "created_at should reflect DB timestamp"

Expected Results

summary.created_at equals persisted_ts (2026-04-17 12:00:00+00:00).
The assertion passes.

Actual Results

Persisted at: 2026-04-17 12:00:00+00:00
Shown in summary: 2026-04-24 12:10:29.669801+00:00
hydrated has 'timestamp' attr: False
AssertionError: created_at should reflect DB timestamp

summary.created_at is datetime.now(timezone.utc) at the moment the
mapper runs. hydrated does not even carry a timestamp attribute,
because AttackResult has no such field.

Screenshots

N/A — reproducible without the SPA.

Versions

  • OS: macOS 26.4.1 (arm64); also expected to reproduce on any OS — the bug is OS-independent.
  • Python version: 3.14.3
  • PyRIT version: main in editable mode, 0.14.0.dev0. Also reproduces on 0.13.0.
  • pyrit.show_versions() output:
    System:
    python: 3.14.3 (main, Feb 3 2026, 15:32:20) [Clang 17.0.0 (clang-1700.6.3.2)]
    executable: /path/to/.venv/bin/python
    machine: macOS-26.4.1-arm64-arm-64bit-Mach-O
Python dependencies:
      pyrit: 0.14.0.dev0                                           
     Cython: None                                                                                                                                                          
      numpy: 2.4.4                                                 
     openai: 2.32.0
  packaging: 26.1
        pip: 26.0.1                                                                                                                                                        
      scipy: 1.17.1
 setuptools: 82.0.1                                                                                                                                                        
    sqlite3: None                                                  
      torch: None

transformers: 5.6.2

Proposed fix (happy to submit a PR if accepted)

Three small, additive changes:

  1. Add an optional timestamp: Optional[datetime] = None field to the
    AttackResult dataclass.
  2. Pass timestamp=_ensure_utc(self.timestamp) in
    AttackResultEntry.get_attack_result(). The _ensure_utc helper
    already exists in memory_models.py and is used by sibling
    hydrators.
  3. In attack_result_to_summary, consult ar.timestamp before falling
    back to datetime.now(timezone.utc). Keep the
    metadata["created_at"] path for callers that set it explicitly.

Backwards compatibility:

  • Adding an optional dataclass field is additive.
  • AttackResultEntries.timestamp has been non-nullable since the
    entry was introduced, so every existing row already has a value to
    hydrate from.
  • Callers that construct AttackResult(...) without specifying
    timestamp get None, which the mapper treats the same way it
    already treats missing metadata["created_at"].

Tests would be added to tests/unit/models/test_attack_result.py and
tests/unit/backend/test_mappers.py covering (a) round-trip of
timestamp through AttackResult → AttackResultEntry → AttackResult
and (b) the mapper preferring ar.timestamp over datetime.now().

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