From 04007dd28a6eea2c3d4eedb1e060fb75cea18685 Mon Sep 17 00:00:00 2001 From: etserend Date: Wed, 21 Oct 2026 11:39:03 -0500 Subject: [PATCH 1/5] fix(crewai): replace MD5 with SHA-256 for deterministic run IDs (VULN-115244) Co-Authored-By: Claude Code --- src/splunk_ao/handlers/crewai/handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/splunk_ao/handlers/crewai/handler.py b/src/splunk_ao/handlers/crewai/handler.py index 6adc870c..0a0909c6 100644 --- a/src/splunk_ao/handlers/crewai/handler.py +++ b/src/splunk_ao/handlers/crewai/handler.py @@ -287,7 +287,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 UUID(bytes=hashlib.sha256(value.encode()).digest()[:16]) def _generate_run_id(self, source: Any, event: Any) -> UUID: """Generate a consistent UUID for event tracing.""" From d5dc509a02051708ca54ecaac6d253a3754a7078 Mon Sep 17 00:00:00 2001 From: etserend Date: Wed, 21 Oct 2026 13:36:20 -0500 Subject: [PATCH 2/5] fix(crewai): set UUID version bits in _hash_to_uuid (VULN-115244) Co-Authored-By: Claude Code --- src/splunk_ao/handlers/crewai/handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/splunk_ao/handlers/crewai/handler.py b/src/splunk_ao/handlers/crewai/handler.py index 0a0909c6..e37d86ee 100644 --- a/src/splunk_ao/handlers/crewai/handler.py +++ b/src/splunk_ao/handlers/crewai/handler.py @@ -287,7 +287,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(bytes=hashlib.sha256(value.encode()).digest()[:16]) + return UUID(bytes=hashlib.sha256(value.encode()).digest()[:16], version=4) def _generate_run_id(self, source: Any, event: Any) -> UUID: """Generate a consistent UUID for event tracing.""" From 6efd50d4458678929a8b5e1849070b65a955cc16 Mon Sep 17 00:00:00 2001 From: etserend Date: Wed, 21 Oct 2026 13:52:41 -0500 Subject: [PATCH 3/5] refactor(uuid): share deterministic_uuid4 and add regression test (VULN-115244) Co-Authored-By: Claude Code --- src/splunk_ao/handlers/crewai/handler.py | 4 ++-- src/splunk_ao/utils/uuid_utils.py | 26 +++++++++++++++++++----- tests/test_crewai_handler.py | 15 ++++++++++++++ 3 files changed, 38 insertions(+), 7 deletions(-) diff --git a/src/splunk_ao/handlers/crewai/handler.py b/src/splunk_ao/handlers/crewai/handler.py index e37d86ee..bea714ff 100644 --- a/src/splunk_ao/handlers/crewai/handler.py +++ b/src/splunk_ao/handlers/crewai/handler.py @@ -1,4 +1,3 @@ -import hashlib import json import logging from datetime import datetime @@ -11,6 +10,7 @@ from splunk_ao.logger import SplunkAOLogger from splunk_ao.schema.handlers import NodeType from splunk_ao.utils.serialization import serialize_to_str +from splunk_ao.utils.uuid_utils import deterministic_uuid4 _logger = logging.getLogger(__name__) @@ -287,7 +287,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(bytes=hashlib.sha256(value.encode()).digest()[:16], version=4) + return deterministic_uuid4(value) def _generate_run_id(self, source: Any, event: Any) -> UUID: """Generate a consistent UUID for event tracing.""" diff --git a/src/splunk_ao/utils/uuid_utils.py b/src/splunk_ao/utils/uuid_utils.py index 460a5eda..62d8c640 100644 --- a/src/splunk_ao/utils/uuid_utils.py +++ b/src/splunk_ao/utils/uuid_utils.py @@ -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. @@ -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 diff --git a/tests/test_crewai_handler.py b/tests/test_crewai_handler.py index 96d36e10..5545d423 100644 --- a/tests/test_crewai_handler.py +++ b/tests/test_crewai_handler.py @@ -1,3 +1,4 @@ +import hashlib import sys import uuid from datetime import datetime @@ -211,6 +212,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"}] From a66ec13c3f408e0defa90157867f0aa572834bfb Mon Sep 17 00:00:00 2001 From: etserend Date: Wed, 21 Oct 2026 14:24:34 -0500 Subject: [PATCH 4/5] docs(changelog): note CrewAI run ID hash change (VULN-115244) Co-Authored-By: Claude Code --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72556781..b61e43c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,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] From 13229e191af34f9caaaca59460dcce06b6b1a344 Mon Sep 17 00:00:00 2001 From: etserend Date: Fri, 30 Oct 2026 10:38:03 -0500 Subject: [PATCH 5/5] test(uuid): pin deterministic_uuid4 outputs and move CHANGELOG entry (VULN-115244) Add pinned-value tests for deterministic_uuid4 and uuid7_to_uuid4 so they run on every Python version (the CrewAI module is skipped on 3.14), and move the CrewAI run ID entry from the released 0.6.0 section to Unreleased. Co-Authored-By: Claude Code --- CHANGELOG.md | 7 +++++-- tests/test_uuid_utils.py | 25 ++++++++++++++++++++++++- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 06ca243e..af0e5b02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- 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.6.1] ### Fixed @@ -49,8 +54,6 @@ 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] diff --git a/tests/test_uuid_utils.py b/tests/test_uuid_utils.py index 85bd5ab3..00a0f378 100644 --- a/tests/test_uuid_utils.py +++ b/tests/test_uuid_utils.py @@ -6,7 +6,7 @@ import pytest -from splunk_ao.utils.uuid_utils import convert_uuid_if_uuid7, uuid7_to_uuid4 +from splunk_ao.utils.uuid_utils import convert_uuid_if_uuid7, deterministic_uuid4, uuid7_to_uuid4 @pytest.fixture @@ -47,3 +47,26 @@ def test_deterministic_conversion(uuid7: UUID) -> None: # All results should be identical assert result1 == result2 == result3 assert result1.version == 4 + + +def test_deterministic_uuid4_matches_pinned_sha256_value() -> None: + # Given: a fixed input string + value = "agent_task" + + # When: it is converted to a UUID + result = deterministic_uuid4(value) + + # Then: the ID is the pinned SHA-256-derived UUIDv4, independent of the implementation + assert result == UUID("6b088e07-2934-42ca-8982-7184d89520ca") + assert result.version == 4 + assert result == deterministic_uuid4(value) + + +def test_uuid7_to_uuid4_matches_pinned_sha256_value(uuid7: UUID) -> None: + # Given: a fixed UUIDv7 + # When: it is converted to a UUIDv4 + result = uuid7_to_uuid4(uuid7) + + # Then: the ID is the pinned SHA-256-derived UUIDv4, so a change to the shared helper is caught + assert result == UUID("a88ebe92-6503-45b7-bd2f-8ea97b486f95") + assert result.version == 4