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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Image and file parts in OpenAI Agents user messages, on both the Responses and Chat Completions
paths, are now recorded as content blocks instead of a JSON dump of the whole message, including
through `SplunkAOLogger(ingestion_hook=...)`.
- CrewAI run IDs are now derived from SHA-256 (as RFC 4122 version 4 UUIDs) instead of MD5, resolving a
SAST finding. IDs for the same inputs differ from earlier releases.

## [0.5.0]

Expand Down
4 changes: 2 additions & 2 deletions src/splunk_ao/handlers/crewai/handler.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import hashlib
import json
import logging
from collections.abc import Callable
Expand All @@ -15,6 +14,7 @@
from splunk_ao.logger import SplunkAOLogger
from splunk_ao.schema.handlers import NodeType
from splunk_ao.utils.serialization import convert_time_delta_to_ns, serialize_to_str
from splunk_ao.utils.uuid_utils import deterministic_uuid4

_logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -436,7 +436,7 @@ def on_memory_retrieval_failed(source: Any, event: MemoryRetrievalFailedEvent) -
@staticmethod
def _hash_to_uuid(value: str) -> UUID:
"""Hash a string to a deterministic UUID."""
return UUID(hashlib.md5(value.encode()).hexdigest())
return deterministic_uuid4(value)

def _generate_run_id(self, source: Any, event: Any) -> UUID:
"""Generate a consistent UUID for event tracing."""
Expand Down
26 changes: 21 additions & 5 deletions src/splunk_ao/utils/uuid_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,26 @@ def is_uuid7(uuid_obj: UUID | str) -> bool:
return False


def deterministic_uuid4(value: str) -> UUID:
"""
Derive a deterministic UUID4 from a string.

The first 16 bytes of the SHA-256 digest of the UTF-8 encoded string are used,
with the RFC 4122 version and variant bits set.

Parameters
----------
value : str
The string to derive the UUID from.

Returns
-------
UUID
A version 4 UUID that is stable for a given input.
"""
return UUID(bytes=hashlib.sha256(value.encode()).digest()[:16], version=4)


def uuid7_to_uuid4(uuid_obj: UUID | str) -> UUID:
"""
Convert a UUID7 to a UUID4 by hashing its string representation.
Expand Down Expand Up @@ -67,11 +87,7 @@ def uuid7_to_uuid4(uuid_obj: UUID | str) -> UUID:
if uuid_obj.version != 7:
raise ValueError(f"Input UUID is version {uuid_obj.version}, not version 7")

# Hash the UUID string using SHA-256
hash_digest = hashlib.sha256(uuid_str.encode()).digest()

# Use the first 16 bytes of the hash to create a version 4 UUID
new_uuid = UUID(bytes=hash_digest[:16], version=4)
new_uuid = deterministic_uuid4(uuid_str)

_logger.debug(f"Converted UUID7 {uuid_str} to UUID4 {new_uuid}")
return new_uuid
Expand Down
15 changes: 15 additions & 0 deletions tests/test_crewai_handler.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import asyncio
import hashlib
import inspect
import sys
import threading
Expand Down Expand Up @@ -991,6 +992,20 @@ def test_generate_run_id_with_source_id(crewai_callback) -> None:
assert result == source_id


def test_hash_to_uuid_is_sha256_v4() -> None:
# Given: a fixed input string
value = "agent_task"

# When: it is hashed to a run ID
result = CrewAIEventListener._hash_to_uuid(value)

# Then: the ID is the stable SHA-256-derived UUIDv4
assert result == uuid.UUID("6b088e07-2934-42ca-8982-7184d89520ca")
assert result == uuid.UUID(bytes=hashlib.sha256(value.encode()).digest()[:16], version=4)
assert result.version == 4
assert result == CrewAIEventListener._hash_to_uuid(value)


def test_generate_run_id_with_messages(crewai_callback) -> None:
"""Test UUID generation from event messages."""
messages = [{"role": "user", "content": "test"}]
Expand Down
Loading