Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Zenml weak credentials #491

Merged
merged 6 commits into from
Jun 25, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@
import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.rabbitmq.RabbitMQCredentialTester;
import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.wordpress.WordpressCredentialTester;
import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.rstudio.RStudioCredentialTester;
import com.google.tsunami.plugins.detectors.credentials.genericweakcredentialdetector.testers.zenml.ZenMlCredentialTester;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.Files;
Expand Down Expand Up @@ -75,6 +77,7 @@ protected void configurePlugin() {
credentialTesterBinder.addBinding().to(GrafanaCredentialTester.class);
credentialTesterBinder.addBinding().to(RStudioCredentialTester.class);
credentialTesterBinder.addBinding().to(RabbitMQCredentialTester.class);
credentialTesterBinder.addBinding().to(ZenMlCredentialTester.class);

Multibinder<CredentialProvider> credentialProviderBinder =
Multibinder.newSetBinder(binder(), CredentialProvider.class);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/*
* Copyright 2024 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.zenml;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.tsunami.common.net.http.HttpRequest.get;
secureness marked this conversation as resolved.
Show resolved Hide resolved
import static com.google.tsunami.common.net.http.HttpRequest.post;
import static java.nio.charset.StandardCharsets.UTF_8;
secureness marked this conversation as resolved.
Show resolved Hide resolved

import com.google.common.base.Strings;
secureness marked this conversation as resolved.
Show resolved Hide resolved
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;
secureness marked this conversation as resolved.
Show resolved Hide resolved
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.nio.charset.StandardCharsets;
import java.util.Base64;
secureness marked this conversation as resolved.
Show resolved Hide resolved
import java.util.List;
import java.util.Random;
secureness marked this conversation as resolved.
Show resolved Hide resolved
import javax.inject.Inject;

/** Credential tester specifically for zenml. */
public final class ZenMlCredentialTester extends CredentialTester {
private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
private static final String ZENML_SERVICE = "zenml";

private final HttpClient httpClient;

@Inject
ZenMlCredentialTester(HttpClient httpClient) {
this.httpClient = checkNotNull(httpClient);
}

@Override
public String name() {
return "ZenMlCredentialTester";
}

@Override
public String description() {
return "ZenMl credential tester.";
}

@Override
public boolean canAccept(NetworkService networkService) {
return NetworkServiceUtils.getWebServiceName(networkService).equals(ZENML_SERVICE);
secureness marked this conversation as resolved.
Show resolved Hide resolved
}

@Override
public boolean batched() {
return true;
}

@Override
public ImmutableList<TestCredential> testValidCredentials(
NetworkService networkService, List<TestCredential> 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 -> isZenMlAccessible(networkService, cred))
.findFirst()
.map(ImmutableList::of)
.orElseGet(ImmutableList::of);
}

private boolean isZenMlAccessible(NetworkService networkService, TestCredential credential) {
logger.atWarning().log(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have test breaking on our side:

error: [FloggerLogString] Arguments to log(String) must be compile-time constants or parameters annotated with @CompileTimeConstant. If possible, use Flogger's formatting log methods instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, it is weird, I think it is not good to log every single username and password and fill the stdout, I should've removed this logger, it was for debugging purposes.

String.format(
"username: %s password: %s", credential.username(), credential.password().orElse("")));
var uriAuthority = NetworkEndpointUtils.toUriAuthority(networkService.getNetworkEndpoint());
var loginApiUrl = String.format("http://%s/%s", uriAuthority, "api/v1/login");
try {
HttpResponse apiLoginResponse =
httpClient.send(
post(loginApiUrl)
.setHeaders(
HttpHeaders.builder()
.addHeader("Content-Type", "application/x-www-form-urlencoded")
.build())
.setRequestBody(
ByteString.copyFromUtf8(
String.format(
"username=%s&password=%s",
credential.username(), credential.password().orElse(""))))
.build());

if (apiLoginResponse.status() == HttpStatus.UNAUTHORIZED
&& apiLoginResponse.bodyString().isPresent()
&& apiLoginResponse
.bodyString()
.get()
.equals(
"{\"detail\":[\"AuthorizationException\","
+ "\"Authentication error: invalid username or password\"]}")) {
return false;
}

if (apiLoginResponse.status() == HttpStatus.OK
&& apiLoginResponse.bodyString().isPresent()
&& bodyContainsSuccessfulAccessToken(apiLoginResponse.bodyString().get())) {
logger.atWarning().log("==============================================");
return true;
}

} catch (IOException e) {
logger.atWarning().withCause(e).log("Unable to query '%s'.", loginApiUrl);
return false;
}
return false;
}

/**
* A successful authenticated request to the /api/v1/login endpoint returns a JSON with a root key
* like the following: {"access_token":"An Access
* Token","token_type":"bearer","expires_in":null,"refresh_token":null,"scope":null}
*/
private static boolean bodyContainsSuccessfulAccessToken(String responseBody) {
try {
JsonObject response = JsonParser.parseString(responseBody).getAsJsonObject();

if (response.has("access_token")
&& response.has("token_type")
&& response.has("refresh_token")
&& response.has("scope")
&& response.has("expires_in")) {
logger.atInfo().log("Successfully logged in as a zenml user");
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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,9 @@ service_default_credentials {
default_usernames: "username"
default_passwords: "password"
}

service_default_credentials {
service_name: "zenml"
default_usernames: "default"
default_passwords: ""
}
secureness marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
/*
* 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.zenml;

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.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.plugins.detectors.credentials.genericweakcredentialdetector.testers.zenml.ZenMlCredentialTester;
import com.google.tsunami.proto.NetworkService;
import java.io.IOException;
import java.nio.charset.Charset;
secureness marked this conversation as resolved.
Show resolved Hide resolved
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.util.Base64;
secureness marked this conversation as resolved.
Show resolved Hide resolved
import java.util.Objects;
import java.util.Optional;
import java.util.Random;
secureness marked this conversation as resolved.
Show resolved Hide resolved
import javax.inject.Inject;
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;

/** Tests for {@link ZenMlCredentialTester}. */
@RunWith(JUnit4.class)
public class ZenMlCredentialTesterTest {
@Rule public MockitoRule rule = MockitoJUnit.rule();
@Mock private ConnectionProviderInterface mockConnectionProvider;
@Mock private Connection mockConnection;
@Inject private ZenMlCredentialTester tester;
private MockWebServer mockWebServer;
private static final TestCredential WEAK_CRED_1 =
TestCredential.create("default", Optional.of(""));
private static final TestCredential WRONG_CRED_1 =
TestCredential.create("wrong", Optional.of("wrong"));

// the default username and password value for an insecure zenml instance
private static final String DEFAULT_USERNAME = "default";
private static final String DEFAULT_PASSWORD = "";

@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("zenml")
.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("zenml")
.build();

assertThat(tester.testValidCredentials(targetNetworkService, ImmutableList.of(WEAK_CRED_1)))
.containsExactly(WEAK_CRED_1);
}

@Test
public void detect_zenmlService_canAccept() throws Exception {
startMockWebServer();
NetworkService targetNetworkService =
NetworkService.newBuilder()
.setNetworkEndpoint(
forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort()))
.setServiceName("zenml")
.build();

assertThat(tester.canAccept(targetNetworkService)).isTrue();
}

@Test
public void detect_weakCredentialsExistAndZenmlInForeignLanguage_returnsFirstWeakCredentials()
throws Exception {
startMockWebServer();
NetworkService targetNetworkService =
NetworkService.newBuilder()
.setNetworkEndpoint(
forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort()))
.setServiceName("zenml")
.build();

assertThat(tester.testValidCredentials(targetNetworkService, ImmutableList.of(WEAK_CRED_1)))
.containsExactly(WEAK_CRED_1);
}

@Test
public void detect_noWeakCredentials_returnsNoCredentials() throws Exception {
startMockWebServer();
NetworkService targetNetworkService =
NetworkService.newBuilder()
.setNetworkEndpoint(
forHostnameAndPort(mockWebServer.getHostName(), mockWebServer.getPort()))
.setServiceName("zenml")
.build();
assertThat(tester.testValidCredentials(targetNetworkService, ImmutableList.of(WRONG_CRED_1)))
.isEmpty();
}

@Test
public void detect_nonZenmlService_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(
"{\"detail\":[\"AuthorizationException\","
+ "\"Authentication error: invalid username or password\"]}");

@Override
public MockResponse dispatch(RecordedRequest request) {
if (request.getPath().matches("/login") && Objects.equals(request.getMethod(), "GET")) {
return new MockResponse()
.setResponseCode(200)
.setBody(" <title>ZenML Dashboard</title> ");
}
if (request.getPath().matches("/api/v1/login")
&& Objects.equals(request.getMethod(), "POST")
&& request
.getBody()
.readString(StandardCharsets.UTF_8)
.contains(
String.format(
"username=%s&password=%s", DEFAULT_USERNAME, DEFAULT_PASSWORD))) {
return new MockResponse()
.setResponseCode(200)
.setBody(
"{\"access_token\":\"An AccessToken\",\"token_type\":\"bearer\","
+ "\"expires_in\":null,\"refresh_token\":null,\"scope\":null}");
} else {
return unauthorizedResponse;
}
}
};
mockWebServer.setDispatcher(dispatcher);
mockWebServer.start();
mockWebServer.url("/");
}
}
Loading
Loading