diff --git a/.github/workflows/ci-golang.yaml b/.github/workflows/ci-golang.yaml
index 3458fdc5..96af7f3d 100644
--- a/.github/workflows/ci-golang.yaml
+++ b/.github/workflows/ci-golang.yaml
@@ -62,17 +62,31 @@ jobs:
image: golang:latest
strategy:
matrix:
- version-directory: ["./", "v2/"]
+ version-directory: ["./", "v2/", "v3/"]
steps:
- uses: actions/checkout@v4
+ - name: set legacy vaas gateway for production
+ run: |
+ if [ "${{ matrix.version-directory }}" = "./" -o "${{ matrix.version-directory }}" = "v2/" ]; then
+ echo "VAAS_URL=wss://gateway.production.vaas.gdatasecurity.de" >> $GITHUB_ENV
+ else
+ echo "VAAS_URL=https://gateway.production.vaas.gdatasecurity.de" >> $GITHUB_ENV
+ fi
+
+
- name: set staging environment
if: (inputs.environment == 'staging' || (startsWith(github.ref, 'refs/tags') && endsWith(github.ref, '-beta')))
run: |
+ echo "Beta version: Testing against staging"
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 "TOKEN_URL=https://account-staging.gdata.de/realms/vaas-staging/protocol/openid-connect/token" >> $GITHUB_ENV
+ if [ "${{ matrix.version-directory }}" = "./" -o "${{ matrix.version-directory }}" = "v2/" ]; then
+ echo "VAAS_URL=wss://gateway.staging.vaas.gdatasecurity.de" >> $GITHUB_ENV
+ else
+ echo "VAAS_URL=https://gateway.staging.vaas.gdatasecurity.de" >> $GITHUB_ENV
+ fi
echo "VAAS_CLIENT_ID=${{ secrets.STAGING_VAAS_CLIENT_ID }}" >> $GITHUB_ENV
echo "VAAS_USER_NAME=${{ secrets.STAGING_VAAS_USER_NAME }}" >> $GITHUB_ENV
echo "VAAS_PASSWORD=${{ secrets.STAGING_VAAS_PASSWORD }}" >> $GITHUB_ENV
@@ -80,10 +94,15 @@ jobs:
- name: set develop environment
if: (inputs.environment == 'develop' || (startsWith(github.ref, 'refs/tags') && endsWith(github.ref, '-alpha')))
run: |
+ echo "Alpha version: Testing against develop"
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 "TOKEN_URL=https://account-staging.gdata.de/realms/vaas-develop/protocol/openid-connect/token" >> $GITHUB_ENV
+ if [ "${{ matrix.version-directory }}" = "./" -o "${{ matrix.version-directory }}" = "v2/" ]; then
+ echo "VAAS_URL=wss://gateway.develop.vaas.gdatasecurity.de" >> $GITHUB_ENV
+ else
+ echo "VAAS_URL=https://gateway.develop.vaas.gdatasecurity.de" >> $GITHUB_ENV
+ fi
echo "VAAS_CLIENT_ID=${{ secrets.DEVELOP_VAAS_CLIENT_ID }}" >> $GITHUB_ENV
echo "VAAS_USER_NAME=${{ secrets.DEVELOP_VAAS_USER_NAME }}" >> $GITHUB_ENV
echo "VAAS_PASSWORD=${{ secrets.DEVELOP_VAAS_PASSWORD }}" >> $GITHUB_ENV
@@ -141,7 +160,7 @@ jobs:
image: golang:latest
strategy:
matrix:
- version-directory: [".", "v2"]
+ version-directory: [".", "v2", "v3"]
steps:
- uses: actions/checkout@v4
- name: Install govulncheck
@@ -176,7 +195,7 @@ jobs:
MAJOR_VERSION: ${{ needs.extract-major-version.outputs.major_version }}
if: startsWith(github.ref, 'refs/tags')
run: |
- if [ "$MAJOR_VERSION" == "v1" ]; then
+ if [ "$MAJOR_VERSION" = "v1" ]; then
GOPROXY=proxy.golang.org go list -m github.com/GDATASoftwareAG/vaas/golang/vaas@${GITHUB_REF#refs/tags/golang/vaas/}
else
GOPROXY=proxy.golang.org go list -m github.com/GDATASoftwareAG/vaas/golang/vaas/${MAJOR_VERSION}@${GITHUB_REF#refs/tags/golang/vaas/}
diff --git a/golang/vaas/v3/README.md b/golang/vaas/v3/README.md
new file mode 100644
index 00000000..865747d4
--- /dev/null
+++ b/golang/vaas/v3/README.md
@@ -0,0 +1,164 @@
+[![vaas-golang-ci](https://github.com/GDATASoftwareAG/vaas/actions/workflows/ci-golang.yaml/badge.svg)](https://github.com/GDATASoftwareAG/vaas/actions/workflows/ci-golang.yaml)
+[![Vulnerability Check](https://github.com/GDATASoftwareAG/vaas/actions/workflows/vulncheck-golang.yml/badge.svg)](https://github.com/GDATASoftwareAG/vaas/actions/workflows/vulncheck-golang.yml)
+[![Go Reference](https://pkg.go.dev/badge/github.com/GDATASoftwareAG/vaas/golang/vaas/.svg)](https://pkg.go.dev/github.com/GDATASoftwareAG/vaas/golang/vaas/)
+[![Go Report Card](https://goreportcard.com/badge/github.com/GDATASoftwareAG/vaas/golang/vaas)](https://goreportcard.com/report/github.com/GDATASoftwareAG/vaas/golang/vaas)
+
+# Go VaaS Client
+
+This is a Golang package that provides a client for the G DATA VaaS API.
+
+_Verdict-as-a-Service_ (VaaS) is a service that provides a platform for scanning files for malware and other threats. It allows easy integration into your application. With a few lines of code, you can start scanning files for malware.
+
+# Table of Contents
+
+- [What does the SDK do?](#what-does-the-sdk-do)
+- [How to use](#how-to-use)
+ - [Installation](#installation)
+ - [Import](#import)
+ - [Authentication](#authentication)
+ - [Client Credentials Grant](#client-credentials-grant)
+ - [Resource Owner Password Grant](#resource-owner-password-grant)
+ - [Request a verdict](#request-a-verdict)
+- [I'm interested in VaaS](#interested)
+- [Developing with Visual Studio Code](#developing-with-visual-studio-code)
+
+
+## What does the SDK do?
+
+It gives you as a developer functions to talk to G DATA VaaS. It wraps away the complexity of the API into basic functions.
+
+### Connect(ctx context.Context, auth authenticator.Authenticator) (errorChan <-chan error, err error)
+
+Connect opens a websocket connection to the VAAS Server. Use Close() to terminate the connection. The errorChan indicates when a connection was closed. In the case of an unexpected close, an error is written to the channel.
+
+### ForSha256(ctx context.Context, sha256 string) (messages.VaasVerdict, error)
+
+Retrieves the verdict for the given SHA256 hash from the G DATA VaaS API. `ctx` is the context for request cancellation, and `sha256` is the SHA256 hash of the file. If the request fails, an error will be returned. Otherwise, a `messages.VaasVerdict` object containing the verdict will be returned.
+
+### ForFile(ctx context.Context, filePath string) (messages.VaasVerdict, error)
+
+Retrieves the verdict for the given file at the specified `filePath` from the G DATA VaaS API. `ctx` is the context for request cancellation. If the file cannot be opened, an error will be returned. Otherwise, a `messages.VaasVerdict` object containing the verdict will be returned.
+
+### ForFileInMemory(ctx context.Context, fileData io.Reader) (messages.VaasVerdict, error)
+
+Retrieves the verdict for file data provided as an `io.Reader` to the G DATA VaaS API. `ctx` is the context for request cancellation. If the request fails, an error will be returned. Otherwise, a `messages.VaasVerdict` object containing the verdict will be returned.
+
+### ForUrl(ctx context.Context, url string) (messages.VaasVerdict, error)
+
+Retrieves the verdict for the given file URL from the G DATA VaaS API. `ctx` is the context for request cancellation. If the request fails, an error will be returned. Otherwise, a `messages.VaasVerdict` object containing the verdict will be returned.
+
+## How to use
+
+### Installation
+
+```sh
+go get github.com/GDATASoftwareAG/vaas/golang/vaas
+```
+
+### Import
+
+```go
+import (
+ "github.com/GDATASoftwareAG/vaas/golang/vaas/pkg/authenticator"
+ "github.com/GDATASoftwareAG/vaas/golang/vaas/pkg/vaas"
+)
+```
+
+### Authentication
+
+VaaS offers two authentication methods:
+
+#### Client Credentials Grant
+This is suitable for cases where you have a `client_id`and `client_secret`. Here's how to use it:
+
+```go
+authenticator := authenticator.New("client_id", "client_secret", "token_endpoint")
+```
+or
+```go
+authenticator := authenticator.NewWithDefaultTokenEndpoint("client_id", "client_secret")
+```
+#### Resource Owner Password Grant
+This method is used when you have a `username` and `password`. Here's how to use it:
+
+```go
+authenticator := authenticator.NewWithResourceOwnerPassword("client_id", "username", "password", "token_endpoint")
+```
+If you do not have a specific Client ID, please use `"vaas-customer"` as the client_id.
+
+### Request a verdict
+
+Authentication & Initialization:
+```go
+// Create a new authenticator with the provided Client ID and Client Secret
+auth := authenticator.NewWithDefaultTokenEndpoint(clientID, clientSecret)
+
+// Create a new VaaS client with default options
+vaasClient := vaas.NewWithDefaultEndpoint(options.VaasOptions{
+ UseHashLookup: true,
+ UseCache: false,
+ EnableLogs: false,
+})
+
+// Create a context with a cancellation function
+ctx, webSocketCancel := context.WithCancel(context.Background())
+
+// Establish a WebSocket connection to the VaaS server
+errorChan, err := vaasClient.Connect(ctx, auth)
+if err != nil {
+ log.Fatalf("failed to connect to VaaS %s", err.Error())
+}
+defer vaasClient.Close()
+
+// Create a context with a timeout for the analysis
+analysisCtx, analysisCancel := context.WithTimeout(context.Background(), 20*time.Second)
+defer analysisCancel()
+```
+
+Verdict Request for SHA256:
+```go
+// Request a verdict for a specific SHA256 hash (replace "sha256-hash" with the actual SHA256 hash)
+result, err := vaasClient.ForFile(analysisCtx, "sha256-hash")
+if err != nil {
+ log.Fatalf("Failed to get verdict: %v", err)
+}
+fmt.Println(result.Verdict)
+```
+
+Verdict Request for a file:
+```go
+// Request a verdict for a specific file (replace "path-to-your-file" with the actual file path)
+result, err := vaasClient.ForFile(analysisCtx, "path-to-your-file")
+if err != nil {
+ log.Fatalf("Failed to get verdict: %v", err)
+}
+fmt.Printf("Verdict: %s\n", result.Verdict)
+```
+
+Verdict Request for file data provided as an io.Reader:
+```go
+fileData := bytes.NewReader([]byte("file contents"))
+result, err := vaasClient.ForFileInMemory(analysisCtx, fileData)
+if err != nil {
+ log.Fatalf("Failed to get verdict: %v", err)
+}
+fmt.Printf("Verdict: %s\n", result.Verdict)
+```
+
+Verdict Request for a file URL:
+```go
+result, err := vaasClient.ForUrl(analysisCtx, "https://example.com/examplefile")
+if err != nil {
+ log.Fatalf("Failed to get verdict: %v", err)
+}
+fmt.Printf("Verdict: %s\n", result.Verdict)
+```
+
+
+## I'm interested in VaaS
+
+You need credentials to use the service in your application. If you are interested in using VaaS, please [contact us](mailto:oem@gdata.de).
+
+## Developing with Visual Studio Code
+
+Every single SDKs also includes [Devcontainer](./.devcontainer/). If you use the [Visual Studio Code Dev Containers extension](https://code.visualstudio.com/docs/devcontainers/containers), you can run the code in a full-featured development environment.
diff --git a/golang/vaas/v3/cmd/git-scan/main.go b/golang/vaas/v3/cmd/git-scan/main.go
new file mode 100644
index 00000000..73fdd991
--- /dev/null
+++ b/golang/vaas/v3/cmd/git-scan/main.go
@@ -0,0 +1,122 @@
+package main
+
+import (
+ "context"
+ "errors"
+ "log"
+ "net/url"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+
+ "github.com/GDATASoftwareAG/vaas/golang/vaas/v3/pkg/authenticator"
+ "github.com/GDATASoftwareAG/vaas/golang/vaas/v3/pkg/messages"
+ "github.com/GDATASoftwareAG/vaas/golang/vaas/v3/pkg/vaas"
+)
+
+func main() {
+ if len(os.Args) < 3 {
+ log.Fatal("need 2 parameter: remote, targetBranch")
+ }
+
+ remote := os.Args[1]
+ if remote == "" {
+ log.Fatal("no remote set")
+ }
+ log.Println("remote:", remote)
+ targetBranch := os.Args[2]
+ if targetBranch == "" {
+ log.Fatal("no targetBranch set")
+ }
+ log.Println("targetBranch:", targetBranch)
+
+ vaasAuthenticator, credentialsError := getAuthenticator(
+ os.Getenv("VAAS_CLIENT_ID"), os.Getenv("VAAS_CLIENT_SECRET"), os.Getenv("VAAS_USERAME"), os.Getenv("VAAS_PASSWORD"))
+ if credentialsError != nil {
+ log.Fatal(credentialsError)
+ }
+
+ vaasURLString, exists := os.LookupEnv("VAAS_URL")
+ if !exists {
+ vaasURLString = "https://gateway.production.vaas.gdatasecurity.de"
+ }
+ vaasURL, err := url.Parse(vaasURLString)
+ if err != nil {
+ log.Fatal("VAAS_URL is not an URL")
+ }
+ log.Println("vaas url:", vaasURL)
+
+ gitRevParseCommand := exec.Command("git", "rev-parse", "--show-toplevel")
+ rootDirectoryBytes, err := gitRevParseCommand.CombinedOutput()
+ if err != nil {
+ log.Fatal("git rev-parse: ", err, " ", string(rootDirectoryBytes))
+ }
+ rootDirectory := strings.Split(strings.ReplaceAll(string(rootDirectoryBytes), "\r\n", "\n"), "\n")[0]
+ log.Println("repository root directory: ", rootDirectory)
+
+ fetchBytesCommand := exec.Command("git", "fetch", remote, targetBranch)
+ fetchBytes, err := fetchBytesCommand.CombinedOutput()
+ if err != nil {
+ log.Fatal("git fetch ", err, " ", string(fetchBytes))
+ }
+ log.Println("fetch result: ", string(fetchBytes))
+
+ gitDiffCommand := exec.Command("git", "diff", "--name-only", remote+"/"+targetBranch)
+ diffBytes, err := gitDiffCommand.CombinedOutput()
+ if err != nil {
+ log.Fatal("git diff ", err, " ", string(diffBytes))
+ }
+ files := strings.Split(strings.ReplaceAll(string(diffBytes), "\r\n", "\n"), "\n")
+ if len(files) < 1 {
+ log.Println("no changed files found in diff")
+ os.Exit(0)
+ }
+
+ vaas := vaas.New(vaasURL, vaasAuthenticator)
+ ctx, webSocketCancel := context.WithCancel(context.Background())
+ var maliciousFileFound bool
+ for _, file := range files {
+ if file == "" {
+ continue
+ }
+ if _, err := os.Stat(file); err != nil {
+ continue
+ }
+ log.Println("checking file: ", file)
+ pathToFile := filepath.Join(rootDirectory, file)
+ verdict, err := vaas.ForFile(ctx, pathToFile, nil)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ log.Println(pathToFile + ": " + string(verdict.Verdict))
+ if verdict.Verdict == messages.Malicious {
+ maliciousFileFound = true
+ }
+ }
+ webSocketCancel()
+ if maliciousFileFound {
+ os.Exit(1)
+ }
+}
+
+func getAuthenticator(clientId, clientSecret, username, password string) (vaasAuthenticator authenticator.Authenticator, credentialsError error) {
+ tokenUrl, exists := os.LookupEnv("VAAS_TOKEN_URL")
+ if !exists {
+ tokenUrl = "https://account.gdata.de/realms/vaas-production/protocol/openid-connect/token"
+ }
+ log.Println("token url:", tokenUrl)
+
+ if (clientId != "" && clientSecret != "") || (username != "" && password != "") {
+ if username != "" && password != "" {
+ vaasAuthenticator = authenticator.NewWithResourceOwnerPassword(username, password, "vaas-github-actions", tokenUrl)
+ } else {
+ vaasAuthenticator = authenticator.New(clientId, clientSecret, tokenUrl)
+ }
+
+ return
+
+ }
+ return nil, errors.New("you either need VAAS_CLIENT_ID and VAAS_CLIENT_SECRET or VAAS_USERAME and VAAS_PASSWORD")
+
+}
diff --git a/golang/vaas/v3/cmd/git-scan/main_test.go b/golang/vaas/v3/cmd/git-scan/main_test.go
new file mode 100644
index 00000000..947f835b
--- /dev/null
+++ b/golang/vaas/v3/cmd/git-scan/main_test.go
@@ -0,0 +1,66 @@
+package main
+
+import (
+ "errors"
+ "fmt"
+ "reflect"
+ "testing"
+)
+
+func TestDoubleMe(t *testing.T) {
+ table := []struct {
+ name string
+ clientId string
+ clientSecret string
+ username string
+ password string
+ exprectedError error
+ expectedAuthrenticator string
+ }{
+ {"no credentials set",
+ "", "", "", "",
+ errors.New("you either need VAAS_CLIENT_ID and VAAS_CLIENT_SECRET or VAAS_USERAME and VAAS_PASSWORD"), ""},
+ {"client_id set but client_secret empty",
+ "client_id", "", "", "",
+ errors.New("you either need VAAS_CLIENT_ID and VAAS_CLIENT_SECRET or VAAS_USERAME and VAAS_PASSWORD"), ""},
+ {"client_secret set but client_id empty",
+ "", "client_secret", "", "",
+ errors.New("you either need VAAS_CLIENT_ID and VAAS_CLIENT_SECRET or VAAS_USERAME and VAAS_PASSWORD"), ""},
+ {"username set but password empty",
+ "", "", "username", "",
+ errors.New("you either need VAAS_CLIENT_ID and VAAS_CLIENT_SECRET or VAAS_USERAME and VAAS_PASSWORD"), ""},
+ {"password set but username empty",
+ "", "", "", "password",
+ errors.New("you either need VAAS_CLIENT_ID and VAAS_CLIENT_SECRET or VAAS_USERAME and VAAS_PASSWORD"), ""},
+ {"only client_id and username set",
+ "client_id", "", "username", "",
+ errors.New("you either need VAAS_CLIENT_ID and VAAS_CLIENT_SECRET or VAAS_USERAME and VAAS_PASSWORD"), ""},
+ {"client_credentials set",
+ "client_id", "client_secret", "", "",
+ nil, "*authenticator.commonOIDCAuthenticator"},
+ {"client_credentials and username set",
+ "client_id", "client_secret", "username", "",
+ nil, "*authenticator.commonOIDCAuthenticator"},
+ {"username and password set",
+ "", "", "username", "password",
+ nil, "*authenticator.commonOIDCAuthenticator"},
+ {"username and password and client_id set",
+ "client_id", "", "username", "password",
+ nil, "*authenticator.commonOIDCAuthenticator"},
+ }
+
+ for _, tc := range table {
+ t.Run(tc.name, func(t *testing.T) {
+ vaasAuthenticator, credentialsError := getAuthenticator(tc.clientId, tc.clientSecret, tc.username, tc.password)
+
+ if reflect.TypeOf(credentialsError) != reflect.TypeOf(tc.exprectedError) {
+ t.Errorf("Expected error to be %s, but got %s", tc.exprectedError, credentialsError)
+ }
+
+ typestring := fmt.Sprint(reflect.TypeOf(vaasAuthenticator))
+ if typestring != tc.expectedAuthrenticator {
+ t.Errorf("Expected type %s, but got %s", tc.expectedAuthrenticator, typestring)
+ }
+ })
+ }
+}
diff --git a/golang/vaas/v3/examples/file-verdict-request/main.go b/golang/vaas/v3/examples/file-verdict-request/main.go
new file mode 100644
index 00000000..d8a2a2d7
--- /dev/null
+++ b/golang/vaas/v3/examples/file-verdict-request/main.go
@@ -0,0 +1,68 @@
+// package main implements a simple example of how to
+// request a verdict for a file from a VaaS (Verdict as a Service) server using Go.
+package main
+
+import (
+ "context"
+ "fmt"
+ "log"
+ "net/url"
+ "os"
+ "time"
+
+ "github.com/joho/godotenv"
+
+ "github.com/GDATASoftwareAG/vaas/golang/vaas/v3/pkg/authenticator"
+ "github.com/GDATASoftwareAG/vaas/golang/vaas/v3/pkg/vaas"
+)
+
+func main() {
+ // Load environment variables from a .env file (if it exists)
+ if err := godotenv.Load(); err != nil {
+ log.Printf("failed to load environment - %v", err)
+ }
+
+ // Retrieve the Client ID and Client Secret from environment variables
+ clientID, exists := os.LookupEnv("CLIENT_ID")
+ if !exists {
+ log.Fatal("no Client ID set")
+ }
+ clientSecret, exists := os.LookupEnv("CLIENT_SECRET")
+ if !exists {
+ log.Fatal("no Client Secret set")
+ }
+ tokenEndpoint, exists := os.LookupEnv("TOKEN_URL")
+ if !exists {
+ tokenEndpoint = "https://account-staging.gdata.de/realms/vaas-staging/protocol/openid-connect/token"
+ }
+ vaasURLString, exists := os.LookupEnv("VAAS_URL")
+ if !exists {
+ vaasURLString = "https://gateway.staging.vaas.gdatasecurity.de"
+ }
+ vaasURL, err := url.Parse(vaasURLString)
+ if err != nil {
+ log.Fatal("VAAS_URL is not an URL")
+ }
+
+ scanPath, exists := os.LookupEnv("SCAN_PATH")
+ if !exists {
+ scanPath = "README.md"
+ }
+
+ // Create a new authenticator with the provided Client ID and Client Secret
+ auth := authenticator.New(clientID, clientSecret, tokenEndpoint)
+
+ // Create a new VaaS client with default options
+ vaasClient := vaas.New(vaasURL, auth)
+
+ // Create a context with a timeout for the analysis
+ analysisCtx, analysisCancel := context.WithTimeout(context.Background(), 20*time.Second)
+ defer analysisCancel()
+
+ // Request a verdict for a specific file
+ result, err := vaasClient.ForFile(analysisCtx, scanPath, nil)
+ if err != nil {
+ log.Fatal(err)
+ }
+ fmt.Println(result.Verdict)
+}
diff --git a/golang/vaas/v3/examples/vaasctl/README.md b/golang/vaas/v3/examples/vaasctl/README.md
new file mode 100644
index 00000000..63735f66
--- /dev/null
+++ b/golang/vaas/v3/examples/vaasctl/README.md
@@ -0,0 +1,60 @@
+# VaaS Command Line Interface (CLI)
+
+This Go programm is an example implementation of a Command Line Interface (CLI) for the G DATA VaaS API. This program allows users to scan URLs, SHA256 hashes and files.
+
+## Installation
+
+1. Clone the repository: `git clone https://github.com/GDATASoftwareAG/vaas.git`
+2. Change directory into the `vaas/golang/vaas/cmd/vaasctl` directory
+3. Build the program: `go build`
+4. Run the program: `./vaas`
+
+Note: Before running the program, please make sure to fill out the required credentials in the environment or `.env` file.
+
+## Usage
+
+### Command Line Arguments
+
+The following command line arguments are supported:
+
++ `-s`: Check one ore multiple SHA256 hashes.
++ `-f`: Check one or multiple files.
++ `-u`: Check one or multiple URLs.
+
+### Example Usage
+
+To check a file:
+
+``` bash
+./vaas -f file_to_check
+```
+
+To check multiple files:
+
+``` bash
+./vaas -f file1_to_check file2_to_check file3_to_check
+```
+
+To check a URL:
+
+``` bash
+./vaas -u url_to_check
+```
+
+To check multiple URLs:
+
+``` bash
+./vaas -f url1_to_check url2_to_check url3_to_check
+```
+
+To check a SHA256 hash:
+
+``` bash
+./vaas -f sha256_to_check
+```
+
+To check multiple SHA256 hashes:
+
+``` bash
+./vaas -f sha2561_to_check sha2562_to_check sha2563_to_check
+```
\ No newline at end of file
diff --git a/golang/vaas/v3/examples/vaasctl/vaas.go b/golang/vaas/v3/examples/vaasctl/vaas.go
new file mode 100644
index 00000000..e4a2a828
--- /dev/null
+++ b/golang/vaas/v3/examples/vaasctl/vaas.go
@@ -0,0 +1,147 @@
+package main
+
+import (
+ "context"
+ "flag"
+ "fmt"
+ "github.com/joho/godotenv"
+ "log"
+ "net/url"
+ "os"
+ "sync"
+ "time"
+
+ "github.com/GDATASoftwareAG/vaas/golang/vaas/v3/pkg/authenticator"
+ "github.com/GDATASoftwareAG/vaas/golang/vaas/v3/pkg/vaas"
+)
+
+var sha256Check = flag.Bool("s", false, "sha256")
+var fileCheck = flag.Bool("f", false, "file")
+var urlCheck = flag.Bool("u", false, "url")
+
+func main() {
+ flag.Parse()
+
+ if err := godotenv.Load(); err != nil {
+ log.Printf("failed to load environment - %v", err)
+ }
+ clientID, exists := os.LookupEnv("CLIENT_ID")
+ if !exists {
+ log.Fatal("no Client ID set")
+ }
+ clientSecret, exists := os.LookupEnv("CLIENT_SECRET")
+ if !exists {
+ log.Fatal("no Client Secret set")
+ }
+ tokenEndpoint, exists := os.LookupEnv("TOKEN_URL")
+ if !exists {
+ tokenEndpoint = "https://account.gdata.de/realms/vaas-production/protocol/openid-connect/token"
+ }
+ vaasURLString, exists := os.LookupEnv("VAAS_URL")
+ if !exists {
+ vaasURLString = "wss://gateway.production.vaas.gdatasecurity.de"
+ }
+ vaasURL, err := url.Parse(vaasURLString)
+ if err != nil {
+ log.Fatal("VAAS_URL is not an URL")
+ }
+
+ auth := authenticator.New(clientID, clientSecret, tokenEndpoint)
+
+ vaasClient := vaas.New(vaasURL, auth)
+
+ analysisCtx, analysisCancel := context.WithTimeout(context.Background(), 15*time.Minute)
+ defer analysisCancel()
+
+ if *sha256Check {
+ sha256List := flag.Args()
+ if err := checkSha256(analysisCtx, sha256List, vaasClient); err != nil {
+ log.Fatal(err)
+ }
+ }
+
+ if *urlCheck {
+ urlList := flag.Args()
+ if err := checkURL(analysisCtx, urlList, vaasClient); err != nil {
+ log.Fatal(err)
+ }
+ }
+
+ if *fileCheck {
+ fileList := flag.Args()
+ if err := checkFile(analysisCtx, fileList, vaasClient); err != nil {
+ log.Fatal(err)
+ }
+ }
+}
+
+func checkFile(ctx context.Context, fileList []string, vaasClient vaas.Vaas) error {
+ if len(fileList) == 0 {
+ log.Fatal("no file entered in arguments")
+ }
+
+ for _, file := range fileList {
+ result, err := vaasClient.ForFile(ctx, file, nil)
+ if err != nil {
+ log.Printf("%s: %s", file, err.Error())
+ continue
+ }
+ fmt.Println(file, result.Sha256, result.Verdict, result.Detection)
+ }
+ return nil
+}
+
+func checkSha256(ctx context.Context, sha256List []string, vaasClient vaas.Vaas) error {
+ if len(sha256List) == 0 {
+ log.Fatal("no sha256 entered in arguments")
+ }
+ for _, sha256 := range sha256List {
+ result, err := vaasClient.ForSha256(ctx, sha256, nil)
+ if err != nil {
+ log.Printf("%s: %s", sha256, err.Error())
+ continue
+ }
+ fmt.Println(sha256, result.Verdict)
+ }
+
+ return nil
+}
+
+func checkURL(ctx context.Context, urlList []string, vaasClient vaas.Vaas) error {
+ if len(urlList) == 0 {
+ log.Fatal("no url entered in arguments")
+ }
+
+ if len(urlList) == 1 {
+ checkUrl, err := url.Parse(urlList[0])
+ if err != nil {
+ return err
+ }
+ result, err := vaasClient.ForUrl(ctx, checkUrl, nil)
+ if err != nil {
+ return err
+ }
+ fmt.Println(result.Verdict)
+
+ } else if len(urlList) > 1 {
+ var waitGroup sync.WaitGroup
+ for _, u := range urlList {
+ waitGroup.Add(1)
+ checkUrl, err := url.Parse(u)
+ if err != nil {
+ return err
+ }
+ go func(checkUrl *url.URL) {
+ defer waitGroup.Done()
+ result, err := vaasClient.ForUrl(ctx, checkUrl, nil)
+ if err != nil {
+ fmt.Println(err)
+ } else {
+ fmt.Println(result)
+ }
+ }(checkUrl)
+ }
+ waitGroup.Wait()
+ }
+ return nil
+}
diff --git a/golang/vaas/v3/git-scan.Dockerfile b/golang/vaas/v3/git-scan.Dockerfile
new file mode 100644
index 00000000..93d74bb9
--- /dev/null
+++ b/golang/vaas/v3/git-scan.Dockerfile
@@ -0,0 +1,13 @@
+FROM ubuntu:24.04 as runner
+
+RUN apt update && apt install -y git
+WORKDIR /app
+
+FROM golang:1.23 as builder
+
+COPY . .
+RUN go build -o /build/git-scan cmd/git-scan/main.go
+
+FROM runner
+COPY --from=builder /build/git-scan /app/git-scan
+ENTRYPOINT ["/app/git-scan"]
\ No newline at end of file
diff --git a/golang/vaas/v3/go.mod b/golang/vaas/v3/go.mod
new file mode 100644
index 00000000..0bd238db
--- /dev/null
+++ b/golang/vaas/v3/go.mod
@@ -0,0 +1,23 @@
+module github.com/GDATASoftwareAG/vaas/golang/vaas/v3
+
+go 1.21
+
+require (
+ github.com/go-playground/validator/v10 v10.23.0
+ github.com/joho/godotenv v1.5.1
+ github.com/stretchr/testify v1.10.0
+)
+
+require (
+ github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/gabriel-vasile/mimetype v1.4.3 // indirect
+ github.com/go-playground/locales v0.14.1 // indirect
+ github.com/go-playground/universal-translator v0.18.1 // indirect
+ github.com/leodido/go-urn v1.4.0 // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
+ golang.org/x/crypto v0.19.0 // indirect
+ golang.org/x/net v0.21.0 // indirect
+ golang.org/x/sys v0.17.0 // indirect
+ golang.org/x/text v0.14.0 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+)
diff --git a/golang/vaas/v3/go.sum b/golang/vaas/v3/go.sum
new file mode 100644
index 00000000..785e02d1
--- /dev/null
+++ b/golang/vaas/v3/go.sum
@@ -0,0 +1,32 @@
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
+github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
+github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
+github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
+github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
+github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
+github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
+github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o=
+github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
+github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
+github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
+github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
+github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
+golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
+golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
+golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
+golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
+golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/golang/vaas/v3/internal/hash/hash.go b/golang/vaas/v3/internal/hash/hash.go
new file mode 100644
index 00000000..f4df7351
--- /dev/null
+++ b/golang/vaas/v3/internal/hash/hash.go
@@ -0,0 +1,18 @@
+// Package hash provides utility functions for hashing data using the SHA-256 algorithm.
+package hash
+
+import (
+ "crypto/sha256"
+ "fmt"
+ "io"
+)
+
+// CalculateSha256 calculates the SHA-256 hash of the data from the given io.Reader.
+// It returns the hexadecimal representation of the hash and any error encountered during the process.
+func CalculateSha256(data io.Reader) (string, error) {
+ sha := sha256.New()
+ if _, err := io.Copy(sha, data); err != nil {
+ return "", err
+ }
+ return fmt.Sprintf("%x", sha.Sum(nil)), nil
+}
diff --git a/golang/vaas/v3/pkg/authenticator/authenticator.go b/golang/vaas/v3/pkg/authenticator/authenticator.go
new file mode 100644
index 00000000..8fade34e
--- /dev/null
+++ b/golang/vaas/v3/pkg/authenticator/authenticator.go
@@ -0,0 +1,202 @@
+// Package authenticator provides a set of implementations for obtaining authentication tokens
+// for G DATA CyberDefense's Verdict as a Service (VaaS) using different grant types.
+//
+// # Overview
+//
+// VaaS (Verdict as a Service) is a service that allows clients to obtain verdicts and security information
+// about files and URLs. To access VaaS, clients need to authenticate themselves by obtaining an access token.
+//
+// This package offers two primary implementations of the Authenticator interface:
+//
+// 1. clientCredentialsGrantAuthenticator: This implementation follows the Client Credentials Grant flow,
+// suitable for machine-to-machine communication where the client directly authenticates with its credentials.
+// To use this grant type, you need a client ID and a client secret.
+//
+// 2. resourceOwnerPasswordGrantAuthenticator: This implementation follows the Resource Owner Password Grant flow,
+// suitable for scenarios where the client has access to the user's credentials and can authenticate on their behalf.
+// To use this grant type, you need a client ID, username, and password.
+//
+// # Usage
+//
+// To use this package, you typically follow these steps:
+//
+// 1. Choose the appropriate grant type based on your use case:
+// - Use `New` or `NewWithDefaultTokenEndpoint` for the Client Credentials Grant.
+// - Use `NewWithResourceOwnerPassword` for the Resource Owner Password Grant.
+//
+// 2. Initialize an authenticator with the required parameters, such as client ID, client secret, username, and password.
+//
+// 3. Call the `GetToken` method to obtain an authentication token.
+//
+// Example:
+//
+// clientID := "your-client-id"
+// clientSecret := "your-client-secret"
+// tokenEndpoint := "https://example.com/token-endpoint"
+//
+// // Create an authenticator for the Client Credentials Grant
+// auth := authenticator.New(clientID, clientSecret, tokenEndpoint)
+//
+// // Obtain an authentication token
+// token, err := auth.GetToken()
+// if err != nil {
+// log.Fatal(err)
+// }
+//
+// // Use the obtained token for accessing VaaS services.
+//
+// Note: Make sure to keep your client credentials secure, and use the appropriate grant type for your use case.
+//
+// For more information about VaaS and the different grant types, refer to the official G DATA CyberDefense VaaS documentation.
+package authenticator
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "sync"
+
+ "net/http"
+ "net/url"
+ "time"
+
+ msg "github.com/GDATASoftwareAG/vaas/golang/vaas/v3/pkg/messages"
+)
+
+// Authenticator represents the interface for obtaining an authentication token using client credentials.
+type Authenticator interface {
+ GetToken() (string, error)
+}
+
+// commonOIDCAuthenticator implements the Authenticator, supporting both the clientCredentials and
+// resourceOwnerPassword flows.
+type commonOIDCAuthenticator struct {
+ httpClient *http.Client
+ tokenEndpoint string
+ parameters url.Values
+ token *cachedToken
+ tokenLock sync.Mutex
+}
+
+// cachedToken is a cached access token that may be reused
+type cachedToken struct {
+ accessToken string
+ expires time.Time
+}
+
+// ShouldRefresh determines whether this token should be replaced by a newer one
+func (c cachedToken) ShouldRefresh() bool {
+ return time.Now().After(c.expires)
+}
+
+func parametersForClientCredentials(clientID, clientSecret string) url.Values {
+ data := url.Values{}
+ data.Set("client_id", clientID)
+ data.Set("client_secret", clientSecret)
+ data.Set("grant_type", "client_credentials")
+ return data
+}
+
+func parametersForResourceOwnerPassword(clientID, username, password string) url.Values {
+ data := url.Values{}
+ data.Set("client_id", clientID)
+ data.Set("username", username)
+ data.Set("password", password)
+ data.Set("grant_type", "password")
+ return data
+}
+
+// New creates a new instance of the clientCredentialsGrantAuthenticator.
+// It requires the client ID, client secret, and token endpoint URL as arguments.
+// Example usage:
+//
+// clientID := "your-client-id"
+// clientSecret := "your-client-secret"
+// tokenEndpoint := "https://example.com/token-endpoint"
+//
+// auth := authenticator.New(clientID, clientSecret, tokenEndpoint)
+// token, err := auth.GetToken()
+// if err != nil {
+// log.Fatal(err)
+// }
+func New(clientID string, clientSecret string, tokenEndpoint string) Authenticator {
+ return &commonOIDCAuthenticator{
+ tokenEndpoint: tokenEndpoint,
+ httpClient: &http.Client{Timeout: 120 * time.Second},
+ parameters: parametersForClientCredentials(clientID, clientSecret),
+ }
+}
+
+// NewWithDefaultTokenEndpoint creates a new instance of the clientCredentialsGrantAuthenticator with a default token endpoint.
+// It requires the client ID and client secret as arguments.
+// Example usage:
+//
+// clientID := "your-client-id"
+// clientSecret := "your-client-secret"
+// tokenEndpoint := "https://example.com/token-endpoint"
+//
+// auth := authenticator.NewWithDefaultTokenEndpoint(clientID, clientSecret)
+// token, err := auth.GetToken()
+// if err != nil {
+// log.Fatal(err)
+// }
+func NewWithDefaultTokenEndpoint(clientID string, clientSecret string) Authenticator {
+ return New(clientID, clientSecret, "https://account.gdata.de/realms/vaas-production/protocol/openid-connect/token")
+}
+
+// GetToken obtains an authentication token using the configured authentication flow.
+// It returns the obtained token and any error encountered during the process.
+func (c *commonOIDCAuthenticator) GetToken() (string, error) {
+ c.tokenLock.Lock()
+ defer c.tokenLock.Unlock()
+
+ if c.token == nil || c.token.ShouldRefresh() {
+ // New token
+ response, err := c.httpClient.Post(c.tokenEndpoint, "application/x-www-form-urlencoded", bytes.NewReader([]byte(c.parameters.Encode())))
+ if err != nil {
+ return "", err
+ }
+
+ if response.StatusCode != 200 {
+ var tokenErrResponse msg.TokenErrorResponse
+ if err := json.NewDecoder(response.Body).Decode(&tokenErrResponse); err != nil {
+ return "", fmt.Errorf("http request failed: %s", response.Status)
+ }
+ return "", fmt.Errorf(tokenErrResponse.Error + ": " + tokenErrResponse.ErrorDescription)
+ }
+
+ var tokenResponse msg.TokenResponse
+ if err = json.NewDecoder(response.Body).Decode(&tokenResponse); err != nil {
+ return "", err
+ }
+
+ c.token = &cachedToken{
+ accessToken: tokenResponse.Accesstoken,
+ expires: time.Now().Add(time.Duration(tokenResponse.ExpiresIn) * time.Second),
+ }
+ }
+ return c.token.accessToken, nil
+}
+
+// NewWithResourceOwnerPassword creates a new instance of the resourceOwnerPasswordGrantAuthenticator.
+// It requires the client ID, username, password, and token endpoint URL as arguments.
+// Example usage:
+//
+// clientID := "your-client-id"
+// username := "your-username"
+// password := "your-password"
+// tokenEndpoint := "https://example.com/token-endpoint"
+//
+// auth := authenticator.NewWithResourceOwnerPassword(clientID, username, password, tokenEndpoint)
+// token, err := auth.GetToken()
+//
+// if err != nil {
+// log.Fatal(err)
+// }
+func NewWithResourceOwnerPassword(clientID string, username string, password string, tokenEndpoint string) Authenticator {
+ return &commonOIDCAuthenticator{
+ tokenEndpoint: tokenEndpoint,
+ httpClient: &http.Client{Timeout: 120 * time.Second},
+ parameters: parametersForResourceOwnerPassword(clientID, username, password),
+ }
+}
diff --git a/golang/vaas/v3/pkg/authenticator/authenticator_test.go b/golang/vaas/v3/pkg/authenticator/authenticator_test.go
new file mode 100644
index 00000000..19b343f8
--- /dev/null
+++ b/golang/vaas/v3/pkg/authenticator/authenticator_test.go
@@ -0,0 +1,215 @@
+package authenticator
+
+import (
+ "log"
+ "os"
+ "testing"
+
+ "github.com/joho/godotenv"
+)
+
+func Test_clientCredentialsGrantAuthenticator_GetToken(t *testing.T) {
+ type args struct {
+ clientID string
+ clientSecret string
+ }
+ tests := []struct {
+ name string
+ args args
+ wantErr bool
+ }{
+ {
+ name: "With valid credentials - got token",
+ args: func() args {
+ if err := godotenv.Load(); err != nil {
+ log.Printf("failed to load environment - %v", err)
+ }
+ clientID, exists := os.LookupEnv("CLIENT_ID")
+ if !exists {
+ log.Fatal("no Client ID set")
+ }
+ clientSecret, exists := os.LookupEnv("CLIENT_SECRET")
+ if !exists {
+ log.Fatal("no Client Secret set")
+ }
+
+ return args{
+ clientID: clientID,
+ clientSecret: clientSecret,
+ }
+ }(),
+ wantErr: false,
+ },
+ {
+ name: "With invalid credentials - error",
+ args: func() args {
+ if err := godotenv.Load(); err != nil {
+ log.Printf("failed to load environment - %v", err)
+ }
+
+ return args{
+ clientID: "foo",
+ clientSecret: "bar",
+ }
+ }(),
+ wantErr: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ tokenEndpoint, exists := os.LookupEnv("TOKEN_URL")
+ if !exists {
+ log.Fatal("no token endpoint configured")
+ }
+
+ authenticator := New(tt.args.clientID, tt.args.clientSecret, tokenEndpoint)
+ accessToken, err := authenticator.GetToken()
+
+ if (err != nil) != tt.wantErr {
+ t.Fatalf("unexpected error - %v", err)
+ }
+
+ if err == nil && accessToken == "" {
+ t.Errorf("token should not be empty")
+ }
+ })
+ }
+}
+
+func Test_clientCredentialsGrantAuthenticator_GetToken_ReusesToken(t *testing.T) {
+ type args struct {
+ clientID string
+ clientSecret string
+ }
+ tests := []struct {
+ name string
+ args args
+ wantErr bool
+ }{
+ {
+ name: "Same token when called twice",
+ args: func() args {
+ if err := godotenv.Load(); err != nil {
+ log.Printf("failed to load environment - %v", err)
+ }
+ clientID, exists := os.LookupEnv("CLIENT_ID")
+ if !exists {
+ log.Fatal("no Client ID set")
+ }
+ clientSecret, exists := os.LookupEnv("CLIENT_SECRET")
+ if !exists {
+ log.Fatal("no Client Secret set")
+ }
+
+ return args{
+ clientID: clientID,
+ clientSecret: clientSecret,
+ }
+ }(),
+ wantErr: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ tokenEndpoint, exists := os.LookupEnv("TOKEN_URL")
+ if !exists {
+ log.Fatal("no token endpoint configured")
+ }
+
+ authenticator := New(tt.args.clientID, tt.args.clientSecret, tokenEndpoint)
+ accessToken, err := authenticator.GetToken()
+
+ if (err != nil) != tt.wantErr {
+ t.Fatalf("unexpected error - %v", err)
+ }
+
+ accessToken2, err := authenticator.GetToken()
+
+ if (err != nil) != tt.wantErr {
+ t.Fatalf("unexpected error - %v", err)
+ }
+
+ if accessToken != accessToken2 {
+ t.Errorf("tokens should be equal")
+ }
+ })
+ }
+}
+
+func Test_resourceOwnerPasswordGrantAuthenticator_GetToken(t *testing.T) {
+ type args struct {
+ clientID string
+ username string
+ password string
+ }
+ tests := []struct {
+ name string
+ args args
+ wantErr bool
+ }{
+ {
+ name: "With valid credentials - got token",
+ args: func() args {
+ if err := godotenv.Load(); err != nil {
+ log.Printf("failed to load environment - %v", err)
+ }
+
+ clientID, exists := os.LookupEnv("VAAS_CLIENT_ID")
+ if !exists {
+ log.Fatal("no client-id set")
+ }
+ username, exists := os.LookupEnv("VAAS_USER_NAME")
+ if !exists {
+ log.Fatal("no username set")
+ }
+ password, exists := os.LookupEnv("VAAS_PASSWORD")
+ if !exists {
+ log.Fatal("no password set")
+ }
+
+ return args{
+ clientID: clientID,
+ username: username,
+ password: password,
+ }
+ }(),
+ wantErr: false,
+ },
+ {
+ name: "With invalid credentials - error",
+ args: func() args {
+ if err := godotenv.Load(); err != nil {
+ log.Printf("failed to load environment - %v", err)
+ }
+
+ return args{
+ clientID: "foo",
+ username: "bar",
+ password: "baz",
+ }
+ }(),
+ wantErr: true,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ tokenEndpoint, exists := os.LookupEnv("TOKEN_URL")
+ if !exists {
+ log.Fatal("no token endpoint configured")
+ }
+
+ authenticator := NewWithResourceOwnerPassword(tt.args.clientID, tt.args.username, tt.args.password, tokenEndpoint)
+ accessToken, err := authenticator.GetToken()
+
+ if (err != nil) != tt.wantErr {
+ t.Fatalf("unexpected error - %v", err)
+ }
+
+ if err == nil && accessToken == "" {
+ t.Errorf("token should not be empty")
+ }
+ })
+ }
+}
diff --git a/golang/vaas/v3/pkg/messages/token_response.go b/golang/vaas/v3/pkg/messages/token_response.go
new file mode 100644
index 00000000..8aca80c4
--- /dev/null
+++ b/golang/vaas/v3/pkg/messages/token_response.go
@@ -0,0 +1,14 @@
+// Package messages provides structures for handling communication messages between the client and the VaaS server.
+package messages
+
+// TokenResponse represents a response containing an access token.
+type TokenResponse struct {
+ Accesstoken string `json:"access_token"`
+ ExpiresIn int `json:"expires_in"`
+}
+
+// TokenErrorResponse represents an error response containing an error message.
+type TokenErrorResponse struct {
+ Error string `json:"error"`
+ ErrorDescription string `json:"error_description"`
+}
diff --git a/golang/vaas/v3/pkg/messages/unmarshaller.go b/golang/vaas/v3/pkg/messages/unmarshaller.go
new file mode 100644
index 00000000..9cba7fce
--- /dev/null
+++ b/golang/vaas/v3/pkg/messages/unmarshaller.go
@@ -0,0 +1,16 @@
+package messages
+
+import (
+ "encoding/json"
+ "github.com/go-playground/validator/v10"
+)
+
+func UnmarshalAndValidate(data []byte, v any) error {
+ err := json.Unmarshal(data, v)
+ if err != nil {
+ return err
+ }
+ validate := validator.New()
+ err = validate.Struct(v)
+ return err
+}
diff --git a/golang/vaas/v3/pkg/messages/vaas_analysis.go b/golang/vaas/v3/pkg/messages/vaas_analysis.go
new file mode 100644
index 00000000..6a13ce5b
--- /dev/null
+++ b/golang/vaas/v3/pkg/messages/vaas_analysis.go
@@ -0,0 +1,9 @@
+package messages
+
+type URLAnalysis struct {
+ JobId string `json:"id" validate:"required"`
+}
+
+type FileAnalysis struct {
+ Sha256 string `json:"sha256" validate:"required"`
+}
diff --git a/golang/vaas/v3/pkg/messages/vaas_error.go b/golang/vaas/v3/pkg/messages/vaas_error.go
new file mode 100644
index 00000000..38f8975f
--- /dev/null
+++ b/golang/vaas/v3/pkg/messages/vaas_error.go
@@ -0,0 +1,6 @@
+package messages
+
+type ProblemDetails struct {
+ Type string `json:"type" validate:"required"`
+ Detail string `json:"detail"`
+}
diff --git a/golang/vaas/v3/pkg/messages/vaas_report.go b/golang/vaas/v3/pkg/messages/vaas_report.go
new file mode 100644
index 00000000..db313f1d
--- /dev/null
+++ b/golang/vaas/v3/pkg/messages/vaas_report.go
@@ -0,0 +1,38 @@
+package messages
+
+type FileReport struct {
+ Sha256 string `json:"sha256" validate:"required"`
+ Verdict Verdict `json:"verdict" validate:"required"`
+ Detection string `json:"detection,omitempty"`
+ MimeType string `json:"mimetype,omitempty"`
+ FileType string `json:"file_type,omitempty"`
+}
+
+func (r *FileReport) ConvertToVaasVerdict() VaasVerdict {
+ return VaasVerdict{
+ Verdict: r.Verdict,
+ Sha256: r.Sha256,
+ Detection: r.Detection,
+ MimeType: r.MimeType,
+ FileType: r.FileType,
+ }
+}
+
+type URLReport struct {
+ Sha256 string `json:"sha256" validate:"required"`
+ Verdict Verdict `json:"verdict" validate:"required"`
+ Detection string `json:"detection,omitempty"`
+ MimeType string `json:"mimetype,omitempty"`
+ FileType string `json:"file_type,omitempty"`
+ URL string `json:"url" validate:"required"`
+}
+
+func (r *URLReport) ConvertToVaasVerdict() VaasVerdict {
+ return VaasVerdict{
+ Verdict: r.Verdict,
+ Sha256: r.Sha256,
+ Detection: r.Detection,
+ MimeType: r.MimeType,
+ FileType: r.FileType,
+ }
+}
diff --git a/golang/vaas/v3/pkg/messages/vaas_url.go b/golang/vaas/v3/pkg/messages/vaas_url.go
new file mode 100644
index 00000000..50450c23
--- /dev/null
+++ b/golang/vaas/v3/pkg/messages/vaas_url.go
@@ -0,0 +1,6 @@
+package messages
+
+type URLAnalysisRequest struct {
+ Url string `json:"url"`
+ UseHashLookup bool `json:"useHashLookup"`
+}
diff --git a/golang/vaas/v3/pkg/messages/vaas_verdict.go b/golang/vaas/v3/pkg/messages/vaas_verdict.go
new file mode 100644
index 00000000..ea52ac3c
--- /dev/null
+++ b/golang/vaas/v3/pkg/messages/vaas_verdict.go
@@ -0,0 +1,11 @@
+// Package messages provides structures for handling communication messages between the client and the VaaS server.
+package messages
+
+// VaasVerdict represents the verdict information returned by the VaaS server.
+type VaasVerdict struct {
+ Verdict Verdict
+ Sha256 string
+ Detection string
+ MimeType string
+ FileType string
+}
diff --git a/golang/vaas/v3/pkg/messages/verdict.go b/golang/vaas/v3/pkg/messages/verdict.go
new file mode 100644
index 00000000..f2e99820
--- /dev/null
+++ b/golang/vaas/v3/pkg/messages/verdict.go
@@ -0,0 +1,13 @@
+// Package messages provides structures for handling communication messages between the client and the VaaS server.
+package messages
+
+// Verdict represents different verdict outcomes.
+type Verdict string
+
+// Verdict outcomes.
+const (
+ Clean Verdict = "Clean"
+ Unknown Verdict = "Unknown"
+ Malicious Verdict = "Malicious"
+ Pup Verdict = "Pup"
+)
diff --git a/golang/vaas/v3/pkg/options/options.go b/golang/vaas/v3/pkg/options/options.go
new file mode 100644
index 00000000..82cb392c
--- /dev/null
+++ b/golang/vaas/v3/pkg/options/options.go
@@ -0,0 +1,40 @@
+// Package options provides structures and functions for configuring options related to the VaaS client.
+package options
+
+type ForSha256Options struct {
+ UseHashLookup bool // UseHashLookup Controls whether SHA256 hash lookups are used.
+ UseCache bool // UseCache enables or disables server-side caching.
+ VaasRequestId string
+}
+
+func NewForSha256Options() ForSha256Options {
+ return ForSha256Options{UseCache: true, UseHashLookup: true}
+}
+
+type ForFileOptions struct {
+ UseHashLookup bool
+ UseCache bool
+ VaasRequestId string
+}
+
+func NewForFileOptions() ForFileOptions {
+ return ForFileOptions{UseCache: true, UseHashLookup: true}
+}
+
+type ForStreamOptions struct {
+ UseHashLookup bool
+ VaasRequestId string
+}
+
+func NewForStreamOptions() ForStreamOptions {
+ return ForStreamOptions{UseHashLookup: true}
+}
+
+type ForUrlOptions struct {
+ UseHashLookup bool
+ VaasRequestId string
+}
+
+func NewForUrlOptions() ForUrlOptions {
+ return ForUrlOptions{UseHashLookup: true}
+}
diff --git a/golang/vaas/v3/pkg/vaas/vaas.go b/golang/vaas/v3/pkg/vaas/vaas.go
new file mode 100644
index 00000000..c12a6da7
--- /dev/null
+++ b/golang/vaas/v3/pkg/vaas/vaas.go
@@ -0,0 +1,431 @@
+// Package vaas provides a client for interacting with G DATA CyberDefense's VaaS Service
+// for sending analysis requests to the Vaas server for various types of data, such as URLs, SHA256 hashes, and files.
+package vaas
+
+import (
+ "bytes"
+ "context"
+ "crypto/tls"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "os"
+ "strconv"
+
+ "github.com/GDATASoftwareAG/vaas/golang/vaas/v3/internal/hash"
+ "github.com/GDATASoftwareAG/vaas/golang/vaas/v3/pkg/authenticator"
+ msg "github.com/GDATASoftwareAG/vaas/golang/vaas/v3/pkg/messages"
+ "github.com/GDATASoftwareAG/vaas/golang/vaas/v3/pkg/options"
+)
+
+const (
+ userAgent = "Go/3.0.11"
+)
+
+// Errors returned by the VaaS API
+var (
+ ErrVaasClient = errors.New("client error")
+ ErrVaasServer = errors.New("server reported an internal error")
+ ErrVaasAuthentication = errors.New("VaaS authentication failed")
+ ErrVaasConnection = errors.New("could not connect to VaaS")
+)
+
+// Vaas provides various ForXXX-functions to send analysis requests to a VaaS server.
+// All kinds of requests can be canceled by the context.
+// Please refer to the individual function comments for more details on their usage and behavior.
+type Vaas interface {
+ ForSha256(ctx context.Context, sha256 string, options *options.ForSha256Options) (msg.VaasVerdict, error)
+ ForFile(ctx context.Context, path string, options *options.ForFileOptions) (msg.VaasVerdict, error)
+ ForStream(ctx context.Context, stream io.Reader, contentLength int64, options *options.ForStreamOptions) (msg.VaasVerdict, error)
+ ForUrl(ctx context.Context, url *url.URL, options *options.ForUrlOptions) (msg.VaasVerdict, error)
+}
+
+// vaas provides the implementation of the Vaas interface.
+type vaas struct {
+ vaasURL *url.URL
+ authenticator authenticator.Authenticator
+ httpClient *http.Client
+}
+
+// New creates a new instance of the Vaas struct, which represents a client for interacting with a Vaas service.
+// The vaasURL parameter specifies the endpoint for the VaaS service.
+func New(vaasURL *url.URL, authenticator authenticator.Authenticator) Vaas {
+ client := &vaas{
+ vaasURL: vaasURL,
+ authenticator: authenticator,
+ httpClient: &http.Client{
+ Transport: &http.Transport{
+ // Disable HTTP/2
+ TLSNextProto: make(map[string]func(authority string, c *tls.Conn) http.RoundTripper),
+ },
+ },
+ }
+ return client
+}
+
+// NewWithDefaultEndpoint creates a new instance of the Vaas struct with a default endpoint.
+// It represents a client for interacting with a Vaas service.
+func NewWithDefaultEndpoint(authenticator authenticator.Authenticator) Vaas {
+ vaasURL, _ := url.Parse("https://gateway.production.vaas.gdatasecurity.de")
+ return New(vaasURL, authenticator)
+}
+
+func parseVaasError(response *http.Response, responseBody []byte) error {
+ var problemDetails msg.ProblemDetails
+ err := msg.UnmarshalAndValidate(responseBody, &problemDetails)
+ var baseErr error
+ if err != nil {
+ statusCode := response.StatusCode
+ switch {
+ case statusCode == 401:
+ baseErr = errors.Join(ErrVaasAuthentication, errors.New("server did not accept token from identity provider. Check if you are using the correct identity provider"))
+ case statusCode >= 400 && statusCode < 500:
+ baseErr = ErrVaasClient
+ case statusCode >= 500:
+ baseErr = ErrVaasServer
+ default:
+ baseErr = ErrVaasServer
+ }
+ // Server did not reply with a parseable error body, returning the HTTP code instead
+ return errors.Join(baseErr, errors.New("HTTP error: "+response.Status))
+ }
+
+ switch problemDetails.Type {
+ case "VaasClientException":
+ baseErr = ErrVaasClient
+ case "VaasServerException":
+ baseErr = ErrVaasServer
+ default:
+ baseErr = ErrVaasServer
+ }
+ return errors.Join(baseErr, errors.New(problemDetails.Detail))
+}
+
+func readHttpResponse(httpClient *http.Client, request *http.Request) (Response *http.Response, Body []byte, Error error) {
+ resp, err := httpClient.Do(request)
+ if err != nil {
+ return nil, nil, errors.Join(ErrVaasConnection, err)
+ }
+
+ data, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return resp, nil, errors.Join(ErrVaasClient, err)
+ }
+ err = resp.Body.Close()
+ if err != nil {
+ return resp, data, errors.Join(ErrVaasClient, err)
+ }
+ return resp, data, nil
+}
+
+func encodeToJsonBuffer(data any) (*bytes.Buffer, error) {
+ encoded, err := json.Marshal(data)
+ if err != nil {
+ return nil, errors.Join(ErrVaasClient, err)
+ }
+ return bytes.NewBuffer(encoded), nil
+}
+
+func (v *vaas) newAuthenticatedRequest(ctx context.Context, method string, requestId string, url string, body io.Reader) (*http.Request, error) {
+ token, err := v.authenticator.GetToken()
+ if err != nil {
+ return nil, errors.Join(ErrVaasAuthentication, err)
+ }
+
+ req, err := http.NewRequestWithContext(ctx, method, url, body)
+ if err != nil {
+ return nil, errors.Join(ErrVaasClient, err)
+ }
+ req.Header.Add("Authorization", "Bearer "+token)
+ req.Header.Add("User-Agent", userAgent)
+ if requestId != "" {
+ req.Header.Add("tracestate", fmt.Sprintf("vaasrequestid=%v", requestId))
+ }
+ return req, nil
+}
+
+func (v *vaas) uploadUrl(ctx context.Context, url *url.URL, opts options.ForUrlOptions) (msg.URLAnalysis, error) {
+ var analysis = msg.URLAnalysis{}
+ submitUrl := v.vaasURL.JoinPath("urls").String()
+ var analysisRequest = msg.URLAnalysisRequest{
+ Url: url.String(),
+ UseHashLookup: opts.UseHashLookup,
+ }
+ buffer, err := encodeToJsonBuffer(&analysisRequest)
+ if err != nil {
+ return analysis, err
+ }
+ req, err := v.newAuthenticatedRequest(ctx, http.MethodPost, opts.VaasRequestId, submitUrl, buffer)
+ if err != nil {
+ return analysis, err
+ }
+ req.Header.Set("Content-Type", "application/json; charset=UTF-8")
+ response, body, err := readHttpResponse(v.httpClient, req)
+ if err != nil {
+ return analysis, err
+ }
+
+ if response.StatusCode != http.StatusCreated {
+ return analysis, parseVaasError(response, body)
+ }
+
+ err = msg.UnmarshalAndValidate(body, &analysis)
+ if err != nil {
+ return analysis, errors.Join(ErrVaasClient, err)
+ }
+ return analysis, nil
+}
+
+func (v *vaas) uploadFile(ctx context.Context, file io.Reader, contentLength int64, opts options.ForStreamOptions) (msg.FileAnalysis, error) {
+ var analysis = msg.FileAnalysis{}
+ uploadUrl := v.vaasURL.JoinPath("files")
+ params := url.Values{}
+ params.Add("useHashLookup", strconv.FormatBool(opts.UseHashLookup))
+ uploadUrl.RawQuery = params.Encode()
+ req, err := v.newAuthenticatedRequest(ctx, http.MethodPost, opts.VaasRequestId, uploadUrl.String(), file)
+ if err != nil {
+ return analysis, err
+ }
+ req.ContentLength = contentLength
+ response, body, err := readHttpResponse(v.httpClient, req)
+ if err != nil {
+ return analysis, err
+ }
+
+ if response.StatusCode != http.StatusCreated {
+ return analysis, parseVaasError(response, body)
+ }
+
+ err = msg.UnmarshalAndValidate(body, &analysis)
+ if err != nil {
+ return analysis, errors.Join(ErrVaasClient, err)
+ }
+ return analysis, nil
+}
+
+func (v *vaas) pollUrlJob(ctx context.Context, urlJobId string, opts options.ForUrlOptions) (*msg.URLReport, error) {
+ submitUrl := v.vaasURL.JoinPath("urls", urlJobId, "report").String()
+ // Loop until 200 or error
+ for {
+ req, err := v.newAuthenticatedRequest(ctx, http.MethodGet, opts.VaasRequestId, submitUrl, nil)
+ if err != nil {
+ return nil, err
+ }
+ response, body, err := readHttpResponse(v.httpClient, req)
+ if err != nil {
+ return nil, err
+ }
+
+ switch response.StatusCode {
+ case http.StatusNotFound:
+ return nil, errors.Join(ErrVaasClient, fmt.Errorf("url job %v not found", urlJobId))
+ case http.StatusAccepted:
+ continue
+ case http.StatusOK:
+ var report msg.URLReport
+ err := msg.UnmarshalAndValidate(body, &report)
+ if err != nil {
+ return nil, errors.Join(ErrVaasClient, err)
+ }
+ return &report, nil
+ default:
+ return nil, parseVaasError(response, body)
+ }
+ }
+}
+
+func (v *vaas) pollFileReport(ctx context.Context, sha256 string, opts options.ForSha256Options) (*msg.FileReport, error) {
+ reportUrl := v.vaasURL.JoinPath("files", sha256, "report")
+ params := url.Values{}
+ params.Add("useCache", strconv.FormatBool(opts.UseCache))
+ params.Add("useHashLookup", strconv.FormatBool(opts.UseHashLookup))
+ reportUrl.RawQuery = params.Encode()
+ reportUrlString := reportUrl.String()
+ // Loop until we get 200 or an error
+ for {
+ req, err := v.newAuthenticatedRequest(ctx, http.MethodGet, opts.VaasRequestId, reportUrlString, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ response, body, err := readHttpResponse(v.httpClient, req)
+ if err != nil {
+ return nil, err
+ }
+
+ switch response.StatusCode {
+ case http.StatusNotFound:
+ return nil, nil
+ case http.StatusAccepted:
+ continue
+ case http.StatusOK:
+ var report msg.FileReport
+ err = msg.UnmarshalAndValidate(body, &report)
+ if err != nil {
+ return nil, errors.Join(ErrVaasClient, err)
+ }
+ return &report, nil
+ default:
+ return nil, parseVaasError(response, body)
+ }
+ }
+}
+
+// ForSha256 sends an analysis request for a file identified by its SHA256 hash to the VaaS server and returns the verdict.
+// The analysis can be canceled using the provided context.
+//
+// Example usage:
+//
+// vaasClient := vaas.NewWithDefaultEndpoint(options, auth)
+// ctx := context.Background()
+// sha256 := "..."
+// verdict, err := vaasClient.ForSha256(ctx, sha256)
+// if err != nil {
+// log.Fatalf("Failed to get verdict: %v", err)
+// }
+// fmt.Printf("Verdict: %s\n", verdict.Verdict)
+// fmt.Printf("SHA256: %s\n", verdict.Sha256)
+func (v *vaas) ForSha256(ctx context.Context, sha256 string, opts *options.ForSha256Options) (msg.VaasVerdict, error) {
+ if opts == nil {
+ newOpts := options.NewForSha256Options()
+ opts = &newOpts
+ }
+
+ report, err := v.pollFileReport(ctx, sha256, *opts)
+ if err != nil {
+ return msg.VaasVerdict{}, err
+ }
+
+ if report == nil {
+ // Not found
+ return msg.VaasVerdict{
+ Verdict: msg.Unknown,
+ Sha256: sha256,
+ }, nil
+ }
+ return report.ConvertToVaasVerdict(), nil
+}
+
+// ForFile sends an analysis request for a file at the given filePath to the VaaS server and returns the verdict.
+// The analysis can be canceled using the provided context.
+//
+// Example usage:
+//
+// vaasClient := vaas.NewWithDefaultEndpoint(options, auth)
+// ctx := context.Background()
+// filePath := "path/to/file.txt"
+// verdict, err := vaasClient.ForFile(ctx, filePath)
+// if err != nil {
+// log.Fatalf("Failed to get verdict: %v", err)
+// }
+// fmt.Printf("Verdict: %s\n", verdict.Verdict)
+// fmt.Printf("SHA256: %s\n", verdict.Sha256)
+func (v *vaas) ForFile(ctx context.Context, filePath string, opts *options.ForFileOptions) (msg.VaasVerdict, error) {
+ if opts == nil {
+ newOpts := options.NewForFileOptions()
+ opts = &newOpts
+ }
+
+ file, err := os.Open(filePath)
+ if err != nil {
+ return msg.VaasVerdict{}, errors.Join(ErrVaasClient, err)
+ }
+ defer func() {
+ _ = file.Close()
+ }()
+
+ sha256, err := hash.CalculateSha256(file)
+ if err != nil {
+ return msg.VaasVerdict{}, errors.Join(ErrVaasClient, err)
+ }
+
+ shaOpts := options.NewForSha256Options()
+ shaOpts.UseCache = opts.UseCache
+ shaOpts.UseHashLookup = opts.UseHashLookup
+ shaOpts.VaasRequestId = opts.VaasRequestId
+ verdict, err := v.ForSha256(ctx, sha256, &shaOpts)
+ // We only care about the hash lookup if it's not failed and has actionable verdict
+ if err == nil && verdict.Verdict != msg.Unknown {
+ return verdict, nil
+ }
+
+ if _, err = file.Seek(0, io.SeekStart); err != nil {
+ return msg.VaasVerdict{}, errors.Join(ErrVaasClient, err)
+ }
+
+ stat, err := file.Stat()
+ if err != nil {
+ return msg.VaasVerdict{}, errors.Join(ErrVaasClient, err)
+ }
+
+ streamOpts := options.NewForStreamOptions()
+ streamOpts.UseHashLookup = opts.UseHashLookup
+ streamOpts.VaasRequestId = opts.VaasRequestId
+ return v.ForStream(ctx, file, stat.Size(), &streamOpts)
+}
+
+// ForStream sends an analysis request for a file stream to the VaaS server and returns the verdict.
+// contentLength must be set to the stream's length, in bytes.
+// The analysis can be canceled using the provided context.
+//
+// Example usage:
+//
+// vaasClient := vaas.NewWithDefaultEndpoint(options, auth)
+// ctx := context.Background()
+// contentLength := 1234
+// verdict, err := vaasClient.ForStream(ctx, stream, contentLength)
+// if err != nil {
+// log.Fatalf("Failed to get verdict: %v", err)
+// }
+// fmt.Printf("Verdict: %s\n", verdict.Verdict)
+// fmt.Printf("SHA256: %s\n", verdict.Sha256)
+func (v *vaas) ForStream(ctx context.Context, stream io.Reader, contentLength int64, opts *options.ForStreamOptions) (msg.VaasVerdict, error) {
+ if opts == nil {
+ newOpts := options.NewForStreamOptions()
+ opts = &newOpts
+ }
+
+ analysis, err := v.uploadFile(ctx, stream, contentLength, *opts)
+ if err != nil {
+ return msg.VaasVerdict{}, err
+ }
+
+ sha256Opts := options.NewForSha256Options()
+ sha256Opts.UseHashLookup = opts.UseHashLookup
+ sha256Opts.VaasRequestId = opts.VaasRequestId
+ return v.ForSha256(ctx, analysis.Sha256, &sha256Opts)
+}
+
+// ForUrl sends an analysis request for a file URL to the VaaS server and returns the verdict.
+// The analysis can be canceled using the provided context.
+//
+// Example usage:
+//
+// vaasClient := vaas.NewWithDefaultEndpoint(options, auth)
+// ctx := context.Background()
+// myUrl, _ := url.Parse("https://example.com/examplefile")
+// verdict, err := vaasClient.ForUrl(ctx, myUrl)
+// if err != nil {
+// log.Fatalf("Failed to get verdict: %v", err)
+// }
+// fmt.Printf("Verdict: %s\n", verdict.Verdict)
+// fmt.Printf("SHA256: %s\n", verdict.Sha256)
+func (v *vaas) ForUrl(ctx context.Context, url *url.URL, opts *options.ForUrlOptions) (msg.VaasVerdict, error) {
+ if opts == nil {
+ newOpts := options.NewForUrlOptions()
+ opts = &newOpts
+ }
+
+ analysis, err := v.uploadUrl(ctx, url, *opts)
+ if err != nil {
+ return msg.VaasVerdict{}, err
+ }
+ report, err := v.pollUrlJob(ctx, analysis.JobId, *opts)
+ if err != nil {
+ return msg.VaasVerdict{}, err
+ }
+ return report.ConvertToVaasVerdict(), nil
+}
diff --git a/golang/vaas/v3/pkg/vaas/vaas_test.go b/golang/vaas/v3/pkg/vaas/vaas_test.go
new file mode 100644
index 00000000..814e84a6
--- /dev/null
+++ b/golang/vaas/v3/pkg/vaas/vaas_test.go
@@ -0,0 +1,965 @@
+package vaas
+
+import (
+ "context"
+ "encoding/base64"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "github.com/GDATASoftwareAG/vaas/golang/vaas/v3/pkg/authenticator"
+ msg "github.com/GDATASoftwareAG/vaas/golang/vaas/v3/pkg/messages"
+ "github.com/GDATASoftwareAG/vaas/golang/vaas/v3/pkg/options"
+ "github.com/joho/godotenv"
+ "github.com/stretchr/testify/assert"
+ "io"
+ "log"
+ "math/rand"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "testing"
+ "time"
+)
+
+type testFixture struct {
+ vaasClient Vaas
+ errorChan <-chan error
+}
+
+const (
+ eicarSha256 = "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f"
+ eicarUrl = "https://secure.eicar.org/eicar.com"
+)
+
+func (tf *testFixture) setUp() Vaas {
+ if err := godotenv.Load(); err != nil {
+ log.Printf("failed to load environment - %v", err)
+ }
+
+ vaasURLString, exists := os.LookupEnv("VAAS_URL")
+ if !exists {
+ log.Fatal("no vaas endpoint configured")
+ }
+
+ return tf.setUpWithVaasURL(vaasURLString)
+}
+
+func (tf *testFixture) setUpWithVaasURL(vaasURLString string) Vaas {
+ if err := godotenv.Load(); err != nil {
+ log.Printf("failed to load environment - %v", err)
+ }
+
+ clientID, exists := os.LookupEnv("CLIENT_ID")
+ if !exists {
+ log.Fatal("no Client ID set")
+ }
+ clientSecret, exists := os.LookupEnv("CLIENT_SECRET")
+ if !exists {
+ log.Fatal("no Client Secret set")
+ }
+ tokenEndpoint, exists := os.LookupEnv("TOKEN_URL")
+ if !exists {
+ log.Fatal("no token endpoint configured")
+ }
+
+ auth := authenticator.New(clientID, clientSecret, tokenEndpoint)
+ tf.setUpWithVaasURLAndAuthenticator(vaasURLString, auth)
+ return tf.vaasClient
+}
+
+func (tf *testFixture) setUpWithVaasURLAndAuthenticator(vaasURLString string, auth authenticator.Authenticator) Vaas {
+ if err := godotenv.Load(); err != nil {
+ log.Printf("failed to load environment - %v", err)
+ }
+
+ vaasURL, err := url.Parse(vaasURLString)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ tf.vaasClient = New(vaasURL, auth)
+
+ return tf.vaasClient
+}
+
+func Test_ForSha256(t *testing.T) {
+ const (
+ cleanSha256 string = "cd617c5c1b1ff1c94a52ab8cf07192654f271a3f8bad49490288131ccb9efc1e"
+ maliciousSha256 string = "ab5788279033b0a96f2d342e5f35159f103f69e0191dd391e036a1cd711791a2"
+ unknownSha256 string = "1f72c1111111111111f912e40b7323a0192a300b376186c10f6803dc5efe28df"
+ )
+ type args struct {
+ sha256 string
+ expectedVerdict msg.Verdict
+ }
+ tests := []struct {
+ args args
+ name string
+ expectedError error
+ }{
+ {
+ name: "With clean sha256 - got verdict clean",
+ args: args{
+ sha256: cleanSha256,
+ expectedVerdict: msg.Clean,
+ },
+ },
+ {
+ name: "With malicious sha256 - got verdict malicious",
+ args: args{
+ sha256: maliciousSha256,
+ expectedVerdict: msg.Malicious,
+ },
+ },
+ {
+ name: "With unknown sha256 - got verdict unknown",
+ args: args{
+ sha256: unknownSha256,
+ expectedVerdict: msg.Unknown,
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ fixture := new(testFixture)
+ vaasClient := fixture.setUp()
+
+ verdict, err := vaasClient.ForSha256(context.Background(), tt.args.sha256, nil)
+
+ if !errors.Is(err, tt.expectedError) {
+ t.Fatalf("unexpected error, expected %v but got %v", tt.expectedError, err)
+ }
+
+ if err == nil && verdict.Verdict != tt.args.expectedVerdict {
+ t.Errorf("verdict should be %v, got %v", tt.args.expectedVerdict, verdict.Verdict)
+ }
+ })
+ }
+}
+
+func Test_ForSha256_IfVaasRequestIdIsSet_SendsTraceState(t *testing.T) {
+ server := getHttpTestServer(func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, r.Header.Get("tracestate"), "vaasrequestid=MyRequestId")
+ defaultHttpHandler(t, w, r)
+ })
+ defer server.Close()
+
+ fixture := new(testFixture)
+ vaasClient := fixture.setUpWithVaasURL(server.URL)
+
+ opts := options.NewForSha256Options()
+ opts.VaasRequestId = "MyRequestId"
+ _, err := vaasClient.ForSha256(context.Background(), eicarSha256, &opts)
+ assert.NoError(t, err, "ForSha256 returned err")
+}
+
+func Test_ForSha256_SendsUserAgent(t *testing.T) {
+ server := getHttpTestServer(func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, r.Header.Get("User-Agent"), userAgent)
+ defaultHttpHandler(t, w, r)
+ })
+ defer server.Close()
+
+ fixture := new(testFixture)
+ vaasClient := fixture.setUpWithVaasURL(server.URL)
+
+ verdict, err := vaasClient.ForSha256(context.Background(), eicarSha256, nil)
+ assert.NoError(t, err, "ForSha256 returned err")
+ assert.Equalf(t, msg.Malicious, verdict.Verdict, "Verdict is not malicious")
+}
+
+func Test_ForSha256_SendsOptions(t *testing.T) {
+ tests := []options.ForSha256Options{
+ {
+ UseHashLookup: true,
+ UseCache: true,
+ },
+ {
+ UseHashLookup: true,
+ UseCache: false,
+ },
+ {
+ UseHashLookup: false,
+ UseCache: true,
+ },
+ {
+ UseHashLookup: false,
+ UseCache: false,
+ },
+ }
+
+ for _, option := range tests {
+ t.Run(fmt.Sprintf("%v", option), func(t *testing.T) {
+ server := getHttpTestServer(func(w http.ResponseWriter, r *http.Request) {
+ useCache := strconv.FormatBool(option.UseCache)
+ useHashLookup := strconv.FormatBool(option.UseHashLookup)
+ expectedUrl := fmt.Sprintf("/files/275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f/report?useCache=%s&useHashLookup=%s", useCache, useHashLookup)
+ assert.Equal(t, expectedUrl, r.URL.String())
+ defaultHttpHandler(t, w, r)
+ })
+ defer server.Close()
+ fixture := new(testFixture)
+ vaasClient := fixture.setUpWithVaasURL(server.URL)
+
+ _, err := vaasClient.ForSha256(context.Background(), eicarSha256, &option)
+ assert.NoError(t, err, "ForSha256 returned err")
+ })
+ }
+}
+
+func Test_ForSha256_IfVaasClientException_ReturnErrVaasClient(t *testing.T) {
+ server := getHttpTestServer(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusBadRequest)
+ })
+ defer server.Close()
+ fixture := new(testFixture)
+ vaasClient := fixture.setUpWithVaasURL(server.URL)
+
+ _, err := vaasClient.ForSha256(context.Background(), "", nil)
+
+ assert.ErrorIs(t, err, ErrVaasClient)
+}
+
+func Test_ForSha256_IfVaasServerException_ReturnErrVaasServer(t *testing.T) {
+ server := getHttpTestServer(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusInternalServerError)
+ })
+ defer server.Close()
+ fixture := new(testFixture)
+ vaasClient := fixture.setUpWithVaasURL(server.URL)
+
+ _, err := vaasClient.ForSha256(context.Background(), eicarSha256, nil)
+
+ assert.ErrorIs(t, err, ErrVaasServer)
+}
+
+func Test_ForSha256_IfVaasReturns401_ReturnErrVaasAuthentication(t *testing.T) {
+ server := getHttpTestServer(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusUnauthorized)
+ })
+ defer server.Close()
+ fixture := new(testFixture)
+ vaasClient := fixture.setUpWithVaasURL(server.URL)
+
+ _, err := vaasClient.ForSha256(context.Background(), eicarSha256, nil)
+
+ assert.ErrorIs(t, err, ErrVaasAuthentication)
+}
+
+func Test_ForSha256_IfAuthenticationFailure_ReturnErrVaasAuthentication(t *testing.T) {
+ server := getHttpTestServer(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusUnauthorized)
+ })
+ defer server.Close()
+ fixture := new(testFixture)
+
+ vaasClient := fixture.setUpWithVaasURLAndAuthenticator(server.URL, mockFailureAuthenticator{})
+
+ _, err := vaasClient.ForSha256(context.Background(), eicarSha256, nil)
+
+ assert.ErrorIs(t, err, ErrVaasAuthentication)
+ assert.ErrorContains(t, err, "placeholder error message")
+}
+
+func Test_ForSha256_WithDeadlineContext_Cancels(t *testing.T) {
+ fixture := new(testFixture)
+ vaasClient := fixture.setUp()
+
+ cancelCtx, cancel := context.WithTimeout(context.Background(), 1*time.Nanosecond)
+ defer cancel()
+ verdict, err := vaasClient.ForSha256(cancelCtx, eicarSha256, nil)
+
+ if err == nil {
+ t.Fatalf("expected error got success instead (%v)", verdict)
+ }
+
+ if !errors.Is(err, context.DeadlineExceeded) {
+ t.Fatalf("expected cancelled error, got %v", err)
+ }
+}
+
+type mockFailureAuthenticator struct {
+}
+
+func (m mockFailureAuthenticator) GetToken() (string, error) {
+ return "", errors.New("placeholder error message")
+}
+
+func getHttpTestServer(handler func(w http.ResponseWriter, r *http.Request)) *httptest.Server {
+ return httptest.NewServer(http.HandlerFunc(handler))
+}
+
+func defaultHttpHandler(t *testing.T, w http.ResponseWriter, r *http.Request) {
+ if r.URL.String() == "/urls" {
+ w.WriteHeader(http.StatusCreated)
+ _, err := w.Write([]byte(`{"id":"1"}`))
+ assert.NoError(t, err)
+ return
+ }
+ if r.URL.String() == "/urls/1/report" {
+ w.WriteHeader(http.StatusOK)
+ _, err := w.Write([]byte(fmt.Sprintf(`{"verdict":"Malicious","sha256":"%s","url":"%s"}`, eicarSha256, eicarUrl)))
+ assert.NoError(t, err)
+ return
+ }
+ w.WriteHeader(http.StatusOK)
+ _, err := w.Write([]byte(fmt.Sprintf(`{"verdict":"Malicious","sha256":"%s"}`, eicarSha256)))
+ assert.NoError(t, err)
+}
+
+func createEicarFile(t *testing.T) string {
+ const (
+ eicarString string = "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*"
+ )
+ testFile := filepath.Join(t.TempDir(), "testfile")
+ if err := os.WriteFile(testFile, []byte(eicarString), 0644); err != nil {
+ t.Fatalf("error while writing file: %v", err)
+ }
+ return testFile
+}
+
+func Test_ForFile(t *testing.T) {
+ const (
+ eicarBase64String string = "WDVPIVAlQEFQWzRcUFpYNTQoUF4pN0NDKTd9JEVJQ0FSLVNUQU5EQVJELUFOVElWSVJVUy1URVNULUZJTEUhJEgrSCo"
+ )
+ type args struct {
+ fileContent string
+ expectedVerdict msg.Verdict
+ }
+ tests := []struct {
+ args args
+ name string
+ expectedError error
+ }{
+ {
+ name: "with eicar file - got verdict malicious",
+ args: args{
+ fileContent: func() string {
+ decodedEicarString, _ := base64.StdEncoding.DecodeString(eicarBase64String)
+ return string(decodedEicarString)
+ }(),
+ expectedVerdict: msg.Malicious,
+ },
+ },
+ {
+ name: "With random file - got verdict clean",
+ args: args{
+ fileContent: RandomString(200),
+ expectedVerdict: msg.Clean,
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ fixture := new(testFixture)
+ vaasClient := fixture.setUp()
+
+ testFile := filepath.Join(t.TempDir(), "testfile")
+ if err := os.WriteFile(testFile, []byte(tt.args.fileContent), 0644); err != nil {
+ t.Fatalf("error while writing file: %v", err)
+ }
+
+ // test disk file
+ verdict, err := vaasClient.ForFile(context.Background(), testFile, nil)
+
+ if !errors.Is(err, tt.expectedError) {
+ t.Fatalf("unexpected error, expected %v but got %v", tt.expectedError, err)
+ }
+
+ if err == nil && verdict.Verdict != tt.args.expectedVerdict {
+ t.Errorf("verdict should be %v, got %v", tt.args.expectedVerdict, verdict.Verdict)
+ }
+ })
+ }
+}
+
+func Test_ForFile_IfVaasRequestIdIsSet_SendsTraceState(t *testing.T) {
+ eicar := createEicarFile(t)
+ server := getHttpTestServer(func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, r.Header.Get("tracestate"), "vaasrequestid=MyRequestId")
+ defaultHttpHandler(t, w, r)
+ })
+ defer server.Close()
+
+ fixture := new(testFixture)
+ vaasClient := fixture.setUpWithVaasURL(server.URL)
+
+ opts := options.NewForFileOptions()
+ opts.VaasRequestId = "MyRequestId"
+ _, err := vaasClient.ForFile(context.Background(), eicar, &opts)
+ assert.NoError(t, err)
+}
+
+func Test_ForFile_SendsUserAgent(t *testing.T) {
+ eicar := createEicarFile(t)
+ server := getHttpTestServer(func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, r.Header.Get("User-Agent"), userAgent)
+ defaultHttpHandler(t, w, r)
+ })
+ defer server.Close()
+
+ fixture := new(testFixture)
+ vaasClient := fixture.setUpWithVaasURL(server.URL)
+
+ verdict, err := vaasClient.ForFile(context.Background(), eicar, nil)
+ assert.NoError(t, err, "ForFile returned err")
+ assert.Equalf(t, msg.Malicious, verdict.Verdict, "Verdict is not malicious")
+}
+
+func Test_ForFile_SendsOptions(t *testing.T) {
+ tests := []options.ForFileOptions{
+ {
+ UseHashLookup: true,
+ UseCache: true,
+ },
+ {
+ UseHashLookup: true,
+ UseCache: false,
+ },
+ {
+ UseHashLookup: false,
+ UseCache: true,
+ },
+ {
+ UseHashLookup: false,
+ UseCache: false,
+ },
+ }
+
+ eicar := createEicarFile(t)
+
+ for _, option := range tests {
+ t.Run(fmt.Sprintf("%v", option), func(t *testing.T) {
+ server := getHttpTestServer(func(w http.ResponseWriter, r *http.Request) {
+ useCache := strconv.FormatBool(option.UseCache)
+ useHashLookup := strconv.FormatBool(option.UseHashLookup)
+ expectedUrl := fmt.Sprintf("/files/275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f/report?useCache=%s&useHashLookup=%s", useCache, useHashLookup)
+ assert.Equal(t, expectedUrl, r.URL.String())
+ defaultHttpHandler(t, w, r)
+ })
+ defer server.Close()
+ fixture := new(testFixture)
+ vaasClient := fixture.setUpWithVaasURL(server.URL)
+
+ _, err := vaasClient.ForFile(context.Background(), eicar, &option)
+ assert.NoError(t, err)
+ })
+ }
+}
+
+func Test_ForFile_IfVaasClientException_ReturnErrVaasClient(t *testing.T) {
+ eicar := createEicarFile(t)
+ server := getHttpTestServer(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusBadRequest)
+ })
+ defer server.Close()
+ fixture := new(testFixture)
+ vaasClient := fixture.setUpWithVaasURL(server.URL)
+
+ _, err := vaasClient.ForFile(context.Background(), eicar, nil)
+
+ assert.ErrorIs(t, err, ErrVaasClient)
+}
+
+func Test_ForFile_IfVaasServerException_ReturnErrVaasServer(t *testing.T) {
+ eicar := createEicarFile(t)
+ server := getHttpTestServer(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusInternalServerError)
+ })
+ defer server.Close()
+ fixture := new(testFixture)
+ vaasClient := fixture.setUpWithVaasURL(server.URL)
+
+ _, err := vaasClient.ForFile(context.Background(), eicar, nil)
+
+ assert.ErrorIs(t, err, ErrVaasServer)
+}
+
+func Test_ForFile_IfVaasReturns401_ReturnErrVaasAuthentication(t *testing.T) {
+ eicar := createEicarFile(t)
+ server := getHttpTestServer(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusUnauthorized)
+ })
+ defer server.Close()
+ fixture := new(testFixture)
+ vaasClient := fixture.setUpWithVaasURL(server.URL)
+
+ _, err := vaasClient.ForFile(context.Background(), eicar, nil)
+
+ assert.ErrorIs(t, err, ErrVaasAuthentication)
+}
+
+func Test_ForFile_IfAuthenticationFailure_ReturnErrVaasAuthentication(t *testing.T) {
+ eicar := createEicarFile(t)
+ server := getHttpTestServer(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusUnauthorized)
+ })
+ defer server.Close()
+ fixture := new(testFixture)
+
+ vaasClient := fixture.setUpWithVaasURLAndAuthenticator(server.URL, mockFailureAuthenticator{})
+
+ _, err := vaasClient.ForFile(context.Background(), eicar, nil)
+
+ assert.ErrorIs(t, err, ErrVaasAuthentication)
+ assert.ErrorContains(t, err, "placeholder error message")
+}
+
+func Test_ForFile_WithDeadlineContext_Cancels(t *testing.T) {
+ eicar := createEicarFile(t)
+ fixture := new(testFixture)
+ vaasClient := fixture.setUp()
+
+ cancelCtx, cancel := context.WithTimeout(context.Background(), 1*time.Nanosecond)
+ defer cancel()
+ verdict, err := vaasClient.ForFile(cancelCtx, eicar, nil)
+
+ if err == nil {
+ t.Fatalf("expected error got success instead (%v)", verdict)
+ }
+
+ if !errors.Is(err, context.DeadlineExceeded) {
+ t.Fatalf("expected cancelled error, got %v", err)
+ }
+}
+
+func Test_ForStream_WithEicarString_ReturnsMalicious(t *testing.T) {
+ eicarReader := strings.NewReader("X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*")
+ fixture := new(testFixture)
+ vaasClient := fixture.setUp()
+
+ verdict, err := vaasClient.ForStream(context.Background(), eicarReader, eicarReader.Size(), nil)
+
+ if err != nil {
+ t.Fatalf("unexpected error - %v", err)
+ }
+
+ if verdict.Verdict != msg.Malicious {
+ t.Errorf("verdict should be %v, got %v", msg.Malicious, verdict.Verdict)
+ }
+}
+
+func Test_ForStream_WitEicarFromUrl_ReturnsMalicious(t *testing.T) {
+ response, _ := http.Get("https://secure.eicar.org/eicar.com.txt")
+
+ fixture := new(testFixture)
+ vaasClient := fixture.setUp()
+
+ verdict, err := vaasClient.ForStream(context.Background(), response.Body, response.ContentLength, nil)
+
+ if err != nil {
+ t.Fatalf("unexpected error - %v", err)
+ }
+
+ if verdict.Verdict != msg.Malicious {
+ t.Errorf("verdict should be %v, got %v", msg.Malicious, verdict.Verdict)
+ }
+}
+
+func Test_ForStream_SendsUserAgent(t *testing.T) {
+ eicarReader := strings.NewReader("X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*")
+ server := getHttpTestServer(func(w http.ResponseWriter, r *http.Request) {
+ if strings.Contains(r.URL.Path, "/report") {
+ assert.Equal(t, r.Header.Get("User-Agent"), userAgent)
+ defaultHttpHandler(t, w, r)
+ } else {
+ assert.Equal(t, r.Header.Get("User-Agent"), userAgent)
+ w.WriteHeader(http.StatusCreated)
+ _, err := w.Write([]byte(`{"sha256": "12345"}`))
+ assert.NoError(t, err)
+ }
+ })
+ defer server.Close()
+
+ fixture := new(testFixture)
+ vaasClient := fixture.setUpWithVaasURL(server.URL)
+
+ verdict, err := vaasClient.ForStream(context.Background(), eicarReader, eicarReader.Size(), nil)
+ assert.NoError(t, err, "ForStream returned err")
+ assert.Equalf(t, msg.Malicious, verdict.Verdict, "Verdict is not malicious")
+}
+
+func Test_ForStream_SendsOptions(t *testing.T) {
+ eicarReader := strings.NewReader("X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*")
+ tests := []options.ForStreamOptions{
+ {
+ UseHashLookup: true,
+ },
+ {
+ UseHashLookup: false,
+ },
+ }
+
+ for _, option := range tests {
+ t.Run(fmt.Sprintf("%v", option), func(t *testing.T) {
+ server := getHttpTestServer(func(w http.ResponseWriter, r *http.Request) {
+ if strings.Contains(r.URL.Path, "/report") {
+ defaultHttpHandler(t, w, r)
+ } else {
+ useHashLookup := strconv.FormatBool(option.UseHashLookup)
+ expectedUrl := fmt.Sprintf("/files?useHashLookup=%s", useHashLookup)
+ assert.Equal(t, expectedUrl, r.URL.String())
+ w.WriteHeader(http.StatusCreated)
+ _, err := w.Write([]byte(`{"sha256": "12345"}`))
+ assert.NoError(t, err)
+ }
+ })
+ defer server.Close()
+ fixture := new(testFixture)
+ vaasClient := fixture.setUpWithVaasURL(server.URL)
+
+ _, err := vaasClient.ForStream(context.Background(), eicarReader, eicarReader.Size(), &option)
+ assert.NoError(t, err)
+ })
+ }
+}
+
+func Test_ForStream_IfVaasRequestIdIsSet_SendsTraceState(t *testing.T) {
+ eicarReader := strings.NewReader("X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*")
+ server := getHttpTestServer(func(w http.ResponseWriter, r *http.Request) {
+ if strings.Contains(r.URL.Path, "/report") {
+ assert.Equal(t, r.Header.Get("tracestate"), "vaasrequestid=MyRequestId")
+ defaultHttpHandler(t, w, r)
+ } else {
+ assert.Equal(t, r.Header.Get("tracestate"), "vaasrequestid=MyRequestId")
+ w.WriteHeader(http.StatusCreated)
+ _, err := w.Write([]byte(`{"sha256": "12345"}`))
+ assert.NoError(t, err)
+ }
+ })
+ defer server.Close()
+
+ fixture := new(testFixture)
+ vaasClient := fixture.setUpWithVaasURL(server.URL)
+
+ opts := options.NewForStreamOptions()
+ opts.VaasRequestId = "MyRequestId"
+ _, err := vaasClient.ForStream(context.Background(), eicarReader, eicarReader.Size(), &opts)
+ assert.NoError(t, err)
+}
+
+func Test_ForStream_IfVaasClientException_ReturnErrVaasClient(t *testing.T) {
+ eicarReader := strings.NewReader("X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*")
+ server := getHttpTestServer(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusBadRequest)
+ })
+ defer server.Close()
+ fixture := new(testFixture)
+ vaasClient := fixture.setUpWithVaasURL(server.URL)
+
+ _, err := vaasClient.ForStream(context.Background(), eicarReader, eicarReader.Size(), nil)
+
+ assert.ErrorIs(t, err, ErrVaasClient)
+}
+
+func Test_ForStream_IfVaasServerException_ReturnErrVaasServer(t *testing.T) {
+ eicarReader := strings.NewReader("X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*")
+ server := getHttpTestServer(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusInternalServerError)
+ })
+ defer server.Close()
+ fixture := new(testFixture)
+ vaasClient := fixture.setUpWithVaasURL(server.URL)
+
+ _, err := vaasClient.ForStream(context.Background(), eicarReader, eicarReader.Size(), nil)
+
+ assert.ErrorIs(t, err, ErrVaasServer)
+}
+
+func Test_ForStream_IfVaasReturns401_ReturnErrVaasAuthentication(t *testing.T) {
+ eicarReader := strings.NewReader("X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*")
+ server := getHttpTestServer(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusUnauthorized)
+ })
+ defer server.Close()
+ fixture := new(testFixture)
+ vaasClient := fixture.setUpWithVaasURL(server.URL)
+
+ _, err := vaasClient.ForStream(context.Background(), eicarReader, eicarReader.Size(), nil)
+
+ assert.ErrorIs(t, err, ErrVaasAuthentication)
+}
+
+func Test_ForStream_IfAuthenticationFailure_ReturnErrVaasAuthentication(t *testing.T) {
+ eicarReader := strings.NewReader("X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*")
+ server := getHttpTestServer(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusUnauthorized)
+ })
+ defer server.Close()
+ fixture := new(testFixture)
+
+ vaasClient := fixture.setUpWithVaasURLAndAuthenticator(server.URL, mockFailureAuthenticator{})
+
+ _, err := vaasClient.ForStream(context.Background(), eicarReader, eicarReader.Size(), nil)
+
+ assert.ErrorIs(t, err, ErrVaasAuthentication)
+ assert.ErrorContains(t, err, "placeholder error message")
+}
+
+func Test_ForStream_WithDeadlineContext_Cancels(t *testing.T) {
+ response, _ := http.Get("https://secure.eicar.org/eicar.com.txt")
+
+ fixture := new(testFixture)
+ vaasClient := fixture.setUp()
+
+ cancelCtx, cancel := context.WithTimeout(context.Background(), 1*time.Nanosecond)
+ defer cancel()
+ verdict, err := vaasClient.ForStream(cancelCtx, response.Body, response.ContentLength, nil)
+
+ if err == nil {
+ t.Fatalf("expected error got success instead (%v)", verdict)
+ }
+
+ if !errors.Is(err, context.DeadlineExceeded) {
+ t.Fatalf("expected cancelled error, got %v", err)
+ }
+}
+
+func Test_ForStream_WithMaliciousStream_RetunsMaliciousWithDetectionsAndMimeType(t *testing.T) {
+ response, _ := http.Get("https://secure.eicar.org/eicar.com.txt")
+
+ fixture := new(testFixture)
+ VaasClient := fixture.setUp()
+
+ verdict, err := VaasClient.ForStream(context.Background(), response.Body, response.ContentLength, nil)
+
+ if err != nil {
+ t.Fatalf("unexpected error - %v", err)
+ }
+
+ if verdict.Verdict != msg.Malicious {
+ t.Errorf("verdict should be %v, got %v", msg.Malicious, verdict.Verdict)
+ }
+
+ if verdict.MimeType != "text/plain" {
+ t.Errorf("expected mime type to be text/plain, got %v", verdict.MimeType)
+ }
+
+ if verdict.Detection == "" {
+ t.Errorf("expected a detection, got empty string")
+ }
+
+ if verdict.Detection != "EICAR-Test-File#462103" {
+ t.Errorf("detection has to be EICAR-Test-File#462103, got %v", verdict.Detection)
+ }
+}
+
+func Test_ForUrl(t *testing.T) {
+ const (
+ cleanURL string = "https://www.gdatasoftware.com/oem/verdict-as-a-service"
+ eicarURL string = "https://secure.eicar.org/eicar.com"
+ invalidURL string = "https://gateway.production.vaas.gdatasecurity.de/swagger/nocontenthere"
+ )
+ type args struct {
+ url string
+ expectedVerdict msg.Verdict
+ }
+ tests := []struct {
+ args args
+ name string
+ expectedError error
+ }{
+ {
+ name: "with clean url - got verdict clean",
+ args: args{
+ url: cleanURL,
+ expectedVerdict: msg.Clean,
+ },
+ },
+ {
+ name: "with eicar url - got verdict malicious",
+ args: args{
+ url: eicarURL,
+ expectedVerdict: msg.Malicious,
+ },
+ },
+ {
+ name: "with invalid url - got client error",
+ args: args{
+ url: invalidURL,
+ expectedVerdict: msg.Malicious,
+ },
+ expectedError: ErrVaasClient,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ fixture := new(testFixture)
+ VaasClient := fixture.setUp()
+
+ testUrl, err := url.Parse(tt.args.url)
+ if err != nil {
+ t.Fatalf("Cannot parse test testUrl - %v", err)
+ }
+ verdict, err := VaasClient.ForUrl(context.Background(), testUrl, nil)
+
+ if !errors.Is(err, tt.expectedError) {
+ t.Fatalf("unexpected error, expected %v but got %v", tt.expectedError, err)
+ }
+
+ if err == nil && verdict.Verdict != tt.args.expectedVerdict {
+ t.Errorf("verdict should be %v, got %v", tt.args.expectedVerdict, verdict.Verdict)
+ }
+ })
+ }
+}
+
+func Test_ForUrl_IfVaasRequestIdIsSet_SendsTraceState(t *testing.T) {
+ server := getHttpTestServer(func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, r.Header.Get("tracestate"), "vaasrequestid=MyRequestId")
+ defaultHttpHandler(t, w, r)
+ })
+ defer server.Close()
+
+ fixture := new(testFixture)
+ vaasClient := fixture.setUpWithVaasURL(server.URL)
+
+ opts := options.NewForUrlOptions()
+ opts.VaasRequestId = "MyRequestId"
+ u, err := url.Parse(eicarUrl)
+ _, err = vaasClient.ForUrl(context.Background(), u, &opts)
+ assert.NoError(t, err, "ForUrl returned err")
+}
+
+func Test_ForUrl_SendsUserAgent(t *testing.T) {
+ server := getHttpTestServer(func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, r.Header.Get("User-Agent"), userAgent)
+ defaultHttpHandler(t, w, r)
+ })
+ defer server.Close()
+
+ fixture := new(testFixture)
+ vaasClient := fixture.setUpWithVaasURL(server.URL)
+
+ u, err := url.Parse(eicarUrl)
+ _, err = vaasClient.ForUrl(context.Background(), u, nil)
+ assert.NoError(t, err, "ForUrl returned err")
+}
+
+func Test_ForUrl_SendsOptions(t *testing.T) {
+ tests := []options.ForUrlOptions{
+ {
+ UseHashLookup: false,
+ },
+ {
+ UseHashLookup: true,
+ },
+ }
+
+ for _, option := range tests {
+ t.Run(fmt.Sprintf("%v", option), func(t *testing.T) {
+ server := getHttpTestServer(func(w http.ResponseWriter, r *http.Request) {
+ if r.URL.String() == "/urls" {
+ analysisRequest := msg.URLAnalysisRequest{
+ Url: eicarUrl,
+ UseHashLookup: option.UseHashLookup,
+ }
+ data, err := io.ReadAll(r.Body)
+ assert.NoError(t, err)
+ err = json.Unmarshal(data, &analysisRequest)
+ assert.Equal(t, option.UseHashLookup, analysisRequest.UseHashLookup)
+
+ }
+ defaultHttpHandler(t, w, r)
+ })
+ defer server.Close()
+ fixture := new(testFixture)
+ vaasClient := fixture.setUpWithVaasURL(server.URL)
+
+ u, err := url.Parse(eicarUrl)
+ _, err = vaasClient.ForUrl(context.Background(), u, &option)
+ assert.NoError(t, err, "ForUrl returned err")
+ })
+ }
+}
+
+func Test_ForUrl_IfVaasClientException_ReturnErrVaasClient(t *testing.T) {
+ server := getHttpTestServer(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusBadRequest)
+ })
+ defer server.Close()
+ fixture := new(testFixture)
+ vaasClient := fixture.setUpWithVaasURL(server.URL)
+
+ u, err := url.Parse(eicarUrl)
+ _, err = vaasClient.ForUrl(context.Background(), u, nil)
+
+ assert.ErrorIs(t, err, ErrVaasClient)
+}
+
+func Test_ForUrl_IfVaasServerException_ReturnErrVaasServer(t *testing.T) {
+ server := getHttpTestServer(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusInternalServerError)
+ })
+ defer server.Close()
+ fixture := new(testFixture)
+ vaasClient := fixture.setUpWithVaasURL(server.URL)
+
+ u, err := url.Parse(eicarUrl)
+ _, err = vaasClient.ForUrl(context.Background(), u, nil)
+
+ assert.ErrorIs(t, err, ErrVaasServer)
+}
+
+func Test_ForUrl_IfVaasReturns401_ReturnErrVaasAuthentication(t *testing.T) {
+ server := getHttpTestServer(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusUnauthorized)
+ })
+ defer server.Close()
+ fixture := new(testFixture)
+ vaasClient := fixture.setUpWithVaasURL(server.URL)
+
+ u, err := url.Parse(eicarUrl)
+ _, err = vaasClient.ForUrl(context.Background(), u, nil)
+
+ assert.ErrorIs(t, err, ErrVaasAuthentication)
+}
+
+func Test_ForUrl_IfAuthenticationFailure_ReturnErrVaasAuthentication(t *testing.T) {
+ server := getHttpTestServer(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusUnauthorized)
+ })
+ defer server.Close()
+ fixture := new(testFixture)
+
+ vaasClient := fixture.setUpWithVaasURLAndAuthenticator(server.URL, mockFailureAuthenticator{})
+
+ u, err := url.Parse(eicarUrl)
+ _, err = vaasClient.ForUrl(context.Background(), u, nil)
+
+ assert.ErrorIs(t, err, ErrVaasAuthentication)
+ assert.ErrorContains(t, err, "placeholder error message")
+}
+
+func Test_ForUrl_WithDeadlineContext_Cancels(t *testing.T) {
+ fixture := new(testFixture)
+ vaasClient := fixture.setUp()
+
+ cancelCtx, cancel := context.WithTimeout(context.Background(), 1*time.Nanosecond)
+ defer cancel()
+ u, err := url.Parse(eicarUrl)
+ verdict, err := vaasClient.ForUrl(cancelCtx, u, nil)
+
+ if err == nil {
+ t.Fatalf("expected error got success instead (%v)", verdict)
+ }
+
+ if !errors.Is(err, context.DeadlineExceeded) {
+ t.Fatalf("expected cancelled error, got %v", err)
+ }
+}
+
+func RandomString(n int) string {
+ var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
+
+ s := make([]rune, n)
+ for i := range s {
+ s[i] = letters[rand.Intn(len(letters))]
+ }
+ return string(s)
+}
diff --git a/justfile b/justfile
index c2ac1140..52435aad 100644
--- a/justfile
+++ b/justfile
@@ -33,10 +33,16 @@ populate-env:
cp .env golang/vaas/.env
cp .env golang/vaas/v2/.env
cp .env golang/vaas/v2/examples/file-verdict-request/.env
+ cp .env golang/vaas/v2/examples/vaasctl/.env
cp .env golang/vaas/v2/pkg/vaas/.env
cp .env golang/vaas/v2/pkg/authenticator/.env
cp .env golang/vaas/pkg/authenticator/.env
cp .env golang/vaas/pkg/vaas/.env
+ cp .env golang/vaas/v3/.env
+ cp .env golang/vaas/v3/examples/file-verdict-request/.env
+ cp .env golang/vaas/v3/examples/vaasctl/.env
+ cp .env golang/vaas/v3/pkg/vaas/.env
+ cp .env golang/vaas/v3/pkg/authenticator/.env
cp .env java/.env
cp .env php/tests/vaas/.env
cp .env ruby/test/.env