From b855c7fddd0926f77f766a9870832430c68e3082 Mon Sep 17 00:00:00 2001 From: pruthviraja Date: Thu, 23 Apr 2026 15:27:52 -0400 Subject: [PATCH 1/5] Fix GitHub App token expiry on Windows static agents On Windows, git credential helpers (wincred / git-credential-manager) cache GitHub App installation tokens in Windows Credential Manager and serve them directly on subsequent git operations, bypassing GIT_ASKPASS entirely. This causes authentication failures every ~1 hour on permanent Windows nodes even though Jenkins correctly generates a fresh token. Fix: inside DelegatingGitHubAppCredentials.getPassword() (which runs on the agent JVM), detect when running on a Windows agent and call cmdkey /delete:git:https:// cmdkey /delete:LegacyGenericCredential:https:// immediately after obtaining a (possibly refreshed) token. This evicts the stale cached entry so that git falls through to GIT_ASKPASS and uses the fresh token Jenkins is about to provide, rather than the expired token Windows Credential Manager has cached from a prior build. The clearing is a no-op when the Credential Manager has no entry for that host (cmdkey exits 1, which is silently ignored). It is skipped entirely on non-Windows agents (Linux, macOS) and on ephemeral agents where Windows Credential Manager is empty at startup anyway. Also adds: - deriveGitHostFromApiUri(): maps https://api.github.com -> github.com and passes GHE host through unchanged. - clearWindowsCredentialManagerCache(): package-private for testing, uses a replaceable Consumer so tests never invoke cmdkey. - CLEAR_WINDOWS_CREDENTIAL_MANAGER_CACHE flag (default true): allows the behaviour to be disabled from the Jenkins script console if needed without redeploying the plugin. - GithubAppCredentialsWindowsAgentTest: unit tests for all helper methods, run on any OS via a recording stub for the cleaner. Verified on GKE Jenkins controller with a permanent Windows agent: git:https://github.com (exit: 0) <- entry evicted LegacyGenericCredential:https://github.com (exit: 1) <- not present (normal) Both checkouts succeeded after the 60s stale threshold was crossed. Fixes: #1515 --- .../GitHubAppCredentials.java | 99 +++++++++++++- .../GithubAppCredentialsWindowsAgentTest.java | 125 ++++++++++++++++++ 2 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 src/test/java/org/jenkinsci/plugins/github_branch_source/GithubAppCredentialsWindowsAgentTest.java diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials.java index 6561f4170..845e6d854 100644 --- a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials.java +++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials.java @@ -23,6 +23,7 @@ import java.io.IOException; import java.io.Serial; import java.io.Serializable; +import java.net.URI; import java.security.GeneralSecurityException; import java.time.Duration; import java.time.Instant; @@ -32,6 +33,7 @@ import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; @@ -95,6 +97,46 @@ public class GitHubAppCredentials extends BaseStandardCredentials implements Sta public static boolean ALLOW_UNSAFE_REPOSITORY_INFERENCE = Boolean.getBoolean(GitHubAppCredentials.class.getName() + ".ALLOW_UNSAFE_REPOSITORY_INFERENCE"); + /** + * On Windows agents, clears the Windows Credential Manager cache entry for the GitHub host + * before each Git credential use. This prevents Git from serving an expired GitHub App + * installation token that was cached by a previous build, and ensures Git falls through to + * {@code GIT_ASKPASS} to receive the fresh token Jenkins is about to provide. + * + *

Disable only if the {@code cmdkey} invocations cause problems in your environment. + * Non-final so it can be adjusted from the Jenkins script console if needed. + */ + @SuppressFBWarnings(value = "MS_SHOULD_BE_FINAL", justification = "Non-final for script console override") + public static boolean CLEAR_WINDOWS_CREDENTIAL_MANAGER_CACHE = Boolean.parseBoolean( + System.getProperty( + GitHubAppCredentials.class.getName() + ".CLEAR_WINDOWS_CREDENTIAL_MANAGER_CACHE", "true")); + + /** + * Replaceable executor for Windows Credential Manager key deletion. + * The string parameter is the credential key (e.g. {@code git:https://github.com}). + * Non-final to allow replacement in tests. + */ + @SuppressFBWarnings(value = "MS_SHOULD_BE_FINAL", justification = "Non-final for testing purposes") + @Restricted(NoExternalUse.class) + static Consumer windowsCredentialCleaner = key -> { + try { + Process process = + new ProcessBuilder("cmdkey", "/delete:" + key).redirectErrorStream(true).start(); + boolean finished = process.waitFor(5, TimeUnit.SECONDS); + if (!finished) { + process.destroyForcibly(); + LOGGER.log(Level.WARNING, "Timed out clearing Windows Credential Manager entry: {0}", key); + } else { + LOGGER.log( + Level.FINE, + "Cleared Windows Credential Manager entry: {0} (exit: {1})", + new Object[] {key, process.exitValue()}); + } + } catch (IOException | InterruptedException e) { + LOGGER.log(Level.WARNING, "Failed to clear Windows Credential Manager entry: " + key, e); + } + }; + @NonNull private final String appID; @@ -612,6 +654,45 @@ Object readResolve() { return this; } + /** + * Derives the Git repository host from a GitHub API URI. + * + *

Returns {@code github.com} for the standard endpoint ({@code https://api.github.com}), or + * the host component of the URI for GitHub Enterprise Server instances. + * + * @param apiUri the GitHub API URI (e.g. {@code https://api.github.com}) + * @return the corresponding Git repository host (e.g. {@code github.com}) + */ + static String deriveGitHostFromApiUri(String apiUri) { + try { + String host = new URI(apiUri).getHost(); + if (host == null) { + return "github.com"; + } + return "api.github.com".equals(host) ? "github.com" : host; + } catch (Exception e) { + LOGGER.log(Level.FINE, "Could not parse API URI to derive git host: " + apiUri, e); + return "github.com"; + } + } + + /** + * Clears cached GitHub credentials from the Windows Credential Manager for the host + * corresponding to {@code apiUri}. + * + *

Removes both the modern ({@code git:https://host}) and legacy + * ({@code LegacyGenericCredential:https://host}) key formats used by the Windows git credential + * helpers, so that Git falls through to {@code GIT_ASKPASS} and uses the fresh token Jenkins is + * about to provide. + * + * @param apiUri the GitHub API URI used to derive the Git repository host + */ + static void clearWindowsCredentialManagerCache(String apiUri) { + String httpsUrl = "https://" + deriveGitHostFromApiUri(apiUri); + windowsCredentialCleaner.accept("git:" + httpsUrl); + windowsCredentialCleaner.accept("LegacyGenericCredential:" + httpsUrl); + } + /** * Ensures that the credentials state as serialized via Remoting to an agent calls back to the * controller. Benefits: @@ -636,6 +717,8 @@ private static final class DelegatingGitHubAppCredentials extends BaseStandardCr implements StandardUsernamePasswordCredentials { private final String appID; + /** The GitHub API URI, used to derive the git host for Windows Credential Manager clearing. */ + private final String apiUri; /** * An encrypted form of all data needed to refresh the token. Used to prevent {@link GetToken} * from being abused by compromised build agents. @@ -650,6 +733,7 @@ private static final class DelegatingGitHubAppCredentials extends BaseStandardCr super(onMaster.getScope(), onMaster.getId(), onMaster.getDescription()); JenkinsJVM.checkJenkinsJVM(); appID = onMaster.getAppID(); + apiUri = onMaster.actualApiUri(); JSONObject j = new JSONObject(); j.put("appID", appID); j.put("privateKey", onMaster.getPrivateKey().getPlainText()); @@ -706,6 +790,7 @@ public String getUsername() { public Secret getPassword() { JenkinsJVM.checkNotJenkinsJVM(); try { + final Secret token; synchronized (this) { try { if (cachedToken == null || cachedToken.isStale()) { @@ -741,10 +826,22 @@ public Secret getPassword() { } } LOGGER.log(Level.FINEST, "Returned GitHub App Installation Token for app ID {0} on agent", appID); + token = cachedToken.getToken(); + } - return cachedToken.getToken(); + // On Windows agents, evict the cached credential from Windows Credential Manager + // so that Git does not serve the previously-cached (possibly expired) token to the + // next Git operation instead of calling GIT_ASKPASS for the fresh token we just + // obtained above. This is the Windows equivalent of the token-refresh fix on + // Linux; see also CLEAR_WINDOWS_CREDENTIAL_MANAGER_CACHE. + if (CLEAR_WINDOWS_CREDENTIAL_MANAGER_CACHE + && System.getProperty("os.name", "") + .toLowerCase(Locale.ROOT) + .startsWith("windows")) { + clearWindowsCredentialManagerCache(apiUri); } + return token; } catch (IOException | InterruptedException x) { throw new RuntimeException(x); } diff --git a/src/test/java/org/jenkinsci/plugins/github_branch_source/GithubAppCredentialsWindowsAgentTest.java b/src/test/java/org/jenkinsci/plugins/github_branch_source/GithubAppCredentialsWindowsAgentTest.java new file mode 100644 index 000000000..bfa8ee1b2 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/github_branch_source/GithubAppCredentialsWindowsAgentTest.java @@ -0,0 +1,125 @@ +package org.jenkinsci.plugins.github_branch_source; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.is; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +/** + * Unit tests for the Windows Credential Manager cache-clearing logic added to + * {@link GitHubAppCredentials}. + * + *

These tests exercise the static helper methods + * ({@link GitHubAppCredentials#deriveGitHostFromApiUri} and + * {@link GitHubAppCredentials#clearWindowsCredentialManagerCache}) and verify that the right + * credential keys are evicted. They run on any OS because the {@link + * GitHubAppCredentials#windowsCredentialCleaner} field is replaced with a recording stub. + */ +public class GithubAppCredentialsWindowsAgentTest { + + private Consumer originalCleaner; + private boolean originalClearFlag; + private List deletedKeys; + + @Before + public void setUp() { + originalCleaner = GitHubAppCredentials.windowsCredentialCleaner; + originalClearFlag = GitHubAppCredentials.CLEAR_WINDOWS_CREDENTIAL_MANAGER_CACHE; + deletedKeys = new ArrayList<>(); + GitHubAppCredentials.windowsCredentialCleaner = deletedKeys::add; + GitHubAppCredentials.CLEAR_WINDOWS_CREDENTIAL_MANAGER_CACHE = true; + } + + @After + public void tearDown() { + GitHubAppCredentials.windowsCredentialCleaner = originalCleaner; + GitHubAppCredentials.CLEAR_WINDOWS_CREDENTIAL_MANAGER_CACHE = originalClearFlag; + } + + // ------------------------------------------------------------------------- + // deriveGitHostFromApiUri + // ------------------------------------------------------------------------- + + @Test + public void deriveGitHost_standardGitHub() { + assertThat(GitHubAppCredentials.deriveGitHostFromApiUri("https://api.github.com"), is("github.com")); + } + + @Test + public void deriveGitHost_githubEnterprise() { + assertThat( + GitHubAppCredentials.deriveGitHostFromApiUri("https://ghe.example.com/api/v3"), + is("ghe.example.com")); + } + + @Test + public void deriveGitHost_enterpriseWithPort() { + assertThat( + GitHubAppCredentials.deriveGitHostFromApiUri("https://github.corp.example.com:8443/api/v3"), + is("github.corp.example.com")); + } + + @Test + public void deriveGitHost_malformedUri_fallsBackToGithubCom() { + assertThat(GitHubAppCredentials.deriveGitHostFromApiUri("not a uri ://???"), is("github.com")); + } + + @Test + public void deriveGitHost_emptyString_fallsBackToGithubCom() { + assertThat(GitHubAppCredentials.deriveGitHostFromApiUri(""), is("github.com")); + } + + // ------------------------------------------------------------------------- + // clearWindowsCredentialManagerCache – key format + // ------------------------------------------------------------------------- + + @Test + public void clearCache_standardGitHub_deletesExpectedKeys() { + GitHubAppCredentials.clearWindowsCredentialManagerCache("https://api.github.com"); + + assertThat( + deletedKeys, + contains("git:https://github.com", "LegacyGenericCredential:https://github.com")); + } + + @Test + public void clearCache_githubEnterprise_deletesExpectedKeys() { + GitHubAppCredentials.clearWindowsCredentialManagerCache("https://ghe.example.com/api/v3"); + + assertThat( + deletedKeys, + contains( + "git:https://ghe.example.com", + "LegacyGenericCredential:https://ghe.example.com")); + } + + @Test + public void clearCache_alwaysDeletesBothKeyFormats() { + GitHubAppCredentials.clearWindowsCredentialManagerCache("https://api.github.com"); + + assertThat("Both wincred and GCM key formats must be cleared", deletedKeys.size(), is(2)); + } + + // ------------------------------------------------------------------------- + // CLEAR_WINDOWS_CREDENTIAL_MANAGER_CACHE flag + // ------------------------------------------------------------------------- + + @Test + public void clearCache_flagDisabled_doesNotDeleteKeys() { + GitHubAppCredentials.CLEAR_WINDOWS_CREDENTIAL_MANAGER_CACHE = false; + + // Simulate what getPassword() does when the flag is false + if (GitHubAppCredentials.CLEAR_WINDOWS_CREDENTIAL_MANAGER_CACHE) { + GitHubAppCredentials.clearWindowsCredentialManagerCache("https://api.github.com"); + } + + assertThat(deletedKeys, is(empty())); + } +} From a3051788c26c54d4482bd5f1487624713d9d18c2 Mon Sep 17 00:00:00 2001 From: pruthviraja Date: Thu, 23 Apr 2026 15:27:52 -0400 Subject: [PATCH 2/5] Apply Spotless formatting --- .../GitHubAppCredentials.java | 17 ++++++++--------- .../GithubAppCredentialsWindowsAgentTest.java | 11 +++-------- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials.java index 845e6d854..2db243558 100644 --- a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials.java +++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials.java @@ -107,9 +107,8 @@ public class GitHubAppCredentials extends BaseStandardCredentials implements Sta * Non-final so it can be adjusted from the Jenkins script console if needed. */ @SuppressFBWarnings(value = "MS_SHOULD_BE_FINAL", justification = "Non-final for script console override") - public static boolean CLEAR_WINDOWS_CREDENTIAL_MANAGER_CACHE = Boolean.parseBoolean( - System.getProperty( - GitHubAppCredentials.class.getName() + ".CLEAR_WINDOWS_CREDENTIAL_MANAGER_CACHE", "true")); + public static boolean CLEAR_WINDOWS_CREDENTIAL_MANAGER_CACHE = Boolean.parseBoolean(System.getProperty( + GitHubAppCredentials.class.getName() + ".CLEAR_WINDOWS_CREDENTIAL_MANAGER_CACHE", "true")); /** * Replaceable executor for Windows Credential Manager key deletion. @@ -120,17 +119,17 @@ public class GitHubAppCredentials extends BaseStandardCredentials implements Sta @Restricted(NoExternalUse.class) static Consumer windowsCredentialCleaner = key -> { try { - Process process = - new ProcessBuilder("cmdkey", "/delete:" + key).redirectErrorStream(true).start(); + Process process = new ProcessBuilder("cmdkey", "/delete:" + key) + .redirectErrorStream(true) + .start(); boolean finished = process.waitFor(5, TimeUnit.SECONDS); if (!finished) { process.destroyForcibly(); LOGGER.log(Level.WARNING, "Timed out clearing Windows Credential Manager entry: {0}", key); } else { - LOGGER.log( - Level.FINE, - "Cleared Windows Credential Manager entry: {0} (exit: {1})", - new Object[] {key, process.exitValue()}); + LOGGER.log(Level.FINE, "Cleared Windows Credential Manager entry: {0} (exit: {1})", new Object[] { + key, process.exitValue() + }); } } catch (IOException | InterruptedException e) { LOGGER.log(Level.WARNING, "Failed to clear Windows Credential Manager entry: " + key, e); diff --git a/src/test/java/org/jenkinsci/plugins/github_branch_source/GithubAppCredentialsWindowsAgentTest.java b/src/test/java/org/jenkinsci/plugins/github_branch_source/GithubAppCredentialsWindowsAgentTest.java index bfa8ee1b2..0c26dab56 100644 --- a/src/test/java/org/jenkinsci/plugins/github_branch_source/GithubAppCredentialsWindowsAgentTest.java +++ b/src/test/java/org/jenkinsci/plugins/github_branch_source/GithubAppCredentialsWindowsAgentTest.java @@ -55,8 +55,7 @@ public void deriveGitHost_standardGitHub() { @Test public void deriveGitHost_githubEnterprise() { assertThat( - GitHubAppCredentials.deriveGitHostFromApiUri("https://ghe.example.com/api/v3"), - is("ghe.example.com")); + GitHubAppCredentials.deriveGitHostFromApiUri("https://ghe.example.com/api/v3"), is("ghe.example.com")); } @Test @@ -84,9 +83,7 @@ public void deriveGitHost_emptyString_fallsBackToGithubCom() { public void clearCache_standardGitHub_deletesExpectedKeys() { GitHubAppCredentials.clearWindowsCredentialManagerCache("https://api.github.com"); - assertThat( - deletedKeys, - contains("git:https://github.com", "LegacyGenericCredential:https://github.com")); + assertThat(deletedKeys, contains("git:https://github.com", "LegacyGenericCredential:https://github.com")); } @Test @@ -95,9 +92,7 @@ public void clearCache_githubEnterprise_deletesExpectedKeys() { assertThat( deletedKeys, - contains( - "git:https://ghe.example.com", - "LegacyGenericCredential:https://ghe.example.com")); + contains("git:https://ghe.example.com", "LegacyGenericCredential:https://ghe.example.com")); } @Test From 9c2f91237e6875683c1f4be97f81dce063c15870 Mon Sep 17 00:00:00 2001 From: pruthviraja Date: Thu, 23 Apr 2026 15:27:52 -0400 Subject: [PATCH 3/5] Remove unnecessary @SuppressFBWarnings on windowsCredentialCleaner SpotBugs does not flag Consumer fields as MS_SHOULD_BE_FINAL so the suppression annotation is redundant and triggers US_USELESS_SUPPRESSION_ON_FIELD. --- .../plugins/github_branch_source/GitHubAppCredentials.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials.java index 2db243558..9fd52eb3d 100644 --- a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials.java +++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials.java @@ -115,7 +115,6 @@ public class GitHubAppCredentials extends BaseStandardCredentials implements Sta * The string parameter is the credential key (e.g. {@code git:https://github.com}). * Non-final to allow replacement in tests. */ - @SuppressFBWarnings(value = "MS_SHOULD_BE_FINAL", justification = "Non-final for testing purposes") @Restricted(NoExternalUse.class) static Consumer windowsCredentialCleaner = key -> { try { From 649084148b022b9c0de7744f0ffb759c1b4a400d Mon Sep 17 00:00:00 2001 From: pruthviraja Date: Thu, 23 Apr 2026 15:27:52 -0400 Subject: [PATCH 4/5] Disable Windows credential cache clearing in testAgentRefresh The test verifies a strict log message sequence using contains(). On Windows CI agents our fix correctly fires cmdkey /delete, producing 'Cleared Windows Credential Manager entry' log lines that interleave with the expected sequence and break the assertion. Disable CLEAR_WINDOWS_CREDENTIAL_MANAGER_CACHE for the duration of this test (save + restore in the existing finally block) since the test is exercising token-refresh logic, not credential manager behaviour. Windows Credential Manager clearing is covered by GithubAppCredentialsWindowsAgentTest. --- .../github_branch_source/GithubAppCredentialsTest.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/test/java/org/jenkinsci/plugins/github_branch_source/GithubAppCredentialsTest.java b/src/test/java/org/jenkinsci/plugins/github_branch_source/GithubAppCredentialsTest.java index 7d9a8fb36..66d95e6d9 100644 --- a/src/test/java/org/jenkinsci/plugins/github_branch_source/GithubAppCredentialsTest.java +++ b/src/test/java/org/jenkinsci/plugins/github_branch_source/GithubAppCredentialsTest.java @@ -353,6 +353,7 @@ public void testProviderRefresh() throws Exception { @Test public void testAgentRefresh() throws Exception { final long notStaleSeconds = GitHubAppCredentials.AppInstallationToken.NOT_STALE_MINIMUM_SECONDS; + final boolean originalClearWindowsCache = GitHubAppCredentials.CLEAR_WINDOWS_CREDENTIAL_MANAGER_CACHE; try { appCredentials.setApiUri(githubApi.baseUrl()); @@ -360,6 +361,11 @@ public void testAgentRefresh() throws Exception { // Must set this to a large enough number to avoid flaky test GitHubAppCredentials.AppInstallationToken.NOT_STALE_MINIMUM_SECONDS = 10; + // Disable Windows Credential Manager cache clearing so its log messages do not + // interfere with the strict log-sequence assertions below. The clearing behaviour + // is tested separately in GithubAppCredentialsWindowsAgentTest. + GitHubAppCredentials.CLEAR_WINDOWS_CREDENTIAL_MANAGER_CACHE = false; + // Ensure we are working from sufficiently clean cache state Thread.sleep(Duration.ofSeconds(GitHubAppCredentials.AppInstallationToken.NOT_STALE_MINIMUM_SECONDS + 2) .toMillis()); @@ -463,6 +469,7 @@ public void testAgentRefresh() throws Exception { 0, RequestPatternBuilder.newRequestPattern(RequestMethod.GET, urlPathEqualTo("/rate_limit"))); } finally { GitHubAppCredentials.AppInstallationToken.NOT_STALE_MINIMUM_SECONDS = notStaleSeconds; + GitHubAppCredentials.CLEAR_WINDOWS_CREDENTIAL_MANAGER_CACHE = originalClearWindowsCache; logRecorder.doClear(); } } From 2dbfbd703101c77ed9bbd7e2d970f4590d7527ff Mon Sep 17 00:00:00 2001 From: pruthviraja Date: Thu, 23 Apr 2026 16:23:36 -0400 Subject: [PATCH 5/5] Filter wincred log messages in getOutputLines to fix testAgentRefresh on Windows CI --- .../github_branch_source/GithubAppCredentialsTest.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/test/java/org/jenkinsci/plugins/github_branch_source/GithubAppCredentialsTest.java b/src/test/java/org/jenkinsci/plugins/github_branch_source/GithubAppCredentialsTest.java index 66d95e6d9..8fa9ce8bd 100644 --- a/src/test/java/org/jenkinsci/plugins/github_branch_source/GithubAppCredentialsTest.java +++ b/src/test/java/org/jenkinsci/plugins/github_branch_source/GithubAppCredentialsTest.java @@ -516,11 +516,12 @@ private List getOutputLines() { result.addAll(agentLogs); } - // sort the logs into chronological order - // then just format the message. + // sort the logs into chronological order, then just format the message. + // Agent JVM has its own static field copy (defaults true), so filter its wincred messages. return result.stream() .sorted(Comparator.comparingLong(LogRecord::getMillis)) .map(formatter::formatMessage) + .filter(msg -> !msg.startsWith("Cleared Windows Credential Manager")) .collect(Collectors.toList()); }