Using the environment url hosts from ServerConfigRepository to redact logs

This commit is contained in:
Andre Rosado
2026-03-20 16:11:46 +00:00
parent 189916a010
commit 646e47e621
4 changed files with 21 additions and 78 deletions

View File

@@ -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<String>
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()
}

View File

@@ -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].
*/

View File

@@ -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>): String =
fun String.redactHostnamesInMessage(configuredHosts: Set<String>): String =
configuredHosts.fold(this) { result, hostname ->
val escapedHostname = Regex.escape(hostname)
val bareHostnamePattern = Regex("""\b$escapedHostname\b""")

View File

@@ -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)
}
}