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
31 changes: 29 additions & 2 deletions src/harness_sdk/instrumentation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,24 @@ class BaseInstrumentorWrapper:
RPC_REQUEST_BODY_PREFIX = 'rpc.request.body'
RPC_RESPONSE_BODY_PREFIX = 'rpc.response.body'

# Headers/metadata whose values are credentials or session secrets and must
# never be exported verbatim in telemetry. Compared case-insensitively.
SENSITIVE_HEADERS = frozenset({
'authorization',
'proxy-authorization',
'x-api-key',
'api-key',
'apikey',
'x-auth-token',
'x-amz-security-token',
'x-goog-api-key',
'cookie',
'set-cookie',
'x-harness-service-token',
'x-harness-token',
})
REDACTED_VALUE = '[REDACTED]'

# Constructor
def __init__(self):
'''constructor'''
Expand All @@ -41,10 +59,19 @@ def lowercase_headers(self, headers):
'''convert all headers to lowercase'''
return {k.lower(): v for k, v in headers.items()}

def _redact_if_sensitive(self, header_key: str, header_value):
'''Return the header value, redacted if the header is credential-bearing.'''
if header_key.lower() in self.SENSITIVE_HEADERS:
return self.REDACTED_VALUE
return header_value

def add_headers_to_span(self, prefix: str, span: Span, headers: dict):
'''set header attributes on the span'''
'''set header attributes on the span, redacting sensitive header values'''
for header_key, header_value in headers.items():
span.set_attribute(f"{prefix}{header_key}", header_value)
span.set_attribute(
f"{prefix}{header_key}",
self._redact_if_sensitive(header_key, header_value),
)

def eligible_based_on_content_type(self, headers: dict):
'''find content-type in headers'''
Expand Down
68 changes: 68 additions & 0 deletions test/instrumentation/base_wrapper_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"""Unit tests for BaseInstrumentorWrapper sensitive-header redaction.

Covers the choke point (`add_headers_to_span`) shared by the HTTP
(requests/httpx/aiohttp) and RPC (gRPC) handlers, so credential-bearing
headers never reach span attributes verbatim.
"""

from harness_sdk.instrumentation import BaseInstrumentorWrapper


class _FakeSpan:
"""Minimal Span stand-in that records set_attribute calls."""

def __init__(self):
self.attributes = {}

def is_recording(self):
return True

def set_attribute(self, key, value):
self.attributes[key] = value


def test_sensitive_headers_are_redacted(agent): # noqa: ARG001 (agent inits Config)
wrapper = BaseInstrumentorWrapper()
span = _FakeSpan()

headers = {
"authorization": "Bearer sk-secret-openai-key",
"x-api-key": "sk-ant-secret-anthropic-key",
"cookie": "session=abc123",
"x-harness-service-token": "pat.acct.secret",
"content-type": "application/json",
"accept": "*/*",
}

wrapper.add_headers_to_span("http.request.header.", span, headers)

# Sensitive headers must be masked.
assert span.attributes["http.request.header.authorization"] == BaseInstrumentorWrapper.REDACTED_VALUE
assert span.attributes["http.request.header.x-api-key"] == BaseInstrumentorWrapper.REDACTED_VALUE
assert span.attributes["http.request.header.cookie"] == BaseInstrumentorWrapper.REDACTED_VALUE
assert span.attributes["http.request.header.x-harness-service-token"] == BaseInstrumentorWrapper.REDACTED_VALUE

# Benign headers must pass through untouched.
assert span.attributes["http.request.header.content-type"] == "application/json"
assert span.attributes["http.request.header.accept"] == "*/*"

# No raw secret value should survive anywhere on the span.
assert "sk-secret-openai-key" not in span.attributes.values()
assert "sk-ant-secret-anthropic-key" not in span.attributes.values()


def test_redaction_is_case_insensitive(agent): # noqa: ARG001
wrapper = BaseInstrumentorWrapper()
span = _FakeSpan()

wrapper.add_headers_to_span(
"http.request.header.", span, {"Authorization": "Bearer sk-secret"}
)

assert span.attributes["http.request.header.Authorization"] == BaseInstrumentorWrapper.REDACTED_VALUE


def test_redact_helper_returns_value_for_benign_header(agent): # noqa: ARG001
wrapper = BaseInstrumentorWrapper()
assert wrapper._redact_if_sensitive("accept", "*/*") == "*/*"
assert wrapper._redact_if_sensitive("authorization", "Bearer x") == BaseInstrumentorWrapper.REDACTED_VALUE