diff --git a/community/README.md b/community/README.md index e651c1cea..fccdfa553 100644 --- a/community/README.md +++ b/community/README.md @@ -59,4 +59,4 @@ This directory contains plugins contributed by community members. #### Exposed UI / API -* [Ollama Exposed API Detector](https://github.com/google/tsunami-security-scanner-plugins/tree/master/community/detectors/ollama_exposed_api) +* [Ollama Exposed API Detector, now updated to Templated format](https://github.com/google/tsunami-security-scanner-plugins/tree/master/templated/templateddetector/plugins/exposedui/Ollama_ExposedUI.textproto) diff --git a/community/detectors/ollama_exposed_api/README.md b/community/detectors/ollama_exposed_api/README.md deleted file mode 100644 index 192b5fbfa..000000000 --- a/community/detectors/ollama_exposed_api/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# Exposed Ollama API Server Detector - -This plugin for Tsunami detects publicly exposed Ollama API Servers. First it -tries to identify an Ollama API server by accessing its' default landing page. -Afterwards it tries to retrieve a list of existing Ollama models through the API -to minimize false-positive findings. - -## Build jar file for this plugin - -Using `gradlew`: - -```shell -./gradlew jar -``` - -Tsunami identifiable jar file is located at `build/libs` directory. diff --git a/community/detectors/ollama_exposed_api/build.gradle b/community/detectors/ollama_exposed_api/build.gradle deleted file mode 100644 index c96c49d26..000000000 --- a/community/detectors/ollama_exposed_api/build.gradle +++ /dev/null @@ -1,40 +0,0 @@ -plugins { - id 'java-library' -} - -description = 'Tsunami detector for exposed Ollama API server.' -group = 'com.google.tsunami' -version = '0.0.1-SNAPSHOT' - -repositories { - maven { // The google mirror is less flaky than mavenCentral() - url 'https://maven-central.storage-download.googleapis.com/repos/central/data/' - } - mavenCentral() - mavenLocal() -} - -def coreRepoBranch = System.getenv("GITBRANCH_TSUNAMI_CORE") ?: "stable" -def tcsRepoBranch = System.getenv("GITBRANCH_TSUNAMI_TCS") ?: "stable" - -dependencies { -implementation("com.google.tsunami:tsunami-common") { - version { branch = "${coreRepoBranch}" } - } - implementation("com.google.tsunami:tsunami-plugin") { - version { branch = "${coreRepoBranch}" } - } - implementation("com.google.tsunami:tsunami-proto") { - version { branch = "${coreRepoBranch}" } - } - implementation "org.jsoup:jsoup:1.9.2" - - testImplementation "junit:junit:4.13.2" - testImplementation "com.google.inject:guice:6.0.0" - testImplementation "com.google.inject.extensions:guice-testlib:6.0.0" - testImplementation "org.mockito:mockito-core:5.18.0" - testImplementation "com.google.truth:truth:1.4.4" - testImplementation "com.google.truth.extensions:truth-java8-extension:1.4.4" - testImplementation "com.google.truth.extensions:truth-proto-extension:1.4.4" - testImplementation "com.squareup.okhttp3:mockwebserver:3.12.0" -} diff --git a/community/detectors/ollama_exposed_api/settings.gradle b/community/detectors/ollama_exposed_api/settings.gradle deleted file mode 100644 index e3f43b56f..000000000 --- a/community/detectors/ollama_exposed_api/settings.gradle +++ /dev/null @@ -1,12 +0,0 @@ -rootProject.name = 'exposedollamaapiserver' - -def coreRepository = System.getenv("GITREPO_TSUNAMI_CORE") ?: "https://github.com/google/tsunami-security-scanner.git" -def tcsRepository = System.getenv("GITREPO_TSUNAMI_TCS") ?: "https://github.com/google/tsunami-security-scanner-callback-server.git" - -sourceControl { - gitRepository("${coreRepository}") { - producesModule("com.google.tsunami:tsunami-common") - producesModule("com.google.tsunami:tsunami-plugin") - producesModule("com.google.tsunami:tsunami-proto") - } -} \ No newline at end of file diff --git a/community/detectors/ollama_exposed_api/src/main/java/com/google/tsunami/plugins/exposedui/ExposedOllamaApiServerDetector.java b/community/detectors/ollama_exposed_api/src/main/java/com/google/tsunami/plugins/exposedui/ExposedOllamaApiServerDetector.java deleted file mode 100644 index f5cfabdee..000000000 --- a/community/detectors/ollama_exposed_api/src/main/java/com/google/tsunami/plugins/exposedui/ExposedOllamaApiServerDetector.java +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Copyright 2025 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.exposedui; - -import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.tsunami.common.net.http.HttpRequest.get; - -import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.ImmutableList; -import com.google.common.flogger.GoogleLogger; -import com.google.protobuf.util.Timestamps; -import com.google.tsunami.common.data.NetworkServiceUtils; -import com.google.tsunami.common.net.http.HttpClient; -import com.google.tsunami.common.net.http.HttpResponse; -import com.google.tsunami.common.net.http.HttpStatus; -import com.google.tsunami.common.time.UtcClock; -import com.google.tsunami.plugin.PluginType; -import com.google.tsunami.plugin.VulnDetector; -import com.google.tsunami.plugin.annotations.ForWebService; -import com.google.tsunami.plugin.annotations.PluginInfo; -import com.google.tsunami.proto.DetectionReport; -import com.google.tsunami.proto.DetectionReportList; -import com.google.tsunami.proto.DetectionStatus; -import com.google.tsunami.proto.NetworkService; -import com.google.tsunami.proto.Severity; -import com.google.tsunami.proto.TargetInfo; -import com.google.tsunami.proto.Vulnerability; -import com.google.tsunami.proto.VulnerabilityId; -import java.io.IOException; -import java.time.Clock; -import javax.inject.Inject; - -/** A VulnDetector plugin for Exposed Ollama API Server. */ -@PluginInfo( - type = PluginType.VULN_DETECTION, - name = "Exposed Ollama API Server Detector", - version = "0.1", - description = - "This detector checks for a publicly exposed Ollama REST API which can be abused by an" - + " attacker for management tasks.", - author = "timoles", - bootstrapModule = ExposedOllamaApiServerDetectorModule.class) -@ForWebService -public final class ExposedOllamaApiServerDetector implements VulnDetector { - private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); - - private final Clock utcClock; - private final HttpClient httpClient; - - @VisibleForTesting - static final String RECOMMENDATION = - "Don't expose the Ollama Rest API to unauthorized users. According to the official" - + " documentation access to the API server must be restricted through a reverse proxy" - + " which implements necessary authentication checks."; - - @Inject - ExposedOllamaApiServerDetector(@UtcClock Clock utcClock, HttpClient httpClient) { - this.utcClock = checkNotNull(utcClock); - this.httpClient = checkNotNull(httpClient).modify().setFollowRedirects(true).build(); - } - - @Override - public DetectionReportList detect( - TargetInfo targetInfo, ImmutableList matchedServices) { - - DetectionReportList.Builder detectionReport = DetectionReportList.newBuilder(); - matchedServices.stream() - .filter(NetworkServiceUtils::isWebService) - .filter(this::isOllamaApi) - .forEach( - networkService -> { - if (isServiceVulnerableCheckResponse(networkService)) { - detectionReport.addDetectionReports( - buildDetectionReport(targetInfo, networkService)); - } - }); - return detectionReport.build(); - } - - public boolean isOllamaApi(NetworkService networkService) { - logger.atInfo().log("Probing Ollama API landing page"); - - var ollamaApiLandingPageUrl = NetworkServiceUtils.buildWebApplicationRootUrl(networkService); - try { - HttpResponse landingPageResponse = - this.httpClient.send(get(ollamaApiLandingPageUrl).withEmptyHeaders().build()); - if (!(landingPageResponse.status() == HttpStatus.OK - && landingPageResponse.bodyString().isPresent())) { - return false; - } - - if (landingPageResponse.bodyString().get().contains("Ollama is running")) { - return true; - } - - } catch (IOException e) { - logger.atWarning().withCause(e).log("Unable to query '%s'.", ollamaApiLandingPageUrl); - return false; - } - - return false; - } - - @Override - public ImmutableList getAdvisories() { - return ImmutableList.of( - Vulnerability.newBuilder() - .setMainId( - VulnerabilityId.newBuilder() - .setPublisher("TSUNAMI_COMMUNITY") - .setValue("OLLAMA_API_SERVER_EXPOSED")) - .setSeverity(Severity.HIGH) - .setTitle("Exposed Ollama API Server") - .setDescription( - "An Ollama API server is exposed to the network. This was confirmed by" - + " investigating the API response for typical response artifacts. " - + " An attacker can abuse an exposed API server to, for example," - + " download or modify existing LLM models, or misuse resources by" - + " using the LLM chat functionality.") - .setRecommendation(RECOMMENDATION) - .build()); - } - - private boolean isServiceVulnerableCheckResponse(NetworkService networkService) { - - var psUri = NetworkServiceUtils.buildWebApplicationRootUrl(networkService) + "api/ps"; - try { - // Minimize false-positives by checking if we can access the models endpoint - HttpResponse psApiResponse = - httpClient.send(get(psUri).withEmptyHeaders().build(), networkService); - if (psApiResponse.status() != HttpStatus.OK || psApiResponse.bodyString().isEmpty()) { - return false; - } - if (psApiResponse.bodyString().get().contains("{\"models\":")) { - return true; - } - - } catch (RuntimeException | IOException e) { - logger.atWarning().withCause(e).log("Request to target %s failed", psUri); - return false; - } - return false; - } - - private DetectionReport buildDetectionReport( - TargetInfo targetInfo, NetworkService vulnerableNetworkService) { - return DetectionReport.newBuilder() - .setTargetInfo(targetInfo) - .setNetworkService(vulnerableNetworkService) - .setDetectionTimestamp(Timestamps.fromMillis(utcClock.instant().toEpochMilli())) - .setDetectionStatus(DetectionStatus.VULNERABILITY_VERIFIED) - .setVulnerability(this.getAdvisories().get(0)) - .build(); - } -} diff --git a/community/detectors/ollama_exposed_api/src/main/java/com/google/tsunami/plugins/exposedui/ExposedOllamaApiServerDetectorModule.java b/community/detectors/ollama_exposed_api/src/main/java/com/google/tsunami/plugins/exposedui/ExposedOllamaApiServerDetectorModule.java deleted file mode 100644 index 0c0a692f1..000000000 --- a/community/detectors/ollama_exposed_api/src/main/java/com/google/tsunami/plugins/exposedui/ExposedOllamaApiServerDetectorModule.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2025 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.exposedui; - -import com.google.tsunami.plugin.PluginBootstrapModule; - -/** A module registering the detector for Exposed Ollama API Server. */ -public final class ExposedOllamaApiServerDetectorModule extends PluginBootstrapModule { - @Override - protected void configurePlugin() { - registerPlugin(ExposedOllamaApiServerDetector.class); - } -} diff --git a/community/detectors/ollama_exposed_api/src/test/java/com/google/tsunami/plugins/exposedui/ExposedOllamaApiServerDetectorTest.java b/community/detectors/ollama_exposed_api/src/test/java/com/google/tsunami/plugins/exposedui/ExposedOllamaApiServerDetectorTest.java deleted file mode 100644 index afb9818c3..000000000 --- a/community/detectors/ollama_exposed_api/src/test/java/com/google/tsunami/plugins/exposedui/ExposedOllamaApiServerDetectorTest.java +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright 2025 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.exposedui; - -import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; -import static com.google.tsunami.common.data.NetworkEndpointUtils.forHostnameAndPort; -import static com.google.tsunami.plugins.exposedui.ExposedOllamaApiServerDetector.RECOMMENDATION; - -import com.google.common.collect.ImmutableList; -import com.google.inject.Guice; -import com.google.protobuf.util.Timestamps; -import com.google.tsunami.common.net.http.HttpClientModule; -import com.google.tsunami.common.time.testing.FakeUtcClock; -import com.google.tsunami.common.time.testing.FakeUtcClockModule; -import com.google.tsunami.plugin.payload.testing.FakePayloadGeneratorModule; -import com.google.tsunami.proto.DetectionReport; -import com.google.tsunami.proto.DetectionReportList; -import com.google.tsunami.proto.DetectionStatus; -import com.google.tsunami.proto.NetworkService; -import com.google.tsunami.proto.Severity; -import com.google.tsunami.proto.TargetInfo; -import com.google.tsunami.proto.Vulnerability; -import com.google.tsunami.proto.VulnerabilityId; -import java.io.IOException; -import java.time.Instant; -import javax.inject.Inject; -import okhttp3.mockwebserver.Dispatcher; -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.MockWebServer; -import okhttp3.mockwebserver.RecordedRequest; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -/** Unit tests for the {@link ExposedOllamaApiServerDetector}. */ -@RunWith(JUnit4.class) -public final class ExposedOllamaApiServerDetectorTest { - private final MockWebServer mockTargetService = new MockWebServer(); - private final FakeUtcClock fakeUtcClock = - FakeUtcClock.create().setNow(Instant.parse("2020-01-01T00:00:00.00Z")); - - @Inject private ExposedOllamaApiServerDetector detector; - - private void createInjector() { - Guice.createInjector( - new FakeUtcClockModule(fakeUtcClock), - new HttpClientModule.Builder().build(), - FakePayloadGeneratorModule.builder().build(), - new ExposedOllamaApiServerDetectorModule()) - .injectMembers(this); - } - - @Test - public void detect_no_ollama_api_server_returnsEmpty() throws IOException { - mockTargetService.enqueue(new MockResponse().setResponseCode(400)); - mockTargetService.start(); - - createInjector(); - NetworkService targetNetworkService = - NetworkService.newBuilder() - .setNetworkEndpoint( - forHostnameAndPort(mockTargetService.getHostName(), mockTargetService.getPort())) - .addSupportedHttpMethods("GET") - .build(); - - TargetInfo targetInfo = - TargetInfo.newBuilder() - .addNetworkEndpoints(targetNetworkService.getNetworkEndpoint()) - .build(); - - DetectionReportList detectionReports = - detector.detect(targetInfo, ImmutableList.of(targetNetworkService)); - - assertThat(detectionReports.getDetectionReportsList()).isEmpty(); - } - - @Test - public void detect_withResponseMatching_exposed_ollama_api_server() throws IOException { - startMockWebServer(); - createInjector(); - NetworkService targetNetworkService = - NetworkService.newBuilder() - .setNetworkEndpoint( - forHostnameAndPort(mockTargetService.getHostName(), mockTargetService.getPort())) - .addSupportedHttpMethods("GET") - .build(); - TargetInfo targetInfo = - TargetInfo.newBuilder() - .addNetworkEndpoints(targetNetworkService.getNetworkEndpoint()) - .build(); - - DetectionReportList detectionReports = - detector.detect(targetInfo, ImmutableList.of(targetNetworkService)); - - assertThat(mockTargetService.getRequestCount()).isEqualTo(2); - assertThat(detectionReports.getDetectionReportsList()) - .comparingExpectedFieldsOnly() - .containsExactly( - DetectionReport.newBuilder() - .setTargetInfo(targetInfo) - .setNetworkService(targetNetworkService) - .setDetectionTimestamp(Timestamps.fromMillis(fakeUtcClock.instant().toEpochMilli())) - .setDetectionStatus(DetectionStatus.VULNERABILITY_VERIFIED) - .setVulnerability( - Vulnerability.newBuilder() - .setMainId( - VulnerabilityId.newBuilder() - .setPublisher("TSUNAMI_COMMUNITY") - .setValue("OLLAMA_API_SERVER_EXPOSED")) - .setSeverity(Severity.HIGH) - .setTitle("Exposed Ollama API Server") - .setDescription( - "An Ollama API server is exposed to the network. This was confirmed by" - + " investigating the API response for typical response artifacts. " - + " An attacker can abuse an exposed API server to, for example," - + " download or modify existing LLM models, or misuse resources by" - + " using the LLM chat functionality.") - .setRecommendation(RECOMMENDATION)) - .build()); - } - - private void startMockWebServer() throws IOException { - final Dispatcher dispatcher = - new Dispatcher() { - - @Override - public MockResponse dispatch(RecordedRequest request) { - return switch (request.getPath()) { - case "/" -> new MockResponse().setResponseCode(200).setBody("Ollama is running"); - case "/api/ps" -> new MockResponse().setResponseCode(200).setBody("{\"models\":[]}"); - default -> new MockResponse().setResponseCode(404).setBody("404 page not found"); - }; - } - }; - mockTargetService.setDispatcher(dispatcher); - mockTargetService.start(); - mockTargetService.url("/"); - } -} diff --git a/templated/templateddetector/plugins/exposedui/Ollama_ExposedUI.textproto b/templated/templateddetector/plugins/exposedui/Ollama_ExposedUI.textproto new file mode 100644 index 000000000..75992ae61 --- /dev/null +++ b/templated/templateddetector/plugins/exposedui/Ollama_ExposedUI.textproto @@ -0,0 +1,77 @@ +# proto-file: proto/templated_plugin.proto +# proto-message: TemplatedPlugin + +############### +# PLUGIN INFO # +############### + +info: { + type: VULN_DETECTION + name: "Ollama_ExposedUI" + author: + "Robert Dick (robert@doyensec.com) for the Templated version, " + "timoles for the original Java version" + version: "2.0" +} + +finding: { + main_id: { + publisher: "TSUNAMI_COMMUNITY" + value: "OLLAMA_API_SERVER_EXPOSED" + } + title: "Exposed Ollama API Server" + description: + "An Ollama API server is exposed to the network. This was confirmed by" + " investigating the API response for typical response artifacts. " + " An attacker can abuse an exposed API server to, for example," + " download or modify existing LLM models, or misuse resources by" + " using the LLM chat functionality." + recommendation: + "Don't expose the Ollama Rest API to unauthorized users. According to the official" + " documentation access to the API server must be restricted through a reverse proxy" + " which implements necessary authentication checks." + severity: HIGH +} + +########### +# ACTIONS # +########### + +actions: { + name: "fingerprint_webroot" + http_request: { + method: GET + uri: "/" + response: { + http_status: 200 + expect_all: { + conditions: { body {} contains: "Ollama is running" } + } + } + } +} + +actions: { + name: "fingerprint_api" + http_request: { + method: GET + uri: "/api/ps" + response: { + http_status: 200 + expect_all: { + conditions: { body {} contains: "{\"models\":" } + } + } + } +} + +############# +# WORKFLOWS # +############# + +workflows: { + actions: [ + "fingerprint_webroot", + "fingerprint_api" + ] +} \ No newline at end of file diff --git a/templated/templateddetector/plugins/exposedui/Ollama_ExposedUI_test.textproto b/templated/templateddetector/plugins/exposedui/Ollama_ExposedUI_test.textproto new file mode 100644 index 000000000..423ac4f82 --- /dev/null +++ b/templated/templateddetector/plugins/exposedui/Ollama_ExposedUI_test.textproto @@ -0,0 +1,87 @@ +# proto-file: proto/templated_plugin_tests.proto +# proto-message: TemplatedPluginTests + +config: { + tested_plugin: "Ollama_ExposedUI" +} + +tests: { + name: "whenUIandAPI_returnsVuln" + expect_vulnerability: true + + mock_http_server: { + mock_responses: [ + { + uri: "/" + status: 200 + body_content: "Ollama is running" + }, + { + uri: "/api/ps" + status: 200 + body_content: "{\"models\":" + }, + { + uri: "TSUNAMI_MAGIC_ANY_URI" + status: 200 + body_content: '{"status":200}' + } + ] + } +} + +tests: { + name: "whenNoApi_returnsNotVuln" + expect_vulnerability: false + + mock_http_server: { + mock_responses: [ + { + uri: "/" + status: 200 + body_content: "Ollama is running" + }, + { + uri: "TSUNAMI_MAGIC_ANY_URI" + status: 200 + body_content: '{"status":200}' + } + ] + } +} + +tests: { + name: "whenNoUI_returnsNotVuln" + expect_vulnerability: false + + mock_http_server: { + mock_responses: [ + { + uri: "/api/ps" + status: 200 + body_content: "{\"models\":" + }, + { + uri: "TSUNAMI_MAGIC_ANY_URI" + status: 200 + body_content: '{"status":200}' + } + ] + } +} + + +tests: { + name: "whenRandomServer_returnsNotVuln" + expect_vulnerability: false + + mock_http_server: { + mock_responses: [ + { + uri: "TSUNAMI_MAGIC_ANY_URI" + status: 200 + body_content: "... Hello world ..." + } + ] + } +}