Skip to content

Commit 6099503

Browse files
committed
Migrate logging obfuscation
1 parent e4d1b2e commit 6099503

5 files changed

Lines changed: 89 additions & 24 deletions

File tree

agent/src/main/kotlin/com/teamscale/jacoco/agent/PreMain.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ object PreMain {
144144
if (!differentAgents.isEmpty()) {
145145
delayedLogger.warn(
146146
"Using multiple java agents could interfere with coverage recording: ${
147-
differentAgents.joinToString()
147+
AgentOptions.obfuscateAccessToken(differentAgents.joinToString())
148148
}"
149149
)
150150
}

agent/src/main/kotlin/com/teamscale/jacoco/agent/options/AgentOptions.kt

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import java.nio.file.Paths
4444
import java.time.LocalDateTime
4545
import java.time.format.DateTimeFormatter
4646
import java.util.*
47+
import java.util.regex.Matcher
4748
import java.util.regex.Pattern
4849
import kotlin.io.path.exists
4950
import kotlin.io.path.isReadable
@@ -231,26 +232,7 @@ open class AgentOptions(private val logger: ILogger) {
231232
* "config-file=jacocoagent.properties,teamscale-access-token=************mNHn"
232233
*/
233234
val obfuscatedOptionsString: String?
234-
get() {
235-
val original = originalOptionsString ?: return ""
236-
237-
val pattern = Pattern.compile("(.*-access-token=)([^,]+)(.*)")
238-
val match = pattern.matcher(original)
239-
if (match.find()) {
240-
val apiKey = match.group(2)
241-
val obfuscatedApiKey = "************${
242-
apiKey.substring(
243-
max(
244-
0,
245-
apiKey.length - 4
246-
)
247-
)
248-
}"
249-
return "${match.group(1)}$obfuscatedApiKey${match.group(3)}"
250-
}
251-
252-
return originalOptionsString
253-
}
235+
get() = obfuscateAccessToken(originalOptionsString)
254236

255237
/**
256238
* Validates the options and returns a validator with all validation errors.
@@ -676,6 +658,39 @@ open class AgentOptions(private val logger: ILogger) {
676658
val DATE_TIME_FORMATTER: DateTimeFormatter =
677659
DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm-ss.SSS", Locale.ENGLISH)
678660

661+
/**
662+
* Obfuscates any "*-access-token=..." value in the given options string for safe logging. Keeps only the
663+
* last 4 characters of each token. Returns an empty string for `null` input and the original string when no
664+
* token pattern matches.
665+
*/
666+
@JvmStatic
667+
fun obfuscateAccessToken(optionsString: String?): String {
668+
if (optionsString == null) {
669+
return ""
670+
}
671+
672+
val pattern = Pattern.compile("(-access-token=)([^,\\n\\r]+)")
673+
val matcher = pattern.matcher(optionsString)
674+
val obfuscated = StringBuffer()
675+
var foundAny = false
676+
while (matcher.find()) {
677+
foundAny = true
678+
val apiKey = matcher.group(2)
679+
val obfuscatedApiKey = "************${
680+
apiKey.substring(max(0, apiKey.length - 4))
681+
}"
682+
matcher.appendReplacement(
683+
obfuscated,
684+
Matcher.quoteReplacement(matcher.group(1) + obfuscatedApiKey)
685+
)
686+
}
687+
if (!foundAny) {
688+
return optionsString
689+
}
690+
matcher.appendTail(obfuscated)
691+
return obfuscated.toString()
692+
}
693+
679694
/**
680695
* The default excludes applied to JaCoCo. These are packages that should never be profiled. Excluding them makes
681696
* debugging traces easier and reduces trace size and warnings about unmatched classes in Teamscale.

agent/src/main/kotlin/com/teamscale/jacoco/agent/options/AgentOptionsParser.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ class AgentOptionsParser @VisibleForTesting internal constructor(
5959
if (optionsString == null) {
6060
optionsString = ""
6161
}
62-
logger.debug("Parsing options: $optionsString")
62+
logger.debug("Parsing options: ${AgentOptions.obfuscateAccessToken(optionsString)}")
6363

6464
val options = AgentOptions(logger)
6565
options.originalOptionsString = optionsString
@@ -365,7 +365,11 @@ class AgentOptionsParser @VisibleForTesting internal constructor(
365365
options.teamscaleServer.userAccessToken!!
366366
)
367367
options.configurationViaTeamscale = configuration
368-
logger.debug("Received the following options from Teamscale: ${configuration.profilerConfiguration!!.configurationOptions}")
368+
logger.debug(
369+
"Received the following options from Teamscale: ${
370+
AgentOptions.obfuscateAccessToken(configuration.profilerConfiguration!!.configurationOptions)
371+
}"
372+
)
369373
readConfigFromString(options, configuration.profilerConfiguration!!.configurationOptions)
370374
}
371375

agent/src/test/kotlin/com/teamscale/jacoco/agent/options/AgentOptionsTest.kt

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -593,6 +593,52 @@ class AgentOptionsTest {
593593
return AgentOptionsParser(CommandLineLogger(), null, null, credentials, null)
594594
}
595595

596+
/** Tests that [AgentOptions.obfuscateAccessToken] hides the token in a comma-separated options string. */
597+
@Test
598+
fun obfuscateAccessTokenHidesTokenInOptionsString() {
599+
val input =
600+
"config-file=jacocoagent.properties,teamscale-access-token=unlYgehaYYYhbPAegNWV3WgjOzxkmNHn,teamscale-partition=p"
601+
Assertions.assertThat(AgentOptions.obfuscateAccessToken(input))
602+
.isEqualTo("config-file=jacocoagent.properties,teamscale-access-token=************mNHn,teamscale-partition=p")
603+
}
604+
605+
/** Tests that obfuscation also covers tokens in newline-separated input (the format Teamscale returns for config-id). */
606+
@Test
607+
fun obfuscateAccessTokenHidesTokenInNewlineSeparatedString() {
608+
val input = "teamscale-access-token=unlYgehaYYYhbPAegNWV3WgjOzxkmNHn\nteamscale-partition=p"
609+
Assertions.assertThat(AgentOptions.obfuscateAccessToken(input))
610+
.isEqualTo("teamscale-access-token=************mNHn\nteamscale-partition=p")
611+
}
612+
613+
/** Tests that obfuscation hides every `*-access-token=` occurrence, not just the last one. */
614+
@Test
615+
fun obfuscateAccessTokenHidesMultipleTokens() {
616+
val input =
617+
"teamscale-access-token=unlYgehaYYYhbPAegNWV3WgjOzxkmNHn,artifactory-access-token=anotherSecretAbcd"
618+
Assertions.assertThat(AgentOptions.obfuscateAccessToken(input))
619+
.isEqualTo("teamscale-access-token=************mNHn,artifactory-access-token=************Abcd")
620+
}
621+
622+
/** Tests that strings without an access token are returned unchanged. */
623+
@Test
624+
fun obfuscateAccessTokenReturnsInputUnchangedWhenNoTokenPresent() {
625+
val input = "config-file=jacocoagent.properties,teamscale-partition=p"
626+
Assertions.assertThat(AgentOptions.obfuscateAccessToken(input)).isEqualTo(input)
627+
}
628+
629+
/** Tests the null-input contract used by [AgentOptions.obfuscatedOptionsString]. */
630+
@Test
631+
fun obfuscateAccessTokenReturnsEmptyStringForNullInput() {
632+
Assertions.assertThat(AgentOptions.obfuscateAccessToken(null)).isEmpty()
633+
}
634+
635+
/** Tests that a token shorter than 4 characters does not throw and is fully obfuscated. */
636+
@Test
637+
fun obfuscateAccessTokenHandlesShortToken() {
638+
Assertions.assertThat(AgentOptions.obfuscateAccessToken("teamscale-access-token=abc"))
639+
.isEqualTo("teamscale-access-token=************abc")
640+
}
641+
596642
/**
597643
* Delete created coverage folders
598644
*/

common-system-test/src/main/kotlin/com/teamscale/test/commons/TeamscaleMockServer.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ class TeamscaleMockServer(val port: Int) {
5252

5353
init {
5454
service.port(port)
55-
service.exception<Exception>(Exception::class.java) { exception, _, response ->
55+
service.exception(Exception::class.java) { exception, _, response ->
5656
response.status(HttpServletResponse.SC_INTERNAL_SERVER_ERROR)
5757
response.body("Exception: " + exception!!.message)
5858
}

0 commit comments

Comments
 (0)