diff --git a/java/examples/VaasExample/src/main/java/de/gdata/vaasexample/ForFileScan.java b/java/examples/VaasExample/src/main/java/de/gdata/vaasexample/ForFileScan.java index cb28b7e99..ce6bb7e65 100644 --- a/java/examples/VaasExample/src/main/java/de/gdata/vaasexample/ForFileScan.java +++ b/java/examples/VaasExample/src/main/java/de/gdata/vaasexample/ForFileScan.java @@ -17,10 +17,10 @@ public static void main(String[] args) throws Exception { var file = Path.of(Environment.getenv("SCAN_PATH")); var verdict = vaas.forFile(file); - System.out.printf("File %s was sync detected as %s", verdict.getSha256(), verdict.getVerdict()); + System.out.printf("File %s was sync detected as %s and is encrypted: %s", verdict.getSha256(), verdict.getVerdict(), verdict.getIsEncrypted()); vaas.forFileAsync(file).thenAccept(vaasResult -> { - System.out.printf("\nFile %s was async detected as %s", vaasResult.getSha256(), vaasResult.getVerdict()); + System.out.printf("\nFile %s was async detected as %s and is encrypted: %s", vaasResult.getSha256(), vaasResult.getVerdict(), vaasResult.getIsEncrypted()); }).get(); } } \ No newline at end of file diff --git a/java/src/main/java/de/gdata/vaas/messages/FileReport.java b/java/src/main/java/de/gdata/vaas/messages/FileReport.java index c9bb3aa90..74d7d2447 100644 --- a/java/src/main/java/de/gdata/vaas/messages/FileReport.java +++ b/java/src/main/java/de/gdata/vaas/messages/FileReport.java @@ -14,14 +14,16 @@ public class FileReport { String detection; String fileType; String mimeType; + Boolean isEncrypted; public FileReport(@NonNull String sha256, @NonNull Verdict verdict, String detection, String fileType, - String mimeType) { + String mimeType, Boolean isEncrypted) { this.sha256 = sha256; this.verdict = verdict; this.detection = detection; this.fileType = fileType; this.mimeType = mimeType; + this.isEncrypted = isEncrypted; } public static FileReport fromJson(String json) { diff --git a/java/src/main/java/de/gdata/vaas/messages/UrlReport.java b/java/src/main/java/de/gdata/vaas/messages/UrlReport.java index 76a8d37e0..95cfd4a16 100644 --- a/java/src/main/java/de/gdata/vaas/messages/UrlReport.java +++ b/java/src/main/java/de/gdata/vaas/messages/UrlReport.java @@ -16,15 +16,17 @@ public class UrlReport { String detection; String fileType; String mimeType; + Boolean isEncrypted; public UrlReport(@NonNull String sha256, @NonNull Verdict verdict, @NonNull String url, String detection, String fileType, - String mimeType) { + String mimeType, Boolean isEncrypted) { this.sha256 = sha256; this.verdict = verdict; this.url = url; this.detection = detection; this.fileType = fileType; this.mimeType = mimeType; + this.isEncrypted = isEncrypted; } public static UrlReport fromJson(String json) { diff --git a/java/src/main/java/de/gdata/vaas/messages/VaasVerdict.java b/java/src/main/java/de/gdata/vaas/messages/VaasVerdict.java index bf5636811..06424d8b0 100644 --- a/java/src/main/java/de/gdata/vaas/messages/VaasVerdict.java +++ b/java/src/main/java/de/gdata/vaas/messages/VaasVerdict.java @@ -12,6 +12,7 @@ public class VaasVerdict { String detection; String fileType; String mimeType; + Boolean isEncrypted; public VaasVerdict(UrlReport urlReport) { this.sha256 = urlReport.sha256; @@ -19,6 +20,7 @@ public VaasVerdict(UrlReport urlReport) { this.detection = urlReport.detection; this.fileType = urlReport.fileType; this.mimeType = urlReport.mimeType; + this.isEncrypted = urlReport.isEncrypted; } public VaasVerdict(FileReport fileReport) { @@ -27,6 +29,7 @@ public VaasVerdict(FileReport fileReport) { this.detection = fileReport.detection; this.fileType = fileReport.fileType; this.mimeType = fileReport.mimeType; + this.isEncrypted = fileReport.isEncrypted; } public static VaasVerdict From(UrlReport urlReport) { diff --git a/java/src/test/java/de/gdata/test/TestDotenv.java b/java/src/test/java/de/gdata/test/TestDotenv.java new file mode 100644 index 000000000..3a963b701 --- /dev/null +++ b/java/src/test/java/de/gdata/test/TestDotenv.java @@ -0,0 +1,42 @@ +package de.gdata.test; + +import io.github.cdimascio.dotenv.Dotenv; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Optional; + +public final class TestDotenv { + private static final String ENV_FILENAME = ".env"; + + private TestDotenv() { + } + + public static Dotenv load() { + return load(Paths.get("").toAbsolutePath()); + } + + public static Dotenv load(Path startDirectory) { + var envDirectory = findEnvDirectory(startDirectory); + var dotenv = Dotenv.configure() + .ignoreIfMissing(); + + envDirectory.ifPresent(path -> dotenv.directory(path.toString()).filename(ENV_FILENAME)); + + return dotenv.load(); + } + + public static Optional findEnvDirectory(Path startDirectory) { + var currentDirectory = startDirectory.toAbsolutePath().normalize(); + + while (currentDirectory != null) { + if (Files.isRegularFile(currentDirectory.resolve(ENV_FILENAME))) { + return Optional.of(currentDirectory); + } + currentDirectory = currentDirectory.getParent(); + } + + return Optional.empty(); + } +} \ No newline at end of file diff --git a/java/src/test/java/de/gdata/test/integration/RealApiIntegrationTests.java b/java/src/test/java/de/gdata/test/integration/RealApiIntegrationTests.java index cf25ff082..12239069a 100644 --- a/java/src/test/java/de/gdata/test/integration/RealApiIntegrationTests.java +++ b/java/src/test/java/de/gdata/test/integration/RealApiIntegrationTests.java @@ -1,6 +1,7 @@ package de.gdata.test.integration; import com.google.gson.Gson; +import de.gdata.test.TestDotenv; import de.gdata.vaas.Sha256; import de.gdata.vaas.Vaas; import de.gdata.vaas.VaasConfig; @@ -51,1389 +52,1441 @@ @Slf4j public class RealApiIntegrationTests { - private static final String EICAR_URL = "https://samples.develop.vaas.gdatasecurity.de/eicar.com.txt"; + private static final String EICAR_URL = "https://s3-eu-central-2.ionoscloud.com/test-samples-vaas/eicar.com.txt"; + private static final String PASSWORD_URL = "https://s3-eu-central-2.ionoscloud.com/test-samples-vaas/password.zip"; + private static final String WITH_AND_WITHOUT_PASSWORD_URL = "https://s3-eu-central-2.ionoscloud.com/test-samples-vaas/with-and-without-password.zip"; - private static final Dotenv dotenv = Dotenv.configure() - .ignoreIfMissing() - .load(); + private static final Dotenv dotenv = TestDotenv.load(); - private static final SamplesFixture samplesFixture = new SamplesFixture(); - private static final IAuthenticator authenticatorFixture; - private static final Vaas vaasWithDefaultConfig; + private static final SamplesFixture samplesFixture = new SamplesFixture(); + private static final IAuthenticator authenticatorFixture; + private static final Vaas vaasWithDefaultConfig; + static { + authenticatorFixture = getAuthenticator(); + vaasWithDefaultConfig = getVaasWithAuthenticator(authenticatorFixture); + } + + private static String getEnvironmentKey(String key) { + var value = dotenv.get(key); + if (value == null) { + throw new IllegalArgumentException("Environment variable " + key + " must be set."); + } + return value; + } + + private static IAuthenticator getAuthenticator() { + var clientId = getEnvironmentKey("CLIENT_ID"); + var clientSecret = getEnvironmentKey("CLIENT_SECRET"); + var tokenUrl = URI.create(getEnvironmentKey("TOKEN_URL")); + return new ClientCredentialsGrantAuthenticator(clientId, clientSecret, tokenUrl); + } + + private static IAuthenticator getAuthenticator(HttpClient httpClient) { + var clientId = getEnvironmentKey("CLIENT_ID"); + var clientSecret = getEnvironmentKey("CLIENT_SECRET"); + var tokenUrl = URI.create(getEnvironmentKey("TOKEN_URL")); + return new ClientCredentialsGrantAuthenticator(clientId, clientSecret, tokenUrl, httpClient); + } + + private static Vaas getVaasWithAuthenticator(IAuthenticator authenticator) { + var vaasUrl = getEnvironmentKey("VAAS_URL"); + var config = new VaasConfig(URI.create(vaasUrl)); + return new Vaas(config, authenticator); + } + + private static Vaas getVaasWithMockedClient(HttpClient httpClient) { + var vaasUrl = getEnvironmentKey("VAAS_URL"); + var config = new VaasConfig(URI.create(vaasUrl)); + return new Vaas(config, authenticatorFixture, httpClient); + } + + public static byte[] readContent(HttpRequest.BodyPublisher bodyPublisher) { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + bodyPublisher.subscribe(new Flow.Subscriber<>() { + + @Override + public void onSubscribe(Flow.Subscription subscription) { + subscription.request(Long.MAX_VALUE); + } + + @Override + public void onNext(ByteBuffer item) { + byte[] bytes = new byte[item.remaining()]; + item.get(bytes); + outputStream.write(bytes, 0, bytes.length); + } + + @Override + public void onError(Throwable throwable) { + throwable.printStackTrace(); + } + + @Override + public void onComplete() { + } + }); + return outputStream.toByteArray(); + } + + @ParameterizedTest + @CsvSource({ + "cd617c5c1b1ff1c94a52ab8cf07192654f271a3f8bad49490288131ccb9efc1e, CLEAN", + "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f, MALICIOUS", + "d6f6c6b9fde37694e12b12009ad11ab9ec8dd0f193e7319c523933bdad8a50ad, PUP" + }) + public void forSha256_ReturnsVerdict(String sha256, Verdict verdict) { + var sha256sum = new Sha256(sha256); + + var vaasVerdict = vaasWithDefaultConfig.forSha256Async(sha256sum).join(); + + assertEquals(sha256, vaasVerdict.getSha256()); + assertEquals(verdict, vaasVerdict.getVerdict()); + } + + @SuppressWarnings("unchecked") + @ParameterizedTest + @CsvSource({ + "false, false", + "false, true", + "true, false", + "true, true", + }) + @Tag("Mock") + public void forSha256_SendOptions(boolean useCache, boolean useHashLookup) { + var sha256 = new Sha256("275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f"); + var requestCaptor = ArgumentCaptor.forClass(HttpRequest.class); + var mockHttpClient = mock(HttpClient.class); + var mockResponse = mock(HttpResponse.class); + var forSha256Options = new ForSha256Options(useCache, useHashLookup, "foobar"); + when(mockResponse.statusCode()).thenReturn(200); + when(mockResponse.body()).thenReturn( + new Gson().toJson(new FileReport( + "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f", + Verdict.UNKNOWN, null, null, null, false))); + when(mockHttpClient.sendAsync(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) + .thenAnswer(invocation -> CompletableFuture.completedFuture(mockResponse)); + + var vaas = getVaasWithMockedClient(mockHttpClient); + var vaasVerdict = vaas.forSha256Async(sha256, forSha256Options).join(); + verify(mockHttpClient).sendAsync(requestCaptor.capture(), any(HttpResponse.BodyHandler.class)); + var capturedUri = requestCaptor.getValue().uri(); + + assertTrue(capturedUri.toString().contains(String.format("useCache=%s", useCache))); + assertTrue(capturedUri.toString() + .contains(String.format("useHashLookup=%s", useHashLookup))); + assertEquals(Verdict.UNKNOWN, vaasVerdict.getVerdict()); + assertTrue("275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f" + .equalsIgnoreCase(vaasVerdict.getSha256())); + } + + @SuppressWarnings("unchecked") + @Test + @Tag("Mock") + public void forSha256_SendUserAgent() { + var sha256 = new Sha256("275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f"); + var requestCaptor = ArgumentCaptor.forClass(HttpRequest.class); + var mockHttpClient = mock(HttpClient.class); + var mockResponse = mock(HttpResponse.class); + when(mockResponse.statusCode()).thenReturn(200); + when(mockResponse.body()).thenReturn( + new Gson().toJson(new FileReport( + "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f", + Verdict.UNKNOWN, null, null, null, false))); + when(mockHttpClient.sendAsync(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) + .thenAnswer(invocation -> CompletableFuture.completedFuture(mockResponse)); + + var vaas = getVaasWithMockedClient(mockHttpClient); + var vaasVerdict = vaas.forSha256Async(sha256).join(); + verify(mockHttpClient).sendAsync(requestCaptor.capture(), any(HttpResponse.BodyHandler.class)); + var capturedUserAgent = requestCaptor.getValue().headers().firstValue("User-Agent"); + + assertTrue(capturedUserAgent.toString().contains("Java")); + assertEquals(Verdict.UNKNOWN, vaasVerdict.getVerdict()); + assertTrue("275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f" + .equalsIgnoreCase(vaasVerdict.getSha256())); + } + + @SuppressWarnings("unchecked") + @Test + @Tag("Mock") + public void forSha256_IfVaasRequestIdIsSet_SendTraceState() { + var sha256 = new Sha256("275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f"); + var requestCaptor = ArgumentCaptor.forClass(HttpRequest.class); + var mockHttpClient = mock(HttpClient.class); + var mockResponse = mock(HttpResponse.class); + when(mockResponse.statusCode()).thenReturn(200); + when(mockResponse.body()).thenReturn( + new Gson().toJson(new FileReport( + "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f", + Verdict.UNKNOWN, null, null, null, false))); + when(mockHttpClient.sendAsync(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) + .thenAnswer(invocation -> CompletableFuture.completedFuture(mockResponse)); + + var vaas = getVaasWithMockedClient(mockHttpClient); + var forSha256Options = new ForSha256Options(true, true, "foobar"); + var vaasVerdict = vaas.forSha256Async(sha256, forSha256Options).join(); + + verify(mockHttpClient).sendAsync(requestCaptor.capture(), any(HttpResponse.BodyHandler.class)); + var capturedTraceState = requestCaptor.getValue().headers().firstValue("tracestate"); + + assertTrue(capturedTraceState.toString().contains("foobar")); + assertEquals(Verdict.UNKNOWN, vaasVerdict.getVerdict()); + assertTrue("275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f" + .equalsIgnoreCase(vaasVerdict.getSha256())); + } + + @SuppressWarnings("unchecked") + @Test + @Tag("Mock") + public void forSha256_IfBadRequest_ThrowsVaasClientException() { + var sha256 = new Sha256("275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f"); + var mockHttpClient = mock(HttpClient.class); + var mockResponse = mock(HttpResponse.class); + when(mockResponse.statusCode()).thenReturn(400); + when(mockHttpClient.sendAsync(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) + .thenAnswer(invocation -> CompletableFuture.completedFuture(mockResponse)); + when(mockResponse.body()).thenReturn( + new Gson().toJson(new ProblemDetails("VaasClientException", "Bad Request"))); + + var vaas = getVaasWithMockedClient(mockHttpClient); + + var exception = assertThrows(CompletionException.class, () -> vaas.forSha256Async(sha256).join()); + assertInstanceOf(VaasClientException.class, exception.getCause()); + } + + @SuppressWarnings("unchecked") + @Test + @Tag("Mock") + public void forSha256_IfInternalServerError_ThrowsVaasServerException() { + var sha256 = new Sha256("275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f"); + var mockHttpClient = mock(HttpClient.class); + var mockResponse = mock(HttpResponse.class); + when(mockResponse.statusCode()).thenReturn(500); + when(mockHttpClient.sendAsync(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) + .thenAnswer(invocation -> CompletableFuture.completedFuture(mockResponse)); + + var vaas = getVaasWithMockedClient(mockHttpClient); + + var exception = assertThrows(CompletionException.class, () -> vaas.forSha256Async(sha256).join()); + assertInstanceOf(VaasServerException.class, exception.getCause()); + } + + @SuppressWarnings("unchecked") + @Test + @Tag("Mock") + public void forSha256_IfUnauthorized_ThrowsVaasAuthenticationException() { + var sha256 = new Sha256("275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f"); + var mockHttpClient = mock(HttpClient.class); + var mockResponse = mock(HttpResponse.class); + when(mockResponse.statusCode()).thenReturn(401); + when(mockHttpClient.sendAsync(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) + .thenAnswer(invocation -> CompletableFuture.completedFuture(mockResponse)); + + var authenticator = getAuthenticator(mockHttpClient); + var vaas = getVaasWithAuthenticator(authenticator); + + assertThrows(VaasAuthenticationException.class, () -> vaas.forSha256(sha256)); + } + + @SuppressWarnings("unchecked") + @Test + @Tag("Mock") + public void forSha256_IfAuthenticatorFailed_ThrowsVaasAuthenticationException() throws Exception { + var sha256 = new Sha256("275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f"); + var mockHttpClient = mock(HttpClient.class); + var mockResponse = mock(HttpResponse.class); + when(mockResponse.statusCode()).thenReturn(401); + when(mockHttpClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) + .thenAnswer(invocation -> mockResponse); + + var authenticator = getAuthenticator(mockHttpClient); + var vaas = getVaasWithAuthenticator(authenticator); + + assertThrows(VaasAuthenticationException.class, () -> vaas.forSha256(sha256)); + } + + @Test + public void forSha256_IfCancellationIsRequested_ThrowsCancellationException() { + var sha256 = new Sha256("275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f"); + + var future = vaasWithDefaultConfig.forSha256Async(sha256); + + var result = future.cancel(true); + assertTrue(result); + assertThrows(CancellationException.class, future::get); + } + + @ParameterizedTest + @MethodSource("provideForFileParams") + public void forFile_ReturnsVerdict(Path tmpFile, Verdict verdict) throws Exception { + var vaasVerdict = vaasWithDefaultConfig.forFileAsync(tmpFile).join(); + + assertEquals(verdict, vaasVerdict.getVerdict()); + } + + private static Stream provideForFileParams() + throws VaasClientException, IOException, InterruptedException { + return Stream.of( + Arguments.of(samplesFixture.getCleanSample(), Verdict.CLEAN), + Arguments.of(samplesFixture.getEicarSample(), Verdict.MALICIOUS), + Arguments.of(samplesFixture.getPupSample(), Verdict.PUP)); + } + + @SuppressWarnings("unchecked") + @ParameterizedTest + @CsvSource({ + "false, false", + "false, true", + "true, false", + "true, true", + }) + @Tag("Mock") + public void forFile_SendOptions(boolean useCache, boolean useHashLookup) throws Exception { + var tmpFile = samplesFixture.getEicarSample(); + var requestCaptor = ArgumentCaptor.forClass(HttpRequest.class); + var mockHttpClient = mock(HttpClient.class); + var mockPostResponse = mock(HttpResponse.class); + var mockGetResponse = mock(HttpResponse.class); + var forFileOptions = new ForFileOptions(useCache, useHashLookup, "foobar"); + + when(mockGetResponse.statusCode()).thenReturn(200); + when(mockGetResponse.body()).thenReturn( + new Gson().toJson(new FileReport( + "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f", + Verdict.UNKNOWN, null, null, null, false))); + when(mockPostResponse.statusCode()).thenReturn(200); + when(mockPostResponse.body()).thenReturn(new Gson().toJson(new FileAnalysisStarted( + "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f"))); + + when(mockHttpClient.sendAsync( + argThat(getRequest -> getRequest != null + && getRequest.method().equals("GET") + && getRequest.uri().toString().contains( + "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f") + && getRequest.uri().toString() + .contains("useCache=" + useCache) + && getRequest.uri().toString().contains( + "useHashLookup=" + useHashLookup)), + any(HttpResponse.BodyHandler.class))) + .thenAnswer(invocation -> CompletableFuture.completedFuture(mockGetResponse)); + + if (!useCache) { + when(mockHttpClient.sendAsync( + argThat(getRequest -> getRequest != null + && getRequest.method().equals("GET") + && getRequest.uri().toString().contains( + "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f") + && getRequest.uri().toString() + .contains("useCache=" + true) + && getRequest.uri().toString() + .contains("useHashLookup=" + useHashLookup)), + any(HttpResponse.BodyHandler.class))) + .thenAnswer(invocation -> CompletableFuture.completedFuture(mockGetResponse)); + } + + when(mockHttpClient.sendAsync( + argThat(postRequest -> postRequest != null + && postRequest.method().equals("POST") + && postRequest.uri().toString().contains("files") + && postRequest.uri().toString().contains( + "useHashLookup=" + useHashLookup)), + any(HttpResponse.BodyHandler.class))) + .thenAnswer(invocation -> CompletableFuture.completedFuture(mockPostResponse)); + + var vaas = getVaasWithMockedClient(mockHttpClient); + var vaasVerdict = vaas.forFileAsync(tmpFile, forFileOptions).join(); + verify(mockHttpClient, times(3)).sendAsync(requestCaptor.capture(), + any(HttpResponse.BodyHandler.class)); + var firstRequestUri = requestCaptor.getAllValues().get(0).uri(); + var secondRequestUri = requestCaptor.getAllValues().get(1).uri(); + var thirdRequestUri = requestCaptor.getAllValues().get(2).uri(); + + assertTrue(firstRequestUri.toString().contains(String.format("useCache=%s", useCache))); + assertTrue(firstRequestUri.toString() + .contains(String.format("useHashLookup=%s", useHashLookup))); + assertTrue(secondRequestUri.toString() + .contains(String.format("useHashLookup=%s", useHashLookup))); + assertTrue(thirdRequestUri.toString() + .contains(String.format("useHashLookup=%s", useHashLookup))); + assertEquals(Verdict.UNKNOWN, vaasVerdict.getVerdict()); + assertTrue("275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f" + .equalsIgnoreCase(vaasVerdict.getSha256())); + } + + @SuppressWarnings("unchecked") + @Test + @Tag("Mock") + public void forFile_SendUserAgent() throws Exception { + var tmpFile = samplesFixture.getEicarSample(); + + var requestCaptor = ArgumentCaptor.forClass(HttpRequest.class); + var mockHttpClient = mock(HttpClient.class); + var mockPostResponse = mock(HttpResponse.class); + var mockGetResponse = mock(HttpResponse.class); + when(mockGetResponse.statusCode()).thenReturn(200); + when(mockGetResponse.body()).thenReturn( + new Gson().toJson(new FileReport( + "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f", + Verdict.UNKNOWN, null, null, null, false))); + when(mockPostResponse.statusCode()).thenReturn(200); + when(mockPostResponse.body()).thenReturn(new Gson().toJson(new FileAnalysisStarted( + "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f"))); + + when(mockHttpClient.sendAsync( + argThat(getRequest -> getRequest != null + && getRequest.method().equals("GET") + && getRequest.uri().toString().contains( + "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f") + && getRequest.uri().toString() + .contains("useCache=" + true) + && getRequest.uri().toString() + .contains("useHashLookup=" + true) + && getRequest.headers().firstValue("User-Agent").toString() + .contains("Java")), + any(HttpResponse.BodyHandler.class))) + .thenAnswer(invocation -> CompletableFuture.completedFuture(mockGetResponse)); + + when(mockHttpClient.sendAsync( + argThat(postRequest -> postRequest != null + && postRequest.method().equals("POST") + && postRequest.uri().toString().contains("files") + && postRequest.uri().toString() + .contains("useHashLookup=" + true) + && postRequest.headers().firstValue("User-Agent").toString() + .contains("Java")), + any(HttpResponse.BodyHandler.class))) + .thenAnswer(invocation -> CompletableFuture.completedFuture(mockPostResponse)); + + var vaas = getVaasWithMockedClient(mockHttpClient); + var vaasVerdict = vaas.forFileAsync(tmpFile).join(); + verify(mockHttpClient, times(3)).sendAsync(requestCaptor.capture(), + any(HttpResponse.BodyHandler.class)); + var firstRequestHeaders = requestCaptor.getAllValues().get(0).headers().firstValue("User-Agent"); + var secondRequestHeaders = requestCaptor.getAllValues().get(1).headers().firstValue("User-Agent"); + var thirdRequestHeaders = requestCaptor.getAllValues().get(2).headers().firstValue("User-Agent"); + + assertTrue(firstRequestHeaders.toString().contains("Java")); + assertTrue(secondRequestHeaders.toString().contains("Java")); + assertTrue(thirdRequestHeaders.toString().contains("Java")); + assertEquals(Verdict.UNKNOWN, vaasVerdict.getVerdict()); + assertTrue("275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f" + .equalsIgnoreCase(vaasVerdict.getSha256())); + } + + @SuppressWarnings("unchecked") + @Test + @Tag("Mock") + public void forFile_IfVaasRequestIdIsSet_SendTraceState() throws Exception { + var tmpFile = samplesFixture.getEicarSample(); + + var requestCaptor = ArgumentCaptor.forClass(HttpRequest.class); + var mockHttpClient = mock(HttpClient.class); + var mockPostResponse = mock(HttpResponse.class); + var mockGetResponse = mock(HttpResponse.class); + when(mockGetResponse.statusCode()).thenReturn(200); + when(mockGetResponse.body()).thenReturn( + new Gson().toJson(new FileReport( + "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f", + Verdict.UNKNOWN, null, null, null, false))); + when(mockPostResponse.statusCode()).thenReturn(200); + when(mockPostResponse.body()).thenReturn(new Gson().toJson(new FileAnalysisStarted( + "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f"))); + + when(mockHttpClient.sendAsync( + argThat(getRequest -> getRequest != null + && getRequest.method().equals("GET") + && getRequest.uri().toString().contains( + "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f") + && getRequest.uri().toString() + .contains("useCache=" + true) + && getRequest.uri().toString() + .contains("useHashLookup=" + true) + && getRequest.headers().firstValue("tracestate").toString() + .contains("foobar")), + any(HttpResponse.BodyHandler.class))) + .thenAnswer(invocation -> CompletableFuture.completedFuture(mockGetResponse)); + + when(mockHttpClient.sendAsync( + argThat(postRequest -> postRequest != null + && postRequest.method().equals("POST") + && postRequest.uri().toString().contains("files") + && postRequest.uri().toString() + .contains("useHashLookup=" + true) + && postRequest.headers().firstValue("tracestate").toString() + .contains("foobar")), + any(HttpResponse.BodyHandler.class))) + .thenAnswer(invocation -> CompletableFuture.completedFuture(mockPostResponse)); + + var vaas = getVaasWithMockedClient(mockHttpClient); + var forFileOptions = new ForFileOptions(true, true, "foobar"); + var vaasVerdict = vaas.forFileAsync(tmpFile, forFileOptions).join(); + verify(mockHttpClient, times(3)).sendAsync(requestCaptor.capture(), + any(HttpResponse.BodyHandler.class)); + var firstRequestHeaders = requestCaptor.getAllValues().get(0).headers().firstValue("tracestate"); + var secondRequestHeaders = requestCaptor.getAllValues().get(1).headers().firstValue("tracestate"); + var thirdRequestHeaders = requestCaptor.getAllValues().get(2).headers().firstValue("tracestate"); + + assertTrue(firstRequestHeaders.toString().contains("foobar")); + assertTrue(secondRequestHeaders.toString().contains("foobar")); + assertTrue(thirdRequestHeaders.toString().contains("foobar")); + assertEquals(Verdict.UNKNOWN, vaasVerdict.getVerdict()); + assertTrue("275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f" + .equalsIgnoreCase(vaasVerdict.getSha256())); + } + + @SuppressWarnings("unchecked") + @Test + @Tag("Mock") + public void forFile_IfBadRequest_ThrowsVaasClientException() throws Exception { + var tmpFile = samplesFixture.getEicarSample(); + + var mockHttpClient = mock(HttpClient.class); + var mockPostResponse = mock(HttpResponse.class); + var mockGetResponse = mock(HttpResponse.class); + when(mockGetResponse.statusCode()).thenReturn(200); + when(mockGetResponse.body()).thenReturn( + new Gson().toJson(new FileReport( + "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f", + Verdict.UNKNOWN, null, null, null, false))); + when(mockPostResponse.statusCode()).thenReturn(400); + when(mockPostResponse.body()).thenReturn(new Gson() + .toJson(new ProblemDetails("VaasClientException", "Client-side error occurred"))); + + when(mockHttpClient.sendAsync( + argThat(getRequest -> getRequest != null + && getRequest.method().equals("GET")), + any(HttpResponse.BodyHandler.class))) + .thenAnswer(invocation -> CompletableFuture.completedFuture(mockGetResponse)); + + when(mockHttpClient.sendAsync( + argThat(postRequest -> postRequest != null + && postRequest.method().equals("POST")), + any(HttpResponse.BodyHandler.class))) + .thenAnswer(invocation -> CompletableFuture.completedFuture(mockPostResponse)); + + var vaas = getVaasWithMockedClient(mockHttpClient); + + var exception = assertThrows(CompletionException.class, () -> vaas.forFileAsync(tmpFile).join()); + assertInstanceOf(VaasClientException.class, exception.getCause()); + } + + @SuppressWarnings("unchecked") + @Test + @Tag("Mock") + public void forFile_IfInternalServerError_ThrowsVaasServerException() + throws Exception { + var tmpFile = samplesFixture.getEicarSample(); + + var mockHttpClient = mock(HttpClient.class); + var mockPostResponse = mock(HttpResponse.class); + var mockGetResponse = mock(HttpResponse.class); + when(mockGetResponse.statusCode()).thenReturn(200); + when(mockGetResponse.body()).thenReturn( + new Gson().toJson(new FileReport( + "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f", + Verdict.UNKNOWN, null, null, null, false))); + when(mockPostResponse.statusCode()).thenReturn(500); + when(mockPostResponse.body()).thenReturn(new Gson() + .toJson(new ProblemDetails("VaasServerException", "Server-side error occurred"))); + + when(mockHttpClient.sendAsync( + argThat(getRequest -> getRequest != null + && getRequest.method().equals("GET")), + any(HttpResponse.BodyHandler.class))) + .thenAnswer(invocation -> CompletableFuture.completedFuture(mockGetResponse)); + + when(mockHttpClient.sendAsync( + argThat(postRequest -> postRequest != null + && postRequest.method().equals("POST")), + any(HttpResponse.BodyHandler.class))) + .thenAnswer(invocation -> CompletableFuture.completedFuture(mockPostResponse)); + + var vaas = getVaasWithMockedClient(mockHttpClient); + + var exception = assertThrows(CompletionException.class, () -> vaas.forFileAsync(tmpFile).join()); + assertInstanceOf(VaasServerException.class, exception.getCause()); + } + + @SuppressWarnings("unchecked") + @Test + @Tag("Mock") + public void forFile_IfUnauthorized_ThrowsVaasAuthenticationException() + throws Exception { + var tmpFile = samplesFixture.getEicarSample(); + + var mockHttpClient = mock(HttpClient.class); + var mockPostResponse = mock(HttpResponse.class); + var mockGetResponse = mock(HttpResponse.class); + when(mockGetResponse.statusCode()).thenReturn(200); + when(mockGetResponse.body()).thenReturn( + new Gson().toJson(new FileReport( + "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f", + Verdict.UNKNOWN, null, null, null, false))); + when(mockPostResponse.statusCode()).thenReturn(401); + when(mockPostResponse.body()).thenReturn(new Gson() + .toJson(new ProblemDetails("VaasAuthenticationException", "Authentication failed."))); + + when(mockHttpClient.sendAsync( + argThat(getRequest -> getRequest != null + && getRequest.method().equals("GET")), + any(HttpResponse.BodyHandler.class))) + .thenAnswer(invocation -> CompletableFuture.completedFuture(mockGetResponse)); + + when(mockHttpClient.sendAsync( + argThat(postRequest -> postRequest != null + && postRequest.method().equals("POST")), + any(HttpResponse.BodyHandler.class))) + .thenAnswer(invocation -> CompletableFuture.completedFuture(mockPostResponse)); + + var vaas = getVaasWithMockedClient(mockHttpClient); + + var exception = assertThrows(CompletionException.class, () -> vaas.forFileAsync(tmpFile).join()); + assertInstanceOf(VaasAuthenticationException.class, exception.getCause()); + } + + @SuppressWarnings("unchecked") + @Test + @Tag("Mock") + public void forFile_IfAuthenticatorFailed_ThrowsVaasAuthenticationException() throws Exception { + var tmpFile = samplesFixture.getEicarSample(); + + var mockHttpClient = mock(HttpClient.class); + var mockResponse = mock(HttpResponse.class); + when(mockResponse.statusCode()).thenReturn(401); + when(mockHttpClient.send(any(HttpRequest.class), + any(HttpResponse.BodyHandler.class))) + .thenAnswer(invocation -> mockResponse); + + var authenticator = getAuthenticator(mockHttpClient); + var vaas = getVaasWithAuthenticator(authenticator); + + assertThrows(VaasAuthenticationException.class, () -> vaas.forFile(tmpFile)); + } + + @Test + public void forFile_IfCancellationIsRequested_ThrowsCancellationException() + throws Exception { + var tmpFile = samplesFixture.getEicarSample(); + + var future = vaasWithDefaultConfig.forFileAsync(tmpFile); + + var result = future.cancel(true); + assertTrue(result); + assertThrows(CancellationException.class, future::get); + } + + @Test + @Disabled("This test is disabled because it takes too long to run.") + public void forFile_BigFileWithSmallTimeout_ThrowsTimeoutException() + throws Exception { + var tmpFile = Path.of(System.getProperty("java.io.tmpdir"), "file.txt"); + var url = URI.create("https://ash-speed.hetzner.com/1GB.bin").toURL(); + var conn = url.openConnection(); + var inputStream = conn.getInputStream(); + Files.copy(inputStream, tmpFile, StandardCopyOption.REPLACE_EXISTING); + + var vaasUrl = getEnvironmentKey("VAAS_URL"); + var config = new VaasConfig(1000, URI.create(vaasUrl)); + var vaas = new Vaas(config, authenticatorFixture); + var forFileOptions = new ForFileOptions(false, false, null); + + var exception = assertThrows(ExecutionException.class, + () -> vaas.forFileAsync(tmpFile, forFileOptions).get()); + assertInstanceOf(TimeoutException.class, exception.getCause()); + } + + @Test + public void forFile_EmptyFile_ReturnsVerdict() throws Exception { + var file = new File(System.getProperty("java.io.tmpdir"), "empty.txt"); + file.createNewFile(); + + var vaasVerdict = vaasWithDefaultConfig.forFileAsync(file.toPath()).join(); + + assertEquals("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + vaasVerdict.getSha256()); + assertEquals(Verdict.CLEAN, vaasVerdict.getVerdict()); + } + + @Test + public void forStream_ReturnsVerdict() throws Exception { + var url = URI.create(EICAR_URL).toURL(); + var conn = url.openConnection(); + var inputStream = conn.getInputStream(); + var contentLength = conn.getContentLength(); + var forStreamOptions = new ForStreamOptions(true, "foobar"); + + var verdict = vaasWithDefaultConfig.forStreamAsync(inputStream, contentLength, forStreamOptions).join(); + + assertEquals(Verdict.MALICIOUS, verdict.getVerdict()); + assertTrue("275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f" + .equalsIgnoreCase(verdict.getSha256())); + } + + @SuppressWarnings("unchecked") + @ParameterizedTest + @CsvSource({ + "false", + "true", + }) + @Tag("Mock") + public void forStream_SendOptions(boolean useHashLookup) throws Exception { + var url = URI.create(EICAR_URL).toURL(); + var conn = url.openConnection(); + var inputStream = conn.getInputStream(); + var contentLength = conn.getContentLength(); + + var requestCaptor = ArgumentCaptor.forClass(HttpRequest.class); + var mockHttpClient = mock(HttpClient.class); + var mockPostResponse = mock(HttpResponse.class); + var mockGetResponse = mock(HttpResponse.class); + var forStreamOptions = new ForStreamOptions(useHashLookup, "foobar"); + + when(mockGetResponse.statusCode()).thenReturn(200); + when(mockGetResponse.body()).thenReturn( + new Gson().toJson(new FileReport( + "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f", + Verdict.UNKNOWN, null, null, null, false))); + when(mockPostResponse.statusCode()).thenReturn(200); + when(mockPostResponse.body()).thenReturn(new Gson().toJson(new FileAnalysisStarted( + "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f"))); + + when(mockHttpClient.sendAsync( + argThat(getRequest -> getRequest != null + && getRequest.method().equals("GET") + && getRequest.uri().toString().contains( + "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f") + && getRequest.uri().toString() + .contains("useCache=" + true) + && getRequest.uri().toString() + .contains("useHashLookup=" + useHashLookup)), + any(HttpResponse.BodyHandler.class))) + .thenAnswer(invocation -> CompletableFuture.completedFuture(mockGetResponse)); + + when(mockHttpClient.sendAsync( + argThat(postRequest -> postRequest != null + && postRequest.method().equals("POST") + && postRequest.uri().toString().contains("files") + && postRequest.uri().toString().contains( + "useHashLookup=" + useHashLookup)), + any(HttpResponse.BodyHandler.class))) + .thenAnswer(invocation -> CompletableFuture.completedFuture(mockPostResponse)); + + var vaas = getVaasWithMockedClient(mockHttpClient); + var vaasVerdict = vaas.forStreamAsync(inputStream, contentLength, forStreamOptions).join(); + verify(mockHttpClient, times(2)).sendAsync(requestCaptor.capture(), + any(HttpResponse.BodyHandler.class)); + var firstRequestUri = requestCaptor.getAllValues().get(0).uri(); + var secondRequestUri = requestCaptor.getAllValues().get(1).uri(); + + assertTrue(firstRequestUri.toString() + .contains(String.format("useHashLookup=%s", useHashLookup))); + assertTrue(secondRequestUri.toString() + .contains(String.format("useHashLookup=%s", useHashLookup))); + assertEquals(Verdict.UNKNOWN, vaasVerdict.getVerdict()); + assertTrue("275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f" + .equalsIgnoreCase(vaasVerdict.getSha256())); + } + + @SuppressWarnings("unchecked") + @Test + @Tag("Mock") + public void forStream_SendUserAgent() throws Exception { + var url = URI.create(EICAR_URL).toURL(); + var conn = url.openConnection(); + var inputStream = conn.getInputStream(); + var contentLength = conn.getContentLength(); + + var requestCaptor = ArgumentCaptor.forClass(HttpRequest.class); + var mockHttpClient = mock(HttpClient.class); + var mockPostResponse = mock(HttpResponse.class); + var mockGetResponse = mock(HttpResponse.class); + when(mockGetResponse.statusCode()).thenReturn(200); + when(mockGetResponse.body()).thenReturn( + new Gson().toJson(new FileReport( + "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f", + Verdict.UNKNOWN, null, null, null, false))); + when(mockPostResponse.statusCode()).thenReturn(200); + when(mockPostResponse.body()).thenReturn(new Gson().toJson(new FileAnalysisStarted( + "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f"))); + + when(mockHttpClient.sendAsync( + argThat(getRequest -> getRequest != null + && getRequest.method().equals("GET") + && getRequest.uri().toString().contains( + "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f") + && getRequest.uri().toString() + .contains("useCache=" + true) + && getRequest.uri().toString() + .contains("useHashLookup=" + true) + && getRequest.headers().firstValue("User-Agent").toString() + .contains("Java")), + any(HttpResponse.BodyHandler.class))) + .thenAnswer(invocation -> CompletableFuture.completedFuture(mockGetResponse)); + + when(mockHttpClient.sendAsync( + argThat(postRequest -> postRequest != null + && postRequest.method().equals("POST") + && postRequest.uri().toString().contains("files") + && postRequest.uri().toString() + .contains("useHashLookup=" + true) + && postRequest.headers().firstValue("User-Agent").toString() + .contains("Java")), + any(HttpResponse.BodyHandler.class))) + .thenAnswer(invocation -> CompletableFuture.completedFuture(mockPostResponse)); + + var vaas = getVaasWithMockedClient(mockHttpClient); + var vaasVerdict = vaas.forStreamAsync(inputStream, contentLength).join(); + verify(mockHttpClient, times(2)).sendAsync(requestCaptor.capture(), + any(HttpResponse.BodyHandler.class)); + var firstRequestHeaders = requestCaptor.getAllValues().get(0).headers().firstValue("User-Agent"); + var secondRequestHeaders = requestCaptor.getAllValues().get(1).headers().firstValue("User-Agent"); + + assertTrue(firstRequestHeaders.toString().contains("Java")); + assertTrue(secondRequestHeaders.toString().contains("Java")); + assertEquals(Verdict.UNKNOWN, vaasVerdict.getVerdict()); + assertTrue("275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f" + .equalsIgnoreCase(vaasVerdict.getSha256())); + } - static { - authenticatorFixture = getAuthenticator(); - vaasWithDefaultConfig = getVaasWithAuthenticator(authenticatorFixture); - } + @SuppressWarnings("unchecked") + @Test + @Tag("Mock") + public void forStream_IfVaasRequestIdIsSet_SendTraceState() throws Exception { + var url = URI.create(EICAR_URL).toURL(); + var conn = url.openConnection(); + var inputStream = conn.getInputStream(); + var contentLength = conn.getContentLength(); + + var requestCaptor = ArgumentCaptor.forClass(HttpRequest.class); + var mockHttpClient = mock(HttpClient.class); + var mockPostResponse = mock(HttpResponse.class); + var mockGetResponse = mock(HttpResponse.class); + when(mockGetResponse.statusCode()).thenReturn(200); + when(mockGetResponse.body()).thenReturn( + new Gson().toJson(new FileReport( + "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f", + Verdict.UNKNOWN, null, null, null, false))); + when(mockPostResponse.statusCode()).thenReturn(200); + when(mockPostResponse.body()).thenReturn(new Gson().toJson(new FileAnalysisStarted( + "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f"))); + + when(mockHttpClient.sendAsync( + argThat(getRequest -> getRequest != null + && getRequest.method().equals("GET") + && getRequest.uri().toString().contains( + "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f") + && getRequest.uri().toString() + .contains("useCache=" + true) + && getRequest.uri().toString() + .contains("useHashLookup=" + true) + && getRequest.headers().firstValue("tracestate").toString() + .contains("foobar")), + any(HttpResponse.BodyHandler.class))) + .thenAnswer(invocation -> CompletableFuture.completedFuture(mockGetResponse)); + + when(mockHttpClient.sendAsync( + argThat(postRequest -> postRequest != null + && postRequest.method().equals("POST") + && postRequest.uri().toString().contains("files") + && postRequest.uri().toString() + .contains("useHashLookup=" + true) + && postRequest.headers().firstValue("tracestate").toString() + .contains("foobar")), + any(HttpResponse.BodyHandler.class))) + .thenAnswer(invocation -> CompletableFuture.completedFuture(mockPostResponse)); + + var vaas = getVaasWithMockedClient(mockHttpClient); + var forFileOptions = new ForStreamOptions(true, "foobar"); + var vaasVerdict = vaas.forStreamAsync(inputStream, contentLength, forFileOptions).join(); + verify(mockHttpClient, times(2)).sendAsync(requestCaptor.capture(), + any(HttpResponse.BodyHandler.class)); + var firstRequestHeaders = requestCaptor.getAllValues().get(0).headers().firstValue("tracestate"); + var secondRequestHeaders = requestCaptor.getAllValues().get(1).headers().firstValue("tracestate"); + + assertTrue(firstRequestHeaders.toString().contains("foobar")); + assertTrue(secondRequestHeaders.toString().contains("foobar")); + assertEquals(Verdict.UNKNOWN, vaasVerdict.getVerdict()); + assertTrue("275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f" + .equalsIgnoreCase(vaasVerdict.getSha256())); + } - private static String getEnvironmentKey(String key) { - var value = dotenv.get(key); - if (value == null) { - throw new IllegalArgumentException("Environment variable " + key + " must be set."); + @SuppressWarnings("unchecked") + @Test + @Tag("Mock") + public void forStream_IfBadRequest_ThrowsVaasClientException() throws Exception { + var url = URI.create(EICAR_URL).toURL(); + var conn = url.openConnection(); + var inputStream = conn.getInputStream(); + var contentLength = conn.getContentLength(); + + var mockHttpClient = mock(HttpClient.class); + var mockPostResponse = mock(HttpResponse.class); + var mockGetResponse = mock(HttpResponse.class); + when(mockGetResponse.statusCode()).thenReturn(200); + when(mockGetResponse.body()).thenReturn( + new Gson().toJson(new FileReport( + "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f", + Verdict.UNKNOWN, null, null, null, false))); + when(mockPostResponse.statusCode()).thenReturn(400); + when(mockPostResponse.body()).thenReturn(new Gson() + .toJson(new ProblemDetails("VaasClientException", "Client-side error occurred"))); + + when(mockHttpClient.sendAsync( + argThat(getRequest -> getRequest != null + && getRequest.method().equals("GET")), + any(HttpResponse.BodyHandler.class))) + .thenAnswer(invocation -> CompletableFuture.completedFuture(mockGetResponse)); + + when(mockHttpClient.sendAsync( + argThat(postRequest -> postRequest != null + && postRequest.method().equals("POST")), + any(HttpResponse.BodyHandler.class))) + .thenAnswer(invocation -> CompletableFuture.completedFuture(mockPostResponse)); + + var vaas = getVaasWithMockedClient(mockHttpClient); + + var exception = assertThrows(CompletionException.class, + () -> vaas.forStreamAsync(inputStream, contentLength).join()); + assertInstanceOf(VaasClientException.class, exception.getCause()); } - return value; - } - - private static IAuthenticator getAuthenticator() { - var clientId = getEnvironmentKey("CLIENT_ID"); - var clientSecret = getEnvironmentKey("CLIENT_SECRET"); - var tokenUrl = URI.create(getEnvironmentKey("TOKEN_URL")); - return new ClientCredentialsGrantAuthenticator(clientId, clientSecret, tokenUrl); - } - - private static IAuthenticator getAuthenticator(HttpClient httpClient) { - var clientId = getEnvironmentKey("CLIENT_ID"); - var clientSecret = getEnvironmentKey("CLIENT_SECRET"); - var tokenUrl = URI.create(getEnvironmentKey("TOKEN_URL")); - return new ClientCredentialsGrantAuthenticator(clientId, clientSecret, tokenUrl, httpClient); - } - - private static Vaas getVaasWithAuthenticator(IAuthenticator authenticator) { - var vaasUrl = getEnvironmentKey("VAAS_URL"); - var config = new VaasConfig(URI.create(vaasUrl)); - return new Vaas(config, authenticator); - } - - private static Vaas getVaasWithMockedClient(HttpClient httpClient) { - var vaasUrl = getEnvironmentKey("VAAS_URL"); - var config = new VaasConfig(URI.create(vaasUrl)); - return new Vaas(config, authenticatorFixture, httpClient); - } - - public static byte[] readContent(HttpRequest.BodyPublisher bodyPublisher) { - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - bodyPublisher.subscribe(new Flow.Subscriber<>() { - - @Override - public void onSubscribe(Flow.Subscription subscription) { - subscription.request(Long.MAX_VALUE); - } - - @Override - public void onNext(ByteBuffer item) { - byte[] bytes = new byte[item.remaining()]; - item.get(bytes); - outputStream.write(bytes, 0, bytes.length); - } - - @Override - public void onError(Throwable throwable) { - throwable.printStackTrace(); - } - - @Override - public void onComplete() { - } - }); - return outputStream.toByteArray(); - } - - @ParameterizedTest - @CsvSource({ - "cd617c5c1b1ff1c94a52ab8cf07192654f271a3f8bad49490288131ccb9efc1e, CLEAN", - "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f, MALICIOUS", - "d6f6c6b9fde37694e12b12009ad11ab9ec8dd0f193e7319c523933bdad8a50ad, PUP" - }) - public void forSha256_ReturnsVerdict(String sha256, Verdict verdict) { - var sha256sum = new Sha256(sha256); - - var vaasVerdict = vaasWithDefaultConfig.forSha256Async(sha256sum).join(); - - assertEquals(sha256, vaasVerdict.getSha256()); - assertEquals(verdict, vaasVerdict.getVerdict()); - } - - @SuppressWarnings("unchecked") - @ParameterizedTest - @CsvSource({ - "false, false", - "false, true", - "true, false", - "true, true", - }) - @Tag("Mock") - public void forSha256_SendOptions(boolean useCache, boolean useHashLookup) { - var sha256 = new Sha256("275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f"); - var requestCaptor = ArgumentCaptor.forClass(HttpRequest.class); - var mockHttpClient = mock(HttpClient.class); - var mockResponse = mock(HttpResponse.class); - var forSha256Options = new ForSha256Options(useCache, useHashLookup, "foobar"); - when(mockResponse.statusCode()).thenReturn(200); - when(mockResponse.body()).thenReturn( - new Gson().toJson(new FileReport( - "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f", - Verdict.UNKNOWN, null, null, null))); - when(mockHttpClient.sendAsync(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) - .thenAnswer(invocation -> CompletableFuture.completedFuture(mockResponse)); - - var vaas = getVaasWithMockedClient(mockHttpClient); - var vaasVerdict = vaas.forSha256Async(sha256, forSha256Options).join(); - verify(mockHttpClient).sendAsync(requestCaptor.capture(), any(HttpResponse.BodyHandler.class)); - var capturedUri = requestCaptor.getValue().uri(); - - assertTrue(capturedUri.toString().contains(String.format("useCache=%s", useCache))); - assertTrue(capturedUri.toString() - .contains(String.format("useHashLookup=%s", useHashLookup))); - assertEquals(Verdict.UNKNOWN, vaasVerdict.getVerdict()); - assertTrue("275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f" - .equalsIgnoreCase(vaasVerdict.getSha256())); - } - - @SuppressWarnings("unchecked") - @Test - @Tag("Mock") - public void forSha256_SendUserAgent() { - var sha256 = new Sha256("275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f"); - var requestCaptor = ArgumentCaptor.forClass(HttpRequest.class); - var mockHttpClient = mock(HttpClient.class); - var mockResponse = mock(HttpResponse.class); - when(mockResponse.statusCode()).thenReturn(200); - when(mockResponse.body()).thenReturn( - new Gson().toJson(new FileReport( - "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f", - Verdict.UNKNOWN, null, null, null))); - when(mockHttpClient.sendAsync(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) - .thenAnswer(invocation -> CompletableFuture.completedFuture(mockResponse)); - - var vaas = getVaasWithMockedClient(mockHttpClient); - var vaasVerdict = vaas.forSha256Async(sha256).join(); - verify(mockHttpClient).sendAsync(requestCaptor.capture(), any(HttpResponse.BodyHandler.class)); - var capturedUserAgent = requestCaptor.getValue().headers().firstValue("User-Agent"); - - assertTrue(capturedUserAgent.toString().contains("Java")); - assertEquals(Verdict.UNKNOWN, vaasVerdict.getVerdict()); - assertTrue("275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f" - .equalsIgnoreCase(vaasVerdict.getSha256())); - } - - @SuppressWarnings("unchecked") - @Test - @Tag("Mock") - public void forSha256_IfVaasRequestIdIsSet_SendTraceState() { - var sha256 = new Sha256("275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f"); - var requestCaptor = ArgumentCaptor.forClass(HttpRequest.class); - var mockHttpClient = mock(HttpClient.class); - var mockResponse = mock(HttpResponse.class); - when(mockResponse.statusCode()).thenReturn(200); - when(mockResponse.body()).thenReturn( - new Gson().toJson(new FileReport( - "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f", - Verdict.UNKNOWN, null, null, null))); - when(mockHttpClient.sendAsync(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) - .thenAnswer(invocation -> CompletableFuture.completedFuture(mockResponse)); - - var vaas = getVaasWithMockedClient(mockHttpClient); - var forSha256Options = new ForSha256Options(true, true, "foobar"); - var vaasVerdict = vaas.forSha256Async(sha256, forSha256Options).join(); - - verify(mockHttpClient).sendAsync(requestCaptor.capture(), any(HttpResponse.BodyHandler.class)); - var capturedTraceState = requestCaptor.getValue().headers().firstValue("tracestate"); - - assertTrue(capturedTraceState.toString().contains("foobar")); - assertEquals(Verdict.UNKNOWN, vaasVerdict.getVerdict()); - assertTrue("275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f" - .equalsIgnoreCase(vaasVerdict.getSha256())); - } - - @SuppressWarnings("unchecked") - @Test - @Tag("Mock") - public void forSha256_IfBadRequest_ThrowsVaasClientException() { - var sha256 = new Sha256("275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f"); - var mockHttpClient = mock(HttpClient.class); - var mockResponse = mock(HttpResponse.class); - when(mockResponse.statusCode()).thenReturn(400); - when(mockHttpClient.sendAsync(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) - .thenAnswer(invocation -> CompletableFuture.completedFuture(mockResponse)); - when(mockResponse.body()).thenReturn( - new Gson().toJson(new ProblemDetails("VaasClientException", "Bad Request"))); - - var vaas = getVaasWithMockedClient(mockHttpClient); - - var exception = assertThrows(CompletionException.class, () -> vaas.forSha256Async(sha256).join()); - assertInstanceOf(VaasClientException.class, exception.getCause()); - } - - @SuppressWarnings("unchecked") - @Test - @Tag("Mock") - public void forSha256_IfInternalServerError_ThrowsVaasServerException() { - var sha256 = new Sha256("275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f"); - var mockHttpClient = mock(HttpClient.class); - var mockResponse = mock(HttpResponse.class); - when(mockResponse.statusCode()).thenReturn(500); - when(mockHttpClient.sendAsync(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) - .thenAnswer(invocation -> CompletableFuture.completedFuture(mockResponse)); - - var vaas = getVaasWithMockedClient(mockHttpClient); - - var exception = assertThrows(CompletionException.class, () -> vaas.forSha256Async(sha256).join()); - assertInstanceOf(VaasServerException.class, exception.getCause()); - } - - @SuppressWarnings("unchecked") - @Test - @Tag("Mock") - public void forSha256_IfUnauthorized_ThrowsVaasAuthenticationException() { - var sha256 = new Sha256("275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f"); - var mockHttpClient = mock(HttpClient.class); - var mockResponse = mock(HttpResponse.class); - when(mockResponse.statusCode()).thenReturn(401); - when(mockHttpClient.sendAsync(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) - .thenAnswer(invocation -> CompletableFuture.completedFuture(mockResponse)); - - var authenticator = getAuthenticator(mockHttpClient); - var vaas = getVaasWithAuthenticator(authenticator); - - assertThrows(VaasAuthenticationException.class, () -> vaas.forSha256(sha256)); - } - - @SuppressWarnings("unchecked") - @Test - @Tag("Mock") - public void forSha256_IfAuthenticatorFailed_ThrowsVaasAuthenticationException() throws Exception { - var sha256 = new Sha256("275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f"); - var mockHttpClient = mock(HttpClient.class); - var mockResponse = mock(HttpResponse.class); - when(mockResponse.statusCode()).thenReturn(401); - when(mockHttpClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) - .thenAnswer(invocation -> mockResponse); - - var authenticator = getAuthenticator(mockHttpClient); - var vaas = getVaasWithAuthenticator(authenticator); - - assertThrows(VaasAuthenticationException.class, () -> vaas.forSha256(sha256)); - } - - @Test - public void forSha256_IfCancellationIsRequested_ThrowsCancellationException() { - var sha256 = new Sha256("275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f"); - - var future = vaasWithDefaultConfig.forSha256Async(sha256); - - var result = future.cancel(true); - assertTrue(result); - assertThrows(CancellationException.class, future::get); - } - - @ParameterizedTest - @MethodSource("provideForFileParams") - public void forFile_ReturnsVerdict(Path tmpFile, Verdict verdict) throws Exception { - var vaasVerdict = vaasWithDefaultConfig.forFileAsync(tmpFile).join(); - - assertEquals(verdict, vaasVerdict.getVerdict()); - } - - private static Stream provideForFileParams() throws VaasClientException, IOException, InterruptedException { - return Stream.of( - Arguments.of(samplesFixture.getCleanSample(), Verdict.CLEAN), - Arguments.of(samplesFixture.getEicarSample(), Verdict.MALICIOUS), - Arguments.of(samplesFixture.getPupSample(), Verdict.PUP) - ); - } - - @SuppressWarnings("unchecked") - @ParameterizedTest - @CsvSource({ - "false, false", - "false, true", - "true, false", - "true, true", - }) - @Tag("Mock") - public void forFile_SendOptions(boolean useCache, boolean useHashLookup) throws Exception { - var tmpFile = samplesFixture.getEicarSample(); - var requestCaptor = ArgumentCaptor.forClass(HttpRequest.class); - var mockHttpClient = mock(HttpClient.class); - var mockPostResponse = mock(HttpResponse.class); - var mockGetResponse = mock(HttpResponse.class); - var forFileOptions = new ForFileOptions(useCache, useHashLookup, "foobar"); - - when(mockGetResponse.statusCode()).thenReturn(200); - when(mockGetResponse.body()).thenReturn( - new Gson().toJson(new FileReport( - "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f", - Verdict.UNKNOWN, null, null, null))); - when(mockPostResponse.statusCode()).thenReturn(200); - when(mockPostResponse.body()).thenReturn(new Gson().toJson(new FileAnalysisStarted( - "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f"))); - - when(mockHttpClient.sendAsync( - argThat(getRequest -> getRequest != null - && getRequest.method().equals("GET") - && getRequest.uri().toString().contains( - "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f") - && getRequest.uri().toString() - .contains("useCache=" + useCache) - && getRequest.uri().toString().contains( - "useHashLookup=" + useHashLookup)), - any(HttpResponse.BodyHandler.class))) - .thenAnswer(invocation -> CompletableFuture.completedFuture(mockGetResponse)); - - if (!useCache) { - when(mockHttpClient.sendAsync( - argThat(getRequest -> getRequest != null - && getRequest.method().equals("GET") - && getRequest.uri().toString().contains( - "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f") - && getRequest.uri().toString() - .contains("useCache=" + true) - && getRequest.uri().toString() - .contains("useHashLookup=" + useHashLookup)), - any(HttpResponse.BodyHandler.class))) - .thenAnswer(invocation -> CompletableFuture.completedFuture(mockGetResponse)); + + @SuppressWarnings("unchecked") + @Test + @Tag("Mock") + public void forStream_IfInternalServerError_ThrowsVaasServerException() + throws Exception { + var url = URI.create(EICAR_URL).toURL(); + var conn = url.openConnection(); + var inputStream = conn.getInputStream(); + var contentLength = conn.getContentLength(); + + var mockHttpClient = mock(HttpClient.class); + var mockPostResponse = mock(HttpResponse.class); + var mockGetResponse = mock(HttpResponse.class); + when(mockGetResponse.statusCode()).thenReturn(200); + when(mockGetResponse.body()).thenReturn( + new Gson().toJson(new FileReport( + "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f", + Verdict.UNKNOWN, null, null, null, false))); + when(mockPostResponse.statusCode()).thenReturn(500); + when(mockPostResponse.body()).thenReturn(new Gson() + .toJson(new ProblemDetails("VaasServerException", "Server-side error occurred"))); + + when(mockHttpClient.sendAsync( + argThat(getRequest -> getRequest != null + && getRequest.method().equals("GET")), + any(HttpResponse.BodyHandler.class))) + .thenAnswer(invocation -> CompletableFuture.completedFuture(mockGetResponse)); + + when(mockHttpClient.sendAsync( + argThat(postRequest -> postRequest != null + && postRequest.method().equals("POST")), + any(HttpResponse.BodyHandler.class))) + .thenAnswer(invocation -> CompletableFuture.completedFuture(mockPostResponse)); + + var vaas = getVaasWithMockedClient(mockHttpClient); + + var exception = assertThrows(CompletionException.class, + () -> vaas.forStreamAsync(inputStream, contentLength).join()); + assertInstanceOf(VaasServerException.class, exception.getCause()); } - when(mockHttpClient.sendAsync( - argThat(postRequest -> postRequest != null - && postRequest.method().equals("POST") - && postRequest.uri().toString().contains("files") - && postRequest.uri().toString().contains( - "useHashLookup=" + useHashLookup)), - any(HttpResponse.BodyHandler.class))) - .thenAnswer(invocation -> CompletableFuture.completedFuture(mockPostResponse)); - - var vaas = getVaasWithMockedClient(mockHttpClient); - var vaasVerdict = vaas.forFileAsync(tmpFile, forFileOptions).join(); - verify(mockHttpClient, times(3)).sendAsync(requestCaptor.capture(), - any(HttpResponse.BodyHandler.class)); - var firstRequestUri = requestCaptor.getAllValues().get(0).uri(); - var secondRequestUri = requestCaptor.getAllValues().get(1).uri(); - var thirdRequestUri = requestCaptor.getAllValues().get(2).uri(); - - assertTrue(firstRequestUri.toString().contains(String.format("useCache=%s", useCache))); - assertTrue(firstRequestUri.toString() - .contains(String.format("useHashLookup=%s", useHashLookup))); - assertTrue(secondRequestUri.toString() - .contains(String.format("useHashLookup=%s", useHashLookup))); - assertTrue(thirdRequestUri.toString() - .contains(String.format("useHashLookup=%s", useHashLookup))); - assertEquals(Verdict.UNKNOWN, vaasVerdict.getVerdict()); - assertTrue("275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f" - .equalsIgnoreCase(vaasVerdict.getSha256())); - } - - @SuppressWarnings("unchecked") - @Test - @Tag("Mock") - public void forFile_SendUserAgent() throws Exception { - var tmpFile = samplesFixture.getEicarSample(); - - var requestCaptor = ArgumentCaptor.forClass(HttpRequest.class); - var mockHttpClient = mock(HttpClient.class); - var mockPostResponse = mock(HttpResponse.class); - var mockGetResponse = mock(HttpResponse.class); - when(mockGetResponse.statusCode()).thenReturn(200); - when(mockGetResponse.body()).thenReturn( - new Gson().toJson(new FileReport( - "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f", - Verdict.UNKNOWN, null, null, null))); - when(mockPostResponse.statusCode()).thenReturn(200); - when(mockPostResponse.body()).thenReturn(new Gson().toJson(new FileAnalysisStarted( - "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f"))); - - when(mockHttpClient.sendAsync( - argThat(getRequest -> getRequest != null - && getRequest.method().equals("GET") - && getRequest.uri().toString().contains( - "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f") - && getRequest.uri().toString() - .contains("useCache=" + true) - && getRequest.uri().toString() - .contains("useHashLookup=" + true) - && getRequest.headers().firstValue("User-Agent").toString() - .contains("Java")), - any(HttpResponse.BodyHandler.class))) - .thenAnswer(invocation -> CompletableFuture.completedFuture(mockGetResponse)); - - when(mockHttpClient.sendAsync( - argThat(postRequest -> postRequest != null - && postRequest.method().equals("POST") - && postRequest.uri().toString().contains("files") - && postRequest.uri().toString() - .contains("useHashLookup=" + true) - && postRequest.headers().firstValue("User-Agent").toString() - .contains("Java")), - any(HttpResponse.BodyHandler.class))) - .thenAnswer(invocation -> CompletableFuture.completedFuture(mockPostResponse)); - - var vaas = getVaasWithMockedClient(mockHttpClient); - var vaasVerdict = vaas.forFileAsync(tmpFile).join(); - verify(mockHttpClient, times(3)).sendAsync(requestCaptor.capture(), - any(HttpResponse.BodyHandler.class)); - var firstRequestHeaders = requestCaptor.getAllValues().get(0).headers().firstValue("User-Agent"); - var secondRequestHeaders = requestCaptor.getAllValues().get(1).headers().firstValue("User-Agent"); - var thirdRequestHeaders = requestCaptor.getAllValues().get(2).headers().firstValue("User-Agent"); - - assertTrue(firstRequestHeaders.toString().contains("Java")); - assertTrue(secondRequestHeaders.toString().contains("Java")); - assertTrue(thirdRequestHeaders.toString().contains("Java")); - assertEquals(Verdict.UNKNOWN, vaasVerdict.getVerdict()); - assertTrue("275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f" - .equalsIgnoreCase(vaasVerdict.getSha256())); - } - - @SuppressWarnings("unchecked") - @Test - @Tag("Mock") - public void forFile_IfVaasRequestIdIsSet_SendTraceState() throws Exception { - var tmpFile = samplesFixture.getEicarSample(); - - var requestCaptor = ArgumentCaptor.forClass(HttpRequest.class); - var mockHttpClient = mock(HttpClient.class); - var mockPostResponse = mock(HttpResponse.class); - var mockGetResponse = mock(HttpResponse.class); - when(mockGetResponse.statusCode()).thenReturn(200); - when(mockGetResponse.body()).thenReturn( - new Gson().toJson(new FileReport( - "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f", - Verdict.UNKNOWN, null, null, null))); - when(mockPostResponse.statusCode()).thenReturn(200); - when(mockPostResponse.body()).thenReturn(new Gson().toJson(new FileAnalysisStarted( - "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f"))); - - when(mockHttpClient.sendAsync( - argThat(getRequest -> getRequest != null - && getRequest.method().equals("GET") - && getRequest.uri().toString().contains( - "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f") - && getRequest.uri().toString() - .contains("useCache=" + true) - && getRequest.uri().toString() - .contains("useHashLookup=" + true) - && getRequest.headers().firstValue("tracestate").toString() - .contains("foobar")), - any(HttpResponse.BodyHandler.class))) - .thenAnswer(invocation -> CompletableFuture.completedFuture(mockGetResponse)); - - when(mockHttpClient.sendAsync( - argThat(postRequest -> postRequest != null - && postRequest.method().equals("POST") - && postRequest.uri().toString().contains("files") - && postRequest.uri().toString() - .contains("useHashLookup=" + true) - && postRequest.headers().firstValue("tracestate").toString() - .contains("foobar")), - any(HttpResponse.BodyHandler.class))) - .thenAnswer(invocation -> CompletableFuture.completedFuture(mockPostResponse)); - - var vaas = getVaasWithMockedClient(mockHttpClient); - var forFileOptions = new ForFileOptions(true, true, "foobar"); - var vaasVerdict = vaas.forFileAsync(tmpFile, forFileOptions).join(); - verify(mockHttpClient, times(3)).sendAsync(requestCaptor.capture(), - any(HttpResponse.BodyHandler.class)); - var firstRequestHeaders = requestCaptor.getAllValues().get(0).headers().firstValue("tracestate"); - var secondRequestHeaders = requestCaptor.getAllValues().get(1).headers().firstValue("tracestate"); - var thirdRequestHeaders = requestCaptor.getAllValues().get(2).headers().firstValue("tracestate"); - - assertTrue(firstRequestHeaders.toString().contains("foobar")); - assertTrue(secondRequestHeaders.toString().contains("foobar")); - assertTrue(thirdRequestHeaders.toString().contains("foobar")); - assertEquals(Verdict.UNKNOWN, vaasVerdict.getVerdict()); - assertTrue("275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f" - .equalsIgnoreCase(vaasVerdict.getSha256())); - } - - @SuppressWarnings("unchecked") - @Test - @Tag("Mock") - public void forFile_IfBadRequest_ThrowsVaasClientException() throws Exception { - var tmpFile = samplesFixture.getEicarSample(); - - var mockHttpClient = mock(HttpClient.class); - var mockPostResponse = mock(HttpResponse.class); - var mockGetResponse = mock(HttpResponse.class); - when(mockGetResponse.statusCode()).thenReturn(200); - when(mockGetResponse.body()).thenReturn( - new Gson().toJson(new FileReport( - "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f", - Verdict.UNKNOWN, null, null, null))); - when(mockPostResponse.statusCode()).thenReturn(400); - when(mockPostResponse.body()).thenReturn(new Gson() - .toJson(new ProblemDetails("VaasClientException", "Client-side error occurred"))); - - when(mockHttpClient.sendAsync( - argThat(getRequest -> getRequest != null - && getRequest.method().equals("GET")), - any(HttpResponse.BodyHandler.class))) - .thenAnswer(invocation -> CompletableFuture.completedFuture(mockGetResponse)); - - when(mockHttpClient.sendAsync( - argThat(postRequest -> postRequest != null - && postRequest.method().equals("POST")), - any(HttpResponse.BodyHandler.class))) - .thenAnswer(invocation -> CompletableFuture.completedFuture(mockPostResponse)); - - var vaas = getVaasWithMockedClient(mockHttpClient); - - var exception = assertThrows(CompletionException.class, () -> vaas.forFileAsync(tmpFile).join()); - assertInstanceOf(VaasClientException.class, exception.getCause()); - } - - @SuppressWarnings("unchecked") - @Test - @Tag("Mock") - public void forFile_IfInternalServerError_ThrowsVaasServerException() - throws Exception { - var tmpFile = samplesFixture.getEicarSample(); - - var mockHttpClient = mock(HttpClient.class); - var mockPostResponse = mock(HttpResponse.class); - var mockGetResponse = mock(HttpResponse.class); - when(mockGetResponse.statusCode()).thenReturn(200); - when(mockGetResponse.body()).thenReturn( - new Gson().toJson(new FileReport( - "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f", - Verdict.UNKNOWN, null, null, null))); - when(mockPostResponse.statusCode()).thenReturn(500); - when(mockPostResponse.body()).thenReturn(new Gson() - .toJson(new ProblemDetails("VaasServerException", "Server-side error occurred"))); - - when(mockHttpClient.sendAsync( - argThat(getRequest -> getRequest != null - && getRequest.method().equals("GET")), - any(HttpResponse.BodyHandler.class))) - .thenAnswer(invocation -> CompletableFuture.completedFuture(mockGetResponse)); - - when(mockHttpClient.sendAsync( - argThat(postRequest -> postRequest != null - && postRequest.method().equals("POST")), - any(HttpResponse.BodyHandler.class))) - .thenAnswer(invocation -> CompletableFuture.completedFuture(mockPostResponse)); - - var vaas = getVaasWithMockedClient(mockHttpClient); - - var exception = assertThrows(CompletionException.class, () -> vaas.forFileAsync(tmpFile).join()); - assertInstanceOf(VaasServerException.class, exception.getCause()); - } - - @SuppressWarnings("unchecked") - @Test - @Tag("Mock") - public void forFile_IfUnauthorized_ThrowsVaasAuthenticationException() - throws Exception { - var tmpFile = samplesFixture.getEicarSample(); - - var mockHttpClient = mock(HttpClient.class); - var mockPostResponse = mock(HttpResponse.class); - var mockGetResponse = mock(HttpResponse.class); - when(mockGetResponse.statusCode()).thenReturn(200); - when(mockGetResponse.body()).thenReturn( - new Gson().toJson(new FileReport( - "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f", - Verdict.UNKNOWN, null, null, null))); - when(mockPostResponse.statusCode()).thenReturn(401); - when(mockPostResponse.body()).thenReturn(new Gson() - .toJson(new ProblemDetails("VaasAuthenticationException", "Authentication failed."))); - - when(mockHttpClient.sendAsync( - argThat(getRequest -> getRequest != null - && getRequest.method().equals("GET")), - any(HttpResponse.BodyHandler.class))) - .thenAnswer(invocation -> CompletableFuture.completedFuture(mockGetResponse)); - - when(mockHttpClient.sendAsync( - argThat(postRequest -> postRequest != null - && postRequest.method().equals("POST")), - any(HttpResponse.BodyHandler.class))) - .thenAnswer(invocation -> CompletableFuture.completedFuture(mockPostResponse)); - - var vaas = getVaasWithMockedClient(mockHttpClient); - - var exception = assertThrows(CompletionException.class, () -> vaas.forFileAsync(tmpFile).join()); - assertInstanceOf(VaasAuthenticationException.class, exception.getCause()); - } - - @SuppressWarnings("unchecked") - @Test - @Tag("Mock") - public void forFile_IfAuthenticatorFailed_ThrowsVaasAuthenticationException() throws Exception { - var tmpFile = samplesFixture.getEicarSample(); - - var mockHttpClient = mock(HttpClient.class); - var mockResponse = mock(HttpResponse.class); - when(mockResponse.statusCode()).thenReturn(401); - when(mockHttpClient.send(any(HttpRequest.class), - any(HttpResponse.BodyHandler.class))) - .thenAnswer(invocation -> mockResponse); - - var authenticator = getAuthenticator(mockHttpClient); - var vaas = getVaasWithAuthenticator(authenticator); - - assertThrows(VaasAuthenticationException.class, () -> vaas.forFile(tmpFile)); - } - - @Test - public void forFile_IfCancellationIsRequested_ThrowsCancellationException() - throws Exception { - var tmpFile = samplesFixture.getEicarSample(); - - var future = vaasWithDefaultConfig.forFileAsync(tmpFile); - - var result = future.cancel(true); - assertTrue(result); - assertThrows(CancellationException.class, future::get); - } - - @Test - @Disabled("This test is disabled because it takes too long to run.") - public void forFile_BigFileWithSmallTimeout_ThrowsTimeoutException() - throws Exception { - var tmpFile = Path.of(System.getProperty("java.io.tmpdir"), "file.txt"); - var url = URI.create("https://ash-speed.hetzner.com/1GB.bin").toURL(); - var conn = url.openConnection(); - var inputStream = conn.getInputStream(); - Files.copy(inputStream, tmpFile, StandardCopyOption.REPLACE_EXISTING); - - var vaasUrl = getEnvironmentKey("VAAS_URL"); - var config = new VaasConfig(1000, URI.create(vaasUrl)); - var vaas = new Vaas(config, authenticatorFixture); - var forFileOptions = new ForFileOptions(false, false, null); - - var exception = assertThrows(ExecutionException.class, () -> vaas.forFileAsync(tmpFile, forFileOptions).get()); - assertInstanceOf(TimeoutException.class, exception.getCause()); - } - - @Test - public void forFile_EmptyFile_ReturnsVerdict() throws Exception { - var file = new File(System.getProperty("java.io.tmpdir"), "empty.txt"); - file.createNewFile(); - - var vaasVerdict = vaasWithDefaultConfig.forFileAsync(file.toPath()).join(); - - assertEquals("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", vaasVerdict.getSha256()); - assertEquals(Verdict.CLEAN, vaasVerdict.getVerdict()); - } - - @Test - public void forStream_ReturnsVerdict() throws Exception { - var url = URI.create(EICAR_URL).toURL(); - var conn = url.openConnection(); - var inputStream = conn.getInputStream(); - var contentLength = conn.getContentLength(); - var forStreamOptions = new ForStreamOptions(true, "foobar"); - - var verdict = vaasWithDefaultConfig.forStreamAsync(inputStream, contentLength, forStreamOptions).join(); - - assertEquals(Verdict.MALICIOUS, verdict.getVerdict()); - assertTrue("275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f" - .equalsIgnoreCase(verdict.getSha256())); - } - - @SuppressWarnings("unchecked") - @ParameterizedTest - @CsvSource({ - "false", - "true", - }) - @Tag("Mock") - public void forStream_SendOptions(boolean useHashLookup) throws Exception { - var url = URI.create(EICAR_URL).toURL(); - var conn = url.openConnection(); - var inputStream = conn.getInputStream(); - var contentLength = conn.getContentLength(); - - var requestCaptor = ArgumentCaptor.forClass(HttpRequest.class); - var mockHttpClient = mock(HttpClient.class); - var mockPostResponse = mock(HttpResponse.class); - var mockGetResponse = mock(HttpResponse.class); - var forStreamOptions = new ForStreamOptions(useHashLookup, "foobar"); - - when(mockGetResponse.statusCode()).thenReturn(200); - when(mockGetResponse.body()).thenReturn( - new Gson().toJson(new FileReport( - "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f", - Verdict.UNKNOWN, null, null, null))); - when(mockPostResponse.statusCode()).thenReturn(200); - when(mockPostResponse.body()).thenReturn(new Gson().toJson(new FileAnalysisStarted( - "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f"))); - - when(mockHttpClient.sendAsync( - argThat(getRequest -> getRequest != null - && getRequest.method().equals("GET") - && getRequest.uri().toString().contains( - "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f") - && getRequest.uri().toString() - .contains("useCache=" + true) - && getRequest.uri().toString() - .contains("useHashLookup=" + useHashLookup)), - any(HttpResponse.BodyHandler.class))) - .thenAnswer(invocation -> CompletableFuture.completedFuture(mockGetResponse)); - - when(mockHttpClient.sendAsync( - argThat(postRequest -> postRequest != null - && postRequest.method().equals("POST") - && postRequest.uri().toString().contains("files") - && postRequest.uri().toString().contains( - "useHashLookup=" + useHashLookup)), - any(HttpResponse.BodyHandler.class))) - .thenAnswer(invocation -> CompletableFuture.completedFuture(mockPostResponse)); - - var vaas = getVaasWithMockedClient(mockHttpClient); - var vaasVerdict = vaas.forStreamAsync(inputStream, contentLength, forStreamOptions).join(); - verify(mockHttpClient, times(2)).sendAsync(requestCaptor.capture(), - any(HttpResponse.BodyHandler.class)); - var firstRequestUri = requestCaptor.getAllValues().get(0).uri(); - var secondRequestUri = requestCaptor.getAllValues().get(1).uri(); - - assertTrue(firstRequestUri.toString() - .contains(String.format("useHashLookup=%s", useHashLookup))); - assertTrue(secondRequestUri.toString() - .contains(String.format("useHashLookup=%s", useHashLookup))); - assertEquals(Verdict.UNKNOWN, vaasVerdict.getVerdict()); - assertTrue("275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f" - .equalsIgnoreCase(vaasVerdict.getSha256())); - } - - @SuppressWarnings("unchecked") - @Test - @Tag("Mock") - public void forStream_SendUserAgent() throws Exception { - var url = URI.create(EICAR_URL).toURL(); - var conn = url.openConnection(); - var inputStream = conn.getInputStream(); - var contentLength = conn.getContentLength(); - - var requestCaptor = ArgumentCaptor.forClass(HttpRequest.class); - var mockHttpClient = mock(HttpClient.class); - var mockPostResponse = mock(HttpResponse.class); - var mockGetResponse = mock(HttpResponse.class); - when(mockGetResponse.statusCode()).thenReturn(200); - when(mockGetResponse.body()).thenReturn( - new Gson().toJson(new FileReport( - "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f", - Verdict.UNKNOWN, null, null, null))); - when(mockPostResponse.statusCode()).thenReturn(200); - when(mockPostResponse.body()).thenReturn(new Gson().toJson(new FileAnalysisStarted( - "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f"))); - - when(mockHttpClient.sendAsync( - argThat(getRequest -> getRequest != null - && getRequest.method().equals("GET") - && getRequest.uri().toString().contains( - "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f") - && getRequest.uri().toString() - .contains("useCache=" + true) - && getRequest.uri().toString() - .contains("useHashLookup=" + true) - && getRequest.headers().firstValue("User-Agent").toString() - .contains("Java")), - any(HttpResponse.BodyHandler.class))) - .thenAnswer(invocation -> CompletableFuture.completedFuture(mockGetResponse)); - - when(mockHttpClient.sendAsync( - argThat(postRequest -> postRequest != null - && postRequest.method().equals("POST") - && postRequest.uri().toString().contains("files") - && postRequest.uri().toString() - .contains("useHashLookup=" + true) - && postRequest.headers().firstValue("User-Agent").toString() - .contains("Java")), - any(HttpResponse.BodyHandler.class))) - .thenAnswer(invocation -> CompletableFuture.completedFuture(mockPostResponse)); - - var vaas = getVaasWithMockedClient(mockHttpClient); - var vaasVerdict = vaas.forStreamAsync(inputStream, contentLength).join(); - verify(mockHttpClient, times(2)).sendAsync(requestCaptor.capture(), - any(HttpResponse.BodyHandler.class)); - var firstRequestHeaders = requestCaptor.getAllValues().get(0).headers().firstValue("User-Agent"); - var secondRequestHeaders = requestCaptor.getAllValues().get(1).headers().firstValue("User-Agent"); - - assertTrue(firstRequestHeaders.toString().contains("Java")); - assertTrue(secondRequestHeaders.toString().contains("Java")); - assertEquals(Verdict.UNKNOWN, vaasVerdict.getVerdict()); - assertTrue("275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f" - .equalsIgnoreCase(vaasVerdict.getSha256())); - } - - @SuppressWarnings("unchecked") - @Test - @Tag("Mock") - public void forStream_IfVaasRequestIdIsSet_SendTraceState() throws Exception { - var url = URI.create(EICAR_URL).toURL(); - var conn = url.openConnection(); - var inputStream = conn.getInputStream(); - var contentLength = conn.getContentLength(); - - var requestCaptor = ArgumentCaptor.forClass(HttpRequest.class); - var mockHttpClient = mock(HttpClient.class); - var mockPostResponse = mock(HttpResponse.class); - var mockGetResponse = mock(HttpResponse.class); - when(mockGetResponse.statusCode()).thenReturn(200); - when(mockGetResponse.body()).thenReturn( - new Gson().toJson(new FileReport( - "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f", - Verdict.UNKNOWN, null, null, null))); - when(mockPostResponse.statusCode()).thenReturn(200); - when(mockPostResponse.body()).thenReturn(new Gson().toJson(new FileAnalysisStarted( - "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f"))); - - when(mockHttpClient.sendAsync( - argThat(getRequest -> getRequest != null - && getRequest.method().equals("GET") - && getRequest.uri().toString().contains( - "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f") - && getRequest.uri().toString() - .contains("useCache=" + true) - && getRequest.uri().toString() - .contains("useHashLookup=" + true) - && getRequest.headers().firstValue("tracestate").toString() - .contains("foobar")), - any(HttpResponse.BodyHandler.class))) - .thenAnswer(invocation -> CompletableFuture.completedFuture(mockGetResponse)); - - when(mockHttpClient.sendAsync( - argThat(postRequest -> postRequest != null - && postRequest.method().equals("POST") - && postRequest.uri().toString().contains("files") - && postRequest.uri().toString() - .contains("useHashLookup=" + true) - && postRequest.headers().firstValue("tracestate").toString() - .contains("foobar")), - any(HttpResponse.BodyHandler.class))) - .thenAnswer(invocation -> CompletableFuture.completedFuture(mockPostResponse)); - - var vaas = getVaasWithMockedClient(mockHttpClient); - var forFileOptions = new ForStreamOptions(true, "foobar"); - var vaasVerdict = vaas.forStreamAsync(inputStream, contentLength, forFileOptions).join(); - verify(mockHttpClient, times(2)).sendAsync(requestCaptor.capture(), - any(HttpResponse.BodyHandler.class)); - var firstRequestHeaders = requestCaptor.getAllValues().get(0).headers().firstValue("tracestate"); - var secondRequestHeaders = requestCaptor.getAllValues().get(1).headers().firstValue("tracestate"); - - assertTrue(firstRequestHeaders.toString().contains("foobar")); - assertTrue(secondRequestHeaders.toString().contains("foobar")); - assertEquals(Verdict.UNKNOWN, vaasVerdict.getVerdict()); - assertTrue("275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f" - .equalsIgnoreCase(vaasVerdict.getSha256())); - } - - @SuppressWarnings("unchecked") - @Test - @Tag("Mock") - public void forStream_IfBadRequest_ThrowsVaasClientException() throws Exception { - var url = URI.create(EICAR_URL).toURL(); - var conn = url.openConnection(); - var inputStream = conn.getInputStream(); - var contentLength = conn.getContentLength(); - - var mockHttpClient = mock(HttpClient.class); - var mockPostResponse = mock(HttpResponse.class); - var mockGetResponse = mock(HttpResponse.class); - when(mockGetResponse.statusCode()).thenReturn(200); - when(mockGetResponse.body()).thenReturn( - new Gson().toJson(new FileReport( - "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f", - Verdict.UNKNOWN, null, null, null))); - when(mockPostResponse.statusCode()).thenReturn(400); - when(mockPostResponse.body()).thenReturn(new Gson() - .toJson(new ProblemDetails("VaasClientException", "Client-side error occurred"))); - - when(mockHttpClient.sendAsync( - argThat(getRequest -> getRequest != null - && getRequest.method().equals("GET")), - any(HttpResponse.BodyHandler.class))) - .thenAnswer(invocation -> CompletableFuture.completedFuture(mockGetResponse)); - - when(mockHttpClient.sendAsync( - argThat(postRequest -> postRequest != null - && postRequest.method().equals("POST")), - any(HttpResponse.BodyHandler.class))) - .thenAnswer(invocation -> CompletableFuture.completedFuture(mockPostResponse)); - - var vaas = getVaasWithMockedClient(mockHttpClient); - - var exception = assertThrows(CompletionException.class, () -> vaas.forStreamAsync(inputStream, contentLength).join()); - assertInstanceOf(VaasClientException.class, exception.getCause()); - } - - @SuppressWarnings("unchecked") - @Test - @Tag("Mock") - public void forStream_IfInternalServerError_ThrowsVaasServerException() - throws Exception { - var url = URI.create(EICAR_URL).toURL(); - var conn = url.openConnection(); - var inputStream = conn.getInputStream(); - var contentLength = conn.getContentLength(); - - var mockHttpClient = mock(HttpClient.class); - var mockPostResponse = mock(HttpResponse.class); - var mockGetResponse = mock(HttpResponse.class); - when(mockGetResponse.statusCode()).thenReturn(200); - when(mockGetResponse.body()).thenReturn( - new Gson().toJson(new FileReport( - "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f", - Verdict.UNKNOWN, null, null, null))); - when(mockPostResponse.statusCode()).thenReturn(500); - when(mockPostResponse.body()).thenReturn(new Gson() - .toJson(new ProblemDetails("VaasServerException", "Server-side error occurred"))); - - when(mockHttpClient.sendAsync( - argThat(getRequest -> getRequest != null - && getRequest.method().equals("GET")), - any(HttpResponse.BodyHandler.class))) - .thenAnswer(invocation -> CompletableFuture.completedFuture(mockGetResponse)); - - when(mockHttpClient.sendAsync( - argThat(postRequest -> postRequest != null - && postRequest.method().equals("POST")), - any(HttpResponse.BodyHandler.class))) - .thenAnswer(invocation -> CompletableFuture.completedFuture(mockPostResponse)); - - var vaas = getVaasWithMockedClient(mockHttpClient); - - var exception = assertThrows(CompletionException.class, () -> vaas.forStreamAsync(inputStream, contentLength).join()); - assertInstanceOf(VaasServerException.class, exception.getCause()); - } - - @SuppressWarnings("unchecked") - @Test - @Tag("Mock") - public void forStream_IfUnauthorized_ThrowsVaasAuthenticationException() - throws Exception { - var url = URI.create(EICAR_URL).toURL(); - var conn = url.openConnection(); - var inputStream = conn.getInputStream(); - var contentLength = conn.getContentLength(); - - var mockHttpClient = mock(HttpClient.class); - var mockPostResponse = mock(HttpResponse.class); - var mockGetResponse = mock(HttpResponse.class); - when(mockGetResponse.statusCode()).thenReturn(200); - when(mockGetResponse.body()).thenReturn( - new Gson().toJson(new FileReport( - "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f", - Verdict.UNKNOWN, null, null, null))); - when(mockPostResponse.statusCode()).thenReturn(401); - when(mockPostResponse.body()).thenReturn(new Gson() - .toJson(new ProblemDetails("VaasAuthenticationException", "Authentication failed."))); - - when(mockHttpClient.sendAsync( - argThat(getRequest -> getRequest != null - && getRequest.method().equals("GET")), - any(HttpResponse.BodyHandler.class))) - .thenAnswer(invocation -> CompletableFuture.completedFuture(mockGetResponse)); - - when(mockHttpClient.sendAsync( - argThat(postRequest -> postRequest != null - && postRequest.method().equals("POST")), - any(HttpResponse.BodyHandler.class))) - .thenAnswer(invocation -> CompletableFuture.completedFuture(mockPostResponse)); - - var vaas = getVaasWithMockedClient(mockHttpClient); - - var exception = assertThrows(CompletionException.class, () -> vaas.forStreamAsync(inputStream, contentLength).join()); - assertInstanceOf(VaasAuthenticationException.class, exception.getCause()); - } - - @SuppressWarnings("unchecked") - @Test - @Tag("Mock") - public void forStream_IfAuthenticatorFailed_ThrowsVaasAuthenticationException() throws Exception { - var url = URI.create(EICAR_URL).toURL(); - var conn = url.openConnection(); - var inputStream = conn.getInputStream(); - var contentLength = conn.getContentLength(); - - var mockHttpClient = mock(HttpClient.class); - var mockResponse = mock(HttpResponse.class); - when(mockResponse.statusCode()).thenReturn(401); - when(mockHttpClient.send(any(HttpRequest.class), - any(HttpResponse.BodyHandler.class))) - .thenAnswer(invocation -> mockResponse); - - var authenticator = getAuthenticator(mockHttpClient); - var vaas = getVaasWithAuthenticator(authenticator); - - assertThrows(VaasAuthenticationException.class, () -> vaas.forStream(inputStream, contentLength)); - } - - @Test - public void forStream_IfCancellationIsRequested_ThrowsCancellationException() - throws Exception { - var url = URI.create(EICAR_URL).toURL(); - var conn = url.openConnection(); - var inputStream = conn.getInputStream(); - var contentLength = conn.getContentLength(); - - var future = vaasWithDefaultConfig.forStreamAsync(inputStream, contentLength); - - var result = future.cancel(true); - assertTrue(result); - assertThrows(CancellationException.class, future::get); - } - - @Test - @Disabled("Fails for unknown reason in some environments. TODO: Rewrite using a stream, that does not provide data.") - public void forStream_BigFileWithSmallTimeout_ThrowsTimeoutException() - throws Exception { - var url = URI.create("https://ash-speed.hetzner.com/1GB.bin").toURL(); - var conn = url.openConnection(); - var inputStream = conn.getInputStream(); - var contentLength = conn.getContentLength(); - - var vaasUrl = getEnvironmentKey("VAAS_URL"); - var config = new VaasConfig(1000, URI.create(vaasUrl)); - var vaas = new Vaas(config, authenticatorFixture); - var forStreamOptions = new ForStreamOptions(false, null); - - var exception = assertThrows(ExecutionException.class, () -> vaas.forStreamAsync(inputStream, contentLength, forStreamOptions).get()); - assertInstanceOf(TimeoutException.class, exception.getCause()); - } - - @Test - public void forStream_EmptyFile_ReturnsVerdict() throws Exception { - var stream = new ByteArrayInputStream("".getBytes(StandardCharsets.UTF_8)); - - var vaasVerdict = vaasWithDefaultConfig.forStreamAsync(stream, 0).join(); - - assertEquals("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", vaasVerdict.getSha256()); - assertEquals(Verdict.CLEAN, vaasVerdict.getVerdict()); - } - - @Test - public void forUrl_ReturnsVerdict() throws Exception { - var url = URI.create(EICAR_URL).toURL(); - - var verdict = vaasWithDefaultConfig.forUrlAsync(url).join(); - - assertEquals(Verdict.MALICIOUS, verdict.getVerdict()); - assertTrue("275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f" - .equalsIgnoreCase(verdict.getSha256())); - } - - @SuppressWarnings("unchecked") - @ParameterizedTest - @CsvSource({ - "false", - "true", - }) - @Tag("Mock") - public void forUrl_SendOptions(boolean useHashLookup) throws Exception { - var url = URI.create(EICAR_URL).toURL(); - - var requestCaptor = ArgumentCaptor.forClass(HttpRequest.class); - var mockHttpClient = mock(HttpClient.class); - var mockPostResponse = mock(HttpResponse.class); - var mockGetResponse = mock(HttpResponse.class); - var forUrlOptions = new ForUrlOptions(useHashLookup, "foobar"); - - when(mockGetResponse.statusCode()).thenReturn(200); - when(mockGetResponse.body()).thenReturn( - new Gson().toJson(new UrlReport( - "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f", - Verdict.UNKNOWN, url.toString(), null, null, null))); - when(mockPostResponse.statusCode()).thenReturn(200); - when(mockPostResponse.body()).thenReturn(new Gson().toJson(new UrlAnalysisStarted("id"))); - - when(mockHttpClient.sendAsync( - argThat(getRequest -> getRequest != null - && getRequest.method().equals("GET") - && getRequest.uri().toString().contains("id")), - any(HttpResponse.BodyHandler.class))) - .thenAnswer(invocation -> CompletableFuture.completedFuture(mockGetResponse)); - - when(mockHttpClient.sendAsync( - argThat(postRequest -> postRequest != null - && postRequest.method().equals("POST") - && postRequest.uri().toString().contains("urls")), - any(HttpResponse.BodyHandler.class))) - .thenAnswer(invocation -> CompletableFuture.completedFuture(mockPostResponse)); - - var vaas = getVaasWithMockedClient(mockHttpClient); - var vaasVerdict = vaas.forUrlAsync(url, forUrlOptions).join(); - verify(mockHttpClient, times(2)).sendAsync(requestCaptor.capture(), - any(HttpResponse.BodyHandler.class)); - var bodyRequest = new String(readContent(Objects.requireNonNull(requestCaptor.getAllValues().get(0).bodyPublisher().orElse(null)))); - var urlAnalysisRequest = new Gson().fromJson(bodyRequest, UrlAnalysisRequest.class); - var reportUri = requestCaptor.getAllValues().get(1).uri(); - - assertEquals(useHashLookup, urlAnalysisRequest.isUseHashLookup()); - assertEquals(EICAR_URL, urlAnalysisRequest.getUrl()); - assertTrue(reportUri.toString() - .contains(String.format("useHashLookup=%s", useHashLookup))); - assertEquals(Verdict.UNKNOWN, vaasVerdict.getVerdict()); - assertTrue("275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f" - .equalsIgnoreCase(vaasVerdict.getSha256())); - } - - @SuppressWarnings("unchecked") - @Test - @Tag("Mock") - public void forUrl_SendUserAgent() throws Exception { - var url = URI.create(EICAR_URL).toURL(); - - var requestCaptor = ArgumentCaptor.forClass(HttpRequest.class); - var mockHttpClient = mock(HttpClient.class); - var mockPostResponse = mock(HttpResponse.class); - var mockGetResponse = mock(HttpResponse.class); - - when(mockGetResponse.statusCode()).thenReturn(200); - when(mockGetResponse.body()).thenReturn( - new Gson().toJson(new UrlReport( - "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f", - Verdict.UNKNOWN, url.toString(), null, null, null))); - when(mockPostResponse.statusCode()).thenReturn(200); - when(mockPostResponse.body()).thenReturn(new Gson().toJson(new UrlAnalysisStarted("id"))); - - when(mockHttpClient.sendAsync( - argThat(getRequest -> getRequest != null - && getRequest.method().equals("GET") - && getRequest.uri().toString().contains("id") - && getRequest.headers().firstValue("User-Agent").toString() - .contains("Java")), - any(HttpResponse.BodyHandler.class))) - .thenAnswer(invocation -> CompletableFuture.completedFuture(mockGetResponse)); - - when(mockHttpClient.sendAsync( - argThat(postRequest -> postRequest != null - && postRequest.method().equals("POST") - && postRequest.uri().toString().contains("urls") - && postRequest.headers().firstValue("User-Agent").toString() - .contains("Java")), - any(HttpResponse.BodyHandler.class))) - .thenAnswer(invocation -> CompletableFuture.completedFuture(mockPostResponse)); - - var vaas = getVaasWithMockedClient(mockHttpClient); - var vaasVerdict = vaas.forUrlAsync(url).join(); - verify(mockHttpClient, times(2)).sendAsync(requestCaptor.capture(), - any(HttpResponse.BodyHandler.class)); - var firstRequestHeaders = requestCaptor.getAllValues().get(0).headers().firstValue("User-Agent"); - var secondRequestHeaders = requestCaptor.getAllValues().get(1).headers().firstValue("User-Agent"); - - assertTrue(firstRequestHeaders.toString().contains("Java")); - assertTrue(secondRequestHeaders.toString().contains("Java")); - assertEquals(Verdict.UNKNOWN, vaasVerdict.getVerdict()); - assertTrue("275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f" - .equalsIgnoreCase(vaasVerdict.getSha256())); - } - - @SuppressWarnings("unchecked") - @Test - @Tag("Mock") - public void forUrl_IfVaasRequestIdIsSet_SendTraceState() throws Exception { - var url = URI.create(EICAR_URL).toURL(); - - var requestCaptor = ArgumentCaptor.forClass(HttpRequest.class); - var mockHttpClient = mock(HttpClient.class); - var mockPostResponse = mock(HttpResponse.class); - var mockGetResponse = mock(HttpResponse.class); - - when(mockGetResponse.statusCode()).thenReturn(200); - when(mockGetResponse.body()).thenReturn( - new Gson().toJson(new UrlReport( - "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f", - Verdict.UNKNOWN, url.toString(), null, null, null))); - when(mockPostResponse.statusCode()).thenReturn(200); - when(mockPostResponse.body()).thenReturn(new Gson().toJson(new UrlAnalysisStarted("id"))); - - when(mockHttpClient.sendAsync( - argThat(getRequest -> getRequest != null - && getRequest.method().equals("GET") - && getRequest.uri().toString().contains("id") - && getRequest.headers().firstValue("tracestate").toString() - .contains("foobar")), - any(HttpResponse.BodyHandler.class))) - .thenAnswer(invocation -> CompletableFuture.completedFuture(mockGetResponse)); - - when(mockHttpClient.sendAsync( - argThat(postRequest -> postRequest != null - && postRequest.method().equals("POST") - && postRequest.uri().toString().contains("urls") - && postRequest.headers().firstValue("tracestate").toString() - .contains("foobar")), - any(HttpResponse.BodyHandler.class))) - .thenAnswer(invocation -> CompletableFuture.completedFuture(mockPostResponse)); - - var vaas = getVaasWithMockedClient(mockHttpClient); - var forUrlOptions = new ForUrlOptions(true, "foobar"); - var vaasVerdict = vaas.forUrlAsync(url, forUrlOptions).join(); - verify(mockHttpClient, times(2)).sendAsync(requestCaptor.capture(), - any(HttpResponse.BodyHandler.class)); - var firstRequestHeaders = requestCaptor.getAllValues().get(0).headers().firstValue("tracestate"); - var secondRequestHeaders = requestCaptor.getAllValues().get(1).headers().firstValue("tracestate"); - - assertTrue(firstRequestHeaders.toString().contains("foobar")); - assertTrue(secondRequestHeaders.toString().contains("foobar")); - assertEquals(Verdict.UNKNOWN, vaasVerdict.getVerdict()); - assertTrue("275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f" - .equalsIgnoreCase(vaasVerdict.getSha256())); - } - - @SuppressWarnings("unchecked") - @Test - @Tag("Mock") - public void forUrl_IfBadRequest_ThrowsVaasClientException() throws Exception { - var url = URI.create(EICAR_URL).toURL(); - - var mockHttpClient = mock(HttpClient.class); - var mockPostResponse = mock(HttpResponse.class); - when(mockPostResponse.statusCode()).thenReturn(400); - when(mockPostResponse.body()).thenReturn(new Gson() - .toJson(new ProblemDetails("VaasClientException", "Client-side error occurred"))); - - when(mockHttpClient.sendAsync( - argThat(postRequest -> postRequest != null - && postRequest.method().equals("POST")), - any(HttpResponse.BodyHandler.class))) - .thenAnswer(invocation -> CompletableFuture.completedFuture(mockPostResponse)); - - var vaas = getVaasWithMockedClient(mockHttpClient); - - var exception = assertThrows(CompletionException.class, () -> vaas.forUrlAsync(url).join()); - assertInstanceOf(VaasClientException.class, exception.getCause()); - } - - @SuppressWarnings("unchecked") - @Test - @Tag("Mock") - public void forUrl_IfInternalServerError_ThrowsVaasServerException() - throws Exception { - var url = URI.create(EICAR_URL).toURL(); - - var mockHttpClient = mock(HttpClient.class); - var mockPostResponse = mock(HttpResponse.class); - when(mockPostResponse.statusCode()).thenReturn(500); - when(mockPostResponse.body()).thenReturn(new Gson() - .toJson(new ProblemDetails("VaasServerException", "Server-side error occurred"))); - - when(mockHttpClient.sendAsync( - argThat(postRequest -> postRequest != null - && postRequest.method().equals("POST")), - any(HttpResponse.BodyHandler.class))) - .thenAnswer(invocation -> CompletableFuture.completedFuture(mockPostResponse)); - - var vaas = getVaasWithMockedClient(mockHttpClient); - - var exception = assertThrows(CompletionException.class, () -> vaas.forUrlAsync(url).join()); - assertInstanceOf(VaasServerException.class, exception.getCause()); - } - - @SuppressWarnings("unchecked") - @Test - @Tag("Mock") - public void forUrl_IfUnauthorized_ThrowsVaasAuthenticationException() - throws Exception { - var url = URI.create(EICAR_URL).toURL(); - - var mockHttpClient = mock(HttpClient.class); - var mockPostResponse = mock(HttpResponse.class); - when(mockPostResponse.statusCode()).thenReturn(401); - when(mockPostResponse.body()).thenReturn(new Gson() - .toJson(new ProblemDetails("VaasAuthenticationException", "Authentication failed."))); - - when(mockHttpClient.sendAsync( - argThat(postRequest -> postRequest != null - && postRequest.method().equals("POST")), - any(HttpResponse.BodyHandler.class))) - .thenAnswer(invocation -> CompletableFuture.completedFuture(mockPostResponse)); - - var vaas = getVaasWithMockedClient(mockHttpClient); - - var exception = assertThrows(CompletionException.class, () -> vaas.forUrlAsync(url).join()); - assertInstanceOf(VaasAuthenticationException.class, exception.getCause()); - } - - @SuppressWarnings("unchecked") - @Test - @Tag("Mock") - public void forUrl_IfAuthenticatorFailed_ThrowsVaasAuthenticationException() throws Exception { - var url = URI.create(EICAR_URL).toURL(); - - var mockHttpClient = mock(HttpClient.class); - var mockResponse = mock(HttpResponse.class); - when(mockResponse.statusCode()).thenReturn(401); - when(mockHttpClient.send(any(HttpRequest.class), - any(HttpResponse.BodyHandler.class))) - .thenAnswer(invocation -> mockResponse); - - var authenticator = getAuthenticator(mockHttpClient); - var vaas = getVaasWithAuthenticator(authenticator); - - assertThrows(VaasAuthenticationException.class, () -> vaas.forUrl(url)); - } - - @Test - public void forUrl_IfCancellationIsRequested_ThrowsCancellationException() - throws Exception { - var url = URI.create(EICAR_URL).toURL(); - - var future = vaasWithDefaultConfig.forUrlAsync(url); - - var result = future.cancel(true); - assertTrue(result); - assertThrows(CancellationException.class, future::get); - } - - @Test - @Disabled("Fails for unknown reason in some environments. TODO: Use samples server.") - public void forUrl_BigFileWithSmallTimeout_ThrowsTimeoutException() - throws Exception { - var url = URI.create("https://ash-speed.hetzner.com/1GB.bin").toURL(); - - var vaasUrl = getEnvironmentKey("VAAS_URL"); - var config = new VaasConfig(1000, URI.create(vaasUrl)); - var vaas = new Vaas(config, authenticatorFixture); - - var exception = assertThrows(ExecutionException.class, () -> vaas.forUrlAsync(url).get()); - assertInstanceOf(TimeoutException.class, exception.getCause()); - } - - @Test - @Disabled() - public void forFileAsync_WithSmallTimeout_DoesNotShowNegativeResources() { - var config = new VaasConfig(45, false, false, URI.create(getEnvironmentKey("VAAS_URL"))); - var vaas = new Vaas(config, authenticatorFixture); - var file = Path.of(System.getProperty("java.io.tmpdir"), "file.txt"); - try (var writer = Files.newBufferedWriter(file)) { - writer.write(UUID.randomUUID().toString()); - } catch (IOException e) { - e.printStackTrace(); + @SuppressWarnings("unchecked") + @Test + @Tag("Mock") + public void forStream_IfUnauthorized_ThrowsVaasAuthenticationException() + throws Exception { + var url = URI.create(EICAR_URL).toURL(); + var conn = url.openConnection(); + var inputStream = conn.getInputStream(); + var contentLength = conn.getContentLength(); + + var mockHttpClient = mock(HttpClient.class); + var mockPostResponse = mock(HttpResponse.class); + var mockGetResponse = mock(HttpResponse.class); + when(mockGetResponse.statusCode()).thenReturn(200); + when(mockGetResponse.body()).thenReturn( + new Gson().toJson(new FileReport( + "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f", + Verdict.UNKNOWN, null, null, null, false))); + when(mockPostResponse.statusCode()).thenReturn(401); + when(mockPostResponse.body()).thenReturn(new Gson() + .toJson(new ProblemDetails("VaasAuthenticationException", "Authentication failed."))); + + when(mockHttpClient.sendAsync( + argThat(getRequest -> getRequest != null + && getRequest.method().equals("GET")), + any(HttpResponse.BodyHandler.class))) + .thenAnswer(invocation -> CompletableFuture.completedFuture(mockGetResponse)); + + when(mockHttpClient.sendAsync( + argThat(postRequest -> postRequest != null + && postRequest.method().equals("POST")), + any(HttpResponse.BodyHandler.class))) + .thenAnswer(invocation -> CompletableFuture.completedFuture(mockPostResponse)); + + var vaas = getVaasWithMockedClient(mockHttpClient); + + var exception = assertThrows(CompletionException.class, + () -> vaas.forStreamAsync(inputStream, contentLength).join()); + assertInstanceOf(VaasAuthenticationException.class, exception.getCause()); } - while (true) { - try { - vaas.forFile(file); - System.out.print("+"); - System.out.flush(); - } catch (Exception e) { - System.out.print("-"); - System.out.flush(); - } + @SuppressWarnings("unchecked") + @Test + @Tag("Mock") + public void forStream_IfAuthenticatorFailed_ThrowsVaasAuthenticationException() throws Exception { + var url = URI.create(EICAR_URL).toURL(); + var conn = url.openConnection(); + var inputStream = conn.getInputStream(); + var contentLength = conn.getContentLength(); + + var mockHttpClient = mock(HttpClient.class); + var mockResponse = mock(HttpResponse.class); + when(mockResponse.statusCode()).thenReturn(401); + when(mockHttpClient.send(any(HttpRequest.class), + any(HttpResponse.BodyHandler.class))) + .thenAnswer(invocation -> mockResponse); + + var authenticator = getAuthenticator(mockHttpClient); + var vaas = getVaasWithAuthenticator(authenticator); + + assertThrows(VaasAuthenticationException.class, () -> vaas.forStream(inputStream, contentLength)); } - } - - @Test - @Disabled() - public void forFileAsync_WithSmallTimeoutInParallel_DoesNotShowNegativeResources() { - var config = new VaasConfig(45, false, false, URI.create(getEnvironmentKey("VAAS_URL"))); - var vaas = new Vaas(config, authenticatorFixture); - var file1 = Path.of(System.getProperty("java.io.tmpdir"), "file.txt"); - var file2 = Path.of(System.getProperty("java.io.tmpdir"), "file2.txt"); - var file3 = Path.of(System.getProperty("java.io.tmpdir"), "file3.txt"); - var file4 = Path.of(System.getProperty("java.io.tmpdir"), "file4.txt"); - var fileList = List.of(file1, file2, file3, file4); - for (var file : fileList) { - try (var writer = Files.newBufferedWriter(file)) { - writer.write(UUID.randomUUID().toString()); - } catch (IOException e) { - e.printStackTrace(); - } + + @Test + public void forStream_IfCancellationIsRequested_ThrowsCancellationException() + throws Exception { + var url = URI.create(EICAR_URL).toURL(); + var conn = url.openConnection(); + var inputStream = conn.getInputStream(); + var contentLength = conn.getContentLength(); + + var future = vaasWithDefaultConfig.forStreamAsync(inputStream, contentLength); + + var result = future.cancel(true); + assertTrue(result); + assertThrows(CancellationException.class, future::get); } - while (true) { - fileList.parallelStream().forEach((key) -> { - try { - vaas.forFile(key); - System.out.print("+"); - System.out.flush(); - } catch (Exception e) { - System.out.print("-"); - System.out.flush(); + @Test + @Disabled("Fails for unknown reason in some environments. TODO: Rewrite using a stream, that does not provide data.") + public void forStream_BigFileWithSmallTimeout_ThrowsTimeoutException() + throws Exception { + var url = URI.create("https://ash-speed.hetzner.com/1GB.bin").toURL(); + var conn = url.openConnection(); + var inputStream = conn.getInputStream(); + var contentLength = conn.getContentLength(); + + var vaasUrl = getEnvironmentKey("VAAS_URL"); + var config = new VaasConfig(1000, URI.create(vaasUrl)); + var vaas = new Vaas(config, authenticatorFixture); + var forStreamOptions = new ForStreamOptions(false, null); + + var exception = assertThrows(ExecutionException.class, + () -> vaas.forStreamAsync(inputStream, contentLength, forStreamOptions).get()); + assertInstanceOf(TimeoutException.class, exception.getCause()); + } + + @Test + public void forStream_EmptyFile_ReturnsVerdict() throws Exception { + var stream = new ByteArrayInputStream("".getBytes(StandardCharsets.UTF_8)); + + var vaasVerdict = vaasWithDefaultConfig.forStreamAsync(stream, 0).join(); + + assertEquals("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + vaasVerdict.getSha256()); + assertEquals(Verdict.CLEAN, vaasVerdict.getVerdict()); + } + + @Test + public void forUrl_ReturnsVerdict() throws Exception { + var url = URI.create(EICAR_URL).toURL(); + + var verdict = vaasWithDefaultConfig.forUrlAsync(url).join(); + + assertEquals(Verdict.MALICIOUS, verdict.getVerdict()); + assertTrue("275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f" + .equalsIgnoreCase(verdict.getSha256())); + } + + @SuppressWarnings("unchecked") + @ParameterizedTest + @CsvSource({ + "false", + "true", + }) + @Tag("Mock") + public void forUrl_SendOptions(boolean useHashLookup) throws Exception { + var url = URI.create(EICAR_URL).toURL(); + + var requestCaptor = ArgumentCaptor.forClass(HttpRequest.class); + var mockHttpClient = mock(HttpClient.class); + var mockPostResponse = mock(HttpResponse.class); + var mockGetResponse = mock(HttpResponse.class); + var forUrlOptions = new ForUrlOptions(useHashLookup, "foobar"); + + when(mockGetResponse.statusCode()).thenReturn(200); + when(mockGetResponse.body()).thenReturn( + new Gson().toJson(new UrlReport( + "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f", + Verdict.UNKNOWN, url.toString(), null, null, null, false))); + when(mockPostResponse.statusCode()).thenReturn(200); + when(mockPostResponse.body()).thenReturn(new Gson().toJson(new UrlAnalysisStarted("id"))); + + when(mockHttpClient.sendAsync( + argThat(getRequest -> getRequest != null + && getRequest.method().equals("GET") + && getRequest.uri().toString().contains("id")), + any(HttpResponse.BodyHandler.class))) + .thenAnswer(invocation -> CompletableFuture.completedFuture(mockGetResponse)); + + when(mockHttpClient.sendAsync( + argThat(postRequest -> postRequest != null + && postRequest.method().equals("POST") + && postRequest.uri().toString().contains("urls")), + any(HttpResponse.BodyHandler.class))) + .thenAnswer(invocation -> CompletableFuture.completedFuture(mockPostResponse)); + + var vaas = getVaasWithMockedClient(mockHttpClient); + var vaasVerdict = vaas.forUrlAsync(url, forUrlOptions).join(); + verify(mockHttpClient, times(2)).sendAsync(requestCaptor.capture(), + any(HttpResponse.BodyHandler.class)); + var bodyRequest = new String(readContent(Objects + .requireNonNull(requestCaptor.getAllValues().get(0).bodyPublisher().orElse(null)))); + var urlAnalysisRequest = new Gson().fromJson(bodyRequest, UrlAnalysisRequest.class); + var reportUri = requestCaptor.getAllValues().get(1).uri(); + + assertEquals(useHashLookup, urlAnalysisRequest.isUseHashLookup()); + assertEquals(EICAR_URL, urlAnalysisRequest.getUrl()); + assertTrue(reportUri.toString() + .contains(String.format("useHashLookup=%s", useHashLookup))); + assertEquals(Verdict.UNKNOWN, vaasVerdict.getVerdict()); + assertTrue("275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f" + .equalsIgnoreCase(vaasVerdict.getSha256())); + } + + @SuppressWarnings("unchecked") + @Test + @Tag("Mock") + public void forUrl_SendUserAgent() throws Exception { + var url = URI.create(EICAR_URL).toURL(); + + var requestCaptor = ArgumentCaptor.forClass(HttpRequest.class); + var mockHttpClient = mock(HttpClient.class); + var mockPostResponse = mock(HttpResponse.class); + var mockGetResponse = mock(HttpResponse.class); + + when(mockGetResponse.statusCode()).thenReturn(200); + when(mockGetResponse.body()).thenReturn( + new Gson().toJson(new UrlReport( + "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f", + Verdict.UNKNOWN, url.toString(), null, null, null, false))); + when(mockPostResponse.statusCode()).thenReturn(200); + when(mockPostResponse.body()).thenReturn(new Gson().toJson(new UrlAnalysisStarted("id"))); + + when(mockHttpClient.sendAsync( + argThat(getRequest -> getRequest != null + && getRequest.method().equals("GET") + && getRequest.uri().toString().contains("id") + && getRequest.headers().firstValue("User-Agent").toString() + .contains("Java")), + any(HttpResponse.BodyHandler.class))) + .thenAnswer(invocation -> CompletableFuture.completedFuture(mockGetResponse)); + + when(mockHttpClient.sendAsync( + argThat(postRequest -> postRequest != null + && postRequest.method().equals("POST") + && postRequest.uri().toString().contains("urls") + && postRequest.headers().firstValue("User-Agent").toString() + .contains("Java")), + any(HttpResponse.BodyHandler.class))) + .thenAnswer(invocation -> CompletableFuture.completedFuture(mockPostResponse)); + + var vaas = getVaasWithMockedClient(mockHttpClient); + var vaasVerdict = vaas.forUrlAsync(url).join(); + verify(mockHttpClient, times(2)).sendAsync(requestCaptor.capture(), + any(HttpResponse.BodyHandler.class)); + var firstRequestHeaders = requestCaptor.getAllValues().get(0).headers().firstValue("User-Agent"); + var secondRequestHeaders = requestCaptor.getAllValues().get(1).headers().firstValue("User-Agent"); + + assertTrue(firstRequestHeaders.toString().contains("Java")); + assertTrue(secondRequestHeaders.toString().contains("Java")); + assertEquals(Verdict.UNKNOWN, vaasVerdict.getVerdict()); + assertTrue("275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f" + .equalsIgnoreCase(vaasVerdict.getSha256())); + } + + @SuppressWarnings("unchecked") + @Test + @Tag("Mock") + public void forUrl_IfVaasRequestIdIsSet_SendTraceState() throws Exception { + var url = URI.create(EICAR_URL).toURL(); + + var requestCaptor = ArgumentCaptor.forClass(HttpRequest.class); + var mockHttpClient = mock(HttpClient.class); + var mockPostResponse = mock(HttpResponse.class); + var mockGetResponse = mock(HttpResponse.class); + + when(mockGetResponse.statusCode()).thenReturn(200); + when(mockGetResponse.body()).thenReturn( + new Gson().toJson(new UrlReport( + "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f", + Verdict.UNKNOWN, url.toString(), null, null, null, false))); + when(mockPostResponse.statusCode()).thenReturn(200); + when(mockPostResponse.body()).thenReturn(new Gson().toJson(new UrlAnalysisStarted("id"))); + + when(mockHttpClient.sendAsync( + argThat(getRequest -> getRequest != null + && getRequest.method().equals("GET") + && getRequest.uri().toString().contains("id") + && getRequest.headers().firstValue("tracestate").toString() + .contains("foobar")), + any(HttpResponse.BodyHandler.class))) + .thenAnswer(invocation -> CompletableFuture.completedFuture(mockGetResponse)); + + when(mockHttpClient.sendAsync( + argThat(postRequest -> postRequest != null + && postRequest.method().equals("POST") + && postRequest.uri().toString().contains("urls") + && postRequest.headers().firstValue("tracestate").toString() + .contains("foobar")), + any(HttpResponse.BodyHandler.class))) + .thenAnswer(invocation -> CompletableFuture.completedFuture(mockPostResponse)); + + var vaas = getVaasWithMockedClient(mockHttpClient); + var forUrlOptions = new ForUrlOptions(true, "foobar"); + var vaasVerdict = vaas.forUrlAsync(url, forUrlOptions).join(); + verify(mockHttpClient, times(2)).sendAsync(requestCaptor.capture(), + any(HttpResponse.BodyHandler.class)); + var firstRequestHeaders = requestCaptor.getAllValues().get(0).headers().firstValue("tracestate"); + var secondRequestHeaders = requestCaptor.getAllValues().get(1).headers().firstValue("tracestate"); + + assertTrue(firstRequestHeaders.toString().contains("foobar")); + assertTrue(secondRequestHeaders.toString().contains("foobar")); + assertEquals(Verdict.UNKNOWN, vaasVerdict.getVerdict()); + assertTrue("275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f" + .equalsIgnoreCase(vaasVerdict.getSha256())); + } + + @SuppressWarnings("unchecked") + @Test + @Tag("Mock") + public void forUrl_IfBadRequest_ThrowsVaasClientException() throws Exception { + var url = URI.create(EICAR_URL).toURL(); + + var mockHttpClient = mock(HttpClient.class); + var mockPostResponse = mock(HttpResponse.class); + when(mockPostResponse.statusCode()).thenReturn(400); + when(mockPostResponse.body()).thenReturn(new Gson() + .toJson(new ProblemDetails("VaasClientException", "Client-side error occurred"))); + + when(mockHttpClient.sendAsync( + argThat(postRequest -> postRequest != null + && postRequest.method().equals("POST")), + any(HttpResponse.BodyHandler.class))) + .thenAnswer(invocation -> CompletableFuture.completedFuture(mockPostResponse)); + + var vaas = getVaasWithMockedClient(mockHttpClient); + + var exception = assertThrows(CompletionException.class, () -> vaas.forUrlAsync(url).join()); + assertInstanceOf(VaasClientException.class, exception.getCause()); + } + + @SuppressWarnings("unchecked") + @Test + @Tag("Mock") + public void forUrl_IfInternalServerError_ThrowsVaasServerException() + throws Exception { + var url = URI.create(EICAR_URL).toURL(); + + var mockHttpClient = mock(HttpClient.class); + var mockPostResponse = mock(HttpResponse.class); + when(mockPostResponse.statusCode()).thenReturn(500); + when(mockPostResponse.body()).thenReturn(new Gson() + .toJson(new ProblemDetails("VaasServerException", "Server-side error occurred"))); + + when(mockHttpClient.sendAsync( + argThat(postRequest -> postRequest != null + && postRequest.method().equals("POST")), + any(HttpResponse.BodyHandler.class))) + .thenAnswer(invocation -> CompletableFuture.completedFuture(mockPostResponse)); + + var vaas = getVaasWithMockedClient(mockHttpClient); + + var exception = assertThrows(CompletionException.class, () -> vaas.forUrlAsync(url).join()); + assertInstanceOf(VaasServerException.class, exception.getCause()); + } + + @SuppressWarnings("unchecked") + @Test + @Tag("Mock") + public void forUrl_IfUnauthorized_ThrowsVaasAuthenticationException() + throws Exception { + var url = URI.create(EICAR_URL).toURL(); + + var mockHttpClient = mock(HttpClient.class); + var mockPostResponse = mock(HttpResponse.class); + when(mockPostResponse.statusCode()).thenReturn(401); + when(mockPostResponse.body()).thenReturn(new Gson() + .toJson(new ProblemDetails("VaasAuthenticationException", "Authentication failed."))); + + when(mockHttpClient.sendAsync( + argThat(postRequest -> postRequest != null + && postRequest.method().equals("POST")), + any(HttpResponse.BodyHandler.class))) + .thenAnswer(invocation -> CompletableFuture.completedFuture(mockPostResponse)); + + var vaas = getVaasWithMockedClient(mockHttpClient); + + var exception = assertThrows(CompletionException.class, () -> vaas.forUrlAsync(url).join()); + assertInstanceOf(VaasAuthenticationException.class, exception.getCause()); + } + + @SuppressWarnings("unchecked") + @Test + @Tag("Mock") + public void forUrl_IfAuthenticatorFailed_ThrowsVaasAuthenticationException() throws Exception { + var url = URI.create(EICAR_URL).toURL(); + + var mockHttpClient = mock(HttpClient.class); + var mockResponse = mock(HttpResponse.class); + when(mockResponse.statusCode()).thenReturn(401); + when(mockHttpClient.send(any(HttpRequest.class), + any(HttpResponse.BodyHandler.class))) + .thenAnswer(invocation -> mockResponse); + + var authenticator = getAuthenticator(mockHttpClient); + var vaas = getVaasWithAuthenticator(authenticator); + + assertThrows(VaasAuthenticationException.class, () -> vaas.forUrl(url)); + } + + @Test + public void forUrl_IfCancellationIsRequested_ThrowsCancellationException() + throws Exception { + var url = URI.create(EICAR_URL).toURL(); + + var future = vaasWithDefaultConfig.forUrlAsync(url); + + var result = future.cancel(true); + assertTrue(result); + assertThrows(CancellationException.class, future::get); + } + + @Test + @Disabled("Fails for unknown reason in some environments. TODO: Use samples server.") + public void forUrl_BigFileWithSmallTimeout_ThrowsTimeoutException() + throws Exception { + var url = URI.create("https://ash-speed.hetzner.com/1GB.bin").toURL(); + + var vaasUrl = getEnvironmentKey("VAAS_URL"); + var config = new VaasConfig(1000, URI.create(vaasUrl)); + var vaas = new Vaas(config, authenticatorFixture); + + var exception = assertThrows(ExecutionException.class, () -> vaas.forUrlAsync(url).get()); + assertInstanceOf(TimeoutException.class, exception.getCause()); + } + + @Test + @Disabled() + public void forFileAsync_WithSmallTimeout_DoesNotShowNegativeResources() { + var config = new VaasConfig(45, false, false, URI.create(getEnvironmentKey("VAAS_URL"))); + var vaas = new Vaas(config, authenticatorFixture); + var file = Path.of(System.getProperty("java.io.tmpdir"), "file.txt"); + try (var writer = Files.newBufferedWriter(file)) { + writer.write(UUID.randomUUID().toString()); + } catch (IOException e) { + e.printStackTrace(); + } + + while (true) { + try { + vaas.forFile(file); + System.out.print("+"); + System.out.flush(); + } catch (Exception e) { + System.out.print("-"); + System.out.flush(); + } + } + } + + @Test + @Disabled() + public void forFileAsync_WithSmallTimeoutInParallel_DoesNotShowNegativeResources() { + var config = new VaasConfig(45, false, false, URI.create(getEnvironmentKey("VAAS_URL"))); + var vaas = new Vaas(config, authenticatorFixture); + var file1 = Path.of(System.getProperty("java.io.tmpdir"), "file.txt"); + var file2 = Path.of(System.getProperty("java.io.tmpdir"), "file2.txt"); + var file3 = Path.of(System.getProperty("java.io.tmpdir"), "file3.txt"); + var file4 = Path.of(System.getProperty("java.io.tmpdir"), "file4.txt"); + var fileList = List.of(file1, file2, file3, file4); + for (var file : fileList) { + try (var writer = Files.newBufferedWriter(file)) { + writer.write(UUID.randomUUID().toString()); + } catch (IOException e) { + e.printStackTrace(); + } + } + + while (true) { + fileList.parallelStream().forEach((key) -> { + try { + vaas.forFile(key); + System.out.print("+"); + System.out.flush(); + } catch (Exception e) { + System.out.print("-"); + System.out.flush(); + } + }); } - }); } - } + + @Test + public void forFile_IfEncrypted_ReturnsCleanAndIsEncrypted() throws Exception { + var file = samplesFixture.getEncryptedSample(); + + var vaasVerdict = vaasWithDefaultConfig.forFileAsync(file).join(); + + assertEquals(Verdict.CLEAN, vaasVerdict.getVerdict()); + assertTrue(vaasVerdict.getIsEncrypted()); + } + + @Test + public void forFile_IfContainsEicarAndEncryptedArchive_ReturnsMaliciousAndIsEncrypted() throws Exception { + var file = samplesFixture.getEicarInEncryptedSample(); + + var vaasVerdict = vaasWithDefaultConfig.forFileAsync(file).join(); + + assertEquals(Verdict.MALICIOUS, vaasVerdict.getVerdict()); + assertTrue(vaasVerdict.getIsEncrypted()); + } + + @Test + public void forUrl_IfEncrypted_ReturnsCleanAndIsEncrypted() throws Exception { + var url = URI.create(PASSWORD_URL).toURL(); + var config = new VaasConfig(false, false, URI.create(getEnvironmentKey("VAAS_URL"))); + var vaas = new Vaas(config, authenticatorFixture); + + var vaasVerdict = vaas.forUrlAsync(url).join(); + + assertEquals(Verdict.CLEAN, vaasVerdict.getVerdict()); + assertTrue(vaasVerdict.getIsEncrypted()); + } + + @Test + public void forUrl_IfContainsEicarAndEncryptedArchive_ReturnsMaliciousAndIsEncrypted() throws Exception { + var url = URI.create(WITH_AND_WITHOUT_PASSWORD_URL).toURL(); + var config = new VaasConfig(false, false, URI.create(getEnvironmentKey("VAAS_URL"))); + var vaas = new Vaas(config, authenticatorFixture); + + var vaasVerdict = vaas.forUrlAsync(url).join(); + + assertEquals(Verdict.MALICIOUS, vaasVerdict.getVerdict()); + assertTrue(vaasVerdict.getIsEncrypted()); + } + } diff --git a/java/src/test/java/de/gdata/test/integration/SamplesFixture.java b/java/src/test/java/de/gdata/test/integration/SamplesFixture.java index 886c194fb..c674e251d 100644 --- a/java/src/test/java/de/gdata/test/integration/SamplesFixture.java +++ b/java/src/test/java/de/gdata/test/integration/SamplesFixture.java @@ -15,12 +15,18 @@ public class SamplesFixture { private static final String CLEAN_SHA256 = "d24dc598b54a8eedb0a4b381fad68af956441dffa9c9d5d9ac81de73fcc0a089"; private static final String EICAR_SHA256 = "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f"; private static final String PUP_SHA256 = "d6f6c6b9fde37694e12b12009ad11ab9ec8dd0f193e7319c523933bdad8a50ad"; + private static final String ENCRYPTED_SHA256 = "c0621519a2dd9336b12dc6caef2cc789f23eef3026916638dcd620d0ac193881"; + private static final String EICAR_IN_ENCRYPTED_SHA256 = "79fae1ed9ff540bc286f6cc79c7fbaef323987d17e182ee20f13b0285038d8ed"; private URL cleanUrl; private URL eicarUrl; private URL pupUrl; + private URL encryptedUrl; + private URL eicarInEncryptedUrl; private Path cleanSample; private Path eicarSample; private Path pupSample; + private Path encryptedSample; + private Path eicarInEncryptedSample; private final ReentrantLock lock = new ReentrantLock(); @@ -29,12 +35,17 @@ public SamplesFixture() { cleanUrl = URI.create("https://samples.develop.vaas.gdatasecurity.de/clean.txt").toURL(); eicarUrl = URI.create("https://samples.develop.vaas.gdatasecurity.de/eicar.com.txt").toURL(); pupUrl = URI.create("https://samples.develop.vaas.gdatasecurity.de/PotentiallyUnwanted.exe").toURL(); + encryptedUrl = URI.create("https://s3-eu-central-2.ionoscloud.com/test-samples-vaas/password.zip").toURL(); + eicarInEncryptedUrl = URI + .create("https://s3-eu-central-2.ionoscloud.com/test-samples-vaas/with-and-without-password.zip") + .toURL(); } catch (MalformedURLException e) { throw new RuntimeException(e); } } - private Path downloadSample(URL url, String fileName, String expectedSha256) throws IOException, InterruptedException, VaasClientException { + private Path downloadSample(URL url, String fileName, String expectedSha256) + throws IOException, InterruptedException, VaasClientException { Path tmpDir = Path.of(System.getProperty("java.io.tmpdir")); Path targetFile = tmpDir.resolve(fileName); @@ -87,4 +98,29 @@ public Path getPupSample() throws IOException, InterruptedException, VaasClientE lock.unlock(); } } + + public Path getEncryptedSample() throws IOException, InterruptedException, VaasClientException { + lock.lock(); + try { + if (encryptedSample == null) { + encryptedSample = downloadSample(encryptedUrl, "password.zip", ENCRYPTED_SHA256); + } + return encryptedSample; + } finally { + lock.unlock(); + } + } + + public Path getEicarInEncryptedSample() throws IOException, InterruptedException, VaasClientException { + lock.lock(); + try { + if (eicarInEncryptedSample == null) { + eicarInEncryptedSample = downloadSample(eicarInEncryptedUrl, "with-and-without-password.zip", + EICAR_IN_ENCRYPTED_SHA256); + } + return eicarInEncryptedSample; + } finally { + lock.unlock(); + } + } } \ No newline at end of file diff --git a/java/src/test/java/de/gdata/test/unit/AuthenticatorTest.java b/java/src/test/java/de/gdata/test/unit/AuthenticatorTest.java index a48660bae..9baff250f 100644 --- a/java/src/test/java/de/gdata/test/unit/AuthenticatorTest.java +++ b/java/src/test/java/de/gdata/test/unit/AuthenticatorTest.java @@ -1,5 +1,6 @@ package de.gdata.test.unit; +import de.gdata.test.TestDotenv; import de.gdata.vaas.authentication.ClientCredentialsGrantAuthenticator; import de.gdata.vaas.authentication.ResourceOwnerPasswordGrantAuthenticator; import io.github.cdimascio.dotenv.Dotenv; @@ -11,9 +12,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; public class AuthenticatorTest { - private static final Dotenv dotenv = Dotenv.configure() - .ignoreIfMissing() - .load(); + private static final Dotenv dotenv = TestDotenv.load(); private static String getEnvironmentKey(String key) { var value = dotenv.get(key); diff --git a/java/src/test/java/de/gdata/test/unit/TestDotenvTest.java b/java/src/test/java/de/gdata/test/unit/TestDotenvTest.java new file mode 100644 index 000000000..53d2e890b --- /dev/null +++ b/java/src/test/java/de/gdata/test/unit/TestDotenvTest.java @@ -0,0 +1,50 @@ +package de.gdata.test.unit; + +import de.gdata.test.TestDotenv; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class TestDotenvTest { + @TempDir + Path tempDir; + + @Test + public void findEnvDirectory_FindsEnvInParentDirectory() throws Exception { + var parentDirectory = Files.createDirectory(tempDir.resolve("parent")); + var nestedDirectory = Files.createDirectories(parentDirectory.resolve("child/grandchild")); + Files.writeString(parentDirectory.resolve(".env"), "CLIENT_ID=test-client\n"); + + var envDirectory = TestDotenv.findEnvDirectory(nestedDirectory); + + assertTrue(envDirectory.isPresent()); + assertEquals(parentDirectory, envDirectory.get()); + } + + @Test + public void load_LoadsVariablesFromParentDirectory() throws Exception { + var parentDirectory = Files.createDirectory(tempDir.resolve("parent")); + var nestedDirectory = Files.createDirectories(parentDirectory.resolve("child/grandchild")); + Files.writeString(parentDirectory.resolve(".env"), "CLIENT_ID=test-client\nTOKEN_URL=https://example.test/token\n"); + + var dotenv = TestDotenv.load(nestedDirectory); + + assertEquals("test-client", dotenv.get("CLIENT_ID")); + assertEquals("https://example.test/token", dotenv.get("TOKEN_URL")); + } + + @Test + public void findEnvDirectory_ReturnsEmptyWhenNoEnvExists() throws Exception { + var nestedDirectory = Files.createDirectories(tempDir.resolve("child/grandchild")); + + var envDirectory = TestDotenv.findEnvDirectory(nestedDirectory); + + assertFalse(envDirectory.isPresent()); + } +} \ No newline at end of file diff --git a/python/src/vaas/messages/file_report.py b/python/src/vaas/messages/file_report.py index 3f2652666..d59a24f5e 100644 --- a/python/src/vaas/messages/file_report.py +++ b/python/src/vaas/messages/file_report.py @@ -7,3 +7,4 @@ class FileReport(BaseModel): detection: Optional[str] fileType: Optional[str] mimeType: Optional[str] + isEncrypted: Optional[bool] = None diff --git a/python/src/vaas/messages/url_report.py b/python/src/vaas/messages/url_report.py index b4986913a..1ef98659f 100644 --- a/python/src/vaas/messages/url_report.py +++ b/python/src/vaas/messages/url_report.py @@ -7,4 +7,5 @@ class UrlReport(BaseModel): url: str detection: Optional[str] fileType: Optional[str] - mimeType: Optional[str] \ No newline at end of file + mimeType: Optional[str] + isEncrypted: Optional[bool] = None \ No newline at end of file diff --git a/python/src/vaas/messages/vaas_verdict.py b/python/src/vaas/messages/vaas_verdict.py index e5150d214..3b389c406 100644 --- a/python/src/vaas/messages/vaas_verdict.py +++ b/python/src/vaas/messages/vaas_verdict.py @@ -7,6 +7,7 @@ class VaasVerdict(BaseModel): detection: Optional[str] fileType: Optional[str] mimeType: Optional[str] + isEncrypted: Optional[bool] = None @staticmethod def from_report(report): @@ -15,5 +16,6 @@ def from_report(report): verdict=report.verdict, detection=report.detection, fileType=report.fileType, - mimeType=report.mimeType + mimeType=report.mimeType, + isEncrypted=report.isEncrypted ) diff --git a/python/tests/test_vaas.py b/python/tests/test_vaas.py index 0d849bb25..4971e66a6 100644 --- a/python/tests/test_vaas.py +++ b/python/tests/test_vaas.py @@ -1099,3 +1099,50 @@ async def test_for_url_cancel_request_raise_cancel_error(self, vaas, post_fails, with pytest.raises(asyncio.CancelledError): await vaas.for_url(url) + +PASSWORD_ZIP_URL = "https://samples.develop.vaas.gdatasecurity.de/password.zip" +WITH_AND_WITHOUT_PASSWORD_ZIP_URL = "https://samples.develop.vaas.gdatasecurity.de/with-and-without-password.zip" + + +class TestVaasEncrypted: + @pytest.mark.asyncio() + async def test_for_file_if_encrypted_returns_clean_and_is_encrypted(self, vaas): + async with httpx.AsyncClient() as client: + filename = "/tmp/password.zip" + response = await client.get(PASSWORD_ZIP_URL) + response.raise_for_status() + with open(filename, mode="wb") as file: + file.write(response.content) + + verdict = await vaas.for_file(filename) + + assert verdict.verdict == "Clean" + assert verdict.isEncrypted is True + + @pytest.mark.asyncio() + async def test_for_file_if_contains_eicar_and_encrypted_returns_malicious_and_is_encrypted(self, vaas): + async with httpx.AsyncClient() as client: + filename = "/tmp/with-and-without-password.zip" + response = await client.get(WITH_AND_WITHOUT_PASSWORD_ZIP_URL) + response.raise_for_status() + with open(filename, mode="wb") as file: + file.write(response.content) + + verdict = await vaas.for_file(filename) + + assert verdict.verdict == "Malicious" + assert verdict.isEncrypted is True + + @pytest.mark.asyncio() + async def test_for_url_if_encrypted_returns_clean_and_is_encrypted(self, vaas): + verdict = await vaas.for_url(PASSWORD_ZIP_URL) + + assert verdict.verdict == "Clean" + assert verdict.isEncrypted is True + + @pytest.mark.asyncio() + async def test_for_url_if_contains_eicar_and_encrypted_returns_malicious_and_is_encrypted(self, vaas): + verdict = await vaas.for_url(WITH_AND_WITHOUT_PASSWORD_ZIP_URL) + + assert verdict.verdict == "Malicious" + assert verdict.isEncrypted is True diff --git a/rust/src/message/report.rs b/rust/src/message/report.rs index 3cc6d7217..842cc2518 100644 --- a/rust/src/message/report.rs +++ b/rust/src/message/report.rs @@ -12,6 +12,7 @@ pub struct FileReport { pub detection: Option, pub file_type: Option, pub mime_type: Option, + pub is_encrypted: Option, } #[derive(Debug, Clone, Deserialize)] @@ -24,4 +25,5 @@ pub struct UrlReport { pub detection: Option, pub file_type: Option, pub mime_type: Option, + pub is_encrypted: Option, } diff --git a/rust/src/vaas.rs b/rust/src/vaas.rs index 177be084e..eea6ab959 100644 --- a/rust/src/vaas.rs +++ b/rust/src/vaas.rs @@ -320,6 +320,7 @@ mod tests { detection: None, file_type: None, mime_type: None, + is_encrypted: None, })) .create_async() .await; @@ -515,6 +516,7 @@ mod tests { detection: None, file_type: None, mime_type: None, + is_encrypted: None, })) .create_async() .await; @@ -739,6 +741,7 @@ mod tests { detection: None, file_type: None, mime_type: None, + is_encrypted: None, })) .create_async() .await; @@ -951,6 +954,7 @@ mod tests { detection: None, file_type: None, mime_type: None, + is_encrypted: None, })) .create_async() .await; diff --git a/rust/src/vaas_verdict.rs b/rust/src/vaas_verdict.rs index 3e80a6118..9428b2885 100644 --- a/rust/src/vaas_verdict.rs +++ b/rust/src/vaas_verdict.rs @@ -19,6 +19,8 @@ pub struct VaasVerdict { pub file_type: Option, /// mime type as classified by https://www.darwinsys.com/file/ pub mime_type: Option, + /// Whether the file is encrypted + pub is_encrypted: Option, } impl From for VaasVerdict { @@ -29,6 +31,7 @@ impl From for VaasVerdict { detection: report.detection, file_type: report.file_type, mime_type: report.mime_type, + is_encrypted: report.is_encrypted, } } } @@ -41,6 +44,7 @@ impl From for VaasVerdict { detection: report.detection, file_type: report.file_type, mime_type: report.mime_type, + is_encrypted: report.is_encrypted, } } } diff --git a/rust/tests/real_api_integration_tests.rs b/rust/tests/real_api_integration_tests.rs index aaf72a5b0..bd243920f 100644 --- a/rust/tests/real_api_integration_tests.rs +++ b/rust/tests/real_api_integration_tests.rs @@ -1,5 +1,6 @@ use reqwest::Url; use std::io::Write; +use std::path::Path; use std::str::FromStr; use std::sync::LazyLock; use tokio_util::sync::CancellationToken; @@ -14,8 +15,26 @@ static EICAR_SHA256: LazyLock = LazyLock::new(|| { }); const EICAR_STRING: &str = "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*"; +fn load_dotenv_from_ancestors() { + let mut dir = std::env::current_dir().expect("Failed to get current directory"); + loop { + let candidate = dir.join(".env"); + if candidate.is_file() { + dotenv::from_path(Path::new(&candidate)).ok(); + return; + } + match dir.parent() { + Some(parent) => dir = parent.to_owned(), + None => return, + } + } +} + +static DOTENV: LazyLock<()> = LazyLock::new(load_dotenv_from_ancestors); + fn vaas_with_authenticator(authenticator: impl Authenticator + 'static) -> Vaas { - let vaas_url = dotenv::var("VAAS_URL") + let _ = *DOTENV; + let vaas_url = std::env::var("VAAS_URL") .expect("No VAAS_URL environment variable set to be used in the integration tests"); Vaas::builder(authenticator) .url(Url::parse(&vaas_url).unwrap()) @@ -25,13 +44,14 @@ fn vaas_with_authenticator(authenticator: impl Authenticator + 'static) -> Vaas #[rstest::fixture] fn vaas_with_client_credentials() -> Vaas { - let token_url: Url = dotenv::var("TOKEN_URL") + let _ = *DOTENV; + let token_url: Url = std::env::var("TOKEN_URL") .expect("No TOKEN_URL environment variable set to be used in the integration tests") .parse() .expect("Failed to parse TOKEN_URL environment variable"); - let client_id = dotenv::var("CLIENT_ID") + let client_id = std::env::var("CLIENT_ID") .expect("No CLIENT_ID environment variable set to be used in the integration tests"); - let client_secret = dotenv::var("CLIENT_SECRET") + let client_secret = std::env::var("CLIENT_SECRET") .expect("No CLIENT_SECRET environment variable set to be used in the integration tests"); let authenticator = ClientCredentials::try_new(client_id, client_secret) .unwrap() @@ -41,15 +61,16 @@ fn vaas_with_client_credentials() -> Vaas { #[rstest::fixture] fn vaas_with_password() -> Vaas { - let token_url: Url = dotenv::var("TOKEN_URL") + let _ = *DOTENV; + let token_url: Url = std::env::var("TOKEN_URL") .expect("No TOKEN_URL environment variable set to be used in the integration tests") .parse() .expect("Failed to parse TOKEN_URL environment variable"); - let client_id = dotenv::var("VAAS_CLIENT_ID") + let client_id = std::env::var("VAAS_CLIENT_ID") .expect("No CLIENT_ID environment variable set to be used in the integration tests"); - let user_name = dotenv::var("VAAS_USER_NAME") + let user_name = std::env::var("VAAS_USER_NAME") .expect("No VAAS_USER_NAME environment variable set to be used in the integration tests"); - let password = dotenv::var("VAAS_PASSWORD") + let password = std::env::var("VAAS_PASSWORD") .expect("No VAAS_PASSWORD environment variable set to be used in the integration tests"); let authenticator = Password::try_new(client_id, user_name, password) .unwrap() @@ -227,3 +248,90 @@ async fn test_for_buf_if_canceled_returns_error( ); Ok(()) } + +const PASSWORD_ZIP_URL: &str = + "https://samples.develop.vaas.gdatasecurity.de/password.zip"; +const WITH_AND_WITHOUT_PASSWORD_ZIP_URL: &str = + "https://samples.develop.vaas.gdatasecurity.de/with-and-without-password.zip"; + +async fn download_to_temp_file(url: &str) -> tempfile::NamedTempFile { + let response = reqwest::get(url).await.expect("Failed to download file"); + let bytes = response.bytes().await.expect("Failed to read response bytes"); + let mut tmp = tempfile::NamedTempFile::new().expect("Failed to create temp file"); + tmp.as_file_mut() + .write_all(&bytes) + .expect("Failed to write to temp file"); + tmp +} + +#[rstest::rstest] +#[tokio::test] +async fn test_for_file_if_encrypted_returns_clean_and_is_encrypted( + vaas_with_client_credentials: Vaas, +) -> Result<(), Error> { + let tmp = download_to_temp_file(PASSWORD_ZIP_URL).await; + let options = ForFileOptions::default(); + let ct = CancellationToken::new(); + + let verdict = vaas_with_client_credentials + .for_file(tmp.path(), options, &ct) + .await?; + + assert_eq!(verdict.verdict, Verdict::Clean); + assert_eq!(verdict.is_encrypted, Some(true)); + Ok(()) +} + +#[rstest::rstest] +#[tokio::test] +async fn test_for_file_if_contains_eicar_and_encrypted_returns_malicious_and_is_encrypted( + vaas_with_client_credentials: Vaas, +) -> Result<(), Error> { + let tmp = download_to_temp_file(WITH_AND_WITHOUT_PASSWORD_ZIP_URL).await; + let options = ForFileOptions::default(); + let ct = CancellationToken::new(); + + let verdict = vaas_with_client_credentials + .for_file(tmp.path(), options, &ct) + .await?; + + assert_eq!(verdict.verdict, Verdict::Malicious); + assert_eq!(verdict.is_encrypted, Some(true)); + Ok(()) +} + +#[rstest::rstest] +#[tokio::test] +async fn test_for_url_if_encrypted_returns_clean_and_is_encrypted( + vaas_with_client_credentials: Vaas, +) -> Result<(), Error> { + let url: Url = PASSWORD_ZIP_URL.parse().unwrap(); + let options = ForUrlOptions::default(); + let ct = CancellationToken::new(); + + let verdict = vaas_with_client_credentials + .for_url(&url, options, &ct) + .await?; + + assert_eq!(verdict.verdict, Verdict::Clean); + assert_eq!(verdict.is_encrypted, Some(true)); + Ok(()) +} + +#[rstest::rstest] +#[tokio::test] +async fn test_for_url_if_contains_eicar_and_encrypted_returns_malicious_and_is_encrypted( + vaas_with_client_credentials: Vaas, +) -> Result<(), Error> { + let url: Url = WITH_AND_WITHOUT_PASSWORD_ZIP_URL.parse().unwrap(); + let options = ForUrlOptions::default(); + let ct = CancellationToken::new(); + + let verdict = vaas_with_client_credentials + .for_url(&url, options, &ct) + .await?; + + assert_eq!(verdict.verdict, Verdict::Malicious); + assert_eq!(verdict.is_encrypted, Some(true)); + Ok(()) +}