From a925403b94a25f755dd74f51c7a24384e1523a68 Mon Sep 17 00:00:00 2001 From: lanced00m Date: Sat, 6 Apr 2024 04:36:36 +0400 Subject: [PATCH 1/6] initial mlflow weak credential tester --- ...WeakCredentialDetectorBootstrapModule.java | 2 + .../mlflow/MlFlowCredentialTester.java | 186 ++++++++++++++++ .../service_default_credentials.textproto | 12 ++ .../mlflow/MlFlowCredentialTesterTest.java | 199 ++++++++++++++++++ 4 files changed, 399 insertions(+) create mode 100644 google/detectors/credentials/generic_weak_credential_detector/src/main/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/mlflow/MlFlowCredentialTester.java create mode 100644 google/detectors/credentials/generic_weak_credential_detector/src/test/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/mlflow/MlFlowCredentialTesterTest.java diff --git a/google/detectors/credentials/generic_weak_credential_detector/src/main/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/GenericWeakCredentialDetectorBootstrapModule.java b/google/detectors/credentials/generic_weak_credential_detector/src/main/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/GenericWeakCredentialDetectorBootstrapModule.java index 9845a3692..d6e512726 100644 --- a/google/detectors/credentials/generic_weak_credential_detector/src/main/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/GenericWeakCredentialDetectorBootstrapModule.java +++ b/google/detectors/credentials/generic_weak_credential_detector/src/main/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/GenericWeakCredentialDetectorBootstrapModule.java @@ -39,6 +39,7 @@ import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.grafana.GrafanaCredentialTester; import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.hydra.HydraCredentialTester; import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.jenkins.JenkinsCredentialTester; +import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.mlflow.MlFlowCredentialTester; import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.mysql.MysqlCredentialTester; import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.ncrack.NcrackCredentialTester; import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.postgres.PostgresCredentialTester; @@ -64,6 +65,7 @@ protected void configurePlugin() { Multibinder credentialTesterBinder = Multibinder.newSetBinder(binder(), CredentialTester.class); credentialTesterBinder.addBinding().to(JenkinsCredentialTester.class); + credentialTesterBinder.addBinding().to(MlFlowCredentialTester.class); credentialTesterBinder.addBinding().to(MysqlCredentialTester.class); credentialTesterBinder.addBinding().to(HydraCredentialTester.class); credentialTesterBinder.addBinding().to(NcrackCredentialTester.class); diff --git a/google/detectors/credentials/generic_weak_credential_detector/src/main/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/mlflow/MlFlowCredentialTester.java b/google/detectors/credentials/generic_weak_credential_detector/src/main/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/mlflow/MlFlowCredentialTester.java new file mode 100644 index 000000000..d7c106c8f --- /dev/null +++ b/google/detectors/credentials/generic_weak_credential_detector/src/main/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/mlflow/MlFlowCredentialTester.java @@ -0,0 +1,186 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.mlflow; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.tsunami.common.net.http.HttpRequest.get; +import static com.google.tsunami.common.net.http.HttpRequest.post; +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import com.google.common.flogger.GoogleLogger; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.JsonSyntaxException; +import com.google.protobuf.ByteString; +import com.google.tsunami.common.data.NetworkEndpointUtils; +import com.google.tsunami.common.data.NetworkServiceUtils; +import com.google.tsunami.common.net.http.HttpClient; +import com.google.tsunami.common.net.http.HttpHeaders; +import com.google.tsunami.common.net.http.HttpResponse; +import com.google.tsunami.common.net.http.HttpStatus; +import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.provider.TestCredential; +import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.tester.CredentialTester; +import com.google.tsunami.proto.NetworkService; + +import java.io.IOException; +import java.util.Base64; +import java.util.List; +import javax.inject.Inject; + +/** + * Credential tester specifically for mlflow. + */ +public final class MlFlowCredentialTester extends CredentialTester { + private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); + private final HttpClient httpClient; + + @Inject + MlFlowCredentialTester(HttpClient httpClient) { + this.httpClient = checkNotNull(httpClient); + } + + @Override + public String name() { + return "MlFlowCredentialTester"; + } + + @Override + public String description() { + return "MlFlow credential tester."; + } + + @Override + public boolean canAccept(NetworkService networkService) { + if (!NetworkServiceUtils.isWebService(networkService)) { + return false; + } + + boolean canAcceptByCustomFingerprint = false; + logger.atInfo().log("probing Mlflow ping - custom fingerprint phase"); + + // we want to test mlflow versions above 2.5 which has basic authentication module + // these versions returned a 401 status code and a link to documentation about how to + // authenticate. + var uriAuthority = NetworkEndpointUtils.toUriAuthority(networkService.getNetworkEndpoint()); + var pingApiUrl = String.format("http://%s/%s", uriAuthority, "ping"); + try { + HttpResponse apiPingResponse = httpClient.send(get(pingApiUrl).withEmptyHeaders().build()); + + if (apiPingResponse.status() == HttpStatus.UNAUTHORIZED + && apiPingResponse.bodyString().isPresent()) { + canAcceptByCustomFingerprint = + apiPingResponse + .bodyString() + .get() + .contains( + "You are not authenticated. " + + "Please see https://www.mlflow.org/docs/latest/auth/index.html#authenticating-to-mlflow " + + "on how to authenticate"); + } + } catch (IOException e) { + logger.atWarning().withCause(e).log("Unable to query '%s'.", pingApiUrl); + return false; + } + + return canAcceptByCustomFingerprint; + } + + @Override + public boolean batched() { + return true; + } + + @Override + public ImmutableList testValidCredentials( + NetworkService networkService, List credentials) { + // Always return 1st weak credential to gracefully handle no auth configured case, where we + // return empty credential instead of all the weak credentials + return credentials.stream() + .filter(cred -> isMlFlowAccessible(networkService, cred)) + .findFirst() + .map(ImmutableList::of) + .orElseGet(ImmutableList::of); + } + + private boolean isMlFlowAccessible(NetworkService networkService, TestCredential credential) { + var uriAuthority = NetworkEndpointUtils.toUriAuthority(networkService.getNetworkEndpoint()); + var url = String.format("http://%s/%s", uriAuthority, "api/2.0/mlflow/users/create"); + try { + logger.atInfo().log( + "url: %s, username: %s, password: %s", + url, credential.username(), credential.password().orElse("")); + HttpResponse response = sendRequestWithCredentials(url, credential); + return response.status().isSuccess() + && response + .bodyString() + .map(MlFlowCredentialTester::bodyContainsSuccessfulUserRegistration) + .orElse(false); + } catch (IOException e) { + logger.atWarning().withCause(e).log("Unable to query '%s'.", url); + return false; + } + } + + private HttpResponse sendRequestWithCredentials(String url, TestCredential credential) + throws IOException { + // For testing no-auth configured case, no auth header is passed in + if (Strings.isNullOrEmpty(credential.username()) + && Strings.isNullOrEmpty(credential.password().orElse(""))) { + return httpClient.send(post(url).withEmptyHeaders().build()); + } + return httpClient.send( + post(url) + .setHeaders( + HttpHeaders.builder() + .addHeader( + "Authorization", + "basic " + + Base64.getEncoder() + .encodeToString( + (credential.username() + ":" + credential.password().orElse("")) + .getBytes(UTF_8))) + .build()) + .setRequestBody( + ByteString.copyFromUtf8( + "{\"username\": \"googleTsunamiSecurityScanner\", \"password\": \"googleTsunamiSecurityScanner\"}")) + .build()); + } + + /** + * A successful authenticated request to the /api/2.0/mlflow/users/create endpoint returns a JSON + * with a root key like the following: + * {"user":{"experiment_permissions":[],"id":4,"is_admin":false,"registered_model_permissions":[], + * "username":"googleTsunamiSecurityScanner"}} + */ + private static boolean bodyContainsSuccessfulUserRegistration(String responseBody) { + try { + JsonObject response = JsonParser.parseString(responseBody).getAsJsonObject(); + + if (response.has("user")) { + logger.atInfo().log("Successfully created a new mlflow user as an admin"); + return true; + } else { + return false; + } + } catch (JsonSyntaxException e) { + logger.atWarning().withCause(e).log( + "An error occurred while parsing the json response: %s", responseBody); + return false; + } + } +} diff --git a/google/detectors/credentials/generic_weak_credential_detector/src/main/resources/detectors/credentials/genericweakcredentialdetector/data/service_default_credentials.textproto b/google/detectors/credentials/generic_weak_credential_detector/src/main/resources/detectors/credentials/genericweakcredentialdetector/data/service_default_credentials.textproto index 35d38fddf..d2a7899f5 100644 --- a/google/detectors/credentials/generic_weak_credential_detector/src/main/resources/detectors/credentials/genericweakcredentialdetector/data/service_default_credentials.textproto +++ b/google/detectors/credentials/generic_weak_credential_detector/src/main/resources/detectors/credentials/genericweakcredentialdetector/data/service_default_credentials.textproto @@ -60,3 +60,15 @@ service_default_credentials { default_usernames: "guest" default_passwords: "guest" } + +service_default_credentials { + service_name: "mlflow" + default_usernames: "user_a" + default_passwords: "password_a" + default_usernames: "user_b" + default_passwords: "password_b" + default_usernames: "admin" + default_passwords: "password" + default_usernames: "username" + default_passwords: "password" +} diff --git a/google/detectors/credentials/generic_weak_credential_detector/src/test/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/mlflow/MlFlowCredentialTesterTest.java b/google/detectors/credentials/generic_weak_credential_detector/src/test/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/mlflow/MlFlowCredentialTesterTest.java new file mode 100644 index 000000000..b06f91e26 --- /dev/null +++ b/google/detectors/credentials/generic_weak_credential_detector/src/test/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/mlflow/MlFlowCredentialTesterTest.java @@ -0,0 +1,199 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.mlflow; + +import com.google.common.collect.ImmutableList; +import com.google.common.flogger.GoogleLogger; +import com.google.inject.Guice; +import com.google.tsunami.common.net.db.ConnectionProviderInterface; +import com.google.tsunami.common.net.http.HttpClientModule; +import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.provider.TestCredential; +import com.google.tsunami.proto.NetworkService; +import okhttp3.mockwebserver.Dispatcher; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import javax.inject.Inject; +import java.io.IOException; +import java.sql.Connection; +import java.util.Objects; +import java.util.Optional; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.tsunami.common.data.NetworkEndpointUtils.forHostnameAndPort; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +/** + * Tests for {@link MlFlowCredentialTester}. + */ +@RunWith(JUnit4.class) +public class MlFlowCredentialTesterTest { + @Rule + public MockitoRule rule = MockitoJUnit.rule(); + @Mock + private ConnectionProviderInterface mockConnectionProvider; + @Mock + private Connection mockConnection; + @Inject + private MlFlowCredentialTester tester; + private MockWebServer mockWebServer; + private static final TestCredential WEAK_CRED_1 = + TestCredential.create("admin", Optional.of("password")); + private static final TestCredential WEAK_CRED_2 = + TestCredential.create("username", Optional.of("password")); + private static final TestCredential WRONG_CRED_1 = + TestCredential.create("wrong", Optional.of("wrong")); + + private static final String WEAK_CRED_AUTH_1 = "basic dXNlcm5hbWU6cGFzc3dvcmQ="; + private static final String WEAK_CRED_AUTH_2 = "basic YWRtaW46cGFzc3dvcmQ="; + private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); + + @Before + public void setup() { + mockWebServer = new MockWebServer(); + Guice.createInjector(new HttpClientModule.Builder().build()).injectMembers(this); + } + + @Test + public void detect_weakCredentialsExists_returnsWeakCredentials() throws Exception { + startMockWebServer(); + NetworkService targetNetworkService = + NetworkService.newBuilder() + .setNetworkEndpoint( + forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort())) + .setServiceName("http") + .build(); + + assertThat(tester.testValidCredentials(targetNetworkService, ImmutableList.of(WEAK_CRED_1))) + .containsExactly(WEAK_CRED_1); + mockWebServer.shutdown(); + } + + @Test + public void detect_weakCredentialsExist_returnsFirstWeakCredentials() throws Exception { + startMockWebServer(); + NetworkService targetNetworkService = + NetworkService.newBuilder() + .setNetworkEndpoint( + forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort())) + .setServiceName("http") + .build(); + + assertThat( + tester.testValidCredentials( + targetNetworkService, ImmutableList.of(WEAK_CRED_1, WEAK_CRED_2))) + .containsExactly(WEAK_CRED_1); + } + + @Test + public void detect_canAccept() throws Exception { + startMockWebServer(); + NetworkService targetNetworkService = + NetworkService.newBuilder() + .setNetworkEndpoint( + forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort())) + .setServiceName("http") + .build(); + + assertThat(tester.canAccept(targetNetworkService)).isTrue(); + } + + @Test + public void detect_weakCredentialsExistAndMlflowInForeignLanguage_returnsFirstWeakCredentials() + throws Exception { + startMockWebServer(); + NetworkService targetNetworkService = + NetworkService.newBuilder() + .setNetworkEndpoint( + forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort())) + .setServiceName("http") + .build(); + + assertThat( + tester.testValidCredentials( + targetNetworkService, ImmutableList.of(WEAK_CRED_1, WEAK_CRED_2))) + .containsExactly(WEAK_CRED_1); + } + + @Test + public void detect_noWeakCredentials_returnsNoCredentials() throws Exception { + startMockWebServer(); + NetworkService targetNetworkService = + NetworkService.newBuilder() + .setNetworkEndpoint( + forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort())) + .setServiceName("http") + .build(); + assertThat(tester.testValidCredentials(targetNetworkService, ImmutableList.of(WRONG_CRED_1))) + .isEmpty(); + } + + @Test + public void detect_nonMlflowService_skips() throws Exception { + when(mockConnectionProvider.getConnection(any(), any(), any())).thenReturn(mockConnection); + NetworkService targetNetworkService = + NetworkService.newBuilder() + .setNetworkEndpoint(forHostnameAndPort("example.com", 8080)) + .setServiceName("http") + .build(); + + assertThat(tester.testValidCredentials(targetNetworkService, ImmutableList.of(WEAK_CRED_1))) + .isEmpty(); + verifyNoInteractions(mockConnectionProvider); + } + + private void startMockWebServer() + throws IOException { + final Dispatcher dispatcher = new Dispatcher() { + final MockResponse unauthorizedResponse = new MockResponse().setResponseCode(401).setBody("You are not authenticated. " + + "Please see https://www.mlflow.org/docs/latest/auth/index.html#authenticating-to-mlflow " + + "on how to authenticate"); + + @Override + public MockResponse dispatch(RecordedRequest request) { + String authorizationHeader = request.getHeaders().get("Authorization"); + if (authorizationHeader == null) { + return unauthorizedResponse; + } + if (Objects.equals(request.getPath(), "/api/2.0/mlflow/users/create") && Objects.equals(request.getMethod(), "POST")) { + boolean isDefaultCredentials = authorizationHeader.equals(WEAK_CRED_AUTH_1) || authorizationHeader.equals(WEAK_CRED_AUTH_2); + if (isDefaultCredentials) { + return new MockResponse().setResponseCode(200) + .setBody("{\"user\":{\"experiment_permissions\":[],\"id\":4,\"is_admin\":false,\"registered_model_permissions\":[],\n" + + " \"username\":\"googleTsunamiSecurityScanner\"}}"); + } else { + return unauthorizedResponse; + } + } + return new MockResponse().setResponseCode(404); + } + }; + mockWebServer.setDispatcher(dispatcher); + mockWebServer.start(); + mockWebServer.url("/"); + } +} From 478abbff3c283eb2da7bed52a4d41104b13a4c28 Mon Sep 17 00:00:00 2001 From: lanced00m Date: Sat, 27 Apr 2024 15:09:20 +0400 Subject: [PATCH 2/6] change create new user method to get user info method, fix tests accordingly --- .../mlflow/MlFlowCredentialTester.java | 264 ++++++++-------- .../mlflow/MlFlowCredentialTesterTest.java | 296 +++++++++--------- 2 files changed, 280 insertions(+), 280 deletions(-) diff --git a/google/detectors/credentials/generic_weak_credential_detector/src/main/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/mlflow/MlFlowCredentialTester.java b/google/detectors/credentials/generic_weak_credential_detector/src/main/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/mlflow/MlFlowCredentialTester.java index d7c106c8f..e24b567f6 100644 --- a/google/detectors/credentials/generic_weak_credential_detector/src/main/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/mlflow/MlFlowCredentialTester.java +++ b/google/detectors/credentials/generic_weak_credential_detector/src/main/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/mlflow/MlFlowCredentialTester.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.mlflow; import static com.google.common.base.Preconditions.checkNotNull; @@ -26,7 +27,6 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.google.gson.JsonSyntaxException; -import com.google.protobuf.ByteString; import com.google.tsunami.common.data.NetworkEndpointUtils; import com.google.tsunami.common.data.NetworkServiceUtils; import com.google.tsunami.common.net.http.HttpClient; @@ -36,151 +36,149 @@ import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.provider.TestCredential; import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.tester.CredentialTester; import com.google.tsunami.proto.NetworkService; - import java.io.IOException; import java.util.Base64; import java.util.List; import javax.inject.Inject; -/** - * Credential tester specifically for mlflow. - */ +/** Credential tester specifically for mlflow. */ public final class MlFlowCredentialTester extends CredentialTester { - private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); - private final HttpClient httpClient; - - @Inject - MlFlowCredentialTester(HttpClient httpClient) { - this.httpClient = checkNotNull(httpClient); + private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); + private final HttpClient httpClient; + + @Inject + MlFlowCredentialTester(HttpClient httpClient) { + this.httpClient = checkNotNull(httpClient); + } + + @Override + public String name() { + return "MlFlowCredentialTester"; + } + + @Override + public String description() { + return "MlFlow credential tester."; + } + + @Override + public boolean canAccept(NetworkService networkService) { + if (!NetworkServiceUtils.isWebService(networkService)) { + return false; } - @Override - public String name() { - return "MlFlowCredentialTester"; + boolean canAcceptByCustomFingerprint = false; + logger.atInfo().log("probing Mlflow ping - custom fingerprint phase"); + + // we want to test mlflow versions above 2.5 which has basic authentication module + // these versions returned a 401 status code and a link to documentation about how to + // authenticate. + var uriAuthority = NetworkEndpointUtils.toUriAuthority(networkService.getNetworkEndpoint()); + var pingApiUrl = String.format("http://%s/%s", uriAuthority, "ping"); + try { + HttpResponse apiPingResponse = httpClient.send(get(pingApiUrl).withEmptyHeaders().build()); + + if (apiPingResponse.status() == HttpStatus.UNAUTHORIZED + && apiPingResponse.bodyString().isPresent()) { + canAcceptByCustomFingerprint = + apiPingResponse + .bodyString() + .get() + .contains( + "You are not authenticated. " + + "Please see https://www.mlflow.org/docs/latest/auth/index.html#authenticating-to-mlflow " + + "on how to authenticate"); + } + } catch (IOException e) { + logger.atWarning().withCause(e).log("Unable to query '%s'.", pingApiUrl); + return false; } - @Override - public String description() { - return "MlFlow credential tester."; + return canAcceptByCustomFingerprint; + } + + @Override + public boolean batched() { + return true; + } + + @Override + public ImmutableList testValidCredentials( + NetworkService networkService, List credentials) { + // Always return 1st weak credential to gracefully handle no auth configured case, where we + // return empty credential instead of all the weak credentials + return credentials.stream() + .filter(cred -> isMlFlowAccessible(networkService, cred)) + .findFirst() + .map(ImmutableList::of) + .orElseGet(ImmutableList::of); + } + + private boolean isMlFlowAccessible(NetworkService networkService, TestCredential credential) { + var uriAuthority = NetworkEndpointUtils.toUriAuthority(networkService.getNetworkEndpoint()); + var url = + String.format( + "http://%s/%s?username=%s", + uriAuthority, "api/2.0/mlflow/users/get", credential.username()); + try { + logger.atInfo().log( + "url: %s, username: %s, password: %s", + url, credential.username(), credential.password().orElse("")); + HttpResponse response = sendRequestWithCredentials(url, credential); + return response.status().isSuccess() + && response + .bodyString() + .map(MlFlowCredentialTester::bodyContainsSuccessfulUserInfo) + .orElse(false); + } catch (IOException e) { + logger.atWarning().withCause(e).log("Unable to query '%s'.", url); + return false; } - - @Override - public boolean canAccept(NetworkService networkService) { - if (!NetworkServiceUtils.isWebService(networkService)) { - return false; - } - - boolean canAcceptByCustomFingerprint = false; - logger.atInfo().log("probing Mlflow ping - custom fingerprint phase"); - - // we want to test mlflow versions above 2.5 which has basic authentication module - // these versions returned a 401 status code and a link to documentation about how to - // authenticate. - var uriAuthority = NetworkEndpointUtils.toUriAuthority(networkService.getNetworkEndpoint()); - var pingApiUrl = String.format("http://%s/%s", uriAuthority, "ping"); - try { - HttpResponse apiPingResponse = httpClient.send(get(pingApiUrl).withEmptyHeaders().build()); - - if (apiPingResponse.status() == HttpStatus.UNAUTHORIZED - && apiPingResponse.bodyString().isPresent()) { - canAcceptByCustomFingerprint = - apiPingResponse - .bodyString() - .get() - .contains( - "You are not authenticated. " - + "Please see https://www.mlflow.org/docs/latest/auth/index.html#authenticating-to-mlflow " - + "on how to authenticate"); - } - } catch (IOException e) { - logger.atWarning().withCause(e).log("Unable to query '%s'.", pingApiUrl); - return false; - } - - return canAcceptByCustomFingerprint; + } + + private HttpResponse sendRequestWithCredentials(String url, TestCredential credential) + throws IOException { + // For testing no-auth configured case, no auth header is passed in + if (Strings.isNullOrEmpty(credential.username()) + && Strings.isNullOrEmpty(credential.password().orElse(""))) { + return httpClient.send(post(url).withEmptyHeaders().build()); } - @Override - public boolean batched() { + return httpClient.send( + get(url) + .setHeaders( + HttpHeaders.builder() + .addHeader( + "Authorization", + "basic " + + Base64.getEncoder() + .encodeToString( + (credential.username() + ":" + credential.password().orElse("")) + .getBytes(UTF_8))) + .build()) + .build()); + } + + /** + * A successful authenticated request to the /api/2.0/mlflow/users/get?username=admin endpoint + * returns a JSON with a root key like the following: + * {"user":{"experiment_permissions":[],"id":1,"is_admin":true,"registered_model_permissions":[], + * "username":"admin"}} + */ + private static boolean bodyContainsSuccessfulUserInfo(String responseBody) { + try { + JsonObject response = JsonParser.parseString(responseBody).getAsJsonObject(); + + if (response.has("user")) { + logger.atInfo().log("Successfully received a mlflow user info"); return true; + } else { + return false; + } + } catch (JsonSyntaxException e) { + logger.atWarning().withCause(e).log( + "An error occurred while parsing the json response: %s", responseBody); + return false; } - - @Override - public ImmutableList testValidCredentials( - NetworkService networkService, List credentials) { - // Always return 1st weak credential to gracefully handle no auth configured case, where we - // return empty credential instead of all the weak credentials - return credentials.stream() - .filter(cred -> isMlFlowAccessible(networkService, cred)) - .findFirst() - .map(ImmutableList::of) - .orElseGet(ImmutableList::of); - } - - private boolean isMlFlowAccessible(NetworkService networkService, TestCredential credential) { - var uriAuthority = NetworkEndpointUtils.toUriAuthority(networkService.getNetworkEndpoint()); - var url = String.format("http://%s/%s", uriAuthority, "api/2.0/mlflow/users/create"); - try { - logger.atInfo().log( - "url: %s, username: %s, password: %s", - url, credential.username(), credential.password().orElse("")); - HttpResponse response = sendRequestWithCredentials(url, credential); - return response.status().isSuccess() - && response - .bodyString() - .map(MlFlowCredentialTester::bodyContainsSuccessfulUserRegistration) - .orElse(false); - } catch (IOException e) { - logger.atWarning().withCause(e).log("Unable to query '%s'.", url); - return false; - } - } - - private HttpResponse sendRequestWithCredentials(String url, TestCredential credential) - throws IOException { - // For testing no-auth configured case, no auth header is passed in - if (Strings.isNullOrEmpty(credential.username()) - && Strings.isNullOrEmpty(credential.password().orElse(""))) { - return httpClient.send(post(url).withEmptyHeaders().build()); - } - return httpClient.send( - post(url) - .setHeaders( - HttpHeaders.builder() - .addHeader( - "Authorization", - "basic " - + Base64.getEncoder() - .encodeToString( - (credential.username() + ":" + credential.password().orElse("")) - .getBytes(UTF_8))) - .build()) - .setRequestBody( - ByteString.copyFromUtf8( - "{\"username\": \"googleTsunamiSecurityScanner\", \"password\": \"googleTsunamiSecurityScanner\"}")) - .build()); - } - - /** - * A successful authenticated request to the /api/2.0/mlflow/users/create endpoint returns a JSON - * with a root key like the following: - * {"user":{"experiment_permissions":[],"id":4,"is_admin":false,"registered_model_permissions":[], - * "username":"googleTsunamiSecurityScanner"}} - */ - private static boolean bodyContainsSuccessfulUserRegistration(String responseBody) { - try { - JsonObject response = JsonParser.parseString(responseBody).getAsJsonObject(); - - if (response.has("user")) { - logger.atInfo().log("Successfully created a new mlflow user as an admin"); - return true; - } else { - return false; - } - } catch (JsonSyntaxException e) { - logger.atWarning().withCause(e).log( - "An error occurred while parsing the json response: %s", responseBody); - return false; - } - } + } } diff --git a/google/detectors/credentials/generic_weak_credential_detector/src/test/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/mlflow/MlFlowCredentialTesterTest.java b/google/detectors/credentials/generic_weak_credential_detector/src/test/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/mlflow/MlFlowCredentialTesterTest.java index b06f91e26..6c6424221 100644 --- a/google/detectors/credentials/generic_weak_credential_detector/src/test/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/mlflow/MlFlowCredentialTesterTest.java +++ b/google/detectors/credentials/generic_weak_credential_detector/src/test/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/mlflow/MlFlowCredentialTesterTest.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.mlflow; import com.google.common.collect.ImmutableList; @@ -34,166 +35,167 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; - import javax.inject.Inject; import java.io.IOException; import java.sql.Connection; import java.util.Objects; import java.util.Optional; - import static com.google.common.truth.Truth.assertThat; import static com.google.tsunami.common.data.NetworkEndpointUtils.forHostnameAndPort; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; -/** - * Tests for {@link MlFlowCredentialTester}. - */ +/** Tests for {@link MlFlowCredentialTester}. */ @RunWith(JUnit4.class) public class MlFlowCredentialTesterTest { - @Rule - public MockitoRule rule = MockitoJUnit.rule(); - @Mock - private ConnectionProviderInterface mockConnectionProvider; - @Mock - private Connection mockConnection; - @Inject - private MlFlowCredentialTester tester; - private MockWebServer mockWebServer; - private static final TestCredential WEAK_CRED_1 = - TestCredential.create("admin", Optional.of("password")); - private static final TestCredential WEAK_CRED_2 = - TestCredential.create("username", Optional.of("password")); - private static final TestCredential WRONG_CRED_1 = - TestCredential.create("wrong", Optional.of("wrong")); - - private static final String WEAK_CRED_AUTH_1 = "basic dXNlcm5hbWU6cGFzc3dvcmQ="; - private static final String WEAK_CRED_AUTH_2 = "basic YWRtaW46cGFzc3dvcmQ="; - private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); - - @Before - public void setup() { - mockWebServer = new MockWebServer(); - Guice.createInjector(new HttpClientModule.Builder().build()).injectMembers(this); - } - - @Test - public void detect_weakCredentialsExists_returnsWeakCredentials() throws Exception { - startMockWebServer(); - NetworkService targetNetworkService = - NetworkService.newBuilder() - .setNetworkEndpoint( - forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort())) - .setServiceName("http") - .build(); - - assertThat(tester.testValidCredentials(targetNetworkService, ImmutableList.of(WEAK_CRED_1))) - .containsExactly(WEAK_CRED_1); - mockWebServer.shutdown(); - } - - @Test - public void detect_weakCredentialsExist_returnsFirstWeakCredentials() throws Exception { - startMockWebServer(); - NetworkService targetNetworkService = - NetworkService.newBuilder() - .setNetworkEndpoint( - forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort())) - .setServiceName("http") - .build(); - - assertThat( - tester.testValidCredentials( - targetNetworkService, ImmutableList.of(WEAK_CRED_1, WEAK_CRED_2))) - .containsExactly(WEAK_CRED_1); - } - - @Test - public void detect_canAccept() throws Exception { - startMockWebServer(); - NetworkService targetNetworkService = - NetworkService.newBuilder() - .setNetworkEndpoint( - forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort())) - .setServiceName("http") - .build(); - - assertThat(tester.canAccept(targetNetworkService)).isTrue(); - } - - @Test - public void detect_weakCredentialsExistAndMlflowInForeignLanguage_returnsFirstWeakCredentials() - throws Exception { - startMockWebServer(); - NetworkService targetNetworkService = - NetworkService.newBuilder() - .setNetworkEndpoint( - forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort())) - .setServiceName("http") - .build(); - - assertThat( - tester.testValidCredentials( - targetNetworkService, ImmutableList.of(WEAK_CRED_1, WEAK_CRED_2))) - .containsExactly(WEAK_CRED_1); - } - - @Test - public void detect_noWeakCredentials_returnsNoCredentials() throws Exception { - startMockWebServer(); - NetworkService targetNetworkService = - NetworkService.newBuilder() - .setNetworkEndpoint( - forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort())) - .setServiceName("http") - .build(); - assertThat(tester.testValidCredentials(targetNetworkService, ImmutableList.of(WRONG_CRED_1))) - .isEmpty(); - } - - @Test - public void detect_nonMlflowService_skips() throws Exception { - when(mockConnectionProvider.getConnection(any(), any(), any())).thenReturn(mockConnection); - NetworkService targetNetworkService = - NetworkService.newBuilder() - .setNetworkEndpoint(forHostnameAndPort("example.com", 8080)) - .setServiceName("http") - .build(); - - assertThat(tester.testValidCredentials(targetNetworkService, ImmutableList.of(WEAK_CRED_1))) - .isEmpty(); - verifyNoInteractions(mockConnectionProvider); - } - - private void startMockWebServer() - throws IOException { - final Dispatcher dispatcher = new Dispatcher() { - final MockResponse unauthorizedResponse = new MockResponse().setResponseCode(401).setBody("You are not authenticated. " - + "Please see https://www.mlflow.org/docs/latest/auth/index.html#authenticating-to-mlflow " - + "on how to authenticate"); - - @Override - public MockResponse dispatch(RecordedRequest request) { - String authorizationHeader = request.getHeaders().get("Authorization"); - if (authorizationHeader == null) { - return unauthorizedResponse; - } - if (Objects.equals(request.getPath(), "/api/2.0/mlflow/users/create") && Objects.equals(request.getMethod(), "POST")) { - boolean isDefaultCredentials = authorizationHeader.equals(WEAK_CRED_AUTH_1) || authorizationHeader.equals(WEAK_CRED_AUTH_2); - if (isDefaultCredentials) { - return new MockResponse().setResponseCode(200) - .setBody("{\"user\":{\"experiment_permissions\":[],\"id\":4,\"is_admin\":false,\"registered_model_permissions\":[],\n" + - " \"username\":\"googleTsunamiSecurityScanner\"}}"); - } else { - return unauthorizedResponse; - } - } - return new MockResponse().setResponseCode(404); + @Rule public MockitoRule rule = MockitoJUnit.rule(); + @Mock private ConnectionProviderInterface mockConnectionProvider; + @Mock private Connection mockConnection; + @Inject private MlFlowCredentialTester tester; + private MockWebServer mockWebServer; + private static final TestCredential WEAK_CRED_1 = + TestCredential.create("admin", Optional.of("password")); + private static final TestCredential WEAK_CRED_2 = + TestCredential.create("username", Optional.of("password")); + private static final TestCredential WRONG_CRED_1 = + TestCredential.create("wrong", Optional.of("wrong")); + + private static final String WEAK_CRED_AUTH_1 = "basic dXNlcm5hbWU6cGFzc3dvcmQ="; + private static final String WEAK_CRED_AUTH_2 = "basic YWRtaW46cGFzc3dvcmQ="; + private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); + + @Before + public void setup() { + mockWebServer = new MockWebServer(); + Guice.createInjector(new HttpClientModule.Builder().build()).injectMembers(this); + } + + @Test + public void detect_weakCredentialsExists_returnsWeakCredentials() throws Exception { + startMockWebServer(); + NetworkService targetNetworkService = + NetworkService.newBuilder() + .setNetworkEndpoint( + forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort())) + .setServiceName("http") + .build(); + + assertThat(tester.testValidCredentials(targetNetworkService, ImmutableList.of(WEAK_CRED_1))) + .containsExactly(WEAK_CRED_1); + mockWebServer.shutdown(); + } + + @Test + public void detect_weakCredentialsExist_returnsFirstWeakCredentials() throws Exception { + startMockWebServer(); + NetworkService targetNetworkService = + NetworkService.newBuilder() + .setNetworkEndpoint( + forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort())) + .setServiceName("http") + .build(); + + assertThat( + tester.testValidCredentials( + targetNetworkService, ImmutableList.of(WEAK_CRED_1, WEAK_CRED_2))) + .containsExactly(WEAK_CRED_1); + } + + @Test + public void detect_canAccept() throws Exception { + startMockWebServer(); + NetworkService targetNetworkService = + NetworkService.newBuilder() + .setNetworkEndpoint( + forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort())) + .setServiceName("http") + .build(); + + assertThat(tester.canAccept(targetNetworkService)).isTrue(); + } + + @Test + public void detect_weakCredentialsExistAndMlflowInForeignLanguage_returnsFirstWeakCredentials() + throws Exception { + startMockWebServer(); + NetworkService targetNetworkService = + NetworkService.newBuilder() + .setNetworkEndpoint( + forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort())) + .setServiceName("http") + .build(); + + assertThat( + tester.testValidCredentials( + targetNetworkService, ImmutableList.of(WEAK_CRED_1, WEAK_CRED_2))) + .containsExactly(WEAK_CRED_1); + } + + @Test + public void detect_noWeakCredentials_returnsNoCredentials() throws Exception { + startMockWebServer(); + NetworkService targetNetworkService = + NetworkService.newBuilder() + .setNetworkEndpoint( + forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort())) + .setServiceName("http") + .build(); + assertThat(tester.testValidCredentials(targetNetworkService, ImmutableList.of(WRONG_CRED_1))) + .isEmpty(); + } + + @Test + public void detect_nonMlflowService_skips() throws Exception { + when(mockConnectionProvider.getConnection(any(), any(), any())).thenReturn(mockConnection); + NetworkService targetNetworkService = + NetworkService.newBuilder() + .setNetworkEndpoint(forHostnameAndPort("example.com", 8080)) + .setServiceName("http") + .build(); + + assertThat(tester.testValidCredentials(targetNetworkService, ImmutableList.of(WEAK_CRED_1))) + .isEmpty(); + verifyNoInteractions(mockConnectionProvider); + } + + private void startMockWebServer() throws IOException { + final Dispatcher dispatcher = + new Dispatcher() { + final MockResponse unauthorizedResponse = + new MockResponse() + .setResponseCode(401) + .setBody( + "You are not authenticated. " + + "Please see https://www.mlflow.org/docs/latest/auth/index.html#authenticating-to-mlflow " + + "on how to authenticate"); + + @Override + public MockResponse dispatch(RecordedRequest request) { + String authorizationHeader = request.getHeaders().get("Authorization"); + if (authorizationHeader == null) { + return unauthorizedResponse; + } + if (request.getPath().matches("/api/2.0/mlflow/users/get\\?.*") + && Objects.equals(request.getMethod(), "GET")) { + boolean isDefaultCredentials = + authorizationHeader.equals(WEAK_CRED_AUTH_1) + || authorizationHeader.equals(WEAK_CRED_AUTH_2); + if (isDefaultCredentials) { + return new MockResponse() + .setResponseCode(200) + .setBody( + "{\"user\":{\"experiment_permissions\":[],\"id\":1,\"is_admin\":true,\"registered_model_permissions\":[]," + + "\"username\":\"admin\"}}"); + } else { + return unauthorizedResponse; + } } + return new MockResponse().setResponseCode(404); + } }; - mockWebServer.setDispatcher(dispatcher); - mockWebServer.start(); - mockWebServer.url("/"); - } + mockWebServer.setDispatcher(dispatcher); + mockWebServer.start(); + mockWebServer.url("/"); + } } From 0de8e9a581b994cce339d60f1743c17b3e2c8d30 Mon Sep 17 00:00:00 2001 From: lanced00m Date: Tue, 30 Apr 2024 17:14:29 +0200 Subject: [PATCH 3/6] revert spaces --- .../mlflow/MlFlowCredentialTester.java | 265 +++++++-------- .../mlflow/MlFlowCredentialTesterTest.java | 309 +++++++++--------- 2 files changed, 292 insertions(+), 282 deletions(-) diff --git a/google/detectors/credentials/generic_weak_credential_detector/src/main/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/mlflow/MlFlowCredentialTester.java b/google/detectors/credentials/generic_weak_credential_detector/src/main/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/mlflow/MlFlowCredentialTester.java index e24b567f6..ec0017986 100644 --- a/google/detectors/credentials/generic_weak_credential_detector/src/main/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/mlflow/MlFlowCredentialTester.java +++ b/google/detectors/credentials/generic_weak_credential_detector/src/main/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/mlflow/MlFlowCredentialTester.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.mlflow; import static com.google.common.base.Preconditions.checkNotNull; @@ -27,6 +26,7 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.google.gson.JsonSyntaxException; +import com.google.protobuf.ByteString; import com.google.tsunami.common.data.NetworkEndpointUtils; import com.google.tsunami.common.data.NetworkServiceUtils; import com.google.tsunami.common.net.http.HttpClient; @@ -36,149 +36,152 @@ import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.provider.TestCredential; import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.tester.CredentialTester; import com.google.tsunami.proto.NetworkService; + import java.io.IOException; import java.util.Base64; import java.util.List; import javax.inject.Inject; -/** Credential tester specifically for mlflow. */ +/** + * Credential tester specifically for mlflow. + */ public final class MlFlowCredentialTester extends CredentialTester { - private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); - private final HttpClient httpClient; - - @Inject - MlFlowCredentialTester(HttpClient httpClient) { - this.httpClient = checkNotNull(httpClient); - } - - @Override - public String name() { - return "MlFlowCredentialTester"; - } - - @Override - public String description() { - return "MlFlow credential tester."; - } - - @Override - public boolean canAccept(NetworkService networkService) { - if (!NetworkServiceUtils.isWebService(networkService)) { - return false; + private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); + private final HttpClient httpClient; + + @Inject + MlFlowCredentialTester(HttpClient httpClient) { + this.httpClient = checkNotNull(httpClient); } - boolean canAcceptByCustomFingerprint = false; - logger.atInfo().log("probing Mlflow ping - custom fingerprint phase"); - - // we want to test mlflow versions above 2.5 which has basic authentication module - // these versions returned a 401 status code and a link to documentation about how to - // authenticate. - var uriAuthority = NetworkEndpointUtils.toUriAuthority(networkService.getNetworkEndpoint()); - var pingApiUrl = String.format("http://%s/%s", uriAuthority, "ping"); - try { - HttpResponse apiPingResponse = httpClient.send(get(pingApiUrl).withEmptyHeaders().build()); - - if (apiPingResponse.status() == HttpStatus.UNAUTHORIZED - && apiPingResponse.bodyString().isPresent()) { - canAcceptByCustomFingerprint = - apiPingResponse - .bodyString() - .get() - .contains( - "You are not authenticated. " - + "Please see https://www.mlflow.org/docs/latest/auth/index.html#authenticating-to-mlflow " - + "on how to authenticate"); - } - } catch (IOException e) { - logger.atWarning().withCause(e).log("Unable to query '%s'.", pingApiUrl); - return false; + @Override + public String name() { + return "MlFlowCredentialTester"; } - return canAcceptByCustomFingerprint; - } - - @Override - public boolean batched() { - return true; - } - - @Override - public ImmutableList testValidCredentials( - NetworkService networkService, List credentials) { - // Always return 1st weak credential to gracefully handle no auth configured case, where we - // return empty credential instead of all the weak credentials - return credentials.stream() - .filter(cred -> isMlFlowAccessible(networkService, cred)) - .findFirst() - .map(ImmutableList::of) - .orElseGet(ImmutableList::of); - } - - private boolean isMlFlowAccessible(NetworkService networkService, TestCredential credential) { - var uriAuthority = NetworkEndpointUtils.toUriAuthority(networkService.getNetworkEndpoint()); - var url = - String.format( - "http://%s/%s?username=%s", - uriAuthority, "api/2.0/mlflow/users/get", credential.username()); - try { - logger.atInfo().log( - "url: %s, username: %s, password: %s", - url, credential.username(), credential.password().orElse("")); - HttpResponse response = sendRequestWithCredentials(url, credential); - return response.status().isSuccess() - && response - .bodyString() - .map(MlFlowCredentialTester::bodyContainsSuccessfulUserInfo) - .orElse(false); - } catch (IOException e) { - logger.atWarning().withCause(e).log("Unable to query '%s'.", url); - return false; + @Override + public String description() { + return "MlFlow credential tester."; } - } - - private HttpResponse sendRequestWithCredentials(String url, TestCredential credential) - throws IOException { - // For testing no-auth configured case, no auth header is passed in - if (Strings.isNullOrEmpty(credential.username()) - && Strings.isNullOrEmpty(credential.password().orElse(""))) { - return httpClient.send(post(url).withEmptyHeaders().build()); + + @Override + public boolean canAccept(NetworkService networkService) { + if (!NetworkServiceUtils.isWebService(networkService)) { + return false; + } + + boolean canAcceptByCustomFingerprint = false; + logger.atInfo().log("probing Mlflow ping - custom fingerprint phase"); + + // we want to test mlflow versions above 2.5 which has basic authentication module + // these versions returned a 401 status code and a link to documentation about how to + // authenticate. + var uriAuthority = NetworkEndpointUtils.toUriAuthority(networkService.getNetworkEndpoint()); + var pingApiUrl = String.format("http://%s/%s", uriAuthority, "ping"); + try { + HttpResponse apiPingResponse = httpClient.send(get(pingApiUrl).withEmptyHeaders().build()); + + if (apiPingResponse.status() == HttpStatus.UNAUTHORIZED + && apiPingResponse.bodyString().isPresent()) { + canAcceptByCustomFingerprint = + apiPingResponse + .bodyString() + .get() + .contains( + "You are not authenticated. " + + "Please see https://www.mlflow.org/docs/latest/auth/index.html#authenticating-to-mlflow " + + "on how to authenticate"); + } + } catch (IOException e) { + logger.atWarning().withCause(e).log("Unable to query '%s'.", pingApiUrl); + return false; + } + + return canAcceptByCustomFingerprint; } - return httpClient.send( - get(url) - .setHeaders( - HttpHeaders.builder() - .addHeader( - "Authorization", - "basic " - + Base64.getEncoder() - .encodeToString( - (credential.username() + ":" + credential.password().orElse("")) - .getBytes(UTF_8))) - .build()) - .build()); - } - - /** - * A successful authenticated request to the /api/2.0/mlflow/users/get?username=admin endpoint - * returns a JSON with a root key like the following: - * {"user":{"experiment_permissions":[],"id":1,"is_admin":true,"registered_model_permissions":[], - * "username":"admin"}} - */ - private static boolean bodyContainsSuccessfulUserInfo(String responseBody) { - try { - JsonObject response = JsonParser.parseString(responseBody).getAsJsonObject(); - - if (response.has("user")) { - logger.atInfo().log("Successfully received a mlflow user info"); + @Override + public boolean batched() { return true; - } else { - return false; - } - } catch (JsonSyntaxException e) { - logger.atWarning().withCause(e).log( - "An error occurred while parsing the json response: %s", responseBody); - return false; } - } + + @Override + public ImmutableList testValidCredentials( + NetworkService networkService, List credentials) { + // Always return 1st weak credential to gracefully handle no auth configured case, where we + // return empty credential instead of all the weak credentials + return credentials.stream() + .filter(cred -> isMlFlowAccessible(networkService, cred)) + .findFirst() + .map(ImmutableList::of) + .orElseGet(ImmutableList::of); + } + + private boolean isMlFlowAccessible(NetworkService networkService, TestCredential credential) { + var uriAuthority = NetworkEndpointUtils.toUriAuthority(networkService.getNetworkEndpoint()); + var url = + String.format( + "http://%s/%s?username=%s", + uriAuthority, "api/2.0/mlflow/users/get", credential.username()); + try { + logger.atInfo().log( + "url: %s, username: %s, password: %s", + url, credential.username(), credential.password().orElse("")); + HttpResponse response = sendRequestWithCredentials(url, credential); + return response.status().isSuccess() + && response + .bodyString() + .map(MlFlowCredentialTester::bodyContainsSuccessfulUserInfo) + .orElse(false); + } catch (IOException e) { + logger.atWarning().withCause(e).log("Unable to query '%s'.", url); + return false; + } + } + + private HttpResponse sendRequestWithCredentials(String url, TestCredential credential) + throws IOException { + // For testing no-auth configured case, no auth header is passed in + if (Strings.isNullOrEmpty(credential.username()) + && Strings.isNullOrEmpty(credential.password().orElse(""))) { + return httpClient.send(post(url).withEmptyHeaders().build()); + } + + return httpClient.send( + get(url) + .setHeaders( + HttpHeaders.builder() + .addHeader( + "Authorization", + "basic " + + Base64.getEncoder() + .encodeToString( + (credential.username() + ":" + credential.password().orElse("")) + .getBytes(UTF_8))) + .build()) + .build()); + } + + /** + * A successful authenticated request to the /api/2.0/mlflow/users/get?username=admin endpoint + * returns a JSON with a root key like the following: + * {"user":{"experiment_permissions":[],"id":1,"is_admin":true,"registered_model_permissions":[], + * "username":"admin"}} + */ + private static boolean bodyContainsSuccessfulUserInfo(String responseBody) { + try { + JsonObject response = JsonParser.parseString(responseBody).getAsJsonObject(); + + if (response.has("user")) { + logger.atInfo().log("Successfully received a mlflow user info"); + return true; + } else { + return false; + } + } catch (JsonSyntaxException e) { + logger.atWarning().withCause(e).log( + "An error occurred while parsing the json response: %s", responseBody); + return false; + } + } } diff --git a/google/detectors/credentials/generic_weak_credential_detector/src/test/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/mlflow/MlFlowCredentialTesterTest.java b/google/detectors/credentials/generic_weak_credential_detector/src/test/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/mlflow/MlFlowCredentialTesterTest.java index 6c6424221..f9864a88f 100644 --- a/google/detectors/credentials/generic_weak_credential_detector/src/test/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/mlflow/MlFlowCredentialTesterTest.java +++ b/google/detectors/credentials/generic_weak_credential_detector/src/test/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/mlflow/MlFlowCredentialTesterTest.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.mlflow; import com.google.common.collect.ImmutableList; @@ -35,167 +34,175 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; + import javax.inject.Inject; import java.io.IOException; import java.sql.Connection; import java.util.Objects; import java.util.Optional; + import static com.google.common.truth.Truth.assertThat; import static com.google.tsunami.common.data.NetworkEndpointUtils.forHostnameAndPort; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; -/** Tests for {@link MlFlowCredentialTester}. */ +/** + * Tests for {@link MlFlowCredentialTester}. + */ @RunWith(JUnit4.class) public class MlFlowCredentialTesterTest { - @Rule public MockitoRule rule = MockitoJUnit.rule(); - @Mock private ConnectionProviderInterface mockConnectionProvider; - @Mock private Connection mockConnection; - @Inject private MlFlowCredentialTester tester; - private MockWebServer mockWebServer; - private static final TestCredential WEAK_CRED_1 = - TestCredential.create("admin", Optional.of("password")); - private static final TestCredential WEAK_CRED_2 = - TestCredential.create("username", Optional.of("password")); - private static final TestCredential WRONG_CRED_1 = - TestCredential.create("wrong", Optional.of("wrong")); - - private static final String WEAK_CRED_AUTH_1 = "basic dXNlcm5hbWU6cGFzc3dvcmQ="; - private static final String WEAK_CRED_AUTH_2 = "basic YWRtaW46cGFzc3dvcmQ="; - private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); - - @Before - public void setup() { - mockWebServer = new MockWebServer(); - Guice.createInjector(new HttpClientModule.Builder().build()).injectMembers(this); - } - - @Test - public void detect_weakCredentialsExists_returnsWeakCredentials() throws Exception { - startMockWebServer(); - NetworkService targetNetworkService = - NetworkService.newBuilder() - .setNetworkEndpoint( - forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort())) - .setServiceName("http") - .build(); - - assertThat(tester.testValidCredentials(targetNetworkService, ImmutableList.of(WEAK_CRED_1))) - .containsExactly(WEAK_CRED_1); - mockWebServer.shutdown(); - } - - @Test - public void detect_weakCredentialsExist_returnsFirstWeakCredentials() throws Exception { - startMockWebServer(); - NetworkService targetNetworkService = - NetworkService.newBuilder() - .setNetworkEndpoint( - forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort())) - .setServiceName("http") - .build(); - - assertThat( - tester.testValidCredentials( - targetNetworkService, ImmutableList.of(WEAK_CRED_1, WEAK_CRED_2))) - .containsExactly(WEAK_CRED_1); - } - - @Test - public void detect_canAccept() throws Exception { - startMockWebServer(); - NetworkService targetNetworkService = - NetworkService.newBuilder() - .setNetworkEndpoint( - forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort())) - .setServiceName("http") - .build(); - - assertThat(tester.canAccept(targetNetworkService)).isTrue(); - } - - @Test - public void detect_weakCredentialsExistAndMlflowInForeignLanguage_returnsFirstWeakCredentials() - throws Exception { - startMockWebServer(); - NetworkService targetNetworkService = - NetworkService.newBuilder() - .setNetworkEndpoint( - forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort())) - .setServiceName("http") - .build(); - - assertThat( - tester.testValidCredentials( - targetNetworkService, ImmutableList.of(WEAK_CRED_1, WEAK_CRED_2))) - .containsExactly(WEAK_CRED_1); - } - - @Test - public void detect_noWeakCredentials_returnsNoCredentials() throws Exception { - startMockWebServer(); - NetworkService targetNetworkService = - NetworkService.newBuilder() - .setNetworkEndpoint( - forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort())) - .setServiceName("http") - .build(); - assertThat(tester.testValidCredentials(targetNetworkService, ImmutableList.of(WRONG_CRED_1))) - .isEmpty(); - } - - @Test - public void detect_nonMlflowService_skips() throws Exception { - when(mockConnectionProvider.getConnection(any(), any(), any())).thenReturn(mockConnection); - NetworkService targetNetworkService = - NetworkService.newBuilder() - .setNetworkEndpoint(forHostnameAndPort("example.com", 8080)) - .setServiceName("http") - .build(); - - assertThat(tester.testValidCredentials(targetNetworkService, ImmutableList.of(WEAK_CRED_1))) - .isEmpty(); - verifyNoInteractions(mockConnectionProvider); - } - - private void startMockWebServer() throws IOException { - final Dispatcher dispatcher = - new Dispatcher() { - final MockResponse unauthorizedResponse = - new MockResponse() - .setResponseCode(401) - .setBody( - "You are not authenticated. " - + "Please see https://www.mlflow.org/docs/latest/auth/index.html#authenticating-to-mlflow " - + "on how to authenticate"); - - @Override - public MockResponse dispatch(RecordedRequest request) { - String authorizationHeader = request.getHeaders().get("Authorization"); - if (authorizationHeader == null) { - return unauthorizedResponse; - } - if (request.getPath().matches("/api/2.0/mlflow/users/get\\?.*") - && Objects.equals(request.getMethod(), "GET")) { - boolean isDefaultCredentials = - authorizationHeader.equals(WEAK_CRED_AUTH_1) - || authorizationHeader.equals(WEAK_CRED_AUTH_2); - if (isDefaultCredentials) { - return new MockResponse() - .setResponseCode(200) - .setBody( - "{\"user\":{\"experiment_permissions\":[],\"id\":1,\"is_admin\":true,\"registered_model_permissions\":[]," - + "\"username\":\"admin\"}}"); - } else { - return unauthorizedResponse; - } - } - return new MockResponse().setResponseCode(404); - } - }; - mockWebServer.setDispatcher(dispatcher); - mockWebServer.start(); - mockWebServer.url("/"); - } + @Rule + public MockitoRule rule = MockitoJUnit.rule(); + @Mock + private ConnectionProviderInterface mockConnectionProvider; + @Mock + private Connection mockConnection; + @Inject + private MlFlowCredentialTester tester; + private MockWebServer mockWebServer; + private static final TestCredential WEAK_CRED_1 = + TestCredential.create("admin", Optional.of("password")); + private static final TestCredential WEAK_CRED_2 = + TestCredential.create("username", Optional.of("password")); + private static final TestCredential WRONG_CRED_1 = + TestCredential.create("wrong", Optional.of("wrong")); + + private static final String WEAK_CRED_AUTH_1 = "basic dXNlcm5hbWU6cGFzc3dvcmQ="; + private static final String WEAK_CRED_AUTH_2 = "basic YWRtaW46cGFzc3dvcmQ="; + private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); + + @Before + public void setup() { + mockWebServer = new MockWebServer(); + Guice.createInjector(new HttpClientModule.Builder().build()).injectMembers(this); + } + + @Test + public void detect_weakCredentialsExists_returnsWeakCredentials() throws Exception { + startMockWebServer(); + NetworkService targetNetworkService = + NetworkService.newBuilder() + .setNetworkEndpoint( + forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort())) + .setServiceName("http") + .build(); + + assertThat(tester.testValidCredentials(targetNetworkService, ImmutableList.of(WEAK_CRED_1))) + .containsExactly(WEAK_CRED_1); + mockWebServer.shutdown(); + } + + @Test + public void detect_weakCredentialsExist_returnsFirstWeakCredentials() throws Exception { + startMockWebServer(); + NetworkService targetNetworkService = + NetworkService.newBuilder() + .setNetworkEndpoint( + forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort())) + .setServiceName("http") + .build(); + + assertThat( + tester.testValidCredentials( + targetNetworkService, ImmutableList.of(WEAK_CRED_1, WEAK_CRED_2))) + .containsExactly(WEAK_CRED_1); + } + + @Test + public void detect_canAccept() throws Exception { + startMockWebServer(); + NetworkService targetNetworkService = + NetworkService.newBuilder() + .setNetworkEndpoint( + forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort())) + .setServiceName("http") + .build(); + + assertThat(tester.canAccept(targetNetworkService)).isTrue(); + } + + @Test + public void detect_weakCredentialsExistAndMlflowInForeignLanguage_returnsFirstWeakCredentials() + throws Exception { + startMockWebServer(); + NetworkService targetNetworkService = + NetworkService.newBuilder() + .setNetworkEndpoint( + forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort())) + .setServiceName("http") + .build(); + + assertThat( + tester.testValidCredentials( + targetNetworkService, ImmutableList.of(WEAK_CRED_1, WEAK_CRED_2))) + .containsExactly(WEAK_CRED_1); + } + + @Test + public void detect_noWeakCredentials_returnsNoCredentials() throws Exception { + startMockWebServer(); + NetworkService targetNetworkService = + NetworkService.newBuilder() + .setNetworkEndpoint( + forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort())) + .setServiceName("http") + .build(); + assertThat(tester.testValidCredentials(targetNetworkService, ImmutableList.of(WRONG_CRED_1))) + .isEmpty(); + } + + @Test + public void detect_nonMlflowService_skips() throws Exception { + when(mockConnectionProvider.getConnection(any(), any(), any())).thenReturn(mockConnection); + NetworkService targetNetworkService = + NetworkService.newBuilder() + .setNetworkEndpoint(forHostnameAndPort("example.com", 8080)) + .setServiceName("http") + .build(); + + assertThat(tester.testValidCredentials(targetNetworkService, ImmutableList.of(WEAK_CRED_1))) + .isEmpty(); + verifyNoInteractions(mockConnectionProvider); + } + + private void startMockWebServer() throws IOException { + final Dispatcher dispatcher = + new Dispatcher() { + final MockResponse unauthorizedResponse = + new MockResponse() + .setResponseCode(401) + .setBody( + "You are not authenticated. " + + "Please see https://www.mlflow.org/docs/latest/auth/index.html#authenticating-to-mlflow " + + "on how to authenticate"); + + @Override + public MockResponse dispatch(RecordedRequest request) { + String authorizationHeader = request.getHeaders().get("Authorization"); + if (authorizationHeader == null) { + return unauthorizedResponse; + } + if (request.getPath().matches("/api/2.0/mlflow/users/get\\?.*") + && Objects.equals(request.getMethod(), "GET")) { + boolean isDefaultCredentials = + authorizationHeader.equals(WEAK_CRED_AUTH_1) + || authorizationHeader.equals(WEAK_CRED_AUTH_2); + if (isDefaultCredentials) { + return new MockResponse() + .setResponseCode(200) + .setBody( + "{\"user\":{\"experiment_permissions\":[],\"id\":1,\"is_admin\":true,\"registered_model_permissions\":[]," + + "\"username\":\"admin\"}}"); + } else { + return unauthorizedResponse; + } + } + return new MockResponse().setResponseCode(404); + } + }; + mockWebServer.setDispatcher(dispatcher); + mockWebServer.start(); + mockWebServer.url("/"); + } } From 2576c7f42ca665c299ab84a6310965216d784a3d Mon Sep 17 00:00:00 2001 From: lanced00m Date: Tue, 30 Apr 2024 17:23:38 +0200 Subject: [PATCH 4/6] apply google format --- .../mlflow/MlFlowCredentialTester.java | 265 +++++++++--------- .../mlflow/MlFlowCredentialTesterTest.java | 14 +- 2 files changed, 138 insertions(+), 141 deletions(-) diff --git a/google/detectors/credentials/generic_weak_credential_detector/src/main/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/mlflow/MlFlowCredentialTester.java b/google/detectors/credentials/generic_weak_credential_detector/src/main/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/mlflow/MlFlowCredentialTester.java index ec0017986..e24b567f6 100644 --- a/google/detectors/credentials/generic_weak_credential_detector/src/main/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/mlflow/MlFlowCredentialTester.java +++ b/google/detectors/credentials/generic_weak_credential_detector/src/main/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/mlflow/MlFlowCredentialTester.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.mlflow; import static com.google.common.base.Preconditions.checkNotNull; @@ -26,7 +27,6 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.google.gson.JsonSyntaxException; -import com.google.protobuf.ByteString; import com.google.tsunami.common.data.NetworkEndpointUtils; import com.google.tsunami.common.data.NetworkServiceUtils; import com.google.tsunami.common.net.http.HttpClient; @@ -36,152 +36,149 @@ import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.provider.TestCredential; import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.tester.CredentialTester; import com.google.tsunami.proto.NetworkService; - import java.io.IOException; import java.util.Base64; import java.util.List; import javax.inject.Inject; -/** - * Credential tester specifically for mlflow. - */ +/** Credential tester specifically for mlflow. */ public final class MlFlowCredentialTester extends CredentialTester { - private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); - private final HttpClient httpClient; - - @Inject - MlFlowCredentialTester(HttpClient httpClient) { - this.httpClient = checkNotNull(httpClient); + private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); + private final HttpClient httpClient; + + @Inject + MlFlowCredentialTester(HttpClient httpClient) { + this.httpClient = checkNotNull(httpClient); + } + + @Override + public String name() { + return "MlFlowCredentialTester"; + } + + @Override + public String description() { + return "MlFlow credential tester."; + } + + @Override + public boolean canAccept(NetworkService networkService) { + if (!NetworkServiceUtils.isWebService(networkService)) { + return false; } - @Override - public String name() { - return "MlFlowCredentialTester"; + boolean canAcceptByCustomFingerprint = false; + logger.atInfo().log("probing Mlflow ping - custom fingerprint phase"); + + // we want to test mlflow versions above 2.5 which has basic authentication module + // these versions returned a 401 status code and a link to documentation about how to + // authenticate. + var uriAuthority = NetworkEndpointUtils.toUriAuthority(networkService.getNetworkEndpoint()); + var pingApiUrl = String.format("http://%s/%s", uriAuthority, "ping"); + try { + HttpResponse apiPingResponse = httpClient.send(get(pingApiUrl).withEmptyHeaders().build()); + + if (apiPingResponse.status() == HttpStatus.UNAUTHORIZED + && apiPingResponse.bodyString().isPresent()) { + canAcceptByCustomFingerprint = + apiPingResponse + .bodyString() + .get() + .contains( + "You are not authenticated. " + + "Please see https://www.mlflow.org/docs/latest/auth/index.html#authenticating-to-mlflow " + + "on how to authenticate"); + } + } catch (IOException e) { + logger.atWarning().withCause(e).log("Unable to query '%s'.", pingApiUrl); + return false; } - @Override - public String description() { - return "MlFlow credential tester."; + return canAcceptByCustomFingerprint; + } + + @Override + public boolean batched() { + return true; + } + + @Override + public ImmutableList testValidCredentials( + NetworkService networkService, List credentials) { + // Always return 1st weak credential to gracefully handle no auth configured case, where we + // return empty credential instead of all the weak credentials + return credentials.stream() + .filter(cred -> isMlFlowAccessible(networkService, cred)) + .findFirst() + .map(ImmutableList::of) + .orElseGet(ImmutableList::of); + } + + private boolean isMlFlowAccessible(NetworkService networkService, TestCredential credential) { + var uriAuthority = NetworkEndpointUtils.toUriAuthority(networkService.getNetworkEndpoint()); + var url = + String.format( + "http://%s/%s?username=%s", + uriAuthority, "api/2.0/mlflow/users/get", credential.username()); + try { + logger.atInfo().log( + "url: %s, username: %s, password: %s", + url, credential.username(), credential.password().orElse("")); + HttpResponse response = sendRequestWithCredentials(url, credential); + return response.status().isSuccess() + && response + .bodyString() + .map(MlFlowCredentialTester::bodyContainsSuccessfulUserInfo) + .orElse(false); + } catch (IOException e) { + logger.atWarning().withCause(e).log("Unable to query '%s'.", url); + return false; } - - @Override - public boolean canAccept(NetworkService networkService) { - if (!NetworkServiceUtils.isWebService(networkService)) { - return false; - } - - boolean canAcceptByCustomFingerprint = false; - logger.atInfo().log("probing Mlflow ping - custom fingerprint phase"); - - // we want to test mlflow versions above 2.5 which has basic authentication module - // these versions returned a 401 status code and a link to documentation about how to - // authenticate. - var uriAuthority = NetworkEndpointUtils.toUriAuthority(networkService.getNetworkEndpoint()); - var pingApiUrl = String.format("http://%s/%s", uriAuthority, "ping"); - try { - HttpResponse apiPingResponse = httpClient.send(get(pingApiUrl).withEmptyHeaders().build()); - - if (apiPingResponse.status() == HttpStatus.UNAUTHORIZED - && apiPingResponse.bodyString().isPresent()) { - canAcceptByCustomFingerprint = - apiPingResponse - .bodyString() - .get() - .contains( - "You are not authenticated. " - + "Please see https://www.mlflow.org/docs/latest/auth/index.html#authenticating-to-mlflow " - + "on how to authenticate"); - } - } catch (IOException e) { - logger.atWarning().withCause(e).log("Unable to query '%s'.", pingApiUrl); - return false; - } - - return canAcceptByCustomFingerprint; + } + + private HttpResponse sendRequestWithCredentials(String url, TestCredential credential) + throws IOException { + // For testing no-auth configured case, no auth header is passed in + if (Strings.isNullOrEmpty(credential.username()) + && Strings.isNullOrEmpty(credential.password().orElse(""))) { + return httpClient.send(post(url).withEmptyHeaders().build()); } - @Override - public boolean batched() { + return httpClient.send( + get(url) + .setHeaders( + HttpHeaders.builder() + .addHeader( + "Authorization", + "basic " + + Base64.getEncoder() + .encodeToString( + (credential.username() + ":" + credential.password().orElse("")) + .getBytes(UTF_8))) + .build()) + .build()); + } + + /** + * A successful authenticated request to the /api/2.0/mlflow/users/get?username=admin endpoint + * returns a JSON with a root key like the following: + * {"user":{"experiment_permissions":[],"id":1,"is_admin":true,"registered_model_permissions":[], + * "username":"admin"}} + */ + private static boolean bodyContainsSuccessfulUserInfo(String responseBody) { + try { + JsonObject response = JsonParser.parseString(responseBody).getAsJsonObject(); + + if (response.has("user")) { + logger.atInfo().log("Successfully received a mlflow user info"); return true; + } else { + return false; + } + } catch (JsonSyntaxException e) { + logger.atWarning().withCause(e).log( + "An error occurred while parsing the json response: %s", responseBody); + return false; } - - @Override - public ImmutableList testValidCredentials( - NetworkService networkService, List credentials) { - // Always return 1st weak credential to gracefully handle no auth configured case, where we - // return empty credential instead of all the weak credentials - return credentials.stream() - .filter(cred -> isMlFlowAccessible(networkService, cred)) - .findFirst() - .map(ImmutableList::of) - .orElseGet(ImmutableList::of); - } - - private boolean isMlFlowAccessible(NetworkService networkService, TestCredential credential) { - var uriAuthority = NetworkEndpointUtils.toUriAuthority(networkService.getNetworkEndpoint()); - var url = - String.format( - "http://%s/%s?username=%s", - uriAuthority, "api/2.0/mlflow/users/get", credential.username()); - try { - logger.atInfo().log( - "url: %s, username: %s, password: %s", - url, credential.username(), credential.password().orElse("")); - HttpResponse response = sendRequestWithCredentials(url, credential); - return response.status().isSuccess() - && response - .bodyString() - .map(MlFlowCredentialTester::bodyContainsSuccessfulUserInfo) - .orElse(false); - } catch (IOException e) { - logger.atWarning().withCause(e).log("Unable to query '%s'.", url); - return false; - } - } - - private HttpResponse sendRequestWithCredentials(String url, TestCredential credential) - throws IOException { - // For testing no-auth configured case, no auth header is passed in - if (Strings.isNullOrEmpty(credential.username()) - && Strings.isNullOrEmpty(credential.password().orElse(""))) { - return httpClient.send(post(url).withEmptyHeaders().build()); - } - - return httpClient.send( - get(url) - .setHeaders( - HttpHeaders.builder() - .addHeader( - "Authorization", - "basic " - + Base64.getEncoder() - .encodeToString( - (credential.username() + ":" + credential.password().orElse("")) - .getBytes(UTF_8))) - .build()) - .build()); - } - - /** - * A successful authenticated request to the /api/2.0/mlflow/users/get?username=admin endpoint - * returns a JSON with a root key like the following: - * {"user":{"experiment_permissions":[],"id":1,"is_admin":true,"registered_model_permissions":[], - * "username":"admin"}} - */ - private static boolean bodyContainsSuccessfulUserInfo(String responseBody) { - try { - JsonObject response = JsonParser.parseString(responseBody).getAsJsonObject(); - - if (response.has("user")) { - logger.atInfo().log("Successfully received a mlflow user info"); - return true; - } else { - return false; - } - } catch (JsonSyntaxException e) { - logger.atWarning().withCause(e).log( - "An error occurred while parsing the json response: %s", responseBody); - return false; - } - } + } } diff --git a/google/detectors/credentials/generic_weak_credential_detector/src/test/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/mlflow/MlFlowCredentialTesterTest.java b/google/detectors/credentials/generic_weak_credential_detector/src/test/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/mlflow/MlFlowCredentialTesterTest.java index f9864a88f..9af08d8cc 100644 --- a/google/detectors/credentials/generic_weak_credential_detector/src/test/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/mlflow/MlFlowCredentialTesterTest.java +++ b/google/detectors/credentials/generic_weak_credential_detector/src/test/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/mlflow/MlFlowCredentialTesterTest.java @@ -13,8 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.mlflow; +import static com.google.common.truth.Truth.assertThat; +import static com.google.tsunami.common.data.NetworkEndpointUtils.forHostnameAndPort; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + import com.google.common.collect.ImmutableList; import com.google.common.flogger.GoogleLogger; import com.google.inject.Guice; @@ -34,19 +41,12 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; - import javax.inject.Inject; import java.io.IOException; import java.sql.Connection; import java.util.Objects; import java.util.Optional; -import static com.google.common.truth.Truth.assertThat; -import static com.google.tsunami.common.data.NetworkEndpointUtils.forHostnameAndPort; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.verifyNoInteractions; -import static org.mockito.Mockito.when; - /** * Tests for {@link MlFlowCredentialTester}. */ From cd2e32edfd08dd0a5c2c64d9a2b9c787337ec56a Mon Sep 17 00:00:00 2001 From: lanced00m Date: Mon, 13 May 2024 11:48:22 +0200 Subject: [PATCH 5/6] fix formattings, add better comments, sort imports in test --- .../mlflow/MlFlowCredentialTester.java | 11 +- .../mlflow/MlFlowCredentialTesterTest.java | 320 +++++++++--------- 2 files changed, 164 insertions(+), 167 deletions(-) diff --git a/google/detectors/credentials/generic_weak_credential_detector/src/main/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/mlflow/MlFlowCredentialTester.java b/google/detectors/credentials/generic_weak_credential_detector/src/main/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/mlflow/MlFlowCredentialTester.java index e24b567f6..c8d85c69f 100644 --- a/google/detectors/credentials/generic_weak_credential_detector/src/main/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/mlflow/MlFlowCredentialTester.java +++ b/google/detectors/credentials/generic_weak_credential_detector/src/main/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/mlflow/MlFlowCredentialTester.java @@ -70,9 +70,9 @@ public boolean canAccept(NetworkService networkService) { boolean canAcceptByCustomFingerprint = false; logger.atInfo().log("probing Mlflow ping - custom fingerprint phase"); - // we want to test mlflow versions above 2.5 which has basic authentication module - // these versions returned a 401 status code and a link to documentation about how to - // authenticate. + // We want to test weak credentials against mlflow versions above 2.5 which has basic + // authentication module.these versions return a 401 status code and a link to documentation + // about how to authenticate. var uriAuthority = NetworkEndpointUtils.toUriAuthority(networkService.getNetworkEndpoint()); var pingApiUrl = String.format("http://%s/%s", uriAuthority, "ping"); try { @@ -85,8 +85,9 @@ public boolean canAccept(NetworkService networkService) { .bodyString() .get() .contains( - "You are not authenticated. " - + "Please see https://www.mlflow.org/docs/latest/auth/index.html#authenticating-to-mlflow " + "You are not authenticated. Please see " + + "https://www.mlflow.org/docs/latest/auth/index.html" + + "#authenticating-to-mlflow" + "on how to authenticate"); } } catch (IOException e) { diff --git a/google/detectors/credentials/generic_weak_credential_detector/src/test/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/mlflow/MlFlowCredentialTesterTest.java b/google/detectors/credentials/generic_weak_credential_detector/src/test/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/mlflow/MlFlowCredentialTesterTest.java index 9af08d8cc..d613df46d 100644 --- a/google/detectors/credentials/generic_weak_credential_detector/src/test/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/mlflow/MlFlowCredentialTesterTest.java +++ b/google/detectors/credentials/generic_weak_credential_detector/src/test/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/mlflow/MlFlowCredentialTesterTest.java @@ -23,12 +23,16 @@ import static org.mockito.Mockito.when; import com.google.common.collect.ImmutableList; -import com.google.common.flogger.GoogleLogger; import com.google.inject.Guice; import com.google.tsunami.common.net.db.ConnectionProviderInterface; import com.google.tsunami.common.net.http.HttpClientModule; import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.provider.TestCredential; import com.google.tsunami.proto.NetworkService; +import java.io.IOException; +import java.sql.Connection; +import java.util.Objects; +import java.util.Optional; +import javax.inject.Inject; import okhttp3.mockwebserver.Dispatcher; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; @@ -41,168 +45,160 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; -import javax.inject.Inject; -import java.io.IOException; -import java.sql.Connection; -import java.util.Objects; -import java.util.Optional; -/** - * Tests for {@link MlFlowCredentialTester}. - */ +/** Tests for {@link MlFlowCredentialTester}. */ @RunWith(JUnit4.class) public class MlFlowCredentialTesterTest { - @Rule - public MockitoRule rule = MockitoJUnit.rule(); - @Mock - private ConnectionProviderInterface mockConnectionProvider; - @Mock - private Connection mockConnection; - @Inject - private MlFlowCredentialTester tester; - private MockWebServer mockWebServer; - private static final TestCredential WEAK_CRED_1 = - TestCredential.create("admin", Optional.of("password")); - private static final TestCredential WEAK_CRED_2 = - TestCredential.create("username", Optional.of("password")); - private static final TestCredential WRONG_CRED_1 = - TestCredential.create("wrong", Optional.of("wrong")); - - private static final String WEAK_CRED_AUTH_1 = "basic dXNlcm5hbWU6cGFzc3dvcmQ="; - private static final String WEAK_CRED_AUTH_2 = "basic YWRtaW46cGFzc3dvcmQ="; - private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); - - @Before - public void setup() { - mockWebServer = new MockWebServer(); - Guice.createInjector(new HttpClientModule.Builder().build()).injectMembers(this); - } - - @Test - public void detect_weakCredentialsExists_returnsWeakCredentials() throws Exception { - startMockWebServer(); - NetworkService targetNetworkService = - NetworkService.newBuilder() - .setNetworkEndpoint( - forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort())) - .setServiceName("http") - .build(); - - assertThat(tester.testValidCredentials(targetNetworkService, ImmutableList.of(WEAK_CRED_1))) - .containsExactly(WEAK_CRED_1); - mockWebServer.shutdown(); - } - - @Test - public void detect_weakCredentialsExist_returnsFirstWeakCredentials() throws Exception { - startMockWebServer(); - NetworkService targetNetworkService = - NetworkService.newBuilder() - .setNetworkEndpoint( - forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort())) - .setServiceName("http") - .build(); - - assertThat( - tester.testValidCredentials( - targetNetworkService, ImmutableList.of(WEAK_CRED_1, WEAK_CRED_2))) - .containsExactly(WEAK_CRED_1); - } - - @Test - public void detect_canAccept() throws Exception { - startMockWebServer(); - NetworkService targetNetworkService = - NetworkService.newBuilder() - .setNetworkEndpoint( - forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort())) - .setServiceName("http") - .build(); - - assertThat(tester.canAccept(targetNetworkService)).isTrue(); - } - - @Test - public void detect_weakCredentialsExistAndMlflowInForeignLanguage_returnsFirstWeakCredentials() - throws Exception { - startMockWebServer(); - NetworkService targetNetworkService = - NetworkService.newBuilder() - .setNetworkEndpoint( - forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort())) - .setServiceName("http") - .build(); - - assertThat( - tester.testValidCredentials( - targetNetworkService, ImmutableList.of(WEAK_CRED_1, WEAK_CRED_2))) - .containsExactly(WEAK_CRED_1); - } - - @Test - public void detect_noWeakCredentials_returnsNoCredentials() throws Exception { - startMockWebServer(); - NetworkService targetNetworkService = - NetworkService.newBuilder() - .setNetworkEndpoint( - forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort())) - .setServiceName("http") - .build(); - assertThat(tester.testValidCredentials(targetNetworkService, ImmutableList.of(WRONG_CRED_1))) - .isEmpty(); - } - - @Test - public void detect_nonMlflowService_skips() throws Exception { - when(mockConnectionProvider.getConnection(any(), any(), any())).thenReturn(mockConnection); - NetworkService targetNetworkService = - NetworkService.newBuilder() - .setNetworkEndpoint(forHostnameAndPort("example.com", 8080)) - .setServiceName("http") - .build(); - - assertThat(tester.testValidCredentials(targetNetworkService, ImmutableList.of(WEAK_CRED_1))) - .isEmpty(); - verifyNoInteractions(mockConnectionProvider); - } - - private void startMockWebServer() throws IOException { - final Dispatcher dispatcher = - new Dispatcher() { - final MockResponse unauthorizedResponse = - new MockResponse() - .setResponseCode(401) - .setBody( - "You are not authenticated. " - + "Please see https://www.mlflow.org/docs/latest/auth/index.html#authenticating-to-mlflow " - + "on how to authenticate"); - - @Override - public MockResponse dispatch(RecordedRequest request) { - String authorizationHeader = request.getHeaders().get("Authorization"); - if (authorizationHeader == null) { - return unauthorizedResponse; - } - if (request.getPath().matches("/api/2.0/mlflow/users/get\\?.*") - && Objects.equals(request.getMethod(), "GET")) { - boolean isDefaultCredentials = - authorizationHeader.equals(WEAK_CRED_AUTH_1) - || authorizationHeader.equals(WEAK_CRED_AUTH_2); - if (isDefaultCredentials) { - return new MockResponse() - .setResponseCode(200) - .setBody( - "{\"user\":{\"experiment_permissions\":[],\"id\":1,\"is_admin\":true,\"registered_model_permissions\":[]," - + "\"username\":\"admin\"}}"); - } else { - return unauthorizedResponse; - } - } - return new MockResponse().setResponseCode(404); - } - }; - mockWebServer.setDispatcher(dispatcher); - mockWebServer.start(); - mockWebServer.url("/"); - } + @Rule public MockitoRule rule = MockitoJUnit.rule(); + @Mock private ConnectionProviderInterface mockConnectionProvider; + @Mock private Connection mockConnection; + @Inject private MlFlowCredentialTester tester; + private MockWebServer mockWebServer; + private static final TestCredential WEAK_CRED_1 = + TestCredential.create("admin", Optional.of("password")); + private static final TestCredential WEAK_CRED_2 = + TestCredential.create("username", Optional.of("password")); + private static final TestCredential WRONG_CRED_1 = + TestCredential.create("wrong", Optional.of("wrong")); + + // The base64 encoding of default authentication username:password pairs which the tester will + // send these headers to our mock webserver + private static final String WEAK_CRED_AUTH_1 = "basic dXNlcm5hbWU6cGFzc3dvcmQ="; + private static final String WEAK_CRED_AUTH_2 = "basic YWRtaW46cGFzc3dvcmQ="; + + @Before + public void setup() { + mockWebServer = new MockWebServer(); + Guice.createInjector(new HttpClientModule.Builder().build()).injectMembers(this); + } + + @Test + public void detect_weakCredentialsExists_returnsWeakCredentials() throws Exception { + startMockWebServer(); + NetworkService targetNetworkService = + NetworkService.newBuilder() + .setNetworkEndpoint( + forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort())) + .setServiceName("http") + .build(); + + assertThat(tester.testValidCredentials(targetNetworkService, ImmutableList.of(WEAK_CRED_1))) + .containsExactly(WEAK_CRED_1); + mockWebServer.shutdown(); + } + + @Test + public void detect_weakCredentialsExist_returnsFirstWeakCredentials() throws Exception { + startMockWebServer(); + NetworkService targetNetworkService = + NetworkService.newBuilder() + .setNetworkEndpoint( + forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort())) + .setServiceName("http") + .build(); + + assertThat( + tester.testValidCredentials( + targetNetworkService, ImmutableList.of(WEAK_CRED_1, WEAK_CRED_2))) + .containsExactly(WEAK_CRED_1); + } + + @Test + public void detect_canAccept() throws Exception { + startMockWebServer(); + NetworkService targetNetworkService = + NetworkService.newBuilder() + .setNetworkEndpoint( + forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort())) + .setServiceName("http") + .build(); + + assertThat(tester.canAccept(targetNetworkService)).isTrue(); + } + + @Test + public void detect_weakCredentialsExistAndMlflowInForeignLanguage_returnsFirstWeakCredentials() + throws Exception { + startMockWebServer(); + NetworkService targetNetworkService = + NetworkService.newBuilder() + .setNetworkEndpoint( + forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort())) + .setServiceName("http") + .build(); + + assertThat( + tester.testValidCredentials( + targetNetworkService, ImmutableList.of(WEAK_CRED_1, WEAK_CRED_2))) + .containsExactly(WEAK_CRED_1); + } + + @Test + public void detect_noWeakCredentials_returnsNoCredentials() throws Exception { + startMockWebServer(); + NetworkService targetNetworkService = + NetworkService.newBuilder() + .setNetworkEndpoint( + forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort())) + .setServiceName("http") + .build(); + assertThat(tester.testValidCredentials(targetNetworkService, ImmutableList.of(WRONG_CRED_1))) + .isEmpty(); + } + + @Test + public void detect_nonMlflowService_skips() throws Exception { + when(mockConnectionProvider.getConnection(any(), any(), any())).thenReturn(mockConnection); + NetworkService targetNetworkService = + NetworkService.newBuilder() + .setNetworkEndpoint(forHostnameAndPort("example.com", 8080)) + .setServiceName("http") + .build(); + + assertThat(tester.testValidCredentials(targetNetworkService, ImmutableList.of(WEAK_CRED_1))) + .isEmpty(); + verifyNoInteractions(mockConnectionProvider); + } + + private void startMockWebServer() throws IOException { + final Dispatcher dispatcher = + new Dispatcher() { + final MockResponse unauthorizedResponse = + new MockResponse() + .setResponseCode(401) + .setBody( + "You are not authenticated. " + + "Please see https://www.mlflow.org/docs/latest/auth/index.html" + + "#authenticating-to-mlflow " + + "on how to authenticate"); + + @Override + public MockResponse dispatch(RecordedRequest request) { + String authorizationHeader = request.getHeaders().get("Authorization"); + if (authorizationHeader == null) { + return unauthorizedResponse; + } + if (request.getPath().matches("/api/2.0/mlflow/users/get\\?.*") + && Objects.equals(request.getMethod(), "GET")) { + boolean isDefaultCredentials = + authorizationHeader.equals(WEAK_CRED_AUTH_1) + || authorizationHeader.equals(WEAK_CRED_AUTH_2); + if (isDefaultCredentials) { + return new MockResponse() + .setResponseCode(200) + .setBody( + "{\"user\":{\"experiment_permissions\":[],\"id\":1,\"is_admin\":true," + + "\"registered_model_permissions\":[]," + + "\"username\":\"admin\"}}"); + } else { + return unauthorizedResponse; + } + } + return new MockResponse().setResponseCode(404); + } + }; + mockWebServer.setDispatcher(dispatcher); + mockWebServer.start(); + mockWebServer.url("/"); + } } From de847eaaa736f732b0cf7fa6d8afaae76c09f1e5 Mon Sep 17 00:00:00 2001 From: lanced00m Date: Mon, 13 May 2024 14:23:00 +0200 Subject: [PATCH 6/6] fix a stupid mistake --- .../testers/mlflow/MlFlowCredentialTester.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/google/detectors/credentials/generic_weak_credential_detector/src/main/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/mlflow/MlFlowCredentialTester.java b/google/detectors/credentials/generic_weak_credential_detector/src/main/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/mlflow/MlFlowCredentialTester.java index c8d85c69f..3d7f90d2f 100644 --- a/google/detectors/credentials/generic_weak_credential_detector/src/main/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/mlflow/MlFlowCredentialTester.java +++ b/google/detectors/credentials/generic_weak_credential_detector/src/main/java/com/google/tsunami/plugins/detectors/credentials/genericweakcredentialdetector/testers/mlflow/MlFlowCredentialTester.java @@ -87,7 +87,7 @@ public boolean canAccept(NetworkService networkService) { .contains( "You are not authenticated. Please see " + "https://www.mlflow.org/docs/latest/auth/index.html" - + "#authenticating-to-mlflow" + + "#authenticating-to-mlflow " + "on how to authenticate"); } } catch (IOException e) {