From 81fc72d4de88496d2469fac1e6206461223f362e Mon Sep 17 00:00:00 2001 From: Andre Rosado Date: Tue, 3 Mar 2026 12:38:31 +0000 Subject: [PATCH 1/3] redacting hostname from flight recorder logs --- .../FlightRecorderWriterImpl.kt | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/data/src/main/kotlin/com/bitwarden/data/manager/flightrecorder/FlightRecorderWriterImpl.kt b/data/src/main/kotlin/com/bitwarden/data/manager/flightrecorder/FlightRecorderWriterImpl.kt index 974a87ce437..4bd9263e6fa 100644 --- a/data/src/main/kotlin/com/bitwarden/data/manager/flightrecorder/FlightRecorderWriterImpl.kt +++ b/data/src/main/kotlin/com/bitwarden/data/manager/flightrecorder/FlightRecorderWriterImpl.kt @@ -103,10 +103,10 @@ internal class FlightRecorderWriterImpl( bw.append(it) } bw.append(" – ") - bw.append(message) + bw.append(message.redactUrls()) throwable?.let { bw.append(" – ") - bw.append(it.getStackTraceString()) + bw.append(it.getStackTraceString().redactUrls()) } bw.newLine() } @@ -141,3 +141,24 @@ private val Int.logLevel: String Log.ASSERT -> "ASSERT" else -> "UNKNOWN" } + +/** + * Redacts URLs and quoted hostnames in the string by replacing them with [REDACTED]. + * Handles both full URLs and hostnames in quotes (e.g., "Unable to resolve host "example.com""). + */ +@Suppress("MagicNumber") +private fun String.redactUrls(): String { + val urlPattern = Regex("""(https?://)([\w.-]+)((?:/[\w./?&=%-]*)?)""") + val afterUrlRedaction = urlPattern.replace(this) { matchResult -> + val protocol = matchResult.groupValues[1] + val path = matchResult.groupValues[3] + "$protocol[REDACTED]$path" + } + + // Redact hostnames that appear in double quotes without protocol and path + // This handles cases like: Unable to resolve host "com.example.server" + val quotedHostnamePattern = Regex(""""([\w-]+\.[\w.-]+)"""") + return quotedHostnamePattern.replace(afterUrlRedaction) { + """"[REDACTED]"""" + } +} From 45c195a7d0f828bb4c19396b8a0f8855ddee7537 Mon Sep 17 00:00:00 2001 From: Andre Rosado Date: Mon, 9 Mar 2026 17:44:26 +0000 Subject: [PATCH 2/3] Redacting hostname when it matches a self-hosted hostname Replacing api url with selfhosted one on toFailure at NetworkResultCall --- .../data/manager/di/DataManagerModule.kt | 3 + .../FlightRecorderWriterImpl.kt | 52 ++++--- .../network/core/NetworkResultCall.kt | 34 +++- .../network/core/NetworkResultCallAdapter.kt | 5 +- .../core/NetworkResultCallAdapterFactory.kt | 10 +- .../interceptor/BaseUrlInterceptors.kt | 2 +- .../network/retrofit/RetrofitsImpl.kt | 6 +- .../network/util/HostnameRedactionUtil.kt | 27 ++++ .../core/NetworkResultCallAdapterTest.kt | 11 +- .../network/retrofit/RetrofitsTest.kt | 1 + .../network/util/HostnameRedactionUtilTest.kt | 146 ++++++++++++++++++ .../bitwarden/network/base/BaseServiceTest.kt | 9 +- 12 files changed, 272 insertions(+), 34 deletions(-) create mode 100644 network/src/main/kotlin/com/bitwarden/network/util/HostnameRedactionUtil.kt create mode 100644 network/src/test/kotlin/com/bitwarden/network/util/HostnameRedactionUtilTest.kt diff --git a/data/src/main/kotlin/com/bitwarden/data/manager/di/DataManagerModule.kt b/data/src/main/kotlin/com/bitwarden/data/manager/di/DataManagerModule.kt index d7bf729e572..6925a65fe69 100644 --- a/data/src/main/kotlin/com/bitwarden/data/manager/di/DataManagerModule.kt +++ b/data/src/main/kotlin/com/bitwarden/data/manager/di/DataManagerModule.kt @@ -17,6 +17,7 @@ import com.bitwarden.data.manager.flightrecorder.FlightRecorderManager import com.bitwarden.data.manager.flightrecorder.FlightRecorderManagerImpl import com.bitwarden.data.manager.flightrecorder.FlightRecorderWriter import com.bitwarden.data.manager.flightrecorder.FlightRecorderWriterImpl +import com.bitwarden.network.interceptor.BaseUrlsProvider import com.bitwarden.network.service.DownloadService import dagger.Module import dagger.Provides @@ -80,11 +81,13 @@ object DataManagerModule { fileManager: FileManager, dispatcherManager: DispatcherManager, buildInfoManager: BuildInfoManager, + baseUrlsProvider: BaseUrlsProvider, ): FlightRecorderWriter = FlightRecorderWriterImpl( clock = clock, fileManager = fileManager, dispatcherManager = dispatcherManager, buildInfoManager = buildInfoManager, + baseUrlsProvider = baseUrlsProvider, ) @Provides diff --git a/data/src/main/kotlin/com/bitwarden/data/manager/flightrecorder/FlightRecorderWriterImpl.kt b/data/src/main/kotlin/com/bitwarden/data/manager/flightrecorder/FlightRecorderWriterImpl.kt index 4bd9263e6fa..2826ad2479e 100644 --- a/data/src/main/kotlin/com/bitwarden/data/manager/flightrecorder/FlightRecorderWriterImpl.kt +++ b/data/src/main/kotlin/com/bitwarden/data/manager/flightrecorder/FlightRecorderWriterImpl.kt @@ -8,7 +8,10 @@ import com.bitwarden.core.data.manager.dispatcher.DispatcherManager import com.bitwarden.core.data.util.toFormattedPattern import com.bitwarden.data.datasource.disk.model.FlightRecorderDataSet import com.bitwarden.data.manager.file.FileManager +import com.bitwarden.network.interceptor.BaseUrlsProvider +import com.bitwarden.network.util.redactHostnamesInMessage import kotlinx.coroutines.withContext +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import timber.log.Timber import java.io.BufferedWriter import java.io.File @@ -30,6 +33,7 @@ internal class FlightRecorderWriterImpl( private val fileManager: FileManager, private val dispatcherManager: DispatcherManager, private val buildInfoManager: BuildInfoManager, + private val baseUrlsProvider: BaseUrlsProvider, ) : FlightRecorderWriter { override suspend fun deleteLog(data: FlightRecorderDataSet.FlightRecorderData) { fileManager.delete(File(File(fileManager.logsDirectory), data.fileName)) @@ -103,16 +107,39 @@ internal class FlightRecorderWriterImpl( bw.append(it) } bw.append(" – ") - bw.append(message.redactUrls()) + bw.append(message.redactUrls()) // Apply hostname redaction throwable?.let { bw.append(" – ") - bw.append(it.getStackTraceString().redactUrls()) + bw.append(it.getStackTraceString().redactUrls()) // Also redact stack traces } bw.newLine() } } } } + + /** + * Redacts ONLY the user's configured self-hosted server hostname. + * + * Preserves ALL Bitwarden domains (including QA/staging). + * Delegates to [com.bitwarden.network.util.redactHostnamesInMessage]. + * + * Examples: + * - "https://api.bitwarden.com/sync" → unchanged (Bitwarden cloud) + * - "https://vault.qa.bitwarden.pw/api" → unchanged (Bitwarden QA) + * - "https://vault.example.com/api" → "https://[REDACTED_SELF_HOST]/api" (self-hosted) + */ + private fun String.redactUrls(): String { + // Get configured hostnames from BaseUrlsProvider + val configuredHosts = setOf( + baseUrlsProvider.getBaseApiUrl().toHttpUrlOrNull()?.host, + baseUrlsProvider.getBaseIdentityUrl().toHttpUrlOrNull()?.host, + baseUrlsProvider.getBaseEventsUrl().toHttpUrlOrNull()?.host, + ).filterNotNull().toSet() + + // Delegate to HostnameRedactionUtil for all redaction logic + return this.redactHostnamesInMessage(configuredHosts) + } } /** @@ -141,24 +168,3 @@ private val Int.logLevel: String Log.ASSERT -> "ASSERT" else -> "UNKNOWN" } - -/** - * Redacts URLs and quoted hostnames in the string by replacing them with [REDACTED]. - * Handles both full URLs and hostnames in quotes (e.g., "Unable to resolve host "example.com""). - */ -@Suppress("MagicNumber") -private fun String.redactUrls(): String { - val urlPattern = Regex("""(https?://)([\w.-]+)((?:/[\w./?&=%-]*)?)""") - val afterUrlRedaction = urlPattern.replace(this) { matchResult -> - val protocol = matchResult.groupValues[1] - val path = matchResult.groupValues[3] - "$protocol[REDACTED]$path" - } - - // Redact hostnames that appear in double quotes without protocol and path - // This handles cases like: Unable to resolve host "com.example.server" - val quotedHostnamePattern = Regex(""""([\w-]+\.[\w.-]+)"""") - return quotedHostnamePattern.replace(afterUrlRedaction) { - """"[REDACTED]"""" - } -} diff --git a/network/src/main/kotlin/com/bitwarden/network/core/NetworkResultCall.kt b/network/src/main/kotlin/com/bitwarden/network/core/NetworkResultCall.kt index 9e6d7c0f0d3..da83170c165 100644 --- a/network/src/main/kotlin/com/bitwarden/network/core/NetworkResultCall.kt +++ b/network/src/main/kotlin/com/bitwarden/network/core/NetworkResultCall.kt @@ -1,6 +1,8 @@ package com.bitwarden.network.core +import com.bitwarden.network.interceptor.BaseUrlsProvider import com.bitwarden.network.model.NetworkResult +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.Request import okio.IOException import okio.Timeout @@ -23,10 +25,12 @@ private const val NO_CONTENT_RESPONSE_CODE: Int = 204 internal class NetworkResultCall( private val backingCall: Call, private val successType: Type, + private val baseUrlsProvider: BaseUrlsProvider? = null, ) : Call> { override fun cancel(): Unit = backingCall.cancel() - override fun clone(): Call> = NetworkResultCall(backingCall, successType) + override fun clone(): Call> = + NetworkResultCall(backingCall, successType, baseUrlsProvider) override fun enqueue(callback: Callback>): Unit = backingCall.enqueue( object : Callback { @@ -67,8 +71,32 @@ internal class NetworkResultCall( fun executeForResult(): NetworkResult = requireNotNull(execute().body()) private fun Throwable.toFailure(): NetworkResult { - // We rebuild the URL without query params, we do not want to log those - val url = backingCall.request().url.toUrl().run { "$protocol://$authority$path" } + val originalUrl = backingCall.request().url.toUrl() + + // Check if this is a hardcoded default URL that will be replaced by BaseUrlInterceptor + // Match against the defaults from RetrofitsImpl.kt line 111 and EnvironmentUrlDataJson + val actualHost = if (baseUrlsProvider != null) { + when (originalUrl.host) { + "api.bitwarden.com" -> baseUrlsProvider.getBaseApiUrl().toHttpUrlOrNull()?.host + "identity.bitwarden.com" -> baseUrlsProvider.getBaseIdentityUrl() + .toHttpUrlOrNull()?.host + + "events.bitwarden.com" -> baseUrlsProvider.getBaseEventsUrl() + .toHttpUrlOrNull()?.host + + else -> null + } + } else { + null + } + + // Rebuild the URL without query params, using actual host if available + val url = if (actualHost != null) { + "${originalUrl.protocol}://$actualHost${originalUrl.path}" + } else { + "${originalUrl.protocol}://${originalUrl.authority}${originalUrl.path}" + } + Timber.w(this, "Network Error: $url") return NetworkResult.Failure(this) } diff --git a/network/src/main/kotlin/com/bitwarden/network/core/NetworkResultCallAdapter.kt b/network/src/main/kotlin/com/bitwarden/network/core/NetworkResultCallAdapter.kt index 2cbd9b41e5b..7265139c6e0 100644 --- a/network/src/main/kotlin/com/bitwarden/network/core/NetworkResultCallAdapter.kt +++ b/network/src/main/kotlin/com/bitwarden/network/core/NetworkResultCallAdapter.kt @@ -1,5 +1,6 @@ package com.bitwarden.network.core +import com.bitwarden.network.interceptor.BaseUrlsProvider import com.bitwarden.network.model.NetworkResult import retrofit2.Call import retrofit2.CallAdapter @@ -10,8 +11,10 @@ import java.lang.reflect.Type */ internal class NetworkResultCallAdapter( private val successType: Type, + private val baseUrlsProvider: BaseUrlsProvider, ) : CallAdapter>> { override fun responseType(): Type = successType - override fun adapt(call: Call): Call> = NetworkResultCall(call, successType) + override fun adapt(call: Call): Call> = + NetworkResultCall(call, successType, baseUrlsProvider) } diff --git a/network/src/main/kotlin/com/bitwarden/network/core/NetworkResultCallAdapterFactory.kt b/network/src/main/kotlin/com/bitwarden/network/core/NetworkResultCallAdapterFactory.kt index 7f08056af4b..fc7c0d5e03d 100644 --- a/network/src/main/kotlin/com/bitwarden/network/core/NetworkResultCallAdapterFactory.kt +++ b/network/src/main/kotlin/com/bitwarden/network/core/NetworkResultCallAdapterFactory.kt @@ -1,5 +1,6 @@ package com.bitwarden.network.core +import com.bitwarden.network.interceptor.BaseUrlsProvider import com.bitwarden.network.model.NetworkResult import retrofit2.Call import retrofit2.CallAdapter @@ -10,7 +11,9 @@ import java.lang.reflect.Type /** * A [retrofit2.CallAdapter.Factory] for wrapping network requests into [NetworkResult]. */ -internal class NetworkResultCallAdapterFactory : CallAdapter.Factory() { +internal class NetworkResultCallAdapterFactory( + private val baseUrlsProvider: BaseUrlsProvider, +) : CallAdapter.Factory() { override fun get( returnType: Type, annotations: Array, @@ -25,7 +28,10 @@ internal class NetworkResultCallAdapterFactory : CallAdapter.Factory() { val requestType = getParameterUpperBound(0, containerType) return if (getRawType(returnType) == Call::class.java) { - NetworkResultCallAdapter(successType = requestType) + NetworkResultCallAdapter( + successType = requestType, + baseUrlsProvider = baseUrlsProvider, + ) } else { null } diff --git a/network/src/main/kotlin/com/bitwarden/network/interceptor/BaseUrlInterceptors.kt b/network/src/main/kotlin/com/bitwarden/network/interceptor/BaseUrlInterceptors.kt index dceaf9f5e54..0f078df146d 100644 --- a/network/src/main/kotlin/com/bitwarden/network/interceptor/BaseUrlInterceptors.kt +++ b/network/src/main/kotlin/com/bitwarden/network/interceptor/BaseUrlInterceptors.kt @@ -7,7 +7,7 @@ import com.bitwarden.annotation.OmitFromCoverage */ @OmitFromCoverage internal class BaseUrlInterceptors( - private val baseUrlsProvider: BaseUrlsProvider, + val baseUrlsProvider: BaseUrlsProvider, ) { /** * An interceptor for "/api" calls. diff --git a/network/src/main/kotlin/com/bitwarden/network/retrofit/RetrofitsImpl.kt b/network/src/main/kotlin/com/bitwarden/network/retrofit/RetrofitsImpl.kt index 786cc9d3895..98da4f7d412 100644 --- a/network/src/main/kotlin/com/bitwarden/network/retrofit/RetrofitsImpl.kt +++ b/network/src/main/kotlin/com/bitwarden/network/retrofit/RetrofitsImpl.kt @@ -23,7 +23,7 @@ import timber.log.Timber @Suppress("LongParameterList") internal class RetrofitsImpl( authTokenManager: AuthTokenManager, - baseUrlInterceptors: BaseUrlInterceptors, + private val baseUrlInterceptors: BaseUrlInterceptors, cookieInterceptor: CookieInterceptor, headersInterceptor: HeadersInterceptor, json: Json, @@ -115,7 +115,9 @@ internal class RetrofitsImpl( private val baseRetrofitBuilder: Retrofit.Builder by lazy { Retrofit.Builder() .addConverterFactory(json.asConverterFactory("application/json".toMediaType())) - .addCallAdapterFactory(NetworkResultCallAdapterFactory()) + .addCallAdapterFactory( + NetworkResultCallAdapterFactory(baseUrlInterceptors.baseUrlsProvider), + ) .client(baseOkHttpClient) } diff --git a/network/src/main/kotlin/com/bitwarden/network/util/HostnameRedactionUtil.kt b/network/src/main/kotlin/com/bitwarden/network/util/HostnameRedactionUtil.kt new file mode 100644 index 00000000000..aa8e557d840 --- /dev/null +++ b/network/src/main/kotlin/com/bitwarden/network/util/HostnameRedactionUtil.kt @@ -0,0 +1,27 @@ +package com.bitwarden.network.util + +/** + * List of official Bitwarden cloud hostnames that are safe to log. + */ +private val BITWARDEN_HOSTS = listOf("bitwarden.com", "bitwarden.eu", "bitwarden.pw") + +/** + * Redacts hostnames in a log message by replacing bare hostnames with [REDACTED_SELF_HOST]. + * + * Only redacts hostnames that match [configuredHosts] AND are not official Bitwarden domains. + * Preserves all Bitwarden domains (including QA/dev environments). + * + * @param configuredHosts Set of hostnames from BaseUrlsProvider + * @return Message with hostnames redacted as [REDACTED_SELF_HOST] + */ +fun String.redactHostnamesInMessage(configuredHosts: Set): String = + configuredHosts.fold(this) { result, hostname -> + val escapedHostname = Regex.escape(hostname) + val bareHostnamePattern = Regex("""\b$escapedHostname\b""") + bareHostnamePattern.replace(result) { hostname.redactIfSelfHosted() } + } + +private fun String.redactIfSelfHosted(): String { + val isBitwardenHost = BITWARDEN_HOSTS.any { this.endsWith(it) } + return if (isBitwardenHost) this else "[REDACTED_SELF_HOST]" +} diff --git a/network/src/test/kotlin/com/bitwarden/network/core/NetworkResultCallAdapterTest.kt b/network/src/test/kotlin/com/bitwarden/network/core/NetworkResultCallAdapterTest.kt index 0c523585477..869a6376682 100644 --- a/network/src/test/kotlin/com/bitwarden/network/core/NetworkResultCallAdapterTest.kt +++ b/network/src/test/kotlin/com/bitwarden/network/core/NetworkResultCallAdapterTest.kt @@ -1,6 +1,9 @@ package com.bitwarden.network.core +import com.bitwarden.network.interceptor.BaseUrlsProvider import com.bitwarden.network.model.NetworkResult +import io.mockk.every +import io.mockk.mockk import kotlinx.coroutines.runBlocking import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer @@ -13,12 +16,18 @@ import retrofit2.http.GET class NetworkResultCallAdapterTest { + private val mockBaseUrlsProvider = mockk { + every { getBaseApiUrl() } returns "https://api.bitwarden.com" + every { getBaseIdentityUrl() } returns "https://identity.bitwarden.com" + every { getBaseEventsUrl() } returns "https://events.bitwarden.com" + } + private val server: MockWebServer = MockWebServer().apply { start() } private val testService: FakeService = Retrofit.Builder() .baseUrl(server.url("/").toString()) // add the adapter being tested - .addCallAdapterFactory(NetworkResultCallAdapterFactory()) + .addCallAdapterFactory(NetworkResultCallAdapterFactory(mockBaseUrlsProvider)) .build() .create() diff --git a/network/src/test/kotlin/com/bitwarden/network/retrofit/RetrofitsTest.kt b/network/src/test/kotlin/com/bitwarden/network/retrofit/RetrofitsTest.kt index eaeb8a77a44..5c54368ed01 100644 --- a/network/src/test/kotlin/com/bitwarden/network/retrofit/RetrofitsTest.kt +++ b/network/src/test/kotlin/com/bitwarden/network/retrofit/RetrofitsTest.kt @@ -37,6 +37,7 @@ class RetrofitsTest { mockIntercept { isAuthInterceptorCalled = true } } private val baseUrlInterceptors = mockk { + every { baseUrlsProvider } returns mockk(relaxed = true) every { apiInterceptor } returns mockk { mockIntercept { isApiInterceptorCalled = true } } diff --git a/network/src/test/kotlin/com/bitwarden/network/util/HostnameRedactionUtilTest.kt b/network/src/test/kotlin/com/bitwarden/network/util/HostnameRedactionUtilTest.kt new file mode 100644 index 00000000000..1dbc3c2cbf2 --- /dev/null +++ b/network/src/test/kotlin/com/bitwarden/network/util/HostnameRedactionUtilTest.kt @@ -0,0 +1,146 @@ +package com.bitwarden.network.util + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class HostnameRedactionUtilTest { + @Test + fun `redactHostnamesInMessage redacts configured self-hosted URLs`() { + val message = "--> GET https://vault.example.com/api/sync HTTP/1.1" + val configuredHosts = setOf("vault.example.com") + + val result = message.redactHostnamesInMessage(configuredHosts) + + assertEquals("--> GET https://[REDACTED_SELF_HOST]/api/sync HTTP/1.1", result) + } + + @Test + fun `redactHostnamesInMessage preserves non-configured URLs`() { + val message = "--> GET https://vault.example.com/api/sync HTTP/1.1" + val configuredHosts = setOf("api.bitwarden.com") // Different host + + val result = message.redactHostnamesInMessage(configuredHosts) + + assertEquals(message, result) // Unchanged - not in configured hosts + } + + @Test + fun `redactHostnamesInMessage preserves Bitwarden URLs even if configured`() { + val message = "--> GET https://vault.qa.bitwarden.pw/api/sync HTTP/1.1" + val configuredHosts = setOf("vault.qa.bitwarden.pw") + + val result = message.redactHostnamesInMessage(configuredHosts) + + assertEquals(message, result) // Unchanged - Bitwarden domain preserved + } + + @Test + fun `redactHostnamesInMessage redacts quoted hostnames in error messages`() { + val message = """Unable to resolve host "vault.example.com": No address""" + val configuredHosts = setOf("vault.example.com") + + val result = message.redactHostnamesInMessage(configuredHosts) + + assertEquals("""Unable to resolve host "[REDACTED_SELF_HOST]": No address""", result) + } + + @Test + fun `redactHostnamesInMessage handles multiple URLs in one message`() { + val message = "Redirect from https://old.corp.com to https://new.corp.com" + val configuredHosts = setOf("old.corp.com", "new.corp.com") + + val result = message.redactHostnamesInMessage(configuredHosts) + + assertEquals( + "Redirect from https://[REDACTED_SELF_HOST] to https://[REDACTED_SELF_HOST]", + result, + ) + } + + @Test + fun `redactHostnamesInMessage handles empty configured hosts`() { + val message = "--> GET https://vault.example.com/api HTTP/1.1" + val configuredHosts = emptySet() + + val result = message.redactHostnamesInMessage(configuredHosts) + + assertEquals(message, result) // Unchanged - no hosts to redact + } + + @Test + fun `redactHostnamesInMessage handles NetworkCookieManagerImpl getCookies pattern`() { + val message = "2026-03-09 12:43:29:857 – DEBUG – NetworkCookieManagerImpl – " + + "getCookies(vault.example.com): resolved=vault.example.com, count=0" + val configuredHosts = setOf("vault.example.com") + + val result = message.redactHostnamesInMessage(configuredHosts) + + assertEquals( + "2026-03-09 12:43:29:857 – DEBUG – NetworkCookieManagerImpl – " + + "getCookies([REDACTED_SELF_HOST]): resolved=[REDACTED_SELF_HOST], count=0", + result, + ) + } + + @Test + fun `redactHostnamesInMessage preserves Bitwarden domains in NetworkCookieManagerImpl logs`() { + val message = "2026-03-09 12:43:29:857 – DEBUG – NetworkCookieManagerImpl – " + + "getCookies(vault.example.com): resolved=vault.qa.bitwarden.pw, count=0" + val configuredHosts = setOf("vault.example.com", "vault.qa.bitwarden.pw") + + val result = message.redactHostnamesInMessage(configuredHosts) + + assertEquals( + "2026-03-09 12:43:29:857 – DEBUG – NetworkCookieManagerImpl – " + + "getCookies([REDACTED_SELF_HOST]): resolved=vault.qa.bitwarden.pw, count=0", + result, + ) + } + + @Test + fun `redactHostnamesInMessage handles UnknownHostException error message`() { + val message = "DEBUG – BitwardenNetworkClient – <-- HTTP FAILED: " + + "java.net.UnknownHostException: Unable to resolve host " + + "\"vault.example.com\": No address associated with hostname." + val configuredHosts = setOf("vault.example.com") + + val result = message.redactHostnamesInMessage(configuredHosts) + + assertEquals( + "DEBUG – BitwardenNetworkClient – <-- HTTP FAILED: " + + "java.net.UnknownHostException: Unable to resolve host " + + "\"[REDACTED_SELF_HOST]\": No address associated with hostname.", + result, + ) + } + + @Test + fun `redactHostnamesInMessage handles needsBootstrap pattern`() { + val message = "2026-03-09 12:43:29:851 – DEBUG – NetworkCookieManagerImpl – " + + "needsBootstrap(vault.example.com): false (cookieDomain=null)" + val configuredHosts = setOf("vault.example.com") + + val result = message.redactHostnamesInMessage(configuredHosts) + + assertEquals( + "2026-03-09 12:43:29:851 – DEBUG – NetworkCookieManagerImpl – " + + "needsBootstrap([REDACTED_SELF_HOST]): false (cookieDomain=null)", + result, + ) + } + + @Test + fun `redactHostnamesInMessage handles resolveHostname pattern`() { + val message = "2026-03-09 12:43:29:855 – DEBUG – NetworkCookieManagerImpl – " + + "resolveHostname(vault.example.com): no stored config found, using original" + val configuredHosts = setOf("vault.example.com") + + val result = message.redactHostnamesInMessage(configuredHosts) + + assertEquals( + "2026-03-09 12:43:29:855 – DEBUG – NetworkCookieManagerImpl – " + + "resolveHostname([REDACTED_SELF_HOST]): no stored config found, using original", + result, + ) + } +} diff --git a/network/src/testFixtures/kotlin/com/bitwarden/network/base/BaseServiceTest.kt b/network/src/testFixtures/kotlin/com/bitwarden/network/base/BaseServiceTest.kt index 5f31899614d..e6ab11bd01b 100644 --- a/network/src/testFixtures/kotlin/com/bitwarden/network/base/BaseServiceTest.kt +++ b/network/src/testFixtures/kotlin/com/bitwarden/network/base/BaseServiceTest.kt @@ -2,6 +2,7 @@ package com.bitwarden.network.base import com.bitwarden.core.di.CoreModule import com.bitwarden.network.core.NetworkResultCallAdapterFactory +import com.bitwarden.network.interceptor.BaseUrlsProvider import okhttp3.HttpUrl import okhttp3.MediaType.Companion.toMediaType import okhttp3.mockwebserver.MockWebServer @@ -22,9 +23,15 @@ abstract class BaseServiceTest { protected val urlPrefix: String get() = "http://${server.hostName}:${server.port}" + private val fakeBaseUrlsProvider = object : BaseUrlsProvider { + override fun getBaseApiUrl(): String = "https://api.bitwarden.com" + override fun getBaseIdentityUrl(): String = "https://identity.bitwarden.com" + override fun getBaseEventsUrl(): String = "https://events.bitwarden.com" + } + protected val retrofit: Retrofit = Retrofit.Builder() .baseUrl(url.toString()) - .addCallAdapterFactory(NetworkResultCallAdapterFactory()) + .addCallAdapterFactory(NetworkResultCallAdapterFactory(fakeBaseUrlsProvider)) .addConverterFactory(json.asConverterFactory("application/json".toMediaType())) .build() From 78b298512d5c6d3c6e54b00ea769746c0a57593a Mon Sep 17 00:00:00 2001 From: Andre Rosado Date: Tue, 10 Mar 2026 13:18:18 +0000 Subject: [PATCH 3/3] simplified method description --- .../manager/flightrecorder/FlightRecorderWriterImpl.kt | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/data/src/main/kotlin/com/bitwarden/data/manager/flightrecorder/FlightRecorderWriterImpl.kt b/data/src/main/kotlin/com/bitwarden/data/manager/flightrecorder/FlightRecorderWriterImpl.kt index 2826ad2479e..e0f13707c0e 100644 --- a/data/src/main/kotlin/com/bitwarden/data/manager/flightrecorder/FlightRecorderWriterImpl.kt +++ b/data/src/main/kotlin/com/bitwarden/data/manager/flightrecorder/FlightRecorderWriterImpl.kt @@ -121,13 +121,7 @@ internal class FlightRecorderWriterImpl( /** * Redacts ONLY the user's configured self-hosted server hostname. * - * Preserves ALL Bitwarden domains (including QA/staging). - * Delegates to [com.bitwarden.network.util.redactHostnamesInMessage]. - * - * Examples: - * - "https://api.bitwarden.com/sync" → unchanged (Bitwarden cloud) - * - "https://vault.qa.bitwarden.pw/api" → unchanged (Bitwarden QA) - * - "https://vault.example.com/api" → "https://[REDACTED_SELF_HOST]/api" (self-hosted) + * Preserves ALL Bitwarden domains (including QA/dev). */ private fun String.redactUrls(): String { // Get configured hostnames from BaseUrlsProvider