@@ -243,6 +243,61 @@ async fn drain_during_backoff(
243243 }
244244}
245245
246+ const REDACTED_LOG_VALUE : & str = "[REDACTED]" ;
247+
248+ fn sanitize_field_value ( field_name : & str , value : & str ) -> String {
249+ if field_name_looks_sensitive ( field_name) || value_looks_sensitive ( value) {
250+ REDACTED_LOG_VALUE . to_string ( )
251+ } else {
252+ value. to_string ( )
253+ }
254+ }
255+
256+ fn field_name_looks_sensitive ( field_name : & str ) -> bool {
257+ let normalized = field_name. to_ascii_lowercase ( ) ;
258+ matches ! (
259+ normalized. as_str( ) ,
260+ "authorization"
261+ | "proxy_authorization"
262+ | "token"
263+ | "secret"
264+ | "password"
265+ | "passwd"
266+ | "api_key"
267+ | "apikey"
268+ ) || matches ! (
269+ normalized. as_str( ) ,
270+ name if name. ends_with( "_token" )
271+ || name. ends_with( "_secret" )
272+ || name. ends_with( "_password" )
273+ || name. ends_with( "_passwd" )
274+ || name. ends_with( "_api_key" )
275+ || name. ends_with( "_apikey" )
276+ )
277+ }
278+
279+ fn value_looks_sensitive ( value : & str ) -> bool {
280+ let candidate = strip_wrapping_quotes ( value. trim ( ) ) ;
281+ let lower = candidate. to_ascii_lowercase ( ) ;
282+ lower. starts_with ( "bearer " )
283+ || lower. starts_with ( "openshell:resolve:" )
284+ || candidate. starts_with ( "sk-" )
285+ }
286+
287+ fn strip_wrapping_quotes ( mut value : & str ) -> & str {
288+ loop {
289+ let trimmed = value. trim ( ) ;
290+ if trimmed. len ( ) >= 2
291+ && ( ( trimmed. starts_with ( '"' ) && trimmed. ends_with ( '"' ) )
292+ || ( trimmed. starts_with ( '\'' ) && trimmed. ends_with ( '\'' ) ) )
293+ {
294+ value = & trimmed[ 1 ..trimmed. len ( ) - 1 ] ;
295+ continue ;
296+ }
297+ return trimmed;
298+ }
299+ }
300+
246301#[ derive( Debug , Default ) ]
247302struct LogVisitor {
248303 message : Option < String > ,
@@ -263,17 +318,22 @@ impl tracing::field::Visit for LogVisitor {
263318 if field. name ( ) == "message" {
264319 self . message = Some ( value. to_string ( ) ) ;
265320 } else {
266- self . fields
267- . push ( ( field. name ( ) . to_string ( ) , value. to_string ( ) ) ) ;
321+ self . fields . push ( (
322+ field. name ( ) . to_string ( ) ,
323+ sanitize_field_value ( field. name ( ) , value) ,
324+ ) ) ;
268325 }
269326 }
270327
271328 fn record_debug ( & mut self , field : & tracing:: field:: Field , value : & dyn std:: fmt:: Debug ) {
272329 if field. name ( ) == "message" {
273330 self . message = Some ( format ! ( "{value:?}" ) ) ;
274331 } else {
275- self . fields
276- . push ( ( field. name ( ) . to_string ( ) , format ! ( "{value:?}" ) ) ) ;
332+ let rendered = format ! ( "{value:?}" ) ;
333+ self . fields . push ( (
334+ field. name ( ) . to_string ( ) ,
335+ sanitize_field_value ( field. name ( ) , & rendered) ,
336+ ) ) ;
277337 }
278338 }
279339}
@@ -282,3 +342,70 @@ fn current_time_ms() -> Option<i64> {
282342 let now = SystemTime :: now ( ) . duration_since ( UNIX_EPOCH ) . ok ( ) ?;
283343 i64:: try_from ( now. as_millis ( ) ) . ok ( )
284344}
345+
346+ #[ cfg( test) ]
347+ mod tests {
348+ use super :: * ;
349+
350+ #[ test]
351+ fn sanitize_field_value_redacts_sensitive_field_names ( ) {
352+ assert_eq ! (
353+ sanitize_field_value( "authorization" , "Basic abc123" ) ,
354+ REDACTED_LOG_VALUE
355+ ) ;
356+ assert_eq ! (
357+ sanitize_field_value( "api_key" , "not-a-pattern-match" ) ,
358+ REDACTED_LOG_VALUE
359+ ) ;
360+ assert_eq ! (
361+ sanitize_field_value( "session_token" , "opaque" ) ,
362+ REDACTED_LOG_VALUE
363+ ) ;
364+ }
365+
366+ #[ test]
367+ fn sanitize_field_value_redacts_known_secret_prefixes ( ) {
368+ assert_eq ! (
369+ sanitize_field_value( "dst_host" , "Bearer abc123" ) ,
370+ REDACTED_LOG_VALUE
371+ ) ;
372+ assert_eq ! (
373+ sanitize_field_value( "dst_host" , "sk-proj-123456" ) ,
374+ REDACTED_LOG_VALUE
375+ ) ;
376+ assert_eq ! (
377+ sanitize_field_value( "dst_host" , "openshell:resolve:provider.token" ) ,
378+ REDACTED_LOG_VALUE
379+ ) ;
380+ }
381+
382+ #[ test]
383+ fn sanitize_field_value_redacts_debug_quoted_secret_values ( ) {
384+ assert_eq ! (
385+ sanitize_field_value( "metadata" , "\" Bearer abc123\" " ) ,
386+ REDACTED_LOG_VALUE
387+ ) ;
388+ assert_eq ! (
389+ sanitize_field_value( "metadata" , "\" sk-secret-value\" " ) ,
390+ REDACTED_LOG_VALUE
391+ ) ;
392+ }
393+
394+ #[ test]
395+ fn sanitize_field_value_preserves_benign_fields ( ) {
396+ assert_eq ! (
397+ sanitize_field_value( "l7_target" , "api.openai.com" ) ,
398+ "api.openai.com"
399+ ) ;
400+ assert_eq ! ( sanitize_field_value( "token_count" , "42" ) , "42" ) ;
401+ assert_eq ! (
402+ sanitize_field_value( "event" , "BearerTokenParsingFailed" ) ,
403+ "BearerTokenParsingFailed"
404+ ) ;
405+ }
406+
407+ #[ test]
408+ fn current_time_ms_returns_some ( ) {
409+ assert ! ( current_time_ms( ) . is_some( ) ) ;
410+ }
411+ }
0 commit comments