diff --git a/.github/workflows/ci-dotnet.yaml b/.github/workflows/ci-dotnet.yaml
index d3ca9f2e2..6aa46d800 100644
--- a/.github/workflows/ci-dotnet.yaml
+++ b/.github/workflows/ci-dotnet.yaml
@@ -28,7 +28,7 @@ on:
env:
CLIENT_ID: ${{ secrets.CLIENT_ID }}
CLIENT_SECRET: ${{secrets.CLIENT_SECRET}}
- VAAS_URL: "wss://gateway.production.vaas.gdatasecurity.de"
+ VAAS_URL: "https://gateway.production.vaas.gdatasecurity.de"
TOKEN_URL: "https://account.gdata.de/realms/vaas-production/protocol/openid-connect/token"
VAAS_CLIENT_ID: ${{ secrets.VAAS_CLIENT_ID }}
VAAS_USER_NAME: ${{ secrets.VAAS_USER_NAME }}
@@ -56,7 +56,7 @@ jobs:
run: |
echo "CLIENT_ID=${{ secrets.STAGING_CLIENT_ID }}" >> $GITHUB_ENV
echo "CLIENT_SECRET=${{ secrets.STAGING_CLIENT_SECRET }}" >> $GITHUB_ENV
- echo "VAAS_URL=wss://gateway.staging.vaas.gdatasecurity.de" >> $GITHUB_ENV
+ echo "VAAS_URL=https://gateway.staging.vaas.gdatasecurity.de" >> $GITHUB_ENV
echo "TOKEN_URL=https://account-staging.gdata.de/realms/vaas-staging/protocol/openid-connect/token" >> $GITHUB_ENV
echo "VAAS_CLIENT_ID=${{ secrets.STAGING_VAAS_CLIENT_ID }}" >> $GITHUB_ENV
echo "VAAS_USER_NAME=${{ secrets.STAGING_VAAS_USER_NAME }}" >> $GITHUB_ENV
@@ -67,7 +67,7 @@ jobs:
run: |
echo "CLIENT_ID=${{ secrets.DEVELOP_CLIENT_ID }}" >> $GITHUB_ENV
echo "CLIENT_SECRET=${{ secrets.DEVELOP_CLIENT_SECRET }}" >> $GITHUB_ENV
- echo "VAAS_URL=wss://gateway.develop.vaas.gdatasecurity.de" >> $GITHUB_ENV
+ echo "VAAS_URL=https://gateway.develop.vaas.gdatasecurity.de" >> $GITHUB_ENV
echo "TOKEN_URL=https://account-staging.gdata.de/realms/vaas-develop/protocol/openid-connect/token" >> $GITHUB_ENV
echo "VAAS_CLIENT_ID=${{ secrets.DEVELOP_VAAS_CLIENT_ID }}" >> $GITHUB_ENV
echo "VAAS_USER_NAME=${{ secrets.DEVELOP_VAAS_USER_NAME }}" >> $GITHUB_ENV
@@ -101,19 +101,19 @@ jobs:
run: dotnet test --no-restore --verbosity normal
working-directory: dotnet/Vaas
- - name: Run example FileScan
- env:
- SCAN_PATH: "Program.cs"
- run: dotnet run FileScan
- working-directory: dotnet/examples/VaasExample
+ # - name: Run example FileScan
+ # env:
+ # SCAN_PATH: "Program.cs"
+ # run: dotnet run FileScan
+ # working-directory: dotnet/examples/VaasExample
- - name: Run example UrlScan
- run: dotnet run UrlScan
- working-directory: dotnet/examples/VaasExample
+ # - name: Run example UrlScan
+ # run: dotnet run UrlScan
+ # working-directory: dotnet/examples/VaasExample
- - name: Run example HashsumScan
- run: dotnet run HashsumScan
- working-directory: dotnet/examples/VaasExample
+ # - name: Run example HashsumScan
+ # run: dotnet run HashsumScan
+ # working-directory: dotnet/examples/VaasExample
- name: Pack
if: startsWith(github.ref, 'refs/tags/cs')
diff --git a/.gitignore b/.gitignore
index a762d5e23..9ff3bd531 100644
--- a/.gitignore
+++ b/.gitignore
@@ -42,6 +42,8 @@ lib/
bin/
obj/
+.idea/
+.mono/
# Python
__pycache__/
diff --git a/dotnet/.devcontainer/devcontainer.json b/dotnet/.devcontainer/devcontainer.json
index 1e394b708..2b76ed5a5 100644
--- a/dotnet/.devcontainer/devcontainer.json
+++ b/dotnet/.devcontainer/devcontainer.json
@@ -3,11 +3,12 @@
{
"name": "C# (.NET)",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
- "image": "mcr.microsoft.com/devcontainers/dotnet:1-6.0-bullseye",
+ "image": "mcr.microsoft.com/devcontainers/dotnet:1-8.0-bookworm",
"customizations": {
"vscode": {
"extensions": [
- "jcamp.dotnet-test-provider-explorer"
+ "jcamp.dotnet-test-provider-explorer",
+ "ms-dotnettools.csdevkit"
]
}
}
diff --git a/dotnet/Vaas/.idea/.idea.Vaas/.idea/CSharpierPlugin.xml b/dotnet/Vaas/.idea/.idea.Vaas/.idea/CSharpierPlugin.xml
new file mode 100644
index 000000000..5e2406196
--- /dev/null
+++ b/dotnet/Vaas/.idea/.idea.Vaas/.idea/CSharpierPlugin.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dotnet/Vaas/.idea/.idea.Vaas/.idea/externalDependencies.xml b/dotnet/Vaas/.idea/.idea.Vaas/.idea/externalDependencies.xml
new file mode 100644
index 000000000..1a9178a92
--- /dev/null
+++ b/dotnet/Vaas/.idea/.idea.Vaas/.idea/externalDependencies.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dotnet/Vaas/Vaas.sln b/dotnet/Vaas/Vaas.sln
index 1813d99a7..9c9fd78b8 100644
--- a/dotnet/Vaas/Vaas.sln
+++ b/dotnet/Vaas/Vaas.sln
@@ -8,6 +8,9 @@ EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Vaas", "src\Vaas\Vaas.csproj", "{F83EA992-9DB1-43FF-89F2-836F4B325345}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{59EEE214-8910-4FBC-AA18-F7070B700D49}"
+ ProjectSection(SolutionItems) = preProject
+ test\.env = test\.env
+ EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Vaas.Test", "test\Vaas.Test\Vaas.Test.csproj", "{5C11F0F4-1483-4850-BB24-4A04E7EED14A}"
EndProject
diff --git a/dotnet/Vaas/src/Vaas/Authentication/Authenticator.cs b/dotnet/Vaas/src/Vaas/Authentication/Authenticator.cs
deleted file mode 100644
index b1f53e46c..000000000
--- a/dotnet/Vaas/src/Vaas/Authentication/Authenticator.cs
+++ /dev/null
@@ -1,93 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IdentityModel.Tokens.Jwt;
-using System.Net.Http;
-using System.Text.Json;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Vaas.Authentication;
-
-public class Authenticator : IAuthenticator, IDisposable
-{
- private readonly HttpClient _httpClient;
- private readonly ISystemClock _systemClock;
- private readonly VaasOptions _options;
- private readonly SemaphoreSlim _semaphore = new(1);
- private readonly JwtSecurityTokenHandler _jwtSecurityTokenHandler = new();
- private TokenResponse? _lastResponse;
- private DateTime _validTo;
-
- public Authenticator(HttpClient httpClient, ISystemClock systemClock, VaasOptions options)
- {
- _httpClient = httpClient;
- _systemClock = systemClock;
- _options = options;
- }
-
- public async Task GetTokenAsync(CancellationToken cancellationToken)
- {
- try
- {
- await _semaphore.WaitAsync(cancellationToken);
-
- if (_lastResponse != null && _validTo.ToUniversalTime() >= _systemClock.UtcNow)
- return _lastResponse.AccessToken;
-
- _lastResponse = await RequestTokenAsync(cancellationToken);
- var accessToken = _jwtSecurityTokenHandler.ReadJwtToken(_lastResponse.AccessToken);
- _validTo = accessToken.ValidTo;
- return _lastResponse.AccessToken;
- }
- finally
- {
- _semaphore.Release();
- }
- }
-
- private async Task RequestTokenAsync(CancellationToken cancellationToken)
- {
- var form = TokenRequestToForm();
- var response = await _httpClient.PostAsync(_options.TokenUrl, form, cancellationToken);
- response.EnsureSuccessStatusCode();
- var stringResponse = await response.Content.ReadAsStringAsync(cancellationToken);
- var tokenResponse = JsonSerializer.Deserialize(stringResponse);
- if (tokenResponse == null)
- throw new JsonException("Access token is null");
- return tokenResponse;
- }
-
- private FormUrlEncodedContent TokenRequestToForm()
- {
- if (_options.Credentials.GrantType == GrantType.ClientCredentials)
- {
- return new FormUrlEncodedContent(
- new List>
- {
- new("client_id", _options.Credentials.ClientId),
- new("client_secret", _options.Credentials.ClientSecret ?? throw new InvalidOperationException()),
- new("grant_type", "client_credentials")
- }
- );
- }
-
- return new FormUrlEncodedContent(
- new List>
- {
- new("client_id", _options.Credentials.ClientId),
- new("username", _options.Credentials.UserName ?? throw new InvalidOperationException()),
- new("password", _options.Credentials.Password ?? throw new InvalidOperationException()),
- new("grant_type", "password")
- });
- }
-
- public Task RefreshTokenAsync(CancellationToken cancellationToken)
- {
- throw new System.NotImplementedException();
- }
-
- public void Dispose()
- {
- _semaphore.Dispose();
- }
-}
diff --git a/dotnet/Vaas/src/Vaas/Authentication/BearerTokenHandler.cs b/dotnet/Vaas/src/Vaas/Authentication/BearerTokenHandler.cs
deleted file mode 100644
index 8a067b619..000000000
--- a/dotnet/Vaas/src/Vaas/Authentication/BearerTokenHandler.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-using System.Net.Http;
-using System.Net.Http.Headers;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Vaas.Authentication;
-
-public class BearerTokenHandler : DelegatingHandler
-{
- private readonly IAuthenticator _authenticator;
-
- public BearerTokenHandler(IAuthenticator authenticator) => _authenticator = authenticator;
-
- protected override async Task SendAsync(HttpRequestMessage request,
- CancellationToken cancellationToken)
- {
- request.Headers.Authorization =
- new AuthenticationHeaderValue("Bearer", await _authenticator.GetTokenAsync(cancellationToken));
- return await base.SendAsync(request, cancellationToken);
- }
-}
\ No newline at end of file
diff --git a/dotnet/Vaas/src/Vaas/Authentication/ClientCredentialsGrantAuthenticator.cs b/dotnet/Vaas/src/Vaas/Authentication/ClientCredentialsGrantAuthenticator.cs
new file mode 100644
index 000000000..3f06fef54
--- /dev/null
+++ b/dotnet/Vaas/src/Vaas/Authentication/ClientCredentialsGrantAuthenticator.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Collections.Generic;
+using System.Net.Http;
+
+namespace Vaas.Authentication;
+
+public class ClientCredentialsGrantAuthenticator(
+ string clientId,
+ string clientSecret,
+ Uri? tokenEndpoint = null,
+ HttpClient? httpClient = null,
+ ISystemClock? systemClock = null
+) : TokenReceiver(tokenEndpoint, httpClient, systemClock), IAuthenticator
+{
+ private string ClientId { get; } = clientId;
+ private string ClientSecret { get; } = clientSecret;
+
+ protected override FormUrlEncodedContent TokenRequestToForm()
+ {
+ return new FormUrlEncodedContent(
+ new List>
+ {
+ new("client_id", ClientId),
+ new("client_secret", ClientSecret ?? throw new InvalidOperationException()),
+ new("grant_type", "client_credentials"),
+ }
+ );
+ }
+}
diff --git a/dotnet/Vaas/src/Vaas/Authentication/ErrorResponse.cs b/dotnet/Vaas/src/Vaas/Authentication/ErrorResponse.cs
new file mode 100644
index 000000000..8f8a1d9a4
--- /dev/null
+++ b/dotnet/Vaas/src/Vaas/Authentication/ErrorResponse.cs
@@ -0,0 +1,12 @@
+using System.Text.Json.Serialization;
+
+namespace Vaas.Authentication;
+
+public class ErrorResponse
+{
+ [JsonPropertyName("error")]
+ public required string Error { get; init; }
+
+ [JsonPropertyName("error_description")]
+ public string? ErrorDescription { get; init; }
+}
diff --git a/dotnet/Vaas/src/Vaas/Authentication/GrantType.cs b/dotnet/Vaas/src/Vaas/Authentication/GrantType.cs
new file mode 100644
index 000000000..e91dcf84b
--- /dev/null
+++ b/dotnet/Vaas/src/Vaas/Authentication/GrantType.cs
@@ -0,0 +1,7 @@
+namespace Vaas.Authentication;
+
+public enum GrantType
+{
+ ClientCredentials,
+ Password,
+}
diff --git a/dotnet/Vaas/src/Vaas/Authentication/ResourceOwnerPasswordGrantAuthenticator.cs b/dotnet/Vaas/src/Vaas/Authentication/ResourceOwnerPasswordGrantAuthenticator.cs
new file mode 100644
index 000000000..5b0ccbefa
--- /dev/null
+++ b/dotnet/Vaas/src/Vaas/Authentication/ResourceOwnerPasswordGrantAuthenticator.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Collections.Generic;
+using System.Net.Http;
+
+namespace Vaas.Authentication;
+
+public class ResourceOwnerPasswordGrantAuthenticator(
+ string clientId,
+ string userName,
+ string password,
+ Uri? tokenEndpoint = null,
+ HttpClient? httpClient = null,
+ ISystemClock? systemClock = null
+) : TokenReceiver(tokenEndpoint, httpClient, systemClock), IAuthenticator
+{
+ private string ClientId { get; } = clientId;
+ private string UserName { get; } = userName;
+ private string Password { get; } = password;
+
+ protected override FormUrlEncodedContent TokenRequestToForm()
+ {
+ return new FormUrlEncodedContent(
+ new List>
+ {
+ new("client_id", ClientId),
+ new("username", UserName ?? throw new InvalidOperationException()),
+ new("password", Password ?? throw new InvalidOperationException()),
+ new("grant_type", "password"),
+ }
+ );
+ }
+}
diff --git a/dotnet/Vaas/src/Vaas/Authentication/SystemClock.cs b/dotnet/Vaas/src/Vaas/Authentication/SystemClock.cs
index 77a6920e4..1524896b6 100644
--- a/dotnet/Vaas/src/Vaas/Authentication/SystemClock.cs
+++ b/dotnet/Vaas/src/Vaas/Authentication/SystemClock.cs
@@ -10,4 +10,4 @@ public interface ISystemClock
public class SystemClock : ISystemClock
{
public DateTimeOffset UtcNow => DateTimeOffset.UtcNow;
-}
\ No newline at end of file
+}
diff --git a/dotnet/Vaas/src/Vaas/Authentication/TokenReceiver.cs b/dotnet/Vaas/src/Vaas/Authentication/TokenReceiver.cs
new file mode 100644
index 000000000..d41e2e077
--- /dev/null
+++ b/dotnet/Vaas/src/Vaas/Authentication/TokenReceiver.cs
@@ -0,0 +1,114 @@
+using System;
+using System.Net.Http;
+using System.Security.Authentication;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Vaas.Authentication;
+
+public abstract class TokenReceiver(
+ Uri? tokenUrl = null,
+ HttpClient? httpClient = null,
+ ISystemClock? systemClock = null
+) : IDisposable
+{
+ private readonly Uri _tokenUrl =
+ tokenUrl
+ ?? new Uri("https://account.gdata.de/realms/vaas-production/protocol/openid-connect/token");
+ private readonly HttpClient _httpClient = httpClient ?? new HttpClient();
+ private readonly SemaphoreSlim _semaphore = new(1);
+ private readonly ISystemClock _systemClock = systemClock ?? new SystemClock();
+ private TokenResponse? _lastResponse;
+ private DateTime _validTo;
+ private DateTime? _lastRequestTime;
+
+ public async Task GetTokenAsync(CancellationToken cancellationToken)
+ {
+ try
+ {
+ await _semaphore.WaitAsync(cancellationToken);
+
+ var now = _systemClock.UtcNow;
+ if (_lastResponse != null && _validTo.ToUniversalTime() >= now)
+ return _lastResponse.AccessToken;
+
+ if (_lastRequestTime != null)
+ {
+ var timeToWait = _lastRequestTime + TimeSpan.FromSeconds(1) - now;
+ if (timeToWait > TimeSpan.Zero)
+ {
+ await Task.Delay(timeToWait.Value, cancellationToken);
+ }
+ }
+
+ _lastRequestTime = now.UtcDateTime;
+ _lastResponse = await RequestTokenAsync(cancellationToken);
+ var expiresInSeconds =
+ _lastResponse.ExpiresInSeconds
+ ?? throw new AuthenticationException("Identity provider did not return expires_in");
+
+ _validTo = _systemClock.UtcNow.Add(TimeSpan.FromSeconds(expiresInSeconds)).UtcDateTime;
+ return _lastResponse.AccessToken;
+ }
+ finally
+ {
+ _semaphore.Release();
+ }
+ }
+
+ private async Task RequestTokenAsync(CancellationToken cancellationToken)
+ {
+ var form = TokenRequestToForm();
+ HttpResponseMessage response;
+ try
+ {
+ response = await _httpClient.PostAsync(_tokenUrl, form, cancellationToken);
+ }
+ catch (Exception ex)
+ {
+ throw new AuthenticationException("Failed to request token", ex);
+ }
+
+ var stringResponse = await response.Content.ReadAsStringAsync(cancellationToken);
+
+ if (!response.IsSuccessStatusCode)
+ {
+ ErrorResponse? errorResponse;
+ var statusCode = (int)response.StatusCode;
+ try
+ {
+ errorResponse = JsonSerializer.Deserialize(stringResponse);
+ }
+ catch (JsonException e)
+ {
+ throw new AuthenticationException(
+ $"Identity provider returned status code {statusCode}: {e.Message}"
+ );
+ }
+
+ if (errorResponse == null)
+ {
+ throw new AuthenticationException(
+ $"Identity provider returned status code {statusCode}: Empty body"
+ );
+ }
+
+ throw new AuthenticationException(
+ $"Identity provider returned status code {statusCode}: {errorResponse.ErrorDescription ?? errorResponse.Error}"
+ );
+ }
+
+ var tokenResponse = JsonSerializer.Deserialize(stringResponse);
+ if (tokenResponse == null)
+ throw new AuthenticationException("Access token is null");
+ return tokenResponse;
+ }
+
+ protected abstract FormUrlEncodedContent TokenRequestToForm();
+
+ public void Dispose()
+ {
+ _semaphore.Dispose();
+ }
+}
diff --git a/dotnet/Vaas/src/Vaas/Authentication/TokenRequest.cs b/dotnet/Vaas/src/Vaas/Authentication/TokenRequest.cs
deleted file mode 100644
index e0d503c95..000000000
--- a/dotnet/Vaas/src/Vaas/Authentication/TokenRequest.cs
+++ /dev/null
@@ -1,54 +0,0 @@
-using System;
-using System.ComponentModel.DataAnnotations;
-using CommunityToolkit.Diagnostics;
-
-namespace Vaas.Authentication;
-
-public enum GrantType
-{
- ClientCredentials,
- Password
-}
-
-public class TokenRequest
-{
- [Required] public GrantType GrantType { get; set; }
-
- [Required] public string ClientId { get; set; } = string.Empty;
- public string? ClientSecret { get; set; }
-
- public string? UserName { get; set; }
- public string? Password { get; set; }
-
- public static ValidationResult IsValid(TokenRequest? request, ValidationContext context)
- {
- Guard.IsNotNull(request);
- var memberNames = new[] { context.MemberName ?? "" };
- if (request.GrantType == GrantType.ClientCredentials)
- {
- if (string.IsNullOrWhiteSpace(request.ClientId) || string.IsNullOrWhiteSpace(request.ClientSecret))
- {
- return new ValidationResult(
- "The fields ClientId and ClientSecret are required for the GrantType ClientCredentials.",
- memberNames);
- }
-
- return ValidationResult.Success!;
- }
-
- if (request.GrantType == GrantType.Password)
- {
- if (string.IsNullOrWhiteSpace(request.ClientId) || string.IsNullOrWhiteSpace(request.UserName) ||
- string.IsNullOrWhiteSpace(request.Password))
- {
- return new ValidationResult(
- "The fields ClientId, UserName and Password are required for the GrantType Password.",
- memberNames);
- }
-
- return ValidationResult.Success!;
- }
-
- throw new ArgumentOutOfRangeException();
- }
-}
\ No newline at end of file
diff --git a/dotnet/Vaas/src/Vaas/Authentication/TokenResponse.cs b/dotnet/Vaas/src/Vaas/Authentication/TokenResponse.cs
index 7e3c5d822..0f8f36b14 100644
--- a/dotnet/Vaas/src/Vaas/Authentication/TokenResponse.cs
+++ b/dotnet/Vaas/src/Vaas/Authentication/TokenResponse.cs
@@ -7,10 +7,14 @@ public class TokenResponse
{
[JsonPropertyName("access_token")]
public string AccessToken { get; init; }
-
- public TokenResponse(string accessToken)
+
+ [JsonPropertyName("expires_in")]
+ public int? ExpiresInSeconds { get; init; }
+
+ public TokenResponse(string accessToken, int? expiresInSeconds)
{
Guard.IsNotNullOrEmpty(accessToken);
AccessToken = accessToken;
+ ExpiresInSeconds = expiresInSeconds;
}
-}
\ No newline at end of file
+}
diff --git a/dotnet/Vaas/src/Vaas/ChecksumSha256.cs b/dotnet/Vaas/src/Vaas/ChecksumSha256.cs
index 9fedb227f..ec3404ad0 100644
--- a/dotnet/Vaas/src/Vaas/ChecksumSha256.cs
+++ b/dotnet/Vaas/src/Vaas/ChecksumSha256.cs
@@ -1,4 +1,6 @@
using System;
+using System.IO;
+using System.Security.Cryptography;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
@@ -7,12 +9,13 @@
namespace Vaas;
[JsonConverter(typeof(ChecksumSha256Converter))]
-public class ChecksumSha256
+public partial class ChecksumSha256
{
- private const string EmptyFileSha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
+ private const string EmptyFileSha256 =
+ "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
- public string Sha256 { get; }
- private static readonly Regex Pattern = new("^[a-fA-F0-9]{64}$", RegexOptions.Compiled);
+ private string Sha256 { get; }
+ private static readonly Regex Pattern = Sha256Regex();
public ChecksumSha256(string sha256)
{
@@ -43,27 +46,47 @@ public static bool TryParse(string value, out ChecksumSha256? result)
}
catch (ArgumentException)
{
- result = default;
+ result = null;
return false;
}
}
- public static implicit operator ChecksumSha256(string sha256) => new (sha256);
-
+ public static string Sha256CheckSum(string filePath)
+ {
+ using var sha256 = SHA256.Create();
+ using var fileStream = File.OpenRead(filePath);
+ return Convert.ToHexString(sha256.ComputeHash(fileStream)).ToLower();
+ }
+
+ public static implicit operator ChecksumSha256(string sha256) => new(sha256);
+
public static implicit operator string(ChecksumSha256 s) => s.Sha256;
public override string ToString() => Sha256;
+
+ [GeneratedRegex("^[a-fA-F0-9]{64}$", RegexOptions.Compiled)]
+ private static partial Regex Sha256Regex();
}
public class ChecksumSha256Converter : JsonConverter
{
- public override ChecksumSha256? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ public override ChecksumSha256 Read(
+ ref Utf8JsonReader reader,
+ Type typeToConvert,
+ JsonSerializerOptions options
+ )
{
- return new ChecksumSha256(reader.GetString() ?? throw new JsonException("Expected SHA256 string"));
+ return new ChecksumSha256(
+ reader.GetString() ?? throw new JsonException("Expected SHA256 string")
+ );
}
- public override void Write(Utf8JsonWriter writer, ChecksumSha256 value, JsonSerializerOptions options)
+ public override void Write(
+ Utf8JsonWriter writer,
+ ChecksumSha256 value,
+ JsonSerializerOptions options
+ )
{
writer.WriteStringValue(value.ToString());
}
-}
\ No newline at end of file
+}
diff --git a/dotnet/Vaas/src/Vaas/Exceptions.cs b/dotnet/Vaas/src/Vaas/Exceptions.cs
deleted file mode 100644
index a4e9dc667..000000000
--- a/dotnet/Vaas/src/Vaas/Exceptions.cs
+++ /dev/null
@@ -1,63 +0,0 @@
-using System;
-
-namespace Vaas;
-
-public class VaasInvalidStateException : Exception
-{
- public VaasInvalidStateException() : base("Connect() was not called")
- {
- }
-}
-
-public class VaasAuthenticationException : Exception
-{
- public VaasAuthenticationException() : base("Authentication failed")
- {
- }
-}
-
-public class VaasConnectionClosedException : Exception
-{
- public VaasConnectionClosedException() : base("Connection closed")
- {
- }
-}
-
-/// The request is malformed or cannot be completed.
-///
-/// Recommended actions:
-///
-/// - Don't repeat the request.
-/// - Log.
-/// - Analyze the error
-///
-///
-public class VaasClientException : Exception
-{
- public VaasClientException(string? message) : base(message)
- {
- }
-
- public VaasClientException(string? message, Exception? innerException) : base(message, innerException)
- {
- }
-}
-
-/// The server encountered an internal error.
-///
-/// Recommended actions:
-///
-/// - You may retry the request after a certain delay.
-/// - If the problem persists contact G DATA.
-///
-///
-public class VaasServerException : Exception
-{
- public VaasServerException(string? message) : base(message)
- {
- }
-
- public VaasServerException(string? message, Exception? innerException) : base(message, innerException)
- {
- }
-}
diff --git a/dotnet/Vaas/src/Vaas/Exceptions/VaasAuthenticationException.cs b/dotnet/Vaas/src/Vaas/Exceptions/VaasAuthenticationException.cs
new file mode 100644
index 000000000..c212f756f
--- /dev/null
+++ b/dotnet/Vaas/src/Vaas/Exceptions/VaasAuthenticationException.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace Vaas.Exceptions;
+
+public class VaasAuthenticationException : Exception
+{
+ public VaasAuthenticationException()
+ : base("Authentication failed") { }
+
+ public VaasAuthenticationException(string message)
+ : base(message) { }
+}
diff --git a/dotnet/Vaas/src/Vaas/Exceptions/VaasClientException.cs b/dotnet/Vaas/src/Vaas/Exceptions/VaasClientException.cs
new file mode 100644
index 000000000..83bbe62ed
--- /dev/null
+++ b/dotnet/Vaas/src/Vaas/Exceptions/VaasClientException.cs
@@ -0,0 +1,21 @@
+using System;
+
+namespace Vaas.Exceptions;
+
+/// The request is malformed or cannot be completed.
+///
+/// Recommended actions:
+///
+/// - Don't repeat the request.
+/// - Log.
+/// - Analyze the error
+///
+///
+public class VaasClientException : Exception
+{
+ public VaasClientException(string? message)
+ : base(message) { }
+
+ public VaasClientException(string? message, Exception? innerException)
+ : base(message, innerException) { }
+}
diff --git a/dotnet/Vaas/src/Vaas/Exceptions/VaasServerException.cs b/dotnet/Vaas/src/Vaas/Exceptions/VaasServerException.cs
new file mode 100644
index 000000000..56259e650
--- /dev/null
+++ b/dotnet/Vaas/src/Vaas/Exceptions/VaasServerException.cs
@@ -0,0 +1,20 @@
+using System;
+
+namespace Vaas.Exceptions;
+
+/// The server encountered an internal error.
+///
+/// Recommended actions:
+///
+/// - You may retry the request after a certain delay.
+/// - If the problem persists contact G DATA.
+///
+///
+public class VaasServerException : Exception
+{
+ public VaasServerException(string? message)
+ : base(message) { }
+
+ public VaasServerException(string? message, Exception? innerException)
+ : base(message, innerException) { }
+}
diff --git a/dotnet/Vaas/src/Vaas/Messages/AuthenticationRequest.cs b/dotnet/Vaas/src/Vaas/Messages/AuthenticationRequest.cs
deleted file mode 100644
index 4296c905e..000000000
--- a/dotnet/Vaas/src/Vaas/Messages/AuthenticationRequest.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using System.Text.Json.Serialization;
-
-namespace Vaas.Messages;
-
-public class AuthenticationRequest
-{
- [JsonPropertyName("kind")] public string Kind => "AuthRequest";
-
- [JsonPropertyName("token")] public string Token { get; }
-
- [JsonPropertyName("session_id")] public string? SessionId { get; }
-
- public AuthenticationRequest(string token, string? sessionId = null)
- {
- Token = token;
- SessionId = sessionId;
- }
-}
\ No newline at end of file
diff --git a/dotnet/Vaas/src/Vaas/Messages/AuthenticationResponse.cs b/dotnet/Vaas/src/Vaas/Messages/AuthenticationResponse.cs
deleted file mode 100644
index ca47f37e3..000000000
--- a/dotnet/Vaas/src/Vaas/Messages/AuthenticationResponse.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using System.Diagnostics.CodeAnalysis;
-using System.Text.Json.Serialization;
-
-namespace Vaas.Messages;
-
-public class AuthenticationResponse
-{
- [JsonPropertyName("kind")]
- public string Kind = "AuthResponse";
-
- [JsonPropertyName("success")]
- public bool Success { get; init; }
-
- [JsonPropertyName("session_id")]
- public string? SessionId { get; init; }
-
- [JsonPropertyName("text")]
- public string? Text { get; init; }
-
- [MemberNotNullWhen(true, nameof(SessionId), nameof(Text))]
- public bool IsValid => !string.IsNullOrWhiteSpace(SessionId)
- && !string.IsNullOrWhiteSpace(Text);
-}
\ No newline at end of file
diff --git a/dotnet/Vaas/src/Vaas/Messages/FileAnalysisStarted.cs b/dotnet/Vaas/src/Vaas/Messages/FileAnalysisStarted.cs
new file mode 100644
index 000000000..be435d4b2
--- /dev/null
+++ b/dotnet/Vaas/src/Vaas/Messages/FileAnalysisStarted.cs
@@ -0,0 +1,6 @@
+namespace Vaas.Messages;
+
+public class FileAnalysisStarted
+{
+ public required ChecksumSha256 Sha256 { get; init; }
+}
diff --git a/dotnet/Vaas/src/Vaas/Messages/FileReport.cs b/dotnet/Vaas/src/Vaas/Messages/FileReport.cs
new file mode 100644
index 000000000..7233f0ef7
--- /dev/null
+++ b/dotnet/Vaas/src/Vaas/Messages/FileReport.cs
@@ -0,0 +1,10 @@
+namespace Vaas.Messages;
+
+public class FileReport
+{
+ public required ChecksumSha256 Sha256 { get; init; }
+ public required Verdict Verdict { get; init; }
+ public string? Detection { get; init; }
+ public string? FileType { get; init; }
+ public string? MimeType { get; init; }
+}
diff --git a/dotnet/Vaas/src/Vaas/Messages/Message.cs b/dotnet/Vaas/src/Vaas/Messages/Message.cs
deleted file mode 100644
index 7d51220ab..000000000
--- a/dotnet/Vaas/src/Vaas/Messages/Message.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-using System.Text.Json.Serialization;
-
-namespace Vaas.Messages;
-
-public class Message
-{
- [JsonPropertyName("kind")]
- public string? Kind { get; init; }
- public bool IsValid => !string.IsNullOrWhiteSpace(Kind);
-}
\ No newline at end of file
diff --git a/dotnet/Vaas/src/Vaas/Messages/ProblemDetails.cs b/dotnet/Vaas/src/Vaas/Messages/ProblemDetails.cs
new file mode 100644
index 000000000..e85ac20c5
--- /dev/null
+++ b/dotnet/Vaas/src/Vaas/Messages/ProblemDetails.cs
@@ -0,0 +1,12 @@
+using System.Text.Json.Serialization;
+
+namespace Vaas.Messages;
+
+public class ProblemDetails
+{
+ [JsonPropertyName("type")]
+ public string? Type { get; init; }
+
+ [JsonPropertyName("detail")]
+ public string? Detail { get; init; }
+}
diff --git a/dotnet/Vaas/src/Vaas/Messages/UrlAnalysisRequest.cs b/dotnet/Vaas/src/Vaas/Messages/UrlAnalysisRequest.cs
new file mode 100644
index 000000000..a40630f05
--- /dev/null
+++ b/dotnet/Vaas/src/Vaas/Messages/UrlAnalysisRequest.cs
@@ -0,0 +1,9 @@
+using System;
+
+namespace Vaas.Messages;
+
+public class UrlAnalysisRequest
+{
+ public required Uri Url { get; set; }
+ public bool? UseHashLookup { get; set; } = true;
+}
diff --git a/dotnet/Vaas/src/Vaas/Messages/UrlAnalysisStarted.cs b/dotnet/Vaas/src/Vaas/Messages/UrlAnalysisStarted.cs
new file mode 100644
index 000000000..58cbd0cfe
--- /dev/null
+++ b/dotnet/Vaas/src/Vaas/Messages/UrlAnalysisStarted.cs
@@ -0,0 +1,6 @@
+namespace Vaas.Messages;
+
+public class UrlAnalysisStarted
+{
+ public required string Id { get; set; }
+}
diff --git a/dotnet/Vaas/src/Vaas/Messages/UrlReport.cs b/dotnet/Vaas/src/Vaas/Messages/UrlReport.cs
new file mode 100644
index 000000000..ae55c1a01
--- /dev/null
+++ b/dotnet/Vaas/src/Vaas/Messages/UrlReport.cs
@@ -0,0 +1,13 @@
+using System;
+
+namespace Vaas.Messages;
+
+public class UrlReport
+{
+ public required ChecksumSha256 Sha256 { get; init; }
+ public required Verdict Verdict { get; init; }
+ public required Uri Url { get; set; }
+ public string? Detection { get; init; }
+ public string? FileType { get; init; }
+ public string? MimeType { get; init; }
+}
diff --git a/dotnet/Vaas/src/Vaas/Messages/VerdictRequest.cs b/dotnet/Vaas/src/Vaas/Messages/VerdictRequest.cs
deleted file mode 100644
index e62f7145d..000000000
--- a/dotnet/Vaas/src/Vaas/Messages/VerdictRequest.cs
+++ /dev/null
@@ -1,37 +0,0 @@
-using System.Collections.Generic;
-using System.Text.Json.Serialization;
-
-namespace Vaas.Messages;
-
-public class VerdictRequest
-{
- [JsonPropertyName("kind")]
- public string Kind => "VerdictRequest";
-
- [JsonPropertyName("sha256")]
- public string Sha256 { get; }
-
- [JsonPropertyName("guid")]
- public string Guid { get; }
-
- [JsonPropertyName("session_id")]
- public string SessionId { get; }
-
- [JsonPropertyName("verdict_request_attributes")]
- public Dictionary? VerdictRequestAttributes { get; set; }
-
- [JsonPropertyName("use_cache")]
- [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
- public bool? UseCache { get; init; }
-
- [JsonPropertyName("use_shed")]
- [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
- public bool? UseShed { get; init; }
-
- public VerdictRequest(string sha256, string sessionId)
- {
- Sha256 = sha256;
- SessionId = sessionId;
- Guid = System.Guid.NewGuid().ToString();
- }
-}
\ No newline at end of file
diff --git a/dotnet/Vaas/src/Vaas/Messages/VerdictRequestForStream.cs b/dotnet/Vaas/src/Vaas/Messages/VerdictRequestForStream.cs
deleted file mode 100644
index e07af587d..000000000
--- a/dotnet/Vaas/src/Vaas/Messages/VerdictRequestForStream.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-using System.Collections.Generic;
-using System.Text.Json.Serialization;
-
-namespace Vaas.Messages;
-
-public class VerdictRequestForStream
-{
- [JsonPropertyName("kind")]
- public string Kind => "VerdictRequestForStream";
-
- [JsonPropertyName("guid")]
- public string Guid { get; }
-
- [JsonPropertyName("session_id")]
- public string SessionId { get; }
-
- [JsonPropertyName("verdict_request_attributes")]
- public Dictionary? VerdictRequestAttributes { get; set; }
-
- [JsonPropertyName("use_cache")]
- [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
- public bool? UseCache { get; init; }
-
- [JsonPropertyName("use_hash_lookup")]
- [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
- public bool? UseHashLookup { get; init; }
-
- public VerdictRequestForStream(string sessionId)
- {
- SessionId = sessionId;
- Guid = System.Guid.NewGuid().ToString();
- }
-}
\ No newline at end of file
diff --git a/dotnet/Vaas/src/Vaas/Messages/VerdictRequestForUrl.cs b/dotnet/Vaas/src/Vaas/Messages/VerdictRequestForUrl.cs
deleted file mode 100644
index 2abb51da3..000000000
--- a/dotnet/Vaas/src/Vaas/Messages/VerdictRequestForUrl.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Text.Json.Serialization;
-
-namespace Vaas.Messages;
-
-public class VerdictRequestForUrl
-{
- [JsonPropertyName("kind")]
- public string Kind => "VerdictRequestForUrl";
-
- [JsonPropertyName("url")]
- public string Url { get; }
-
- [JsonPropertyName("guid")]
- public string Guid { get; }
-
- [JsonPropertyName("session_id")]
- public string SessionId { get; }
-
- [JsonPropertyName("verdict_request_attributes")]
- public Dictionary? VerdictRequestAttributes { get; set; }
-
- [JsonPropertyName("use_cache")]
- [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
- public bool? UseCache { get; init; }
-
- [JsonPropertyName("use_shed")]
- [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
- public bool? UseShed { get; init; }
-
- public VerdictRequestForUrl(Uri uri, string sessionId)
- {
- Url = uri.ToString();
- SessionId = sessionId;
- Guid = System.Guid.NewGuid().ToString();
- }
-}
\ No newline at end of file
diff --git a/dotnet/Vaas/src/Vaas/Messages/VerdictResponse.cs b/dotnet/Vaas/src/Vaas/Messages/VerdictResponse.cs
deleted file mode 100644
index 9ea289db1..000000000
--- a/dotnet/Vaas/src/Vaas/Messages/VerdictResponse.cs
+++ /dev/null
@@ -1,49 +0,0 @@
-using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
-using System.Text.Json.Serialization;
-using CommunityToolkit.Diagnostics;
-
-namespace Vaas.Messages;
-
-public class VerdictResponse
-{
- public VerdictResponse(string sha256, Verdict verdict)
- {
- Guard.IsNotNull(sha256);
- Guard.IsNotNull(verdict);
- Sha256 = sha256;
- Verdict = verdict;
- }
-
- [JsonPropertyName("kind")]
- public string Kind { get; init; } = "VerdictResponse";
-
- [JsonPropertyName("sha256")]
- public string? Sha256 { get; init; }
-
- [JsonPropertyName("guid")]
- public string? Guid { get; init; }
-
- [JsonPropertyName("verdict")]
- [JsonConverter(typeof(JsonStringEnumConverter))]
- public Verdict Verdict { get; init; }
-
- [JsonPropertyName("url")]
- public string? Url { get; init; }
-
- [JsonPropertyName("upload_token")]
- public string? UploadToken { get; init; }
-
- [JsonPropertyName("detection")]
- public string? Detection { get; init; }
-
- [JsonPropertyName("file_type")]
- public string? FileType { get; init; }
-
- [JsonPropertyName("mime_type")]
- public string? MimeType { get; init; }
-
- [MemberNotNullWhen(true, nameof(Sha256), nameof(Guid))]
- public bool IsValid => !string.IsNullOrWhiteSpace(Sha256)
- && !string.IsNullOrWhiteSpace(Guid);
-}
\ No newline at end of file
diff --git a/dotnet/Vaas/src/Vaas/Messages/WebSocketErrorMessage.cs b/dotnet/Vaas/src/Vaas/Messages/WebSocketErrorMessage.cs
deleted file mode 100644
index 7dd907604..000000000
--- a/dotnet/Vaas/src/Vaas/Messages/WebSocketErrorMessage.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-using System;
-using System.Text.Json.Serialization;
-
-namespace Vaas.Messages;
-
-public class ProblemDetails
-{
- [JsonPropertyName("type")] public string? Type { get; set; }
-
- [JsonPropertyName("detail")] public string? Detail { get; set; }
-}
-
-public class WebSocketErrorMessage : Message
-{
- [JsonPropertyName("type")] public string Type { get; }
-
- [JsonPropertyName("problem_details")] public ProblemDetails? ProblemDetails { get; init; }
-
- [JsonPropertyName("request_id")] public string? RequestId { get; set; }
-}
\ No newline at end of file
diff --git a/dotnet/Vaas/src/Vaas/Options/ForFileOptions.cs b/dotnet/Vaas/src/Vaas/Options/ForFileOptions.cs
new file mode 100644
index 000000000..5bd2d8dff
--- /dev/null
+++ b/dotnet/Vaas/src/Vaas/Options/ForFileOptions.cs
@@ -0,0 +1,18 @@
+namespace Vaas.Options;
+
+public class ForFileOptions
+{
+ public bool UseCache { get; init; } = true;
+ public bool UseHashLookup { get; init; } = true;
+ public string? VaasRequestId { get; init; }
+
+ public static ForFileOptions From(VaasOptions options)
+ {
+ return new ForFileOptions
+ {
+ UseCache = options.UseCache,
+ UseHashLookup = options.UseHashLookup,
+ VaasRequestId = null,
+ };
+ }
+}
diff --git a/dotnet/Vaas/src/Vaas/Options/ForSha256Options.cs b/dotnet/Vaas/src/Vaas/Options/ForSha256Options.cs
new file mode 100644
index 000000000..7d4dcd772
--- /dev/null
+++ b/dotnet/Vaas/src/Vaas/Options/ForSha256Options.cs
@@ -0,0 +1,19 @@
+namespace Vaas.Options;
+
+public class ForSha256Options
+{
+ public bool UseCache { get; init; } = true;
+ public bool UseHashLookup { get; init; } = true;
+
+ public string? VaasRequestId { get; init; }
+
+ public static ForSha256Options From(VaasOptions options)
+ {
+ return new ForSha256Options
+ {
+ UseCache = options.UseCache,
+ UseHashLookup = options.UseHashLookup,
+ VaasRequestId = null,
+ };
+ }
+}
diff --git a/dotnet/Vaas/src/Vaas/Options/ForStreamOptions.cs b/dotnet/Vaas/src/Vaas/Options/ForStreamOptions.cs
new file mode 100644
index 000000000..192b0301d
--- /dev/null
+++ b/dotnet/Vaas/src/Vaas/Options/ForStreamOptions.cs
@@ -0,0 +1,12 @@
+namespace Vaas.Options;
+
+public class ForStreamOptions
+{
+ public bool UseHashLookup { get; init; } = true;
+ public string? VaasRequestId { get; init; }
+
+ public static ForStreamOptions From(VaasOptions options)
+ {
+ return new ForStreamOptions { UseHashLookup = options.UseHashLookup, VaasRequestId = null };
+ }
+}
diff --git a/dotnet/Vaas/src/Vaas/Options/ForUrlOptions.cs b/dotnet/Vaas/src/Vaas/Options/ForUrlOptions.cs
new file mode 100644
index 000000000..57647c25a
--- /dev/null
+++ b/dotnet/Vaas/src/Vaas/Options/ForUrlOptions.cs
@@ -0,0 +1,12 @@
+namespace Vaas.Options;
+
+public class ForUrlOptions
+{
+ public bool UseHashLookup { get; init; } = true;
+ public string? VaasRequestId { get; init; }
+
+ public static ForUrlOptions From(VaasOptions options)
+ {
+ return new ForUrlOptions { UseHashLookup = options.UseHashLookup, VaasRequestId = null };
+ }
+}
diff --git a/dotnet/Vaas/src/Vaas/Options/VaasOptions.cs b/dotnet/Vaas/src/Vaas/Options/VaasOptions.cs
new file mode 100644
index 000000000..628e15a9f
--- /dev/null
+++ b/dotnet/Vaas/src/Vaas/Options/VaasOptions.cs
@@ -0,0 +1,13 @@
+using System;
+
+namespace Vaas.Options;
+
+public class VaasOptions
+{
+ public bool UseHashLookup { get; init; } = true;
+ public bool UseCache { get; init; } = true;
+
+ public Uri VaasUrl { get; init; } = new("https://gateway.production.vaas.gdatasecurity.de");
+
+ public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(300);
+}
diff --git a/dotnet/Vaas/src/Vaas/ServiceCollectionExtensions.cs b/dotnet/Vaas/src/Vaas/ServiceCollectionExtensions.cs
index c8560cb18..05a4945e8 100644
--- a/dotnet/Vaas/src/Vaas/ServiceCollectionExtensions.cs
+++ b/dotnet/Vaas/src/Vaas/ServiceCollectionExtensions.cs
@@ -1,31 +1,81 @@
+using System;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Options;
using Vaas.Authentication;
+using Vaas.Options;
namespace Vaas;
public static class ServiceCollectionExtensions
{
private const string SectionKey = "VerdictAsAService";
-
- public static IServiceCollection AddVerdictAsAService(this IServiceCollection services, IConfiguration configuration)
+
+ public static IServiceCollection AddVerdictAsAService(
+ this IServiceCollection services,
+ IConfiguration configuration
+ )
{
var configurationSection = configuration.GetSection(SectionKey);
- services
- .AddOptions()
- .Bind(configurationSection)
- .ValidateDataAnnotations();
-
- services
- .AddSingleton(p => p.GetRequiredService>().Value)
- .AddSingleton()
- .AddSingleton();
-
- services
- .AddTransient()
- .AddHttpClient()
- .AddHttpMessageHandler();
+
+ var optionsSection = configurationSection.GetSection("Options");
+ var vaasOptions = new VaasOptions
+ {
+ UseHashLookup = optionsSection.GetValue("UseHashLookup"),
+ UseCache = optionsSection.GetValue("UseCache"),
+ VaasUrl =
+ optionsSection.GetValue("VaasUrl")
+ ?? new Uri("https://gateway.production.vaas.gdatasecurity.de"),
+ Timeout = TimeSpan.FromSeconds(optionsSection.GetValue("Timeout")),
+ };
+
+ IAuthenticator authenticator;
+ if (
+ configurationSection.GetSection("Credentials").GetValue("GrantType")
+ == GrantType.ClientCredentials.ToString()
+ )
+ {
+ authenticator = new ClientCredentialsGrantAuthenticator(
+ configurationSection.GetSection("Credentials").GetValue("ClientId")
+ ?? throw new ArgumentException(
+ "ClientId is required in VerdictAsAService configuration"
+ ),
+ configurationSection.GetSection("Credentials").GetValue("ClientSecret")
+ ?? throw new ArgumentException(
+ "ClientSecret is required in VerdictAsAService configuration"
+ ),
+ configurationSection.GetSection("Credentials").GetValue("TokenUrl")
+ );
+ }
+ else if (
+ configurationSection.GetSection("Credentials").GetValue("GrantType")
+ == GrantType.Password.ToString()
+ )
+ {
+ authenticator = new ResourceOwnerPasswordGrantAuthenticator(
+ configurationSection.GetSection("Credentials").GetValue("ClientId")
+ ?? throw new ArgumentException(
+ "ClientId is required in VerdictAsAService configuration"
+ ),
+ configurationSection.GetSection("Credentials").GetValue("Username")
+ ?? throw new ArgumentException(
+ "UserName is required in VerdictAsAService configuration"
+ ),
+ configurationSection.GetSection("Credentials").GetValue("Password")
+ ?? throw new ArgumentException(
+ "Password is required in VerdictAsAService configuration"
+ ),
+ configurationSection.GetSection("Credentials").GetValue("TokenUrl")
+ );
+ }
+ else
+ {
+ throw new ArgumentException("GrantType must be either ClientCredentials or Password");
+ }
+
+ services.AddSingleton(authenticator);
+ services.AddSingleton(vaasOptions);
+ services.AddHttpClient();
+ services.AddSingleton();
return services;
}
diff --git a/dotnet/Vaas/src/Vaas/Vaas.cs b/dotnet/Vaas/src/Vaas/Vaas.cs
index d762bec4b..684e82122 100644
--- a/dotnet/Vaas/src/Vaas/Vaas.cs
+++ b/dotnet/Vaas/src/Vaas/Vaas.cs
@@ -1,421 +1,340 @@
using System;
-using System.Collections.Concurrent;
-using System.Collections.Generic;
using System.IO;
-using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
-using System.Net.WebSockets;
+using System.Net.Http.Json;
using System.Reflection;
-using System.Security.Cryptography;
+using System.Security.Authentication;
using System.Text.Json;
-using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
-using CommunityToolkit.Diagnostics;
using Vaas.Authentication;
+using Vaas.Exceptions;
using Vaas.Messages;
-using Websocket.Client;
-using Websocket.Client.Exceptions;
+using Vaas.Options;
namespace Vaas;
-public class ForSha256Options
-{
- public bool UseCache { get; set; } = true;
-
- public static ForSha256Options Default { get; } = new();
-}
-
public interface IVaas
{
- Task Connect(CancellationToken cancellationToken);
-
- Task ForUrlAsync(Uri uri, CancellationToken cancellationToken,
- Dictionary? verdictRequestAttributes = null);
-
+ /// Authentication failed.
/// The request is malformed or cannot be completed.
/// The server encountered an internal error.
/// The request failed due to timeout.
- Task ForSha256Async(ChecksumSha256 sha256, CancellationToken cancellationToken,
- ForSha256Options? options = null);
+ Task ForSha256Async(
+ ChecksumSha256 sha256,
+ CancellationToken cancellationToken,
+ ForSha256Options? options = null
+ );
- Task ForFileAsync(string path, CancellationToken cancellationToken,
- Dictionary? verdictRequestAttributes = null);
+ /// Authentication failed.
+ /// The request is malformed or cannot be completed.
+ /// The server encountered an internal error.
+ /// The request failed due to timeout.
+ Task ForFileAsync(
+ string path,
+ CancellationToken cancellationToken,
+ ForFileOptions? options = null
+ );
+ /// Authentication failed.
+ /// The request is malformed or cannot be completed.
+ /// The server encountered an internal error.
+ /// The request failed due to timeout.
Task ForStreamAsync(
Stream stream,
CancellationToken cancellationToken,
- Dictionary? verdictRequestAttributes = null
+ ForStreamOptions? options = null
+ );
+
+ /// Authentication failed.
+ /// The request is malformed or cannot be completed.
+ /// The server encountered an internal error.
+ /// The request failed due to timeout.
+ Task ForUrlAsync(
+ Uri uri,
+ CancellationToken cancellationToken,
+ ForUrlOptions? options = null
);
}
-public class Vaas : IDisposable, IVaas
+public class Vaas : IVaas
{
- private const int AuthenticationTimeoutInMs = 1000;
+ private const string ProductName = "Cs";
- private WebsocketClient? _client;
- private WebsocketClient AuthenticatedClient => GetAuthenticatedWebSocket();
+ private static string ProductVersion =>
+ Assembly.GetAssembly(typeof(Vaas))?.GetName().Version?.ToString() ?? "0.0.0";
private readonly HttpClient _httpClient;
-
- // Uploads use a custom token instead of the identity provider token
- // TODO: Use the identity provider token for uploads
- private readonly HttpClient _uploadHttpClient = new();
-
- private string? SessionId { get; set; }
- private bool AuthenticatedErrorOccured { get; set; }
-
- private readonly TaskCompletionSource _authenticatedSource = new();
- private Task Authenticated => _authenticatedSource.Task;
-
- private readonly ConcurrentDictionary> _verdictResponses = new();
-
private readonly IAuthenticator _authenticator;
private readonly VaasOptions _options;
- public Vaas(HttpClient httpClient, IAuthenticator authenticator, VaasOptions options)
+ public Vaas(
+ IAuthenticator authenticator,
+ VaasOptions? options = null,
+ HttpClient? httpClient = null
+ )
{
- Guard.IsNotNullOrWhiteSpace(options.Url.Host);
- _httpClient = httpClient;
_authenticator = authenticator;
- _options = options;
- _httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(ProductName, ProductVersion));
+ _options = options ?? new VaasOptions();
+ _httpClient = httpClient ?? new HttpClient();
+ _httpClient.DefaultRequestHeaders.UserAgent.Add(
+ new ProductInfoHeaderValue(ProductName, ProductVersion)
+ );
+ _httpClient.Timeout = _options.Timeout;
}
- private const string ProductName = "VaaS_C#_SDK";
-
- private static string ProductVersion =>
- Assembly.GetAssembly(typeof(Vaas))?.GetName().Version?.ToString() ?? "0.0.0";
-
- public async Task Connect(CancellationToken cancellationToken)
+ public async Task ForSha256Async(
+ ChecksumSha256 sha256,
+ CancellationToken cancellationToken,
+ ForSha256Options? options = null
+ )
{
- string token;
- try
- {
- token = await _authenticator.GetTokenAsync(cancellationToken);
- }
- catch (HttpRequestException e)
+ options ??= ForSha256Options.From(_options);
+ var reportUri = new Uri(
+ _options.VaasUrl,
+ $"/files/{sha256}/report?useCache={JsonSerializer.Serialize(options.UseCache)}&useHashLookup={JsonSerializer.Serialize(options.UseHashLookup)}"
+ );
+
+ while (true)
{
- if (e.StatusCode != null)
+ var request = new HttpRequestMessage
+ {
+ RequestUri = reportUri,
+ Method = HttpMethod.Get,
+ };
+ await AddRequestHeadersAsync(request, cancellationToken, options.VaasRequestId);
+ var response = await _httpClient.SendAsync(request, cancellationToken);
+ switch (response.StatusCode)
{
- EnsureSuccess(e.StatusCode.Value);
+ case HttpStatusCode.OK:
+ var fileReport = await response.Content.ReadFromJsonAsync(
+ cancellationToken
+ );
+ return VaasVerdict.From(
+ fileReport
+ ?? throw new VaasServerException(
+ $"Unable to deserialize FileReport {fileReport}"
+ )
+ );
+ case HttpStatusCode.Accepted:
+ continue;
+ case HttpStatusCode.Unauthorized:
+ throw new VaasAuthenticationException();
+ case HttpStatusCode.BadRequest:
+ default:
+ throw await ParseVaasError(response);
}
-
- // How can this happen?
- throw new VaasClientException("Unknown authentication error", e);
- }
-
- _client = new WebsocketClient(_options.Url, WebsocketClientFactory);
- _client.ReconnectTimeout = null;
- _client.MessageReceived.Subscribe(HandleResponseMessage);
- await _client.Start();
- if (!_client.IsStarted)
- {
- throw new WebsocketException("Could not start client");
}
-
- await Authenticate(token);
}
- private void HandleResponseMessage(ResponseMessage msg)
- {
- if (msg.MessageType != WebSocketMessageType.Text || msg.Text == null) return;
- var message = JsonSerializer.Deserialize(msg.Text);
- TaskCompletionSource? tcs;
- switch (message?.Kind)
- {
- case "AuthResponse":
- var authenticationResponse = JsonSerializer.Deserialize(msg.Text);
- if (authenticationResponse is { Success: true })
- {
- AuthenticatedErrorOccured = false;
- SessionId = authenticationResponse.SessionId;
- _authenticatedSource.SetResult();
- }
- else
- AuthenticatedErrorOccured = true;
-
- break;
-
- case "VerdictResponse":
- var options = new JsonSerializerOptions { Converters = { new JsonStringEnumConverter() } };
- var verdictResponse = JsonSerializer.Deserialize(msg.Text, options);
- if (verdictResponse is not { IsValid: true })
- {
- return;
- }
-
- if (!_verdictResponses.TryRemove(verdictResponse.Guid, out tcs))
- {
- // Error: Server sent guid we are not waiting for, ignore it
- return;
- }
-
- tcs.SetResult(verdictResponse);
- break;
-
- case "Error":
- var webSocketErrorResponse = JsonSerializer.Deserialize(msg.Text);
- var requestId = webSocketErrorResponse?.RequestId;
- if (requestId == null || !_verdictResponses.TryRemove(requestId, out tcs))
- {
- return;
- }
-
- var problemDetails = webSocketErrorResponse?.ProblemDetails;
- tcs.SetException(ProblemDetailsToException(problemDetails));
- break;
- }
- }
-
- private static Exception ProblemDetailsToException(ProblemDetails? problemDetails) => problemDetails?.Type switch
- {
- "VaasClientException" => new VaasClientException(problemDetails.Detail),
- _ => new VaasServerException(problemDetails?.Detail)
- };
-
- private async Task Authenticate(string token)
- {
- var authenticationRequest = new AuthenticationRequest(token);
- var jsonString = JsonSerializer.Serialize(authenticationRequest);
- _client?.Send(jsonString);
-
- var delay = Task.Delay(AuthenticationTimeoutInMs);
- if (await Task.WhenAny(Authenticated, delay) == delay)
- {
- throw new VaasAuthenticationException();
- }
- }
-
- public async Task ForUrlAsync(Uri uri, CancellationToken cancellationToken,
- Dictionary? verdictRequestAttributes = null)
+ private async Task AddRequestHeadersAsync(
+ HttpRequestMessage request,
+ CancellationToken cancellationToken,
+ string? requestId = null
+ )
{
- var verdictResponse = await ForUrlRequestAsync(
- new VerdictRequestForUrl(uri, SessionId ?? throw new VaasInvalidStateException())
- {
- UseCache = _options.UseCache,
- UseShed = _options.UseHashLookup,
- VerdictRequestAttributes = verdictRequestAttributes
- });
- return new VaasVerdict(verdictResponse);
+ var token = await _authenticator.GetTokenAsync(cancellationToken);
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
+ if (!string.IsNullOrWhiteSpace(requestId))
+ request.Headers.Add("tracestate", $"vaasrequestid={requestId}");
}
- public async Task ForStreamAsync(
- Stream stream,
+ public async Task ForFileAsync(
+ string path,
CancellationToken cancellationToken,
- Dictionary? verdictRequestAttributes = null
+ ForFileOptions? options = null
)
{
- if (stream == null)
- throw new VaasClientException("Stream was null.");
+ if (!File.Exists(path))
+ throw new VaasClientException("File does not exist: " + path);
- var verdictResponse = await ForStreamRequestAsync(
- new VerdictRequestForStream(SessionId ?? throw new InvalidOperationException())
- {
- UseCache = _options.UseCache,
- UseHashLookup = _options.UseHashLookup,
- VerdictRequestAttributes = verdictRequestAttributes
- });
- if (!verdictResponse.IsValid)
- throw new JsonException("VerdictResponse is not valid");
- if (verdictResponse.Verdict != Verdict.Unknown)
- throw new VaasServerException("Server returned verdict without receiving content.");
-
- if (
- string.IsNullOrWhiteSpace(verdictResponse.Url)
- || string.IsNullOrWhiteSpace(verdictResponse.UploadToken)
- )
- {
- throw new JsonException(
- "VerdictResponse missing URL or UploadToken for stream upload."
- );
- }
-
- var response = WaitForResponseAsync(verdictResponse.Guid);
- await UploadStream(stream, verdictResponse.Url, verdictResponse.UploadToken, cancellationToken);
+ options ??= ForFileOptions.From(_options);
- return new VaasVerdict(await response);
- }
-
- private async Task UploadStream(Stream stream, string url, string token, CancellationToken cancellationToken)
- {
- using var requestContent = new StreamContent(stream);
- using var requestMessage = new HttpRequestMessage(HttpMethod.Put, url);
- requestMessage.Version = HttpVersion.Version11;
- requestMessage.VersionPolicy = HttpVersionPolicy.RequestVersionOrLower;
- requestMessage.Content = requestContent;
- requestMessage.Headers.Authorization = new AuthenticationHeaderValue(token);
-
- var response = await _uploadHttpClient.SendAsync(requestMessage, cancellationToken);
- if (!response.IsSuccessStatusCode)
+ if (options.UseCache || options.UseHashLookup)
{
- var responseBody = await response.Content.ReadAsStringAsync(cancellationToken);
- ProblemDetails? problemDetails;
+ var forSha256Options = new ForSha256Options
+ {
+ VaasRequestId = options.VaasRequestId,
+ UseHashLookup = options.UseHashLookup,
+ UseCache = options.UseCache,
+ };
+
try
{
- problemDetails = JsonSerializer.Deserialize(responseBody);
+ var sha256 = ChecksumSha256.Sha256CheckSum(path);
+ var response = await ForSha256Async(sha256, cancellationToken, forSha256Options);
+ var verdictWithoutDetection =
+ response.Verdict is Verdict.Malicious or Verdict.Pup
+ && string.IsNullOrEmpty(response.Detection);
+ if (
+ response.Verdict != Verdict.Unknown
+ && !verdictWithoutDetection
+ && !string.IsNullOrWhiteSpace(response.FileType)
+ && !string.IsNullOrEmpty(response.MimeType)
+ )
+ {
+ return response;
+ }
}
- catch (JsonException)
+ catch (Exception)
{
- throw new VaasServerException("Server did not return proper ProblemDetails");
+ // ignore
}
-
- throw ProblemDetailsToException(problemDetails);
}
- }
- public async Task ForSha256Async(ChecksumSha256 sha256, CancellationToken cancellationToken,
- ForSha256Options? options = null)
- {
- var verdictResponse = await ForRequestAsync(
- new VerdictRequest(sha256, SessionId ?? throw new InvalidOperationException())
- {
- UseCache = _options.UseCache,
- UseShed = _options.UseHashLookup,
- VerdictRequestAttributes = null
- });
- if (!verdictResponse.IsValid)
- throw new JsonException("VerdictResponse is not valid");
- return new VaasVerdict(verdictResponse);
- }
-
- private static void EnsureSuccess(HttpStatusCode status)
- {
- switch ((int)status)
+ await using var stream = File.OpenRead(path);
+ var forStreamOptions = new ForStreamOptions
{
- case 401 or 403:
- throw new VaasAuthenticationException();
- case >= 400 and < 500:
- throw new VaasClientException("Client-side error");
- case >= 500 and < 600:
- throw new VaasServerException("Server-side error");
- }
- }
-
- public async Task ForFileAsync(string path, CancellationToken cancellationToken, Dictionary? verdictRequestAttributes = null)
- {
- var sha256 = Sha256CheckSum(path);
- var verdictResponse = await ForRequestAsync(
- new VerdictRequest(sha256, SessionId ?? throw new InvalidOperationException())
- {
- UseCache = _options.UseCache,
- UseShed = _options.UseHashLookup,
- VerdictRequestAttributes = verdictRequestAttributes
- });
- if (!verdictResponse.IsValid)
- throw new JsonException("VerdictResponse is not valid");
- if (verdictResponse.Verdict != Verdict.Unknown)
- return new VaasVerdict(verdictResponse);
- if (string.IsNullOrWhiteSpace(verdictResponse.Url) ||
- string.IsNullOrWhiteSpace(verdictResponse.UploadToken))
- {
- throw new JsonException("VerdictResponse is not valid");
- }
-
- var response = WaitForResponseAsync(verdictResponse.Guid);
- await UploadFile(path, verdictResponse.Url, verdictResponse.UploadToken, cancellationToken);
-
- return new VaasVerdict(await response);
+ VaasRequestId = options.VaasRequestId,
+ UseHashLookup = options.UseHashLookup,
+ };
+ return await ForStreamAsync(stream, cancellationToken, forStreamOptions);
}
- private async Task UploadFile(string path, string url, string token, CancellationToken cancellationToken)
+ public async Task ForStreamAsync(
+ Stream stream,
+ CancellationToken cancellationToken,
+ ForStreamOptions? options = null
+ )
{
- await using var fileStream = File.OpenRead(path);
- await UploadStream(fileStream, url, token, cancellationToken);
- }
+ options ??= ForStreamOptions.From(_options);
- public async Task> ForSha256ListAsync(IEnumerable sha256List,
- CancellationToken cancellationToken)
- {
- return (await Task.WhenAll(sha256List.Select(async sha256 =>
- await ForSha256Async(new ChecksumSha256(sha256), cancellationToken)))).ToList();
- }
+ var url = new Uri(
+ _options.VaasUrl,
+ $"/files?useHashLookup={JsonSerializer.Serialize(options.UseHashLookup)}&useCache={JsonSerializer.Serialize(true)}"
+ );
- public async Task> ForFileListAsync(IEnumerable fileList,
- CancellationToken cancellationToken)
- {
- return (await Task.WhenAll(fileList.Select(async filePath => await ForFileAsync(filePath, cancellationToken))))
- .ToList();
- }
-
- private async Task ForRequestAsync(VerdictRequest verdictRequest)
- {
- var jsonString = JsonSerializer.Serialize(verdictRequest);
- AuthenticatedClient.Send(jsonString);
+ var request = new HttpRequestMessage
+ {
+ RequestUri = url,
+ Method = HttpMethod.Post,
+ Content = new StreamContent(stream),
+ };
+ await AddRequestHeadersAsync(request, cancellationToken, options.VaasRequestId);
- return await WaitForResponseAsync(verdictRequest.Guid);
- }
+ var response = await _httpClient.SendAsync(request, cancellationToken);
+ if (!response.IsSuccessStatusCode)
+ await ParseVaasError(response);
- private async Task ForStreamRequestAsync(VerdictRequestForStream verdictRequest)
- {
- var jsonString = JsonSerializer.Serialize(verdictRequest);
- AuthenticatedClient.Send(jsonString);
+ var fileAnalysisStarted = await response.Content.ReadFromJsonAsync(
+ cancellationToken
+ );
- return await WaitForResponseAsync(verdictRequest.Guid);
- }
+ var forSha256Options = new ForSha256Options
+ {
+ VaasRequestId = options.VaasRequestId,
+ UseHashLookup = options.UseHashLookup,
+ };
- private async Task ForUrlRequestAsync(VerdictRequestForUrl verdictRequestForUrl)
- {
- var jsonString = JsonSerializer.Serialize(verdictRequestForUrl);
- AuthenticatedClient.Send(jsonString);
+ if (fileAnalysisStarted?.Sha256 != null)
+ return await ForSha256Async(
+ fileAnalysisStarted.Sha256,
+ cancellationToken,
+ forSha256Options
+ );
- return await WaitForResponseAsync(verdictRequestForUrl.Guid);
+ throw new VaasServerException(
+ "Unexpected response from Vaas server, expected Sha256 in response: "
+ + response.StatusCode
+ );
}
- private Task WaitForResponseAsync(string guid)
+ public async Task ForUrlAsync(
+ Uri uri,
+ CancellationToken cancellationToken,
+ ForUrlOptions? options = null
+ )
{
- var tcs = _verdictResponses.GetOrAdd(guid, _ => new TaskCompletionSource());
- return tcs.Task;
- }
+ options ??= ForUrlOptions.From(_options);
+ var urlAnalysisUri = new Uri(_options.VaasUrl, "/urls");
- public static string Sha256CheckSum(string filePath)
- {
- using var sha256 = SHA256.Create();
- using var fileStream = File.OpenRead(filePath);
- return Convert.ToHexString(sha256.ComputeHash(fileStream)).ToLower();
- }
+ var urlAnalysisRequest = new HttpRequestMessage
+ {
+ RequestUri = urlAnalysisUri,
+ Method = HttpMethod.Post,
+ Content = JsonContent.Create(
+ new UrlAnalysisRequest { Url = uri, UseHashLookup = options.UseHashLookup }
+ ),
+ };
- private static ClientWebSocket WebsocketClientFactory()
- {
- var clientWebSocket = new ClientWebSocket
+ await AddRequestHeadersAsync(urlAnalysisRequest, cancellationToken, options.VaasRequestId);
+ var urlAnalysisResponse = await _httpClient.SendAsync(
+ urlAnalysisRequest,
+ cancellationToken
+ );
+ if (!urlAnalysisResponse.IsSuccessStatusCode)
+ await ParseVaasError(urlAnalysisResponse);
+
+ var id = (
+ await urlAnalysisResponse.Content.ReadFromJsonAsync(
+ cancellationToken
+ )
+ )?.Id;
+
+ while (true)
{
- Options =
+ var reportUri = new Uri(_options.VaasUrl, $"/urls/{id}/report");
+ var reportRequest = new HttpRequestMessage
{
- KeepAliveInterval = TimeSpan.FromSeconds(20)
- }
- };
- return clientWebSocket;
- }
+ RequestUri = reportUri,
+ Method = HttpMethod.Get,
+ };
- protected virtual void Dispose(bool disposing)
- {
- if (!disposing) return;
+ await AddRequestHeadersAsync(reportRequest, cancellationToken, options.VaasRequestId);
+ var reportResponse = await _httpClient.SendAsync(reportRequest, cancellationToken);
- _client?.Dispose();
- _httpClient.Dispose();
+ switch (reportResponse.StatusCode)
+ {
+ case HttpStatusCode.OK:
+ var urlReport = await reportResponse.Content.ReadFromJsonAsync(
+ cancellationToken
+ );
+ return VaasVerdict.From(
+ urlReport
+ ?? throw new VaasServerException(
+ $"Unable to deserialize UrlReport {urlReport}"
+ )
+ );
+ case HttpStatusCode.Accepted:
+ continue;
+ case HttpStatusCode.Unauthorized:
+ throw new VaasAuthenticationException();
+ case HttpStatusCode.BadRequest:
+ default:
+ throw await ParseVaasError(reportResponse);
+ }
+ }
}
- public void Dispose()
- {
- Dispose(true);
- GC.SuppressFinalize(this);
- }
+ private static Exception ProblemDetailsToException(ProblemDetails? problemDetails) =>
+ problemDetails?.Type switch
+ {
+ "VaasClientException" => new VaasClientException(problemDetails.Detail),
+ _ => new VaasServerException(problemDetails?.Detail),
+ };
- private WebsocketClient GetAuthenticatedWebSocket()
+ private static async Task ParseVaasError(HttpResponseMessage response)
{
- if (_client == null)
- throw new VaasInvalidStateException();
- if (!_client.IsRunning)
- throw new VaasConnectionClosedException();
- if (SessionId == null)
+ var responseBody = await response.Content.ReadAsStringAsync();
+ try
{
- if (AuthenticatedErrorOccured)
- throw new VaasAuthenticationException();
- throw new VaasInvalidStateException();
+ var problemDetails = JsonSerializer.Deserialize(responseBody);
+ throw ProblemDetailsToException(problemDetails);
+ }
+ catch (JsonException)
+ {
+ throw (int)response.StatusCode switch
+ {
+ 401 => new VaasAuthenticationException(
+ "server did not accept token from identity provider. Check if you are using the correct identity provider"
+ ),
+ >= 400 and <= 500 => new VaasClientException(
+ "HTTP Error: " + (int)response.StatusCode
+ ),
+ _ => new VaasServerException("HTTP Error: " + (int)response.StatusCode),
+ };
}
-
- return _client;
}
-}
\ No newline at end of file
+}
diff --git a/dotnet/Vaas/src/Vaas/Vaas.csproj b/dotnet/Vaas/src/Vaas/Vaas.csproj
index 732e0a0de..a4c4b3d94 100644
--- a/dotnet/Vaas/src/Vaas/Vaas.csproj
+++ b/dotnet/Vaas/src/Vaas/Vaas.csproj
@@ -29,8 +29,6 @@
-
-
diff --git a/dotnet/Vaas/src/Vaas/VaasFactory.cs b/dotnet/Vaas/src/Vaas/VaasFactory.cs
index 0a55942f6..834bab281 100644
--- a/dotnet/Vaas/src/Vaas/VaasFactory.cs
+++ b/dotnet/Vaas/src/Vaas/VaasFactory.cs
@@ -1,21 +1,21 @@
using System.Net.Http;
using Vaas.Authentication;
+using Vaas.Options;
namespace Vaas;
public static class VaasFactory
{
- private static readonly HttpClient HttpClient = new ();
-
- public static IVaas Create(VaasOptions vaasOptions)
+ public static IVaas Create(
+ IAuthenticator authenticator,
+ VaasOptions? vaasOptions = null,
+ HttpClient? httpClient = null
+ )
{
- var systemClock = new SystemClock();
- var authenticator = new Authenticator(HttpClient, systemClock, vaasOptions);
-
- var bearerTokenHandler = new BearerTokenHandler(authenticator);
- bearerTokenHandler.InnerHandler = new HttpClientHandler();
- var httpClient = new HttpClient(bearerTokenHandler);
-
- return new Vaas(httpClient, authenticator, vaasOptions);
+ return new Vaas(
+ authenticator,
+ vaasOptions ?? new VaasOptions(),
+ httpClient ?? new HttpClient()
+ );
}
-}
\ No newline at end of file
+}
diff --git a/dotnet/Vaas/src/Vaas/VaasOptions.cs b/dotnet/Vaas/src/Vaas/VaasOptions.cs
deleted file mode 100644
index 008ce768c..000000000
--- a/dotnet/Vaas/src/Vaas/VaasOptions.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using System;
-using System.ComponentModel.DataAnnotations;
-using Vaas.Authentication;
-
-namespace Vaas;
-
-public class VaasOptions
-{
- public Uri Url { get; set; } = new("https://upload.production.vaas.gdatasecurity.de");
- public bool? UseHashLookup { get; init; } = null;
- public bool? UseCache { get; init; } = null;
-
- public Uri TokenUrl { get; set; } =
- new Uri("https://account.gdata.de/realms/vaas-production/protocol/openid-connect/token");
-
- [Required]
- [CustomValidation(typeof(TokenRequest), nameof(TokenRequest.IsValid))]
- public TokenRequest Credentials { get; set; } = null!;
-}
\ No newline at end of file
diff --git a/dotnet/Vaas/src/Vaas/VaasVerdict.cs b/dotnet/Vaas/src/Vaas/VaasVerdict.cs
index deb500e85..217dad5a9 100644
--- a/dotnet/Vaas/src/Vaas/VaasVerdict.cs
+++ b/dotnet/Vaas/src/Vaas/VaasVerdict.cs
@@ -1,12 +1,39 @@
-using System.Collections.Generic;
+using System.Text.Json;
+using Vaas.Messages;
-namespace Vaas.Messages;
+namespace Vaas;
-public class VaasVerdict(VerdictResponse verdictResponse)
+public class VaasVerdict
{
- public string Sha256 { get; init; } = verdictResponse.Sha256 ?? "";
- public Verdict Verdict { get; init; } = verdictResponse.Verdict;
- public string? Detection { get; init; } = verdictResponse.Detection;
- public string? MimeType { get; init; } = verdictResponse.FileType;
- public string? FileType { get; init; } = verdictResponse.MimeType;
+ public required string Sha256 { get; init; }
+ public Verdict Verdict { get; init; }
+ public string? Detection { get; init; }
+ public string? MimeType { get; init; }
+ public string? FileType { get; init; }
+
+ public static VaasVerdict From(FileReport fileReport)
+ {
+ return new VaasVerdict
+ {
+ Sha256 = fileReport.Sha256,
+ Verdict = fileReport.Verdict,
+ Detection = fileReport.Detection,
+ MimeType = fileReport.MimeType,
+ FileType = fileReport.FileType,
+ };
+ }
+
+ public static VaasVerdict From(UrlReport urlReport)
+ {
+ return new VaasVerdict
+ {
+ Sha256 = urlReport.Sha256,
+ Verdict = urlReport.Verdict,
+ Detection = urlReport.Detection,
+ MimeType = urlReport.MimeType,
+ FileType = urlReport.FileType,
+ };
+ }
+
+ public override string ToString() => JsonSerializer.Serialize(this);
}
diff --git a/dotnet/Vaas/src/Vaas/Verdict.cs b/dotnet/Vaas/src/Vaas/Verdict.cs
index 9def1f7d0..7cb6fc236 100644
--- a/dotnet/Vaas/src/Vaas/Verdict.cs
+++ b/dotnet/Vaas/src/Vaas/Verdict.cs
@@ -1,6 +1,12 @@
+using System.Text.Json.Serialization;
+
namespace Vaas;
+[JsonConverter(typeof(JsonStringEnumConverter))]
public enum Verdict
{
- Clean, Unknown, Malicious, Pup
-}
\ No newline at end of file
+ Clean,
+ Unknown,
+ Malicious,
+ Pup,
+}
diff --git a/dotnet/Vaas/test/TestFiles/Program.cs b/dotnet/Vaas/test/TestFiles/Program.cs
index 700837b0d..9596548ff 100644
--- a/dotnet/Vaas/test/TestFiles/Program.cs
+++ b/dotnet/Vaas/test/TestFiles/Program.cs
@@ -1,5 +1,6 @@
using Vaas;
using Vaas.Authentication;
+using Vaas.Options;
if (args.Length == 0)
{
@@ -7,7 +8,7 @@
Environment.Exit(1);
}
-var vaas = await AuthenticateWithCredentials();
+var vaas = AuthenticateWithCredentials();
foreach (var path in args)
{
@@ -16,31 +17,31 @@
Console.WriteLine($"Tested {path}: Verdict {verdict}");
}
-static async Task AuthenticateWithCredentials()
+return;
+
+static IVaas AuthenticateWithCredentials()
{
DotNetEnv.Env.NoClobber().TraversePath().Load();
var url = DotNetEnv.Env.GetString(
"VAAS_URL",
- "wss://gateway.production.vaas.gdatasecurity.de");
- var tokenEndpoint = new Uri(DotNetEnv.Env.GetString(
- "TOKEN_URL",
- "https://account.gdata.de/realms/vaas-production/protocol/openid-connect/token"));
+ "https://gateway.production.vaas.gdatasecurity.de"
+ );
+ var tokenEndpoint = new Uri(
+ DotNetEnv.Env.GetString(
+ "TOKEN_URL",
+ "https://account.gdata.de/realms/vaas-production/protocol/openid-connect/token"
+ )
+ );
var clientId = DotNetEnv.Env.GetString("CLIENT_ID");
var clientSecret = DotNetEnv.Env.GetString("CLIENT_SECRET");
-
- var vaas = VaasFactory.Create(new VaasOptions()
- {
- Url = new Uri(url),
- TokenUrl = tokenEndpoint,
- Credentials = new TokenRequest
- {
- GrantType = GrantType.ClientCredentials,
- ClientId = clientId,
- ClientSecret = clientSecret,
- }
- });
-
- await vaas.Connect(CancellationToken.None);
- Console.WriteLine($"Connected to Vaas {url}", url);
+
+ var authenticator = new ClientCredentialsGrantAuthenticator(
+ clientId,
+ clientSecret,
+ tokenEndpoint
+ );
+
+ var vaas = VaasFactory.Create(authenticator, new VaasOptions { VaasUrl = new Uri(url) });
+
return vaas;
-}
\ No newline at end of file
+}
diff --git a/dotnet/Vaas/test/TestFiles/create_big_sample.sh b/dotnet/Vaas/test/TestFiles/create_big_sample.sh
index 570fb1492..4590a5bb5 100644
--- a/dotnet/Vaas/test/TestFiles/create_big_sample.sh
+++ b/dotnet/Vaas/test/TestFiles/create_big_sample.sh
@@ -13,7 +13,7 @@ then
exit 1
fi
-dd if=/dev/urandom of=big/big.dat bs=1M count=$1
+dd if=/dev/urandom of=big/big.dat bs=1M count="$1"
curl https://secure.eicar.org/eicar.com.txt -o big/eicar.com.txt
zip -0 -r big big/
diff --git a/dotnet/Vaas/test/Vaas.Test/Authentication/AuthenticationEnvironment.cs b/dotnet/Vaas/test/Vaas.Test/Authentication/AuthenticationEnvironment.cs
index 3e05ab98a..3fcd43d7d 100644
--- a/dotnet/Vaas/test/Vaas.Test/Authentication/AuthenticationEnvironment.cs
+++ b/dotnet/Vaas/test/Vaas.Test/Authentication/AuthenticationEnvironment.cs
@@ -4,13 +4,18 @@ namespace Vaas.Test.Authentication;
public static class AuthenticationEnvironment
{
- public static Uri TokenUrl => new Uri(DotNetEnv.Env.GetString(
- "TOKEN_URL",
- "https://account.gdata.de/realms/vaas-production/protocol/openid-connect/token"));
+ public static Uri TokenUrl =>
+ new(
+ DotNetEnv.Env.GetString(
+ "TOKEN_URL",
+ "https://account.gdata.de/realms/vaas-production/protocol/openid-connect/token"
+ )
+ );
public static string ClientId => DotNetEnv.Env.GetString("CLIENT_ID");
public static string ClientSecret => DotNetEnv.Env.GetString("CLIENT_SECRET");
- public static string ClientIdForResourceOwnerPasswordGrant => DotNetEnv.Env.GetString("VAAS_CLIENT_ID");
+ public static string ClientIdForResourceOwnerPasswordGrant =>
+ DotNetEnv.Env.GetString("VAAS_CLIENT_ID");
public static string UserName => DotNetEnv.Env.GetString("VAAS_USER_NAME");
public static string Password => DotNetEnv.Env.GetString("VAAS_PASSWORD");
}
diff --git a/dotnet/Vaas/test/Vaas.Test/Authentication/AuthenticatorTest.cs b/dotnet/Vaas/test/Vaas.Test/Authentication/AuthenticatorTest.cs
index 5ce49d66f..f616ffb46 100644
--- a/dotnet/Vaas/test/Vaas.Test/Authentication/AuthenticatorTest.cs
+++ b/dotnet/Vaas/test/Vaas.Test/Authentication/AuthenticatorTest.cs
@@ -1,8 +1,13 @@
using System;
+using System.Diagnostics;
+using System.Net;
using System.Net.Http;
+using System.Security.Authentication;
using System.Threading;
using System.Threading.Tasks;
+using FluentAssertions;
using Moq;
+using Moq.Contrib.HttpClient;
using Vaas.Authentication;
using Xunit;
@@ -12,8 +17,10 @@ public class CountingDelegatingHandler : DelegatingHandler
{
public int Requests { get; private set; }
- protected override async Task SendAsync(HttpRequestMessage request,
- CancellationToken cancellationToken)
+ protected override async Task SendAsync(
+ HttpRequestMessage request,
+ CancellationToken cancellationToken
+ )
{
Requests++;
return await base.SendAsync(request, cancellationToken);
@@ -24,15 +31,59 @@ public class AuthenticatorTest
{
private readonly CountingDelegatingHandler _handler = new();
private readonly Mock _systemClock = new();
- private readonly Authenticator _authenticator;
+ private readonly HttpClient _httpClient;
+ private readonly ClientCredentialsGrantAuthenticator _authenticator;
public AuthenticatorTest()
{
DotNetEnv.Env.TraversePath().Load();
_handler.InnerHandler = new HttpClientHandler();
- var httpClient = new HttpClient(_handler);
+ _httpClient = new HttpClient(_handler);
_systemClock.Setup(x => x.UtcNow).Returns(() => DateTimeOffset.UtcNow);
- _authenticator = new Authenticator(httpClient, _systemClock.Object, GetVaasOptions());
+ _authenticator = GetClientCredentialsGrantAuthenticator();
+ }
+
+ private ClientCredentialsGrantAuthenticator GetClientCredentialsGrantAuthenticator()
+ {
+ return new ClientCredentialsGrantAuthenticator(
+ AuthenticationEnvironment.ClientId,
+ AuthenticationEnvironment.ClientSecret,
+ AuthenticationEnvironment.TokenUrl,
+ _httpClient,
+ _systemClock.Object
+ );
+ }
+
+ private ResourceOwnerPasswordGrantAuthenticator GetResourceOwnerPasswordGrantAuthenticator()
+ {
+ return new ResourceOwnerPasswordGrantAuthenticator(
+ AuthenticationEnvironment.ClientIdForResourceOwnerPasswordGrant,
+ AuthenticationEnvironment.UserName,
+ AuthenticationEnvironment.Password,
+ AuthenticationEnvironment.TokenUrl,
+ _httpClient,
+ _systemClock.Object
+ );
+ }
+
+ [Fact]
+ public async Task GetTokenAsync_WithClientCredentials_ReturnsToken()
+ {
+ var authenticator = GetClientCredentialsGrantAuthenticator();
+
+ _ = await authenticator.GetTokenAsync(CancellationToken.None);
+
+ Assert.Equal(1, _handler.Requests);
+ }
+
+ [Fact]
+ public async Task GetTokenAsync_WithPassword_ReturnsToken()
+ {
+ var authenticator = GetResourceOwnerPasswordGrantAuthenticator();
+
+ _ = await authenticator.GetTokenAsync(CancellationToken.None);
+
+ Assert.Equal(1, _handler.Requests);
}
[Fact]
@@ -40,26 +91,139 @@ public async Task GetTokenAsync_IfTokenNotExpired_ReturnsLastToken()
{
_ = await _authenticator.GetTokenAsync(CancellationToken.None);
_ = await _authenticator.GetTokenAsync(CancellationToken.None);
+
Assert.Equal(1, _handler.Requests);
}
- private VaasOptions GetVaasOptions() => new()
+ [Fact]
+ public async Task GetTokenAsync_IfTokenExpired_GetsNewToken()
{
- TokenUrl = AuthenticationEnvironment.TokenUrl,
- Credentials = new()
- {
- GrantType = GrantType.ClientCredentials,
- ClientId = AuthenticationEnvironment.ClientId,
- ClientSecret = AuthenticationEnvironment.ClientSecret
- }
- };
+ _ = await _authenticator.GetTokenAsync(CancellationToken.None);
+ _systemClock
+ .Setup(x => x.UtcNow)
+ .Returns(() => DateTimeOffset.UtcNow + TimeSpan.FromHours(1));
+
+ _ = await _authenticator.GetTokenAsync(CancellationToken.None);
+
+ Assert.Equal(2, _handler.Requests);
+ }
[Fact]
- public async Task GetTokenAsync_IfTokenExpired_RefreshesToken()
+ public async Task GetTokenAsync_IfClockSkew_ReusesToken()
{
+ _systemClock
+ .Setup(x => x.UtcNow)
+ .Returns(() => DateTimeOffset.UtcNow + TimeSpan.FromHours(1));
_ = await _authenticator.GetTokenAsync(CancellationToken.None);
- _systemClock.Setup(x => x.UtcNow).Returns(() => DateTimeOffset.UtcNow + TimeSpan.FromHours(1));
_ = await _authenticator.GetTokenAsync(CancellationToken.None);
- Assert.Equal(2, _handler.Requests);
+ Assert.Equal(1, _handler.Requests);
}
-}
\ No newline at end of file
+
+ [Fact]
+ public async Task GetTokenAsync_IfNoExpiresIn_ThrowsAuthenticationException()
+ {
+ var handlerMock = UseHttpMessageHandlerMock();
+ handlerMock
+ .SetupRequest(HttpMethod.Post, AuthenticationEnvironment.TokenUrl)
+ .ReturnsResponse("""{"access_token": "My great token"}""");
+
+ var e = await Assert.ThrowsAsync(
+ () => _authenticator.GetTokenAsync(CancellationToken.None)
+ );
+
+ e.Message.Should().Be("Identity provider did not return expires_in");
+ }
+
+ [Fact]
+ public async Task GetTokenAsync_IfUnauthorized_ThrowsAuthenticationException()
+ {
+ var handlerMock = UseHttpMessageHandlerMock();
+ MockUnauthorizedClient(handlerMock);
+
+ var e = await Assert.ThrowsAsync(
+ () => _authenticator.GetTokenAsync(CancellationToken.None)
+ );
+
+ e.Message.Should()
+ .Be(
+ "Identity provider returned status code 401: Invalid client or Invalid client credentials"
+ );
+ }
+
+ private static void MockUnauthorizedClient(Mock handlerMock)
+ {
+ handlerMock
+ .SetupRequest(HttpMethod.Post, AuthenticationEnvironment.TokenUrl)
+ .ReturnsResponse(
+ """{"error":"unauthorized_client","error_description":"Invalid client or Invalid client credentials"}""",
+ configure: response =>
+ {
+ response.StatusCode = HttpStatusCode.Unauthorized;
+ }
+ );
+ }
+
+ [Fact]
+ public async Task GetTokenAsync_IfHttpError_ThrowsAuthenticationException()
+ {
+ var handlerMock = UseHttpMessageHandlerMock();
+ handlerMock
+ .SetupRequest(HttpMethod.Post, AuthenticationEnvironment.TokenUrl)
+ .ReturnsResponse(HttpStatusCode.InternalServerError);
+
+ var e = await Assert.ThrowsAsync(
+ () => _authenticator.GetTokenAsync(CancellationToken.None)
+ );
+
+ e.Message.Should()
+ .Be(
+ "Identity provider returned status code 500: The input does not contain any JSON tokens. Expected the input to start with a valid JSON token, when isFinalBlock is true. Path: $ | LineNumber: 0 | BytePositionInLine: 0."
+ );
+ }
+
+ [Fact]
+ public async Task GetTokenAsync_IfHttpRequestException_ThrowsAuthenticationException()
+ {
+ var handlerMock = UseHttpMessageHandlerMock();
+ handlerMock
+ .SetupRequest(HttpMethod.Post, AuthenticationEnvironment.TokenUrl)
+ .Throws(
+ new HttpRequestException(
+ "Name or service not known (dsdkfsdufsdufoweuiruierlknclxoijfiowejf.de:80)"
+ )
+ );
+
+ var e = await Assert.ThrowsAsync(
+ () => _authenticator.GetTokenAsync(CancellationToken.None)
+ );
+ e.Message.Should().Be("Failed to request token");
+ e.InnerException.Should().BeOfType();
+ e.InnerException!.Message.Should()
+ .Be("Name or service not known (dsdkfsdufsdufoweuiruierlknclxoijfiowejf.de:80)");
+ }
+
+ private Mock UseHttpMessageHandlerMock()
+ {
+ var handlerMock = new Mock();
+ _handler.InnerHandler = handlerMock.Object;
+ return handlerMock;
+ }
+
+ [Fact]
+ public async Task GetTokenAsync_IfLastRequestFailed_Waits1sBeforeNextRequest()
+ {
+ var handlerMock = UseHttpMessageHandlerMock();
+ MockUnauthorizedClient(handlerMock);
+ var staticNow = DateTimeOffset.UtcNow;
+ _systemClock.Setup(x => x.UtcNow).Returns(() => staticNow);
+
+ await Assert.ThrowsAsync(
+ () => _authenticator.GetTokenAsync(CancellationToken.None)
+ );
+ var sw = Stopwatch.StartNew();
+ await Assert.ThrowsAsync(
+ () => _authenticator.GetTokenAsync(CancellationToken.None)
+ );
+ sw.Elapsed.Should().BeGreaterThanOrEqualTo(TimeSpan.FromSeconds(1));
+ }
+}
diff --git a/dotnet/Vaas/test/Vaas.Test/Authentication/TokenResponseTest.cs b/dotnet/Vaas/test/Vaas.Test/Authentication/TokenResponseTest.cs
index 76e80b819..0e16935d4 100644
--- a/dotnet/Vaas/test/Vaas.Test/Authentication/TokenResponseTest.cs
+++ b/dotnet/Vaas/test/Vaas.Test/Authentication/TokenResponseTest.cs
@@ -1,10 +1,9 @@
using System;
using System.Text.Json;
using Vaas.Authentication;
-using Vaas.Messages;
using Xunit;
-namespace Vaas.Test;
+namespace Vaas.Test.Authentication;
public class TokenResponseTest
{
@@ -13,4 +12,4 @@ public void Deserialize_IfFieldIsMissing_ThrowsArgumentNullException()
{
Assert.Throws(() => JsonSerializer.Deserialize("{}"));
}
-}
\ No newline at end of file
+}
diff --git a/dotnet/Vaas/test/Vaas.Test/IntegrationTests.cs b/dotnet/Vaas/test/Vaas.Test/IntegrationTests.cs
deleted file mode 100644
index a9dcc1035..000000000
--- a/dotnet/Vaas/test/Vaas.Test/IntegrationTests.cs
+++ /dev/null
@@ -1,338 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Net.Http;
-using System.Threading;
-using System.Threading.Tasks;
-using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.Configuration.Memory;
-using Microsoft.Extensions.DependencyInjection;
-using Vaas.Test.Authentication;
-using Xunit;
-using Xunit.Abstractions;
-
-namespace Vaas.Test;
-
-public class IntegrationTests
-{
- private static Uri VaasUrl => new Uri(DotNetEnv.Env.GetString(
- "VAAS_URL",
- "wss://gateway.production.vaas.gdatasecurity.de"));
-
- private readonly ITestOutputHelper _output;
- private readonly HttpClient _httpClient = new();
-
- public IntegrationTests(ITestOutputHelper output)
- {
- _output = output;
- DotNetEnv.Env.TraversePath().Load();
- }
-
- [Fact]
- public async Task ConnectWithWrongCredentialsThrowsVaasAuthenticationException()
- {
- var services = GetServices(new Dictionary()
- {
- { "VerdictAsAService:Url", VaasUrl.ToString() },
- { "VerdictAsAService:TokenUrl", AuthenticationEnvironment.TokenUrl.ToString() },
- { "VerdictAsAService:Credentials:GrantType", "ClientCredentials" },
- { "VerdictAsAService:Credentials:ClientId", "foobar" },
- { "VerdictAsAService:Credentials:ClientSecret", "foobar2" },
- });
- var provider = services.BuildServiceProvider();
-
- var vaas = provider.GetRequiredService();
- await Assert.ThrowsAsync(async () =>
- await vaas.Connect(CancellationToken.None));
- }
-
- [Fact]
- public async Task FromSha256SingleMaliciousHash()
- {
- var vaas = await AuthenticateWithCredentials();
- var verdict = await vaas.ForSha256Async(
- new ChecksumSha256("ab5788279033b0a96f2d342e5f35159f103f69e0191dd391e036a1cd711791a2"),
- CancellationToken.None);
- Assert.Equal(Verdict.Malicious, verdict.Verdict);
- Assert.Equal("ab5788279033b0a96f2d342e5f35159f103f69e0191dd391e036a1cd711791a2", verdict.Sha256);
- }
-
- [Fact]
- public async Task FromSha256SingleCleanHash()
- {
- var vaas = await AuthenticateWithCredentials();
- var verdict = await vaas.ForSha256Async(
- new ChecksumSha256("cd617c5c1b1ff1c94a52ab8cf07192654f271a3f8bad49490288131ccb9efc1e"),
- CancellationToken.None);
- Assert.Equal(Verdict.Clean, verdict.Verdict);
- Assert.Equal("cd617c5c1b1ff1c94a52ab8cf07192654f271a3f8bad49490288131ccb9efc1e", verdict.Sha256, true);
- }
-
- [Fact(Skip = "Remove Skip to test keepalive")]
- public async Task FromSha256_WorksAfter40s()
- {
- var vaas = await AuthenticateWithCredentials();
- const string guid = "3A78F382E8E2968EC201B33178102E06DB72E4F2D1505E058A4613C1E977825C";
- var verdict = await vaas.ForSha256Async(new ChecksumSha256(guid), CancellationToken.None);
- Assert.Equal(Verdict.Clean, verdict.Verdict);
- Assert.Equal("3A78F382E8E2968EC201B33178102E06DB72E4F2D1505E058A4613C1E977825C", verdict.Sha256, true);
- await Task.Delay(40000);
- verdict = await vaas.ForSha256Async(new ChecksumSha256(guid), CancellationToken.None);
- Assert.Equal(Verdict.Clean, verdict.Verdict);
- Assert.Equal("3A78F382E8E2968EC201B33178102E06DB72E4F2D1505E058A4613C1E977825C", verdict.Sha256, true);
- }
-
- [Fact]
- public async Task FromSha256SingleUnknownHash()
- {
- var vaas = await AuthenticateWithCredentials();
- var verdict = await vaas.ForSha256Async(
- new ChecksumSha256("110005c43196142f01d615a67b7da8a53cb0172f8e9317a2ec9a0a39a1da6fe9"),
- CancellationToken.None);
- Assert.Equal(Verdict.Unknown, verdict.Verdict);
- Assert.Equal("110005c43196142f01d615a67b7da8a53cb0172f8e9317a2ec9a0a39a1da6fe9", verdict.Sha256);
- }
-
- [Fact]
- public async Task From256ListMultipleHashes()
- {
- var myList = new List
- {
- "ab5788279033b0a96f2d342e5f35159f103f69e0191dd391e036a1cd711791a2",
- "cd617c5c1b1ff1c94a52ab8cf07192654f271a3f8bad49490288131ccb9efc1e",
- "1f72c1111111111111f912e40b7323a0192a300b376186c10f6803dc5efe28df"
- };
- var vaas = await AuthenticateWithCredentials();
- var verdictList = await vaas.ForSha256ListAsync(myList, CancellationToken.None);
- Assert.Equal(Verdict.Malicious, verdictList[0].Verdict);
- Assert.Equal("ab5788279033b0a96f2d342e5f35159f103f69e0191dd391e036a1cd711791a2", verdictList[0].Sha256, true);
- Assert.Equal(Verdict.Clean, verdictList[1].Verdict);
- Assert.Equal("cd617c5c1b1ff1c94a52ab8cf07192654f271a3f8bad49490288131ccb9efc1e", verdictList[1].Sha256, true);
- Assert.Equal(Verdict.Unknown, verdictList[2].Verdict);
- Assert.Equal("1f72c1111111111111f912e40b7323a0192a300b376186c10f6803dc5efe28df", verdictList[2].Sha256, true);
- }
-
-
- [Fact]
- public async Task GenerateFileUnknownHash()
- {
- var rnd = new Random();
- var b = new byte[50];
- rnd.NextBytes(b);
- await File.WriteAllBytesAsync("test.txt", b);
- var vaas = await AuthenticateWithCredentials();
- var result = await vaas.ForFileAsync("test.txt", CancellationToken.None);
- Assert.Equal(Verdict.Clean, result.Verdict);
- Assert.Equal(Vaas.Sha256CheckSum("test.txt"), result.Sha256);
- }
-
- [Fact]
- public async Task GenerateFileList()
- {
- var rnd = new Random();
- var b = new byte[50];
- rnd.NextBytes(b);
- await File.WriteAllBytesAsync("test1.txt", b);
- rnd.NextBytes(b);
- await File.WriteAllBytesAsync("test2.txt", b);
- rnd.NextBytes(b);
- await File.WriteAllBytesAsync("test3.txt", b);
- var vaas = await AuthenticateWithCredentials();
- var resultList = await vaas.ForFileListAsync(new List { "test1.txt", "test2.txt", "test3.txt" },
- CancellationToken.None);
- Assert.Equal(Verdict.Clean, resultList[0].Verdict);
- Assert.Equal(Vaas.Sha256CheckSum("test1.txt"), resultList[0].Sha256);
- Assert.Equal(Verdict.Clean, resultList[1].Verdict);
- Assert.Equal(Vaas.Sha256CheckSum("test2.txt"), resultList[1].Sha256);
- Assert.Equal(Verdict.Clean, resultList[2].Verdict);
- Assert.Equal(Vaas.Sha256CheckSum("test3.txt"), resultList[2].Sha256);
- }
-
- // [Fact]
- // public async Task FromSha256_ReturnsPup_ForAmtsoSample()
- // {
- // var vaas = await AuthenticateWithCredentials();
- // var actual = await vaas.ForSha256Async(
- // new ChecksumSha256("d6f6c6b9fde37694e12b12009ad11ab9ec8dd0f193e7319c523933bdad8a50ad"),
- // CancellationToken.None);
- // Assert.Equal(Verdict.Pup, actual.Verdict);
- // Assert.Equal("d6f6c6b9fde37694e12b12009ad11ab9ec8dd0f193e7319c523933bdad8a50ad", actual.Sha256, true);
- // }
-
- [Theory]
- [InlineData("https://www.gdatasoftware.com/oem/verdict-as-a-service", Verdict.Clean)]
- [InlineData("https://secure.eicar.org/eicar.com", Verdict.Malicious)]
- public async Task FromUrlReturnVerdict(string url, Verdict verdict)
- {
- var vaas = await AuthenticateWithCredentials();
- var actual = await vaas.ForUrlAsync(new Uri(url), CancellationToken.None);
- Assert.Equal(verdict, actual.Verdict);
- }
-
- [Fact]
- public async Task ForUrl_WithUrlWithStatusCode4xx_ThrowsVaasClientException()
- {
- var vaas = await AuthenticateWithCredentials();
- var e = await Assert.ThrowsAsync(() =>
- vaas.ForUrlAsync(new Uri("https://gateway.production.vaas.gdatasecurity.de/swagger/nocontenthere"),
- CancellationToken.None));
- Assert.Equal(
- "Call failed with status code 404 (Not Found): GET https://gateway.production.vaas.gdatasecurity.de/swagger/nocontenthere",
- e.Message);
- }
-
- [Fact]
- public async Task ForStream_WithEicarString_ReturnsMalicious()
- {
- // Arrange
- var vaas = await AuthenticateWithCredentials();
- var targetStream = new MemoryStream();
- var eicarBytes = System.Text.Encoding.UTF8.GetBytes("X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*");
- targetStream.Write(eicarBytes, 0, eicarBytes.Length);
- targetStream.Position = 0;
-
- // Act
- var verdict = await vaas.ForStreamAsync(targetStream, CancellationToken.None);
-
- // Assert
- Assert.Equal(Verdict.Malicious, verdict.Verdict);
- }
-
- [Fact]
- public async Task ForStream_WithCleanString_ReturnsClean()
- {
- // Arrange
- var vaas = await AuthenticateWithCredentials();
- var targetStream = new MemoryStream();
- var cleanBytes = System.Text.Encoding.UTF8.GetBytes("This is a clean file");
- targetStream.Write(cleanBytes, 0, cleanBytes.Length);
- targetStream.Position = 0;
-
- // Act
- var verdict = await vaas.ForStreamAsync(targetStream, CancellationToken.None);
-
- // Assert
- Assert.Equal(Verdict.Clean, verdict.Verdict);
- }
-
- [Fact]
- public async Task ForStream_WithCleanUrl_ReturnsClean()
- {
- // Arrange
- var vaas = await AuthenticateWithCredentials();
- var url = new Uri("https://raw.githubusercontent.com/GDATASoftwareAG/vaas/main/Readme.md");
- var response = await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, url), CancellationToken.None);
- var targetStream = await response.Content.ReadAsStreamAsync();
-
- // Act
- var verdict = await vaas.ForStreamAsync(targetStream, CancellationToken.None);
-
- // Assert
- Assert.Equal(Verdict.Clean, verdict.Verdict);
- }
-
- [Fact]
- public async Task ForStream_WithEicarUrl_ReturnsEicar()
- {
- // Arrange
- var vaas = await AuthenticateWithCredentials();
- var url = new Uri("https://secure.eicar.org/eicar.com.txt");
- var response = await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, url), CancellationToken.None);
- var targetStream = await response.Content.ReadAsStreamAsync();
-
- // Act
- var verdict = await vaas.ForStreamAsync(targetStream, CancellationToken.None);
-
- // Assert
- Assert.Equal(Verdict.Malicious, verdict.Verdict);
- }
-
- private async Task AuthenticateWithCredentials()
- {
- var services = GetServices(new Dictionary()
- {
- { "VerdictAsAService:Url", VaasUrl.ToString() },
- { "VerdictAsAService:TokenUrl", AuthenticationEnvironment.TokenUrl.ToString() },
- { "VerdictAsAService:Credentials:GrantType", "ClientCredentials" },
- { "VerdictAsAService:Credentials:ClientId", AuthenticationEnvironment.ClientId },
- { "VerdictAsAService:Credentials:ClientSecret", AuthenticationEnvironment.ClientSecret },
- { "VerdictAsAService:UseCache", "false" }
- });
- ServiceCollectionTools.Output(_output, services);
- var provider = services.BuildServiceProvider();
-
- var vaas = provider.GetRequiredService();
- await vaas.Connect(CancellationToken.None);
- return (Vaas)vaas;
- }
-
- private static IServiceCollection GetServices(Dictionary data)
- {
- var s = new MemoryConfigurationSource() { InitialData = data };
- var configuration = new ConfigurationBuilder()
- .Add(s)
- .Build();
-
- var services = new ServiceCollection();
- services.AddVerdictAsAService(configuration);
- return services;
- }
-
- [Fact]
- public async Task UploadEmptyFile()
- {
- await File.WriteAllBytesAsync("empty.txt", Array.Empty());
- var vaas = await AuthenticateWithCredentials();
- var result = await vaas.ForFileAsync("empty.txt", CancellationToken.None);
- Assert.Equal(Verdict.Clean, result.Verdict);
- Assert.Equal(Vaas.Sha256CheckSum("empty.txt"), result.Sha256);
- }
-
- [Fact]
- public async Task Connect_WithResourceOwnerPasswordGrantAuthenticator()
- {
- var services = GetServices(new Dictionary()
- {
- { "VerdictAsAService:Url", VaasUrl.ToString() },
- { "VerdictAsAService:TokenUrl", AuthenticationEnvironment.TokenUrl.ToString() },
- { "VerdictAsAService:Credentials:GrantType", "Password" },
- { "VerdictAsAService:Credentials:ClientId", AuthenticationEnvironment.ClientIdForResourceOwnerPasswordGrant },
- { "VerdictAsAService:Credentials:UserName", AuthenticationEnvironment.UserName },
- { "VerdictAsAService:Credentials:Password", AuthenticationEnvironment.Password },
- });
- var provider = services.BuildServiceProvider();
-
- var vaas = provider.GetRequiredService();
- await vaas.Connect(CancellationToken.None);
- }
-
- [Fact]
- public async Task ForStream_WithEicarUrl_ReturnsMaliciousWithDetectionsAndMimeType()
- {
- var vaas = await AuthenticateWithCredentials();
- var url = new Uri("https://secure.eicar.org/eicar.com.txt");
- var response = await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, url), CancellationToken.None);
- var targetStream = await response.Content.ReadAsStreamAsync();
-
- var verdict = await vaas.ForStreamAsync(targetStream, CancellationToken.None);
-
- Assert.Equal(Verdict.Malicious, verdict.Verdict);
- Assert.Equal("text/plain", verdict.FileType);
- Assert.Equal("EICAR virus test files", verdict.MimeType);
- Assert.Contains("EICAR-Test-File", verdict.Detection);
- }
-
- [Fact]
- public async Task ForUrl_WithEicarUrl_ReturnsMaliciousWithDetectionAndMimeType()
- {
- var vaas = await AuthenticateWithCredentials();
- var uri = new Uri("https://secure.eicar.org/eicar.com");
-
- var verdict = await vaas.ForUrlAsync(uri, CancellationToken.None);
-
- Assert.Equal(Verdict.Malicious, verdict.Verdict);
- Assert.Equal("text/plain", verdict.FileType);
- Assert.Equal("EICAR virus test files", verdict.MimeType);
- Assert.Contains("EICAR-Test-File", verdict.Detection);
- }
-}
diff --git a/dotnet/Vaas/test/Vaas.Test/Messages/VerdictRequestTest.cs b/dotnet/Vaas/test/Vaas.Test/Messages/VerdictRequestTest.cs
deleted file mode 100644
index f2b145a9f..000000000
--- a/dotnet/Vaas/test/Vaas.Test/Messages/VerdictRequestTest.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using System.Text.Json;
-using Snapshooter.Xunit;
-using Vaas.Messages;
-using Xunit;
-
-namespace Vaas.Test.Messages;
-
-public class VerdictRequestTest
-{
- [Fact]
- public void Serialize()
- {
- var json = JsonSerializer.Serialize(new VerdictRequest("", ""));
- Snapshot.Match(json, matchOptions => matchOptions.IgnoreField("guid"));
- }
-
- [Fact]
- public void Serialize_WithOptions()
- {
- var json = JsonSerializer.Serialize(new VerdictRequest("", "") { UseCache = false, UseShed = false });
- Snapshot.Match(json, matchOptions => matchOptions.IgnoreField("guid"));
- }
-}
\ No newline at end of file
diff --git a/dotnet/Vaas/test/Vaas.Test/Messages/__snapshots__/VerdictRequestTest.Serialize.snap b/dotnet/Vaas/test/Vaas.Test/Messages/__snapshots__/VerdictRequestTest.Serialize.snap
deleted file mode 100644
index f81f24986..000000000
--- a/dotnet/Vaas/test/Vaas.Test/Messages/__snapshots__/VerdictRequestTest.Serialize.snap
+++ /dev/null
@@ -1 +0,0 @@
-{"kind":"VerdictRequest","sha256":"","guid":"5208034c-44f2-4b8e-a96b-4c05bd663224","session_id":"","verdict_request_attributes":null}
diff --git a/dotnet/Vaas/test/Vaas.Test/Messages/__snapshots__/VerdictRequestTest.Serialize_WithOptions.snap b/dotnet/Vaas/test/Vaas.Test/Messages/__snapshots__/VerdictRequestTest.Serialize_WithOptions.snap
deleted file mode 100644
index ad0023df2..000000000
--- a/dotnet/Vaas/test/Vaas.Test/Messages/__snapshots__/VerdictRequestTest.Serialize_WithOptions.snap
+++ /dev/null
@@ -1 +0,0 @@
-{"kind":"VerdictRequest","sha256":"","guid":"d77c0f61-ac73-4320-b77b-c7ffe64c3327","session_id":"","verdict_request_attributes":null,"use_cache":false,"use_shed":false}
diff --git a/dotnet/Vaas/test/Vaas.Test/ServiceCollectionTools.cs b/dotnet/Vaas/test/Vaas.Test/ServiceCollectionTools.cs
index 694c7b536..2904feaa0 100644
--- a/dotnet/Vaas/test/Vaas.Test/ServiceCollectionTools.cs
+++ b/dotnet/Vaas/test/Vaas.Test/ServiceCollectionTools.cs
@@ -13,7 +13,8 @@ public static void Output(ITestOutputHelper output, IServiceCollection services)
foreach (var s in sortedServices)
{
output.WriteLine(
- $"{GetNameWithTypeParameters(s.ServiceType)} {GetImplementationName(s)}");
+ $"{GetNameWithTypeParameters(s.ServiceType)} {GetImplementationName(s)}"
+ );
}
}
@@ -26,7 +27,7 @@ private static string GetImplementationName(ServiceDescriptor s)
if (s.ImplementationInstance != null)
{
- return $"instance";
+ return "instance";
}
if (s.ImplementationFactory != null)
@@ -36,20 +37,20 @@ private static string GetImplementationName(ServiceDescriptor s)
throw new ArgumentException("Unknown type of service descriptor", nameof(s));
}
-
+
private static string GetNameWithTypeParameters(Type type)
{
- if (!type.IsGenericType) return type.Name;
-
- string genericArguments = type.GetGenericArguments()
+ if (!type.IsGenericType)
+ return type.Name;
+
+ var genericArguments = type.GetGenericArguments()
.Select(x => x.Name)
.Aggregate((x1, x2) => $"{x1}, {x2}");
- var indexOfBacktick = type.Name.IndexOf("`", StringComparison.InvariantCulture);
+ var indexOfBacktick = type.Name.IndexOf('`');
if (indexOfBacktick == -1)
{
return type.Name;
}
- return $"{type.Name.Substring(0, indexOfBacktick)}"
- + $"<{genericArguments}>";
+ return $"{type.Name[..indexOfBacktick]}" + $"<{genericArguments}>";
}
-}
\ No newline at end of file
+}
diff --git a/dotnet/Vaas/test/Vaas.Test/Vaas.Test.csproj b/dotnet/Vaas/test/Vaas.Test/Vaas.Test.csproj
index 92a88959e..85274ffe9 100644
--- a/dotnet/Vaas/test/Vaas.Test/Vaas.Test.csproj
+++ b/dotnet/Vaas/test/Vaas.Test/Vaas.Test.csproj
@@ -8,6 +8,7 @@
+
@@ -26,8 +27,4 @@
-
-
-
-
diff --git a/dotnet/Vaas/test/Vaas.Test/VaasOptionsTest.cs b/dotnet/Vaas/test/Vaas.Test/VaasOptionsTest.cs
deleted file mode 100644
index 2f279dedb..000000000
--- a/dotnet/Vaas/test/Vaas.Test/VaasOptionsTest.cs
+++ /dev/null
@@ -1,110 +0,0 @@
-using System;
-using System.Collections.Generic;
-using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.Configuration.Memory;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Options;
-using Snapshooter.Xunit;
-using Xunit;
-
-namespace Vaas.Test;
-
-public class VaasOptionsTest
-{
- [Fact]
- public void Value_ForPassword_ReturnsOptions()
- {
- var provider = GetServices(new()
- {
- { "TokenUrl", "https://account-staging.gdata.de/realms/vaas-staging/protocol/openid-connect/token" },
- { "Credentials:GrantType", "Password" },
- { "Credentials:ClientId", "clientId" },
- { "Credentials:UserName", "userName" },
- { "Credentials:Password", "password" },
- });
-
- var options = provider.GetRequiredService>().Value;
-
- options.MatchSnapshot();
- }
-
- [Fact]
- public void Value_ForClientCredentials_ReturnsOptions()
- {
- var provider = GetServices(new()
- {
- { "Credentials:GrantType", "ClientCredentials" },
- { "Credentials:ClientId", "clientId" },
- { "Credentials:ClientSecret", "clientSecret" },
- });
-
- var options = provider.GetRequiredService>().Value;
-
- options.MatchSnapshot();
- }
-
- [Fact]
- public void Value_IfFieldsAreMissing_ThrowsOptionsValidationException()
- {
- var provider = GetServices(new());
-
- // Exception is thrown, when Value is called
- var e = Assert.Throws(() =>
- provider.GetRequiredService>().Value);
-
- Assert.Equal(
- "DataAnnotation validation failed for 'VaasOptions' members: 'Credentials' with the error: 'The Credentials field is required.'.",
- e.Message);
- }
-
- [Fact]
- public void Value_IfClientCredentialsAndSecretIsMissing_ThrowsOptionsValidationException()
- {
- var provider = GetServices(new()
- {
- { "Credentials:GrantType", "ClientCredentials" },
- { "Credentials:ClientId", "ClientId" }
- });
-
- // Exception is thrown, when Value is called
- var e = Assert.Throws(() =>
- provider.GetRequiredService>().Value);
-
- Assert.Equal(
- "DataAnnotation validation failed for 'VaasOptions' members: 'Credentials' with the error: 'The fields ClientId and ClientSecret are required for the GrantType ClientCredentials.'.",
- e.Message);
- }
-
- [Fact]
- public void Value_IfPasswordAndUserNameIsMissing_ThrowsOptionsValidationException()
- {
- var provider = GetServices(new()
- {
- { "Credentials:GrantType", "Password" },
- { "Credentials:ClientId", "ClientId" }
- });
-
- // Exception is thrown, when Value is called
- var e = Assert.Throws(() =>
- provider.GetRequiredService>().Value);
-
- Assert.Equal(
- "DataAnnotation validation failed for 'VaasOptions' members: 'Credentials' with the error: 'The fields ClientId, UserName and Password are required for the GrantType Password.'.",
- e.Message);
- }
-
- private static IServiceProvider GetServices(Dictionary data)
- {
- var s = new MemoryConfigurationSource() { InitialData = data };
- var configuration = new ConfigurationBuilder()
- .Add(s)
- .Build();
-
- var services = new ServiceCollection();
- services
- .AddOptions()
- .Bind(configuration)
- .ValidateDataAnnotations();
- return services.BuildServiceProvider();
- }
-}
\ No newline at end of file
diff --git a/dotnet/Vaas/test/Vaas.Test/VaasTest.cs b/dotnet/Vaas/test/Vaas.Test/VaasTest.cs
new file mode 100644
index 000000000..f955018ab
--- /dev/null
+++ b/dotnet/Vaas/test/Vaas.Test/VaasTest.cs
@@ -0,0 +1,1284 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Net.Http.Json;
+using System.Reflection;
+using System.Security.Authentication;
+using System.Text;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using FluentAssertions;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Configuration.Memory;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Moq;
+using Moq.Contrib.HttpClient;
+using Vaas.Authentication;
+using Vaas.Exceptions;
+using Vaas.Messages;
+using Vaas.Options;
+using Vaas.Test.Authentication;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Vaas.Test;
+
+public class VaasTest
+{
+ private static Uri VaasUrl =>
+ new(
+ DotNetEnv.Env.GetString("VAAS_URL", "https://gateway.production.vaas.gdatasecurity.de")
+ );
+
+ private readonly ITestOutputHelper _output;
+ private IVaas _vaas;
+
+ private const string EicarSha256 =
+ "ab5788279033b0a96f2d342e5f35159f103f69e0191dd391e036a1cd711791a2";
+
+ public VaasTest(ITestOutputHelper output)
+ {
+ _output = output;
+ DotNetEnv.Env.TraversePath().Load();
+ CreateVaas();
+ }
+
+ private void CreateVaas()
+ {
+ var services = GetServices();
+ ServiceCollectionTools.Output(_output, services);
+ var provider = services.BuildServiceProvider();
+
+ _vaas = provider.GetRequiredService();
+ }
+
+ private static ServiceCollection GetServices()
+ {
+ return GetServices(
+ new Dictionary
+ {
+ { "VerdictAsAService:Options:UseHashLookup", "true" },
+ { "VerdictAsAService:Options:UseCache", "false" },
+ { "VerdictAsAService:Options:VaasUrl", VaasUrl.ToString() },
+ { "VerdictAsAService:Options:Timeout", "10" },
+ {
+ "VerdictAsAService:Credentials:TokenUrl",
+ AuthenticationEnvironment.TokenUrl.ToString()
+ },
+ {
+ "VerdictAsAService:Credentials:GrantType",
+ GrantType.ClientCredentials.ToString()
+ },
+ { "VerdictAsAService:Credentials:ClientId", AuthenticationEnvironment.ClientId },
+ {
+ "VerdictAsAService:Credentials:ClientSecret",
+ AuthenticationEnvironment.ClientSecret
+ },
+ }
+ );
+ }
+
+ private static ServiceCollection GetServices(Dictionary data)
+ {
+ var s = new MemoryConfigurationSource { InitialData = data };
+ var configuration = new ConfigurationBuilder().Add(s).Build();
+
+ var services = new ServiceCollection();
+ services.AddVerdictAsAService(configuration);
+ return services;
+ }
+
+ [Theory]
+ [InlineData(
+ "110005c43196142f01d615a67b7da8a53cb0172f8e9317a2ec9a0a39a1da6fe9",
+ Verdict.Unknown
+ )]
+ [InlineData("cd617c5c1b1ff1c94a52ab8cf07192654f271a3f8bad49490288131ccb9efc1e", Verdict.Clean)]
+ [InlineData(
+ "ab5788279033b0a96f2d342e5f35159f103f69e0191dd391e036a1cd711791a2",
+ Verdict.Malicious
+ )]
+ [InlineData("d6f6c6b9fde37694e12b12009ad11ab9ec8dd0f193e7319c523933bdad8a50ad", Verdict.Pup)]
+ public async Task ForSha256Async_ReturnsVerdict(string sha256, Verdict verdict)
+ {
+ var verdictResponse = await _vaas.ForSha256Async(sha256, CancellationToken.None);
+ Assert.Equal(verdict, verdictResponse.Verdict);
+ Assert.Equal(sha256, verdictResponse.Sha256, true);
+ }
+
+ [Theory]
+ [InlineData(false, false)]
+ [InlineData(false, true)]
+ [InlineData(true, false)]
+ [InlineData(true, true)]
+ public async Task ForSha256Async_SendsOptions(bool useCache, bool useHashLookup)
+ {
+ ChecksumSha256 sha256 = "cd617c5c1b1ff1c94a52ab8cf07192654f271a3f8bad49490288131ccb9efc1e";
+
+ var services = GetServices();
+ ServiceCollectionTools.Output(_output, services);
+
+ var handlerMock = new Mock();
+ handlerMock
+ .SetupRequest(request =>
+ request.RequestUri != null
+ && request.RequestUri.ToString().Contains(sha256)
+ && request
+ .RequestUri.ToString()
+ .Contains("useCache=" + JsonSerializer.Serialize(useCache))
+ && request
+ .RequestUri.ToString()
+ .Contains("useHashLookup=" + JsonSerializer.Serialize(useHashLookup))
+ )
+ .ReturnsResponse(
+ JsonSerializer.Serialize(
+ new FileReport { Sha256 = EicarSha256, Verdict = Verdict.Clean }
+ )
+ );
+ services
+ .AddHttpClient()
+ .ConfigurePrimaryHttpMessageHandler(() => handlerMock.Object);
+ var provider = services.BuildServiceProvider();
+ var vaas = provider.GetRequiredService();
+
+ await vaas.ForSha256Async(
+ sha256,
+ CancellationToken.None,
+ new ForSha256Options { UseCache = useCache, UseHashLookup = useHashLookup }
+ );
+
+ handlerMock.VerifyAll();
+ }
+
+ [Fact]
+ public async Task ForSha256Async_IfVaasRequestIdIsSet_SendsTraceState()
+ {
+ ChecksumSha256 sha256 = "cd617c5c1b1ff1c94a52ab8cf07192654f271a3f8bad49490288131ccb9efc1e";
+
+ var services = GetServices();
+ ServiceCollectionTools.Output(_output, services);
+
+ var handlerMock = new Mock();
+ const string requestId = "foobar";
+ handlerMock
+ .SetupRequest(request =>
+ request.RequestUri != null
+ && request.RequestUri.ToString().Contains(sha256)
+ && request
+ .RequestUri.ToString()
+ .Contains("useCache=" + JsonSerializer.Serialize(true))
+ && request
+ .RequestUri.ToString()
+ .Contains("useHashLookup=" + JsonSerializer.Serialize(true))
+ && request.Headers.GetValues("tracestate").Contains($"vaasrequestid={requestId}")
+ )
+ .ReturnsResponse(
+ JsonSerializer.Serialize(
+ new FileReport { Sha256 = EicarSha256, Verdict = Verdict.Unknown }
+ )
+ );
+ services
+ .AddHttpClient()
+ .ConfigurePrimaryHttpMessageHandler(() => handlerMock.Object);
+ var provider = services.BuildServiceProvider();
+ var vaas = provider.GetRequiredService();
+ var sha256Options = new ForSha256Options { VaasRequestId = requestId };
+
+ await vaas.ForSha256Async(sha256, CancellationToken.None, sha256Options);
+
+ handlerMock.VerifyAll();
+ }
+
+ [Fact]
+ public async Task ForSha256Async_IfVaasClientException_ThrowsVaasClientException()
+ {
+ var services = GetServices();
+ ServiceCollectionTools.Output(_output, services);
+
+ var handlerMock = new Mock();
+ handlerMock
+ .SetupRequest(request =>
+ request.RequestUri != null && request.RequestUri.ToString().Contains(EicarSha256)
+ )
+ .ReturnsResponse(
+ statusCode: HttpStatusCode.BadRequest,
+ configure: message =>
+ {
+ message.Content = JsonContent.Create(
+ new ProblemDetails
+ {
+ Detail = "Mocked client-side error",
+ Type = "VaasClientException",
+ }
+ );
+ }
+ );
+ services
+ .AddHttpClient()
+ .ConfigurePrimaryHttpMessageHandler(() => handlerMock.Object);
+ var provider = services.BuildServiceProvider();
+ var vaas = provider.GetRequiredService();
+
+ await vaas.Invoking(async v => await v.ForSha256Async(EicarSha256, CancellationToken.None))
+ .Should()
+ .ThrowAsync();
+ }
+
+ [Theory]
+ [InlineData(HttpStatusCode.InternalServerError)]
+ [InlineData(HttpStatusCode.GatewayTimeout)]
+ [InlineData(HttpStatusCode.HttpVersionNotSupported)]
+ [InlineData(HttpStatusCode.BadGateway)]
+ [InlineData(HttpStatusCode.ServiceUnavailable)]
+ public async Task ForSha256Async_IfVaasServerException_ThrowsVaasServerException(
+ HttpStatusCode serverError
+ )
+ {
+ var services = GetServices();
+ ServiceCollectionTools.Output(_output, services);
+
+ var handlerMock = new Mock();
+ handlerMock
+ .SetupRequest(request =>
+ request.RequestUri != null && request.RequestUri.ToString().Contains(EicarSha256)
+ )
+ .ReturnsResponse(
+ statusCode: serverError,
+ configure: message =>
+ {
+ message.Content = JsonContent.Create(
+ new ProblemDetails
+ {
+ Detail = "Mocked server-side error",
+ Type = "VaasServerException",
+ }
+ );
+ }
+ );
+ services
+ .AddHttpClient()
+ .ConfigurePrimaryHttpMessageHandler(() => handlerMock.Object);
+ var provider = services.BuildServiceProvider();
+ var vaas = provider.GetRequiredService();
+
+ await vaas.Invoking(async v => await v.ForSha256Async(EicarSha256, CancellationToken.None))
+ .Should()
+ .ThrowAsync();
+ }
+
+ [Fact]
+ public async Task ForSha256Async_IfAuthenticatorThrowsAuthenticationException_ThrowsAuthenticationException()
+ {
+ var services = GetServices();
+ ServiceCollectionTools.Output(_output, services);
+
+ var handlerMock = new Mock();
+ handlerMock
+ .Setup(a => a.GetTokenAsync(CancellationToken.None))
+ .Throws();
+ services.RemoveAll();
+ services.AddSingleton(handlerMock.Object);
+ var provider = services.BuildServiceProvider();
+ var vaas = provider.GetRequiredService();
+
+ await vaas.Invoking(async v => await v.ForSha256Async(EicarSha256, CancellationToken.None))
+ .Should()
+ .ThrowAsync();
+ }
+
+ [Fact]
+ public async Task ForSha256Async_If401_ThrowsAuthenticationException()
+ {
+ var services = GetServices();
+ ServiceCollectionTools.Output(_output, services);
+
+ var handlerMock = new Mock();
+ handlerMock
+ .SetupRequest(request =>
+ request.RequestUri != null && request.RequestUri.ToString().Contains(EicarSha256)
+ )
+ .ReturnsResponse(HttpStatusCode.Unauthorized);
+ services
+ .AddHttpClient()
+ .ConfigurePrimaryHttpMessageHandler(() => handlerMock.Object);
+ var provider = services.BuildServiceProvider();
+ var vaas = provider.GetRequiredService();
+
+ await vaas.Invoking(async v => await v.ForSha256Async(EicarSha256, CancellationToken.None))
+ .Should()
+ .ThrowAsync();
+ }
+
+ [Fact]
+ public async Task ForSha256Async_IfCancellationRequested_ThrowsOperationCancelledException()
+ {
+ var services = GetServices();
+ ServiceCollectionTools.Output(_output, services);
+ var ct = new CancellationToken(true);
+
+ await _vaas
+ .Invoking(async v => await v.ForSha256Async(EicarSha256, ct))
+ .Should()
+ .ThrowAsync();
+ }
+
+ [Theory]
+ [InlineData("", Verdict.Clean)]
+ [InlineData("foobar", Verdict.Clean)]
+ [InlineData(
+ "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*",
+ Verdict.Malicious
+ )]
+ public async Task ForFileAsync_ReturnsVerdict(string content, Verdict verdict)
+ {
+ var fileName = Guid.NewGuid() + ".txt";
+ await File.WriteAllBytesAsync(fileName, Encoding.UTF8.GetBytes(content));
+
+ var actual = await _vaas.ForFileAsync(fileName, CancellationToken.None);
+
+ File.Delete(fileName);
+
+ Assert.Equal(verdict, actual.Verdict);
+ }
+
+ [Theory]
+ [InlineData(false, false)]
+ [InlineData(false, true)]
+ [InlineData(true, false)]
+ [InlineData(true, true)]
+ public async Task ForFileOptions_SendsOptions(bool useCache, bool useHashLookup)
+ {
+ var services = GetServices();
+ ServiceCollectionTools.Output(_output, services);
+ const string content =
+ "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*";
+ const string sha256 = "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f";
+ var fileName = Guid.NewGuid() + ".txt";
+
+ await File.WriteAllBytesAsync(fileName, Encoding.UTF8.GetBytes(content));
+
+ var handlerMock = new Mock(MockBehavior.Strict);
+
+ if (useCache || useHashLookup)
+ {
+ handlerMock
+ .SetupRequest(request =>
+ request.RequestUri != null
+ && request.Method == HttpMethod.Get
+ && request.RequestUri.ToString().Contains(sha256)
+ && request
+ .RequestUri.ToString()
+ .Contains("useCache=" + JsonSerializer.Serialize(useCache))
+ && request
+ .RequestUri.ToString()
+ .Contains("useHashLookup=" + JsonSerializer.Serialize(useHashLookup))
+ )
+ .ReturnsResponse(
+ JsonSerializer.Serialize(
+ new FileReport { Sha256 = EicarSha256, Verdict = Verdict.Unknown }
+ )
+ );
+ }
+
+ if (!useCache)
+ {
+ handlerMock
+ .SetupRequest(request =>
+ request.RequestUri != null
+ && request.Method == HttpMethod.Get
+ && request.RequestUri.ToString().Contains(sha256)
+ && request
+ .RequestUri.ToString()
+ .Contains("useCache=" + JsonSerializer.Serialize(true))
+ && request
+ .RequestUri.ToString()
+ .Contains("useHashLookup=" + JsonSerializer.Serialize(useHashLookup))
+ )
+ .ReturnsResponse(
+ JsonSerializer.Serialize(
+ new FileReport { Sha256 = EicarSha256, Verdict = Verdict.Unknown }
+ )
+ );
+ }
+
+ handlerMock
+ .SetupRequest(request =>
+ request.RequestUri != null
+ && request.Method == HttpMethod.Post
+ && request.RequestUri.ToString().Contains("/files")
+ && request
+ .RequestUri.ToString()
+ .Contains("useHashLookup=" + JsonSerializer.Serialize(useHashLookup))
+ )
+ .ReturnsResponse(JsonSerializer.Serialize(new FileAnalysisStarted { Sha256 = sha256 }));
+ services
+ .AddHttpClient()
+ .ConfigurePrimaryHttpMessageHandler(() => handlerMock.Object);
+ var provider = services.BuildServiceProvider();
+ var vaas = provider.GetRequiredService();
+
+ await vaas.ForFileAsync(
+ fileName,
+ CancellationToken.None,
+ new ForFileOptions { UseCache = useCache, UseHashLookup = useHashLookup }
+ );
+ File.Delete(fileName);
+
+ handlerMock.VerifyAll();
+ }
+
+ [Fact]
+ public async Task ForFileAsync_IfVaasRequestIdIsSet_SendsTraceState()
+ {
+ var services = GetServices();
+ ServiceCollectionTools.Output(_output, services);
+ const string content =
+ "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*";
+ const string sha256 = "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f";
+ await File.WriteAllBytesAsync("file.txt", Encoding.UTF8.GetBytes(content));
+
+ var handlerMock = new Mock();
+ const string requestId = "foobar";
+ handlerMock
+ .SetupRequest(request =>
+ request.RequestUri != null
+ && request.RequestUri.ToString().Contains(sha256)
+ && request
+ .RequestUri.ToString()
+ .Contains("useCache=" + JsonSerializer.Serialize(true))
+ && request
+ .RequestUri.ToString()
+ .Contains("useHashLookup=" + JsonSerializer.Serialize(true))
+ && request.Headers.GetValues("tracestate").Contains($"vaasrequestid={requestId}")
+ )
+ .ReturnsResponse(
+ JsonSerializer.Serialize(
+ new FileReport { Sha256 = EicarSha256, Verdict = Verdict.Unknown }
+ )
+ );
+
+ handlerMock
+ .SetupRequest(request =>
+ request.RequestUri != null
+ && request.Method == HttpMethod.Post
+ && request.RequestUri.ToString().Contains("/files")
+ && request
+ .RequestUri.ToString()
+ .Contains("useHashLookup=" + JsonSerializer.Serialize(true))
+ )
+ .ReturnsResponse(JsonSerializer.Serialize(new FileAnalysisStarted { Sha256 = sha256 }));
+ services
+ .AddHttpClient()
+ .ConfigurePrimaryHttpMessageHandler(() => handlerMock.Object);
+ var provider = services.BuildServiceProvider();
+ var vaas = provider.GetRequiredService();
+ var forFileOptions = new ForFileOptions() { VaasRequestId = requestId };
+
+ await vaas.ForFileAsync("file.txt", CancellationToken.None, forFileOptions);
+
+ handlerMock.VerifyAll();
+ }
+
+ [Fact]
+ public async Task ForFileAsync_IfVaasClientException_ThrowsVaasClientException()
+ {
+ var services = GetServices();
+ ServiceCollectionTools.Output(_output, services);
+ const string content =
+ "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*";
+ await File.WriteAllBytesAsync("file.txt", Encoding.UTF8.GetBytes(content));
+
+ var handlerMock = new Mock();
+ handlerMock
+ .SetupRequest(request =>
+ request.RequestUri != null && request.RequestUri.ToString().Contains("/files")
+ )
+ .ReturnsResponse(
+ statusCode: HttpStatusCode.BadRequest,
+ configure: message =>
+ {
+ message.Content = JsonContent.Create(
+ new ProblemDetails
+ {
+ Detail = "Mocked client-side error",
+ Type = "VaasClientException",
+ }
+ );
+ }
+ );
+ services
+ .AddHttpClient()
+ .ConfigurePrimaryHttpMessageHandler(() => handlerMock.Object);
+ var provider = services.BuildServiceProvider();
+ var vaas = provider.GetRequiredService();
+
+ await vaas.Invoking(async v => await v.ForFileAsync("file.txt", CancellationToken.None))
+ .Should()
+ .ThrowAsync();
+ }
+
+ [Theory]
+ [InlineData(HttpStatusCode.InternalServerError)]
+ [InlineData(HttpStatusCode.GatewayTimeout)]
+ [InlineData(HttpStatusCode.HttpVersionNotSupported)]
+ [InlineData(HttpStatusCode.BadGateway)]
+ [InlineData(HttpStatusCode.ServiceUnavailable)]
+ public async Task ForFileAsync_IfVaasServerException_ThrowsVaasServerException(
+ HttpStatusCode serverError
+ )
+ {
+ var services = GetServices();
+ ServiceCollectionTools.Output(_output, services);
+ const string content =
+ "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*";
+ await File.WriteAllBytesAsync("file.txt", Encoding.UTF8.GetBytes(content));
+
+ var handlerMock = new Mock();
+ handlerMock
+ .SetupRequest(request =>
+ request.RequestUri != null && request.RequestUri.ToString().Contains("/files")
+ )
+ .ReturnsResponse(
+ statusCode: serverError,
+ configure: message =>
+ {
+ message.Content = JsonContent.Create(
+ new ProblemDetails
+ {
+ Detail = "Mocked server-side error",
+ Type = "VaasServerException",
+ }
+ );
+ }
+ );
+ services
+ .AddHttpClient()
+ .ConfigurePrimaryHttpMessageHandler(() => handlerMock.Object);
+ var provider = services.BuildServiceProvider();
+ var vaas = provider.GetRequiredService();
+
+ await vaas.Invoking(async v => await v.ForFileAsync("file.txt", CancellationToken.None))
+ .Should()
+ .ThrowAsync();
+ }
+
+ [Fact]
+ public async Task ForFileAsync_IfAuthenticatorThrowsAuthenticationException_ThrowsAuthenticationException()
+ {
+ var services = GetServices();
+ ServiceCollectionTools.Output(_output, services);
+ const string content =
+ "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*";
+ await File.WriteAllBytesAsync("file.txt", Encoding.UTF8.GetBytes(content));
+
+ var handlerMock = new Mock();
+ handlerMock
+ .Setup(a => a.GetTokenAsync(CancellationToken.None))
+ .Throws();
+ services.RemoveAll();
+ services.AddSingleton(handlerMock.Object);
+ var provider = services.BuildServiceProvider();
+ var vaas = provider.GetRequiredService();
+
+ await vaas.Invoking(async v => await v.ForFileAsync("file.txt", CancellationToken.None))
+ .Should()
+ .ThrowAsync();
+ }
+
+ [Fact]
+ public async Task ForFileAsync_If401_ThrowsAuthenticationException()
+ {
+ var services = GetServices();
+ ServiceCollectionTools.Output(_output, services);
+ const string content =
+ "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*";
+ await File.WriteAllBytesAsync("file.txt", Encoding.UTF8.GetBytes(content));
+
+ var handlerMock = new Mock();
+ handlerMock
+ .SetupRequest(request =>
+ request.RequestUri != null && request.RequestUri.ToString().Contains("/files")
+ )
+ .ReturnsResponse(HttpStatusCode.Unauthorized);
+ services
+ .AddHttpClient()
+ .ConfigurePrimaryHttpMessageHandler(() => handlerMock.Object);
+ var provider = services.BuildServiceProvider();
+ var vaas = provider.GetRequiredService();
+
+ await vaas.Invoking(async v => await v.ForFileAsync("file.txt", CancellationToken.None))
+ .Should()
+ .ThrowAsync();
+ }
+
+ [Fact]
+ public async Task ForFileAsync_IfCancellationRequested_ThrowsOperationCancelledException()
+ {
+ var services = GetServices();
+ ServiceCollectionTools.Output(_output, services);
+ const string content =
+ "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*";
+ await File.WriteAllBytesAsync("file.txt", Encoding.UTF8.GetBytes(content));
+
+ var ct = new CancellationToken(true);
+
+ await _vaas
+ .Invoking(async v => await v.ForFileAsync("file.txt", ct))
+ .Should()
+ .ThrowAsync();
+ }
+
+ [Fact]
+ public async Task ForStreamAsync_ReturnsVerdict()
+ {
+ var targetStream = new MemoryStream(
+ "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*"u8.ToArray()
+ );
+
+ var verdict = await _vaas.ForStreamAsync(targetStream, CancellationToken.None);
+
+ Assert.Equal(Verdict.Malicious, verdict.Verdict);
+ }
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task ForStreamOptions_SendsOptions(bool useHashLookup)
+ {
+ var services = GetServices();
+ ServiceCollectionTools.Output(_output, services);
+ var targetStream = new MemoryStream(
+ "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*"u8.ToArray()
+ );
+
+ var handlerMock = new Mock(MockBehavior.Strict);
+
+ handlerMock
+ .SetupRequest(request =>
+ request.RequestUri != null
+ && request.Method == HttpMethod.Get
+ && request.RequestUri.ToString().Contains(EicarSha256)
+ && request
+ .RequestUri.ToString()
+ .Contains("useCache=" + JsonSerializer.Serialize(true))
+ && request
+ .RequestUri.ToString()
+ .Contains("useHashLookup=" + JsonSerializer.Serialize(useHashLookup))
+ )
+ .ReturnsResponse(
+ JsonSerializer.Serialize(
+ new FileReport { Sha256 = EicarSha256, Verdict = Verdict.Unknown }
+ )
+ );
+
+ handlerMock
+ .SetupRequest(request =>
+ request.RequestUri != null
+ && request.Method == HttpMethod.Post
+ && request.RequestUri.ToString().Contains("/files")
+ && request
+ .RequestUri.ToString()
+ .Contains("useHashLookup=" + JsonSerializer.Serialize(useHashLookup))
+ )
+ .ReturnsResponse(
+ JsonSerializer.Serialize(new FileAnalysisStarted { Sha256 = EicarSha256 })
+ );
+ services
+ .AddHttpClient()
+ .ConfigurePrimaryHttpMessageHandler(() => handlerMock.Object);
+ var provider = services.BuildServiceProvider();
+ var vaas = provider.GetRequiredService();
+
+ await vaas.ForStreamAsync(
+ targetStream,
+ CancellationToken.None,
+ new ForStreamOptions { UseHashLookup = useHashLookup }
+ );
+
+ handlerMock.VerifyAll();
+ }
+
+ [Fact]
+ public async Task ForStreamAsync_SendsUserAgent()
+ {
+ var services = GetServices();
+ ServiceCollectionTools.Output(_output, services);
+ var targetStream = new MemoryStream(
+ "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*"u8.ToArray()
+ );
+
+ var handlerMock = new Mock();
+ handlerMock
+ .SetupRequest(request =>
+ request.RequestUri != null
+ && request
+ .Headers.UserAgent.ToString()
+ .Contains("Cs/" + Assembly.GetAssembly(typeof(Vaas))?.GetName().Version)
+ )
+ .ReturnsResponse(
+ JsonSerializer.Serialize(
+ new FileReport { Sha256 = EicarSha256, Verdict = Verdict.Unknown }
+ )
+ );
+
+ services
+ .AddHttpClient()
+ .ConfigurePrimaryHttpMessageHandler(() => handlerMock.Object);
+ var provider = services.BuildServiceProvider();
+ var vaas = provider.GetRequiredService();
+
+ var verdict = await vaas.ForStreamAsync(targetStream, CancellationToken.None);
+ _output.WriteLine(verdict.ToString());
+
+ handlerMock.VerifyAll();
+ }
+
+ [Fact]
+ public async Task ForStreamAsync_IfVaasRequestIdIsSet_SendsTraceState()
+ {
+ var services = GetServices();
+ ServiceCollectionTools.Output(_output, services);
+ var targetStream = new MemoryStream(
+ "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*"u8.ToArray()
+ );
+
+ var handlerMock = new Mock();
+ const string requestId = "foobar";
+ handlerMock
+ .SetupRequest(request =>
+ request.RequestUri != null
+ && request.RequestUri.ToString().Contains(EicarSha256)
+ && request
+ .RequestUri.ToString()
+ .Contains("useCache=" + JsonSerializer.Serialize(true))
+ && request
+ .RequestUri.ToString()
+ .Contains("useHashLookup=" + JsonSerializer.Serialize(true))
+ && request.Headers.GetValues("tracestate").Contains($"vaasrequestid={requestId}")
+ )
+ .ReturnsResponse(
+ JsonSerializer.Serialize(
+ new FileReport { Sha256 = EicarSha256, Verdict = Verdict.Unknown }
+ )
+ );
+
+ handlerMock
+ .SetupRequest(request =>
+ request.RequestUri != null
+ && request.Method == HttpMethod.Post
+ && request.RequestUri.ToString().Contains("/files")
+ && request
+ .RequestUri.ToString()
+ .Contains("useHashLookup=" + JsonSerializer.Serialize(true))
+ )
+ .ReturnsResponse(
+ JsonSerializer.Serialize(new FileAnalysisStarted { Sha256 = EicarSha256 })
+ );
+ services
+ .AddHttpClient()
+ .ConfigurePrimaryHttpMessageHandler(() => handlerMock.Object);
+ var provider = services.BuildServiceProvider();
+ var vaas = provider.GetRequiredService();
+ var forStreamOptions = new ForStreamOptions() { VaasRequestId = requestId };
+
+ await vaas.ForStreamAsync(targetStream, CancellationToken.None, forStreamOptions);
+
+ handlerMock.VerifyAll();
+ }
+
+ [Fact]
+ public async Task ForStreamAsync_IfVaasClientException_ThrowsVaasClientException()
+ {
+ var services = GetServices();
+ ServiceCollectionTools.Output(_output, services);
+ var targetStream = new MemoryStream(
+ "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*"u8.ToArray()
+ );
+
+ var handlerMock = new Mock();
+ handlerMock
+ .SetupRequest(request =>
+ request.RequestUri != null && request.RequestUri.ToString().Contains("/files")
+ )
+ .ReturnsResponse(
+ statusCode: HttpStatusCode.BadRequest,
+ configure: message =>
+ {
+ message.Content = JsonContent.Create(
+ new ProblemDetails
+ {
+ Detail = "Mocked client-side error",
+ Type = "VaasClientException",
+ }
+ );
+ }
+ );
+ services
+ .AddHttpClient()
+ .ConfigurePrimaryHttpMessageHandler(() => handlerMock.Object);
+ var provider = services.BuildServiceProvider();
+ var vaas = provider.GetRequiredService();
+
+ await vaas.Invoking(async v => await v.ForStreamAsync(targetStream, CancellationToken.None))
+ .Should()
+ .ThrowAsync();
+ }
+
+ [Theory]
+ [InlineData(HttpStatusCode.InternalServerError)]
+ [InlineData(HttpStatusCode.GatewayTimeout)]
+ [InlineData(HttpStatusCode.HttpVersionNotSupported)]
+ [InlineData(HttpStatusCode.BadGateway)]
+ [InlineData(HttpStatusCode.ServiceUnavailable)]
+ public async Task ForStreamAsync_IfVaasServerException_ThrowsVaasServerException(
+ HttpStatusCode serverError
+ )
+ {
+ var services = GetServices();
+ ServiceCollectionTools.Output(_output, services);
+ var targetStream = new MemoryStream(
+ "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*"u8.ToArray()
+ );
+
+ var handlerMock = new Mock();
+ handlerMock
+ .SetupRequest(request =>
+ request.RequestUri != null && request.RequestUri.ToString().Contains("/files")
+ )
+ .ReturnsResponse(
+ statusCode: serverError,
+ configure: message =>
+ {
+ message.Content = JsonContent.Create(
+ new ProblemDetails
+ {
+ Detail = "Mocked server-side error",
+ Type = "VaasServerException",
+ }
+ );
+ }
+ );
+ services
+ .AddHttpClient()
+ .ConfigurePrimaryHttpMessageHandler(() => handlerMock.Object);
+ var provider = services.BuildServiceProvider();
+ var vaas = provider.GetRequiredService();
+
+ await vaas.Invoking(async v => await v.ForStreamAsync(targetStream, CancellationToken.None))
+ .Should()
+ .ThrowAsync();
+ }
+
+ [Fact]
+ public async Task ForStreamAsync_IfAuthenticatorThrowsAuthenticationException_ThrowsAuthenticationException()
+ {
+ var services = GetServices();
+ ServiceCollectionTools.Output(_output, services);
+ var targetStream = new MemoryStream(
+ "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*"u8.ToArray()
+ );
+
+ var handlerMock = new Mock();
+ handlerMock
+ .Setup(a => a.GetTokenAsync(CancellationToken.None))
+ .Throws();
+ services.RemoveAll();
+ services.AddSingleton(handlerMock.Object);
+ var provider = services.BuildServiceProvider();
+ var vaas = provider.GetRequiredService();
+
+ await vaas.Invoking(async v => await v.ForStreamAsync(targetStream, CancellationToken.None))
+ .Should()
+ .ThrowAsync();
+ }
+
+ [Fact]
+ public async Task ForStreamAsync_If401_ThrowsAuthenticationException()
+ {
+ var services = GetServices();
+ ServiceCollectionTools.Output(_output, services);
+ var targetStream = new MemoryStream(
+ "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*"u8.ToArray()
+ );
+
+ var handlerMock = new Mock();
+ handlerMock
+ .SetupRequest(request =>
+ request.RequestUri != null && request.RequestUri.ToString().Contains("/files")
+ )
+ .ReturnsResponse(HttpStatusCode.Unauthorized);
+ services
+ .AddHttpClient()
+ .ConfigurePrimaryHttpMessageHandler(() => handlerMock.Object);
+ var provider = services.BuildServiceProvider();
+ var vaas = provider.GetRequiredService();
+
+ await vaas.Invoking(async v => await v.ForStreamAsync(targetStream, CancellationToken.None))
+ .Should()
+ .ThrowAsync();
+ }
+
+ [Fact]
+ public async Task ForStreamAsync_IfCancellationRequested_ThrowsOperationCancelledException()
+ {
+ var services = GetServices();
+ ServiceCollectionTools.Output(_output, services);
+ var targetStream = new MemoryStream(
+ "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*"u8.ToArray()
+ );
+
+ var ct = new CancellationToken(true);
+
+ await _vaas
+ .Invoking(async v => await v.ForStreamAsync(targetStream, ct))
+ .Should()
+ .ThrowAsync();
+ }
+
+ [Theory]
+ [InlineData("https://www.gdatasoftware.com/oem/verdict-as-a-service", Verdict.Clean)]
+ [InlineData("https://secure.eicar.org/eicar.com", Verdict.Malicious)]
+ public async Task ForUrlAsync_ReturnsVerdict(string url, Verdict verdict)
+ {
+ var actual = await _vaas.ForUrlAsync(new Uri(url), CancellationToken.None);
+ Assert.Equal(verdict, actual.Verdict);
+ }
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task ForUrlOptions_SendsOptions(bool useHashLookup)
+ {
+ var services = GetServices();
+ ServiceCollectionTools.Output(_output, services);
+ var url = new Uri("https://secure.eicar.org/eicar.com");
+ var urlAnalysisStarted = new UrlAnalysisStarted { Id = Guid.NewGuid().ToString() };
+
+ var handlerMock = new Mock(MockBehavior.Strict);
+
+ handlerMock
+ .SetupRequest(request =>
+ request.RequestUri != null
+ && request.Method == HttpMethod.Get
+ && request.RequestUri.ToString().Contains(urlAnalysisStarted.Id)
+ )
+ .ReturnsResponse(
+ JsonSerializer.Serialize(
+ new UrlReport
+ {
+ Sha256 = EicarSha256,
+ Verdict = Verdict.Unknown,
+ Url = url,
+ }
+ )
+ );
+
+ handlerMock
+ .SetupRequest(request =>
+ request.RequestUri != null
+ && request.Method == HttpMethod.Post
+ && request.RequestUri.ToString().Contains("/urls")
+ )
+ .ReturnsResponse(JsonSerializer.Serialize(urlAnalysisStarted));
+ services
+ .AddHttpClient()
+ .ConfigurePrimaryHttpMessageHandler(() => handlerMock.Object);
+ var provider = services.BuildServiceProvider();
+ var vaas = provider.GetRequiredService();
+
+ await vaas.ForUrlAsync(
+ url,
+ CancellationToken.None,
+ new ForUrlOptions { UseHashLookup = useHashLookup }
+ );
+
+ handlerMock.VerifyAll();
+ }
+
+ [Fact]
+ public async Task ForUrlAsync_SendsUserAgent()
+ {
+ var services = GetServices();
+ ServiceCollectionTools.Output(_output, services);
+ var url = new Uri("https://secure.eicar.org/eicar.com");
+ var urlAnalysisStarted = new UrlAnalysisStarted { Id = Guid.NewGuid().ToString() };
+
+ var handlerMock = new Mock();
+
+ handlerMock
+ .SetupRequest(request =>
+ request.RequestUri != null
+ && request.Method == HttpMethod.Get
+ && request.RequestUri.ToString().Contains(urlAnalysisStarted.Id)
+ && request.Headers.UserAgent.ToString()
+ == new ProductInfoHeaderValue(
+ "Cs",
+ Assembly.GetAssembly(typeof(Vaas))?.GetName().Version?.ToString()
+ ).ToString()
+ )
+ .ReturnsResponse(
+ JsonSerializer.Serialize(
+ new UrlReport
+ {
+ Sha256 = EicarSha256,
+ Verdict = Verdict.Unknown,
+ Url = url,
+ }
+ )
+ );
+
+ handlerMock
+ .SetupRequest(request =>
+ request.RequestUri != null
+ && request.Method == HttpMethod.Post
+ && request.RequestUri.ToString().Contains("/urls")
+ && request.Headers.UserAgent.ToString()
+ == new ProductInfoHeaderValue(
+ "Cs",
+ Assembly.GetAssembly(typeof(Vaas))?.GetName().Version?.ToString()
+ ).ToString()
+ )
+ .ReturnsResponse(JsonSerializer.Serialize(urlAnalysisStarted));
+ services
+ .AddHttpClient()
+ .ConfigurePrimaryHttpMessageHandler(() => handlerMock.Object);
+ var provider = services.BuildServiceProvider();
+ var vaas = provider.GetRequiredService();
+
+ await vaas.ForUrlAsync(url, CancellationToken.None);
+
+ handlerMock.VerifyAll();
+ }
+
+ [Fact]
+ public async Task ForUrlAsync_IfVaasRequestIdIsSet_SendsTraceState()
+ {
+ var services = GetServices();
+ ServiceCollectionTools.Output(_output, services);
+ var url = new Uri("https://secure.eicar.org/eicar.com");
+ var urlAnalysisStarted = new UrlAnalysisStarted { Id = Guid.NewGuid().ToString() };
+
+ var handlerMock = new Mock();
+ const string requestId = "foobar";
+ handlerMock
+ .SetupRequest(request =>
+ request.RequestUri != null
+ && request.Method == HttpMethod.Get
+ && request.RequestUri.ToString().Contains(urlAnalysisStarted.Id)
+ && request.Headers.GetValues("tracestate").Contains($"vaasrequestid={requestId}")
+ )
+ .ReturnsResponse(
+ JsonSerializer.Serialize(
+ new UrlReport
+ {
+ Sha256 = EicarSha256,
+ Verdict = Verdict.Unknown,
+ Url = url,
+ }
+ )
+ );
+
+ handlerMock
+ .SetupRequest(request =>
+ request.RequestUri != null
+ && request.Method == HttpMethod.Post
+ && request.RequestUri.ToString().Contains("/urls")
+ && request.Headers.GetValues("tracestate").Contains($"vaasrequestid={requestId}")
+ )
+ .ReturnsResponse(JsonSerializer.Serialize(urlAnalysisStarted));
+ services
+ .AddHttpClient()
+ .ConfigurePrimaryHttpMessageHandler(() => handlerMock.Object);
+ var provider = services.BuildServiceProvider();
+ var vaas = provider.GetRequiredService();
+ var forUrlOptions = new ForUrlOptions() { VaasRequestId = requestId };
+
+ await vaas.ForUrlAsync(url, CancellationToken.None, forUrlOptions);
+
+ handlerMock.VerifyAll();
+ }
+
+ [Fact]
+ public async Task ForUrlAsync_IfVaasClientException_ThrowsVaasClientException()
+ {
+ var services = GetServices();
+ ServiceCollectionTools.Output(_output, services);
+ var url = new Uri("https://secure.eicar.org/eicar.com");
+
+ var handlerMock = new Mock();
+ handlerMock
+ .SetupRequest(request =>
+ request.RequestUri != null
+ && request.Method == HttpMethod.Post
+ && request.RequestUri.ToString().Contains("/urls")
+ )
+ .ReturnsResponse(
+ statusCode: HttpStatusCode.BadRequest,
+ configure: message =>
+ {
+ message.Content = JsonContent.Create(
+ new ProblemDetails
+ {
+ Detail = "Mocked client-side error",
+ Type = "VaasClientException",
+ }
+ );
+ }
+ );
+
+ services
+ .AddHttpClient()
+ .ConfigurePrimaryHttpMessageHandler(() => handlerMock.Object);
+ var provider = services.BuildServiceProvider();
+ var vaas = provider.GetRequiredService();
+
+ await vaas.Invoking(async v => await v.ForUrlAsync(url, CancellationToken.None))
+ .Should()
+ .ThrowAsync();
+ }
+
+ [Theory]
+ [InlineData(HttpStatusCode.InternalServerError)]
+ [InlineData(HttpStatusCode.GatewayTimeout)]
+ [InlineData(HttpStatusCode.HttpVersionNotSupported)]
+ [InlineData(HttpStatusCode.BadGateway)]
+ [InlineData(HttpStatusCode.ServiceUnavailable)]
+ public async Task ForUrlAsync_IfVaasServerException_ThrowsVaasServerException(
+ HttpStatusCode serverError
+ )
+ {
+ var services = GetServices();
+ ServiceCollectionTools.Output(_output, services);
+ var url = new Uri("https://secure.eicar.org/eicar.com");
+
+ var handlerMock = new Mock();
+ handlerMock
+ .SetupRequest(request =>
+ request.RequestUri != null
+ && request.Method == HttpMethod.Post
+ && request.RequestUri.ToString().Contains("/urls")
+ )
+ .ReturnsResponse(
+ statusCode: serverError,
+ configure: message =>
+ {
+ message.Content = JsonContent.Create(
+ new ProblemDetails
+ {
+ Detail = "Mocked server-side error",
+ Type = "VaasServerException",
+ }
+ );
+ }
+ );
+ services
+ .AddHttpClient()
+ .ConfigurePrimaryHttpMessageHandler(() => handlerMock.Object);
+ var provider = services.BuildServiceProvider();
+ var vaas = provider.GetRequiredService();
+
+ await vaas.Invoking(async v => await v.ForUrlAsync(url, CancellationToken.None))
+ .Should()
+ .ThrowAsync();
+ }
+
+ [Fact]
+ public async Task ForUrlAsync_IfAuthenticatorThrowsAuthenticationException_ThrowsAuthenticationException()
+ {
+ var services = GetServices();
+ ServiceCollectionTools.Output(_output, services);
+ var url = new Uri("https://secure.eicar.org/eicar.com");
+
+ var handlerMock = new Mock();
+ handlerMock
+ .Setup(a => a.GetTokenAsync(CancellationToken.None))
+ .Throws();
+ services.RemoveAll();
+ services.AddSingleton(handlerMock.Object);
+ var provider = services.BuildServiceProvider();
+ var vaas = provider.GetRequiredService();
+
+ await vaas.Invoking(async v => await v.ForUrlAsync(url, CancellationToken.None))
+ .Should()
+ .ThrowAsync();
+ }
+
+ [Fact]
+ public async Task ForUrlAsync_If401_ThrowsAuthenticationException()
+ {
+ var services = GetServices();
+ ServiceCollectionTools.Output(_output, services);
+ var url = new Uri("https://secure.eicar.org/eicar.com");
+
+ var handlerMock = new Mock();
+ handlerMock
+ .SetupRequest(request =>
+ request.RequestUri != null
+ && request.Method == HttpMethod.Post
+ && request.RequestUri.ToString().Contains("/urls")
+ )
+ .ReturnsResponse(HttpStatusCode.Unauthorized);
+ services
+ .AddHttpClient()
+ .ConfigurePrimaryHttpMessageHandler(() => handlerMock.Object);
+ var provider = services.BuildServiceProvider();
+ var vaas = provider.GetRequiredService();
+
+ await vaas.Invoking(async v => await v.ForUrlAsync(url, CancellationToken.None))
+ .Should()
+ .ThrowAsync();
+ }
+
+ [Fact]
+ public async Task ForUrlAsync_IfCancellationRequested_ThrowsOperationCancelledException()
+ {
+ var services = GetServices();
+ ServiceCollectionTools.Output(_output, services);
+ var url = new Uri("https://secure.eicar.org/eicar.com");
+
+ var ct = new CancellationToken(true);
+
+ await _vaas
+ .Invoking(async v => await v.ForUrlAsync(url, ct))
+ .Should()
+ .ThrowAsync();
+ }
+
+ [Fact]
+ public async Task ForFile_WithTimeout_ThrowsException()
+ {
+ var authenticator = new ClientCredentialsGrantAuthenticator(
+ AuthenticationEnvironment.ClientId,
+ AuthenticationEnvironment.ClientSecret,
+ AuthenticationEnvironment.TokenUrl
+ );
+ var options = new VaasOptions
+ {
+ Timeout = TimeSpan.FromSeconds(1),
+ UseCache = false,
+ UseHashLookup = false,
+ VaasUrl = new Uri("https://gateway.staging.vaas.gdatasecurity.de"),
+ };
+ var vaas = new Vaas(authenticator, options);
+
+ try
+ {
+ var random100Mb = new byte[1000 * 1024 * 1024];
+ new Random().NextBytes(random100Mb);
+ await File.WriteAllBytesAsync("file.txt", random100Mb);
+ await vaas.Invoking(async v => await v.ForFileAsync("file.txt", CancellationToken.None))
+ .Should()
+ .ThrowAsync();
+ }
+ finally
+ {
+ File.Delete("file.txt");
+ }
+ }
+}
diff --git a/dotnet/Vaas/test/Vaas.Test/__snapshots__/VaasOptionsTest.Value_ForClientCredentials_ReturnsOptions.snap b/dotnet/Vaas/test/Vaas.Test/__snapshots__/VaasOptionsTest.Value_ForClientCredentials_ReturnsOptions.snap
deleted file mode 100644
index bcd79bef6..000000000
--- a/dotnet/Vaas/test/Vaas.Test/__snapshots__/VaasOptionsTest.Value_ForClientCredentials_ReturnsOptions.snap
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "Url": "https://upload.production.vaas.gdatasecurity.de",
- "UseHashLookup": null,
- "UseCache": null,
- "TokenUrl": "https://account.gdata.de/realms/vaas-production/protocol/openid-connect/token",
- "Credentials": {
- "GrantType": "ClientCredentials",
- "ClientId": "clientId",
- "ClientSecret": "clientSecret",
- "UserName": null,
- "Password": null
- }
-}
diff --git a/dotnet/Vaas/test/Vaas.Test/__snapshots__/VaasOptionsTest.Value_ForPassword_ReturnsOptions.snap b/dotnet/Vaas/test/Vaas.Test/__snapshots__/VaasOptionsTest.Value_ForPassword_ReturnsOptions.snap
deleted file mode 100644
index fb19e18d3..000000000
--- a/dotnet/Vaas/test/Vaas.Test/__snapshots__/VaasOptionsTest.Value_ForPassword_ReturnsOptions.snap
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "Url": "https://upload.production.vaas.gdatasecurity.de",
- "UseHashLookup": null,
- "UseCache": null,
- "TokenUrl": "https://account-staging.gdata.de/realms/vaas-staging/protocol/openid-connect/token",
- "Credentials": {
- "GrantType": "Password",
- "ClientId": "clientId",
- "ClientSecret": null,
- "UserName": "userName",
- "Password": "password"
- }
-}
diff --git a/dotnet/examples/VaasExample/Program.cs b/dotnet/examples/VaasExample/Program.cs
index bc6a6e537..3661d7aca 100644
--- a/dotnet/examples/VaasExample/Program.cs
+++ b/dotnet/examples/VaasExample/Program.cs
@@ -10,10 +10,10 @@ public static class Program
private static string ClientSecret => Environment.GetEnvironmentVariable("CLIENT_SECRET") ?? string.Empty;
private static string UserName => Environment.GetEnvironmentVariable("VAAS_USER_NAME") ?? string.Empty;
private static string Password => Environment.GetEnvironmentVariable("VAAS_PASSWORD") ?? string.Empty;
- private static Uri VaasUrl => new Uri(Environment.GetEnvironmentVariable("VAAS_URL") ??
- "wss://gateway.production.vaas.gdatasecurity.de");
- private static Uri TokenUrl => new Uri(Environment.GetEnvironmentVariable("TOKEN_URL") ??
- "https://account.gdata.de/realms/vaas-production/protocol/openid-connect/token");
+ private static Uri VaasUrl => new(Environment.GetEnvironmentVariable("VAAS_URL") ??
+ "wss://gateway.production.vaas.gdatasecurity.de");
+ private static Uri TokenUrl => new(Environment.GetEnvironmentVariable("TOKEN_URL") ??
+ "https://account.gdata.de/realms/vaas-production/protocol/openid-connect/token");
public static async Task Main(string[] args)
{
@@ -58,7 +58,7 @@ private static async Task CreateVaasAndConnect()
// If you got a username and password from us, you can use the GrantType.Password like this
// You may use self registration and create a new username and password for the
// Credentials by yourself like the example above on https://vaas.gdata.de/login
- var vaas = VaasFactory.Create(new VaasOptions()
+ var vaas = VaasFactory.Create(new VaasOptions
{
Url = VaasUrl,
TokenUrl = TokenUrl,