Skip to content
Merged
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
24 changes: 24 additions & 0 deletions src/sap_cloud_sdk/core/auditlog_ng/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,29 @@ def _fill_common_from_auth_context(event: Message) -> None:
common.timestamp.FromDatetime(datetime.now(timezone.utc))


def _strip_message_context_text(event: Message) -> None:
"""Remove free-text fields from an event's ``message_context``, if present.

Several AI audit events embed an ``AiMessageContext`` (proto field
``message_context``) whose ``prompt_text`` and ``response_text`` fields hold
the verbatim model input/output and may contain sensitive data. We never
forward that content to the audit log; correlation fields (uid, name,
ai_role_id) and token counts are left intact.

Safe to call on any event: events without a ``message_context`` field, or
with one that was never set, are left untouched (we only act when the field
is actually present, so we never auto-create an empty sub-message).
"""
if not hasattr(event, "message_context"):
return
event_any = cast(Any, event)
if not event_any.HasField("message_context"):
return
message_context = event_any.message_context
message_context.ClearField("prompt_text")
message_context.ClearField("response_text")


def _create_log_exporter(
config: AuditLogNGConfig,
credentials: Optional[grpc.ChannelCredentials],
Expand Down Expand Up @@ -216,6 +239,7 @@ def send(
raise ValueError("format must be 'protobuf-binary' or 'json'")

_fill_common_from_auth_context(event)
_strip_message_context_text(event)

try:
protovalidate.validate(event)
Expand Down
54 changes: 54 additions & 0 deletions tests/core/unit/auditlog_ng/unit/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from sap_cloud_sdk.core.auditlog_ng.client import (
AuditClient,
_fill_common_from_auth_context,
_strip_message_context_text,
)
from sap_cloud_sdk.core.auditlog_ng.config import AuditLogNGConfig, SCHEMA_URL
from sap_cloud_sdk.core.auditlog_ng.exceptions import ValidationError
Expand Down Expand Up @@ -401,3 +402,56 @@ def test_does_not_overwrite_explicit_timestamp(self):
def test_event_without_common_field_is_skipped(self):
event = MagicMock(spec=[]) # no 'common' attribute
_fill_common_from_auth_context(event) # must not raise


class TestStripMessageContextText:
def test_strips_prompt_and_response_text(self):
event = pb.AiPromptReceived()
event.message_context.prompt_text = "verbatim user prompt"
event.message_context.response_text = "verbatim model response"
_strip_message_context_text(event)
assert event.message_context.prompt_text == ""
assert event.message_context.response_text == ""

def test_preserves_correlation_and_token_fields(self):
event = pb.AiPromptReceived()
event.message_context.prompt_text = "secret"
event.message_context.response_text = "secret"
event.message_context.uid = "conversation-123"
event.message_context.name = "thread-name"
event.message_context.prompt_tokens = 42
event.message_context.completion_tokens = 7
event.message_context.total_tokens = 49
_strip_message_context_text(event)
assert event.message_context.uid == "conversation-123"
assert event.message_context.name == "thread-name"
assert event.message_context.prompt_tokens == 42
assert event.message_context.completion_tokens == 7
assert event.message_context.total_tokens == 49

def test_unset_message_context_is_not_created(self):
event = pb.AiPromptReceived()
_strip_message_context_text(event)
assert not event.HasField("message_context")

def test_event_without_message_context_field_is_skipped(self):
# AiSkillSelected has no message_context field at all.
event = pb.AiSkillSelected()
_strip_message_context_text(event) # must not raise

def test_send_strips_message_context_text_before_emit(self):
client, mock_logger, _, _, patcher, _ = _make_mocked_client()
try:
event = pb.AiPromptReceived()
event.common.tenant_id = "tenant-123"
event.message_context.prompt_text = "sensitive prompt"
event.message_context.response_text = "sensitive response"

client.send(event)

# The event object itself is scrubbed in place before serialization.
assert event.message_context.prompt_text == ""
assert event.message_context.response_text == ""
mock_logger.emit.assert_called_once()
finally:
patcher.stop()
Loading