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 2b815092b5..c105d84030 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 @@ -10,7 +10,7 @@ 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.data.repository.ServerConfigRepository -import com.bitwarden.network.util.redactSelfHostedHostnames +import com.bitwarden.network.util.redactHostnamesInMessage import kotlinx.coroutines.withContext import timber.log.Timber import java.io.BufferedWriter @@ -18,6 +18,7 @@ import java.io.File import java.io.FileWriter import java.io.PrintWriter import java.io.StringWriter +import java.net.URI import java.time.Clock import java.time.Instant import kotlin.time.Duration.Companion.milliseconds @@ -35,6 +36,19 @@ internal class FlightRecorderWriterImpl( private val buildInfoManager: BuildInfoManager, private val serverConfigRepository: ServerConfigRepository, ) : FlightRecorderWriter { + private val configuredHosts: Set + get() { + val environment = serverConfigRepository.serverConfigStateFlow.value + ?.serverData?.environment ?: return emptySet() + return listOfNotNull( + environment.vaultUrl, + environment.apiUrl, + environment.identityUrl, + environment.notificationsUrl, + environment.ssoUrl, + ).mapNotNull { runCatching { URI(it).host }.getOrNull() }.toSet() + } + override suspend fun deleteLog(data: FlightRecorderDataSet.FlightRecorderData) { fileManager.delete(File(File(fileManager.logsDirectory), data.fileName)) } @@ -99,6 +113,7 @@ internal class FlightRecorderWriterImpl( val formattedTime = clock .instant() .toFormattedPattern(pattern = LOG_TIME_PATTERN, clock = clock) + val hosts = configuredHosts withContext(context = dispatcherManager.io) { runCatching { BufferedWriter(FileWriter(logFile, true)).use { bw -> @@ -110,10 +125,10 @@ internal class FlightRecorderWriterImpl( bw.append(it) } bw.append(" – ") - bw.append(message.redactSelfHostedHostnames()) + bw.append(message.redactHostnamesInMessage(hosts)) throwable?.let { bw.append(" – ") - bw.append(it.getStackTraceString().redactSelfHostedHostnames()) + bw.append(it.getStackTraceString().redactHostnamesInMessage(hosts)) } bw.newLine() } 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 5a4db3de1f..2ec1350b4c 100644 --- a/network/src/main/kotlin/com/bitwarden/network/core/NetworkResultCall.kt +++ b/network/src/main/kotlin/com/bitwarden/network/core/NetworkResultCall.kt @@ -1,7 +1,6 @@ package com.bitwarden.network.core import com.bitwarden.network.model.NetworkResult -import com.bitwarden.network.util.UNKNOWN_HOST_REGEX import okhttp3.Request import okio.IOException import okio.Timeout @@ -17,6 +16,8 @@ import java.lang.reflect.Type */ private const val NO_CONTENT_RESPONSE_CODE: Int = 204 +private val UNKNOWN_HOST_REGEX = Regex("""Unable to resolve host "([^"]+)"""") + /** * A [Call] for wrapping a network request into a [NetworkResult]. */ diff --git a/network/src/main/kotlin/com/bitwarden/network/util/HostnameRedactionUtil.kt b/network/src/main/kotlin/com/bitwarden/network/util/HostnameRedactionUtil.kt index 811266565f..807ab54fcc 100644 --- a/network/src/main/kotlin/com/bitwarden/network/util/HostnameRedactionUtil.kt +++ b/network/src/main/kotlin/com/bitwarden/network/util/HostnameRedactionUtil.kt @@ -1,38 +1,10 @@ package com.bitwarden.network.util -internal val UNKNOWN_HOST_REGEX = Regex("""Unable to resolve host "([^"]+)"""") /** * List of official Bitwarden cloud hostnames that are safe to log. */ private val BITWARDEN_HOSTS = listOf("bitwarden.com", "bitwarden.eu", "bitwarden.pw") -private val URL_HOST_REGEX = Regex("""https?://([^/?#\s"]+)""") - -// Matches hostnames as single-argument method calls, e.g. getCookies(vault.example.com) -private val METHOD_CALL_HOST_REGEX = - Regex("""\b\w+\(([a-zA-Z0-9][a-zA-Z0-9.\-]*\.[a-zA-Z]{2,})\)""") - -/** - * Extracts hostnames from URLs, UnknownHostException messages, and method-call log patterns - * present in this string, then redacts any self-hosted (non-Bitwarden) hostnames with - * [REDACTED_SELF_HOST]. - * - * Recognized patterns: - * - Full URLs: `https://hostname/path` - * - UnknownHostException: `Unable to resolve host "hostname"` - * - Method-call logs: `methodName(hostname)` - */ -internal fun String.redactSelfHostedHostnames(): String { - val urlHosts = URL_HOST_REGEX.findAll(this).map { it.groupValues[1] } - val exceptionHosts = UNKNOWN_HOST_REGEX.findAll(this).map { it.groupValues[1] } - val methodCallHosts = METHOD_CALL_HOST_REGEX.findAll(this).map { it.groupValues[1] } - val extractedHosts = (urlHosts + exceptionHosts + methodCallHosts) - .map { it.substringBefore(':') } // strip port if present - .filter { host -> BITWARDEN_HOSTS.none { host.endsWith(it) } } - .toSet() - return this.redactHostnamesInMessage(extractedHosts) -} - /** * Redacts hostnames in a log message by replacing bare hostnames with [REDACTED_SELF_HOST]. * @@ -42,7 +14,7 @@ internal fun String.redactSelfHostedHostnames(): String { * @param configuredHosts Set of hostnames to redact * @return Message with hostnames redacted as [REDACTED_SELF_HOST] */ -internal fun String.redactHostnamesInMessage(configuredHosts: Set): String = +fun String.redactHostnamesInMessage(configuredHosts: Set): String = configuredHosts.fold(this) { result, hostname -> val escapedHostname = Regex.escape(hostname) val bareHostnamePattern = Regex("""\b$escapedHostname\b""") diff --git a/network/src/test/kotlin/com/bitwarden/network/util/HostnameRedactionUtilTest.kt b/network/src/test/kotlin/com/bitwarden/network/util/HostnameRedactionUtilTest.kt index 8abdd88f1e..1d98e7aa1b 100644 --- a/network/src/test/kotlin/com/bitwarden/network/util/HostnameRedactionUtilTest.kt +++ b/network/src/test/kotlin/com/bitwarden/network/util/HostnameRedactionUtilTest.kt @@ -143,49 +143,4 @@ class HostnameRedactionUtilTest { result, ) } - - @Test - fun `redactSelfHostedHostnames redacts hostname in getCookies method-call log`() { - val message = "getCookies(vault.example.com): resolved=vault.example.com, count=0" - - val result = message.redactSelfHostedHostnames() - - assertEquals( - "getCookies([REDACTED_SELF_HOST]): resolved=[REDACTED_SELF_HOST], count=0", - result, - ) - } - - @Test - fun `redactSelfHostedHostnames redacts hostname in needsBootstrap method-call log`() { - val message = "needsBootstrap(vault.example.com): false (cookieDomain=null)" - - val result = message.redactSelfHostedHostnames() - - assertEquals( - "needsBootstrap([REDACTED_SELF_HOST]): false (cookieDomain=null)", - result, - ) - } - - @Test - fun `redactSelfHostedHostnames redacts hostname in resolveHostname method-call log`() { - val message = "resolveHostname(vault.example.com): no stored config found, using original" - - val result = message.redactSelfHostedHostnames() - - assertEquals( - "resolveHostname([REDACTED_SELF_HOST]): no stored config found, using original", - result, - ) - } - - @Test - fun `redactSelfHostedHostnames preserves Bitwarden domain in method-call log`() { - val message = "getCookies(api.bitwarden.com): resolved=api.bitwarden.com, count=3" - - val result = message.redactSelfHostedHostnames() - - assertEquals(message, result) - } }