diff --git a/.github/workflows/ci-cpp-toolchain.yaml b/.github/workflows/ci-cpp-toolchain.yaml new file mode 100644 index 00000000..f87d92ea --- /dev/null +++ b/.github/workflows/ci-cpp-toolchain.yaml @@ -0,0 +1,56 @@ +name: vaas-cpp-toolchain-ci +on: + push: + tags: + - "cpp-toolchain*" + paths: + - "cpp/Dockerfile.build" + - ".github/workflows/ci-cpp-toolchain.yaml" + +jobs: + cpp-toolchain: + name: Build & Push C++ toolchain + permissions: + contents: read + packages: write + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + sparse-checkout: | + cpp/Dockerfile.build + sparse-checkout-cone-mode: false + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: set version + run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/cpp-toolchain}" >> $GITHUB_ENV + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: | + ghcr.io/gdatasoftwareag/vaas/cpp-toolchain + tags: | + type=semver,pattern={{version}},value=${{ env.RELEASE_VERSION }} + type=semver,pattern={{major}}.{{minor}},value=${{ env.RELEASE_VERSION }} + type=semver,pattern={{major}},value=${{ env.RELEASE_VERSION }} + flavor: | + latest=auto + + - name: login to ghcr.io/gdatasoftwareag + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ secrets.GHCR_IO_GDATASOFTWAREAG_USERNAME }} + password: ${{ secrets.GHCR_IO_GDATASOFTWAREAG_PASSWORD }} + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: "cpp/" + file: "cpp/Dockerfile.build" + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} \ No newline at end of file diff --git a/.github/workflows/ci-cpp.yaml b/.github/workflows/ci-cpp.yaml new file mode 100644 index 00000000..0cdfec31 --- /dev/null +++ b/.github/workflows/ci-cpp.yaml @@ -0,0 +1,85 @@ +name: vaas-cpp-ci +on: + push: + paths: + - "cpp/**" + - ".github/workflows/ci-cpp.yaml" + pull_request: + paths: + - "cpp/**" + - ".github/workflows/ci-cpp.yaml" + workflow_dispatch: + inputs: + environment: + type: choice + description: "Test environment" + options: + - production + - staging + - develop + default: "production" + +env: + CLIENT_ID: ${{ secrets.CLIENT_ID }} + CLIENT_SECRET: ${{secrets.CLIENT_SECRET}} + VAAS_URL: "https://upload.production.vaas.gdatasecurity.de" + TOKEN_URL: "https://account.gdata.de/realms/vaas-production/protocol/openid-connect/token" + VAAS_CLIENT_ID: ${{ secrets.VAAS_CLIENT_ID }} + VAAS_USER_NAME: ${{ secrets.VAAS_USER_NAME }} + VAAS_PASSWORD: ${{secrets.VAAS_PASSWORD}} + +jobs: + cpp-build: + name: Build & Test C++ SDK + runs-on: ubuntu-latest + container: ghcr.io/gdatasoftwareag/vaas/cpp-toolchain + steps: + - uses: actions/checkout@v4 + + - name: Scan for Viruses + uses: ./.github/actions/vaas-scan-action + with: + VAAS_CLIENT_ID: ${{ secrets.VAAS_SCAN_CLIENT_ID }} + VAAS_CLIENT_SECRET: ${{ secrets.VAAS_SCAN_CLIENT_SECRET }} + + - name: set staging environment + if: (inputs.environment == 'staging' || (startsWith(github.ref, 'refs/tags/cpp') && endsWith(github.ref, '-beta'))) + run: | + echo "CLIENT_ID=${{ secrets.STAGING_CLIENT_ID }}" >> $GITHUB_ENV + echo "CLIENT_SECRET=${{ secrets.STAGING_CLIENT_SECRET }}" >> $GITHUB_ENV + echo "VAAS_URL=https://upload.staging.vaas.gdatasecurity.de" >> $GITHUB_ENV + echo "TOKEN_URL=https://account-staging.gdata.de/realms/vaas-staging/protocol/openid-connect/token" >> $GITHUB_ENV + echo "VAAS_CLIENT_ID=${{ secrets.STAGING_VAAS_CLIENT_ID }}" >> $GITHUB_ENV + echo "VAAS_USER_NAME=${{ secrets.STAGING_VAAS_USER_NAME }}" >> $GITHUB_ENV + echo "VAAS_PASSWORD=${{ secrets.STAGING_VAAS_PASSWORD }}" >> $GITHUB_ENV + + - name: set develop environment + if: (inputs.environment == 'develop' || (startsWith(github.ref, 'refs/tags/cpp') && endsWith(github.ref, '-alpha'))) + run: | + echo "CLIENT_ID=${{ secrets.DEVELOP_CLIENT_ID }}" >> $GITHUB_ENV + echo "CLIENT_SECRET=${{ secrets.DEVELOP_CLIENT_SECRET }}" >> $GITHUB_ENV + echo "VAAS_URL=https://upload.develop.vaas.gdatasecurity.de" >> $GITHUB_ENV + echo "TOKEN_URL=https://account-staging.gdata.de/realms/vaas-develop/protocol/openid-connect/token" >> $GITHUB_ENV + echo "VAAS_CLIENT_ID=${{ secrets.DEVELOP_VAAS_CLIENT_ID }}" >> $GITHUB_ENV + echo "VAAS_USER_NAME=${{ secrets.DEVELOP_VAAS_USER_NAME }}" >> $GITHUB_ENV + echo "VAAS_PASSWORD=${{ secrets.DEVELOP_VAAS_PASSWORD }}" >> $GITHUB_ENV + + # https://learn.microsoft.com/en-us/vcpkg/consume/binary-caching-github-actions-cache + - name: Export GitHub Actions cache environment variables + uses: actions/github-script@v7 + with: + script: | + core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); + core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); + + - name: Build C++ SDK + env: + VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" + run: | + ./build.sh + working-directory: cpp + + - name: Test C++ SDK + run: | + ./test.sh + working-directory: cpp diff --git a/c++/.clang-format b/cpp/.clang-format similarity index 100% rename from c++/.clang-format rename to cpp/.clang-format diff --git a/c++/.gitignore b/cpp/.gitignore similarity index 100% rename from c++/.gitignore rename to cpp/.gitignore diff --git a/c++/.vscode/launch.json b/cpp/.vscode/launch.json similarity index 100% rename from c++/.vscode/launch.json rename to cpp/.vscode/launch.json diff --git a/c++/.vscode/tasks.json b/cpp/.vscode/tasks.json similarity index 100% rename from c++/.vscode/tasks.json rename to cpp/.vscode/tasks.json diff --git a/c++/CMakeLists.txt b/cpp/CMakeLists.txt similarity index 54% rename from c++/CMakeLists.txt rename to cpp/CMakeLists.txt index a75ed0b9..27c01eea 100644 --- a/c++/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -6,7 +6,14 @@ set(CMAKE_CXX_STANDARD 17) # Find and link libraries find_package(CURL REQUIRED) find_package(jsoncpp CONFIG REQUIRED) +find_package(doctest CONFIG REQUIRED) -add_executable(vaas_example main.cpp vaas.h) +# vaas_example +add_executable(vaas_example main.cpp) target_link_libraries(vaas_example PRIVATE CURL::libcurl JsonCpp::JsonCpp) + +# test +add_executable(vaas_test vaas_test.cpp) + +target_link_libraries(vaas_test PRIVATE CURL::libcurl JsonCpp::JsonCpp) diff --git a/c++/CMakePresets.json b/cpp/CMakePresets.json similarity index 100% rename from c++/CMakePresets.json rename to cpp/CMakePresets.json diff --git a/cpp/Dockerfile.build b/cpp/Dockerfile.build new file mode 100644 index 00000000..abdd51e1 --- /dev/null +++ b/cpp/Dockerfile.build @@ -0,0 +1,16 @@ +#### Base Image +FROM ubuntu:24.04 + +RUN apt-get update -qq && \ + # install dependencies + apt-get install -y --no-install-recommends git ca-certificates curl zip unzip tar build-essential cmake make pkg-config libssl3 libssl-dev linux-libc-dev && \ + # install vcpkg + cd /usr/local && \ + git clone https://github.com/microsoft/vcpkg.git && \ + cd vcpkg && ./bootstrap-vcpkg.sh + +ENV VCPKG_ROOT="/usr/local/vcpkg/" +ENV PATH="$VCPKG_ROOT:$PATH" + + +ENTRYPOINT ["/bin/bash"] \ No newline at end of file diff --git a/c++/README.md b/cpp/README.md similarity index 100% rename from c++/README.md rename to cpp/README.md diff --git a/cpp/build.sh b/cpp/build.sh new file mode 100755 index 00000000..766b0414 --- /dev/null +++ b/cpp/build.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -ex +cmake --preset release +cmake --build build \ No newline at end of file diff --git a/c++/main.cpp b/cpp/main.cpp similarity index 100% rename from c++/main.cpp rename to cpp/main.cpp diff --git a/cpp/test.sh b/cpp/test.sh new file mode 100755 index 00000000..b18da5ad --- /dev/null +++ b/cpp/test.sh @@ -0,0 +1,3 @@ +#!/bin/bash +set -ex +./build/vaas_test --exit=true diff --git a/c++/vaas.h b/cpp/vaas.h similarity index 90% rename from c++/vaas.h rename to cpp/vaas.h index edb2e7e4..c27c3d1c 100644 --- a/c++/vaas.h +++ b/cpp/vaas.h @@ -74,13 +74,13 @@ static size_t writeAppendToString(void* contents, const size_t size, const size_ static long getServerResponse(CURL* curl, Json::Value& jsonResponse) { std::string response; - vaas_internals::ensureCurlOk(curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, vaas_internals::writeAppendToString)); - vaas_internals::ensureCurlOk(curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response)); + ensureCurlOk(curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, vaas_internals::writeAppendToString)); + ensureCurlOk(curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response)); - vaas_internals::ensureCurlOk(curl_easy_perform(curl)); + ensureCurlOk(curl_easy_perform(curl)); long response_code; - vaas_internals::ensureCurlOk(curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code)); + ensureCurlOk(curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code)); if (response_code < 200 || response_code >= 300) { return response_code; @@ -177,12 +177,20 @@ namespace vaas { class OIDCClient { public: OIDCClient(std::string tokenEndpoint, std::string clientId, std::string clientSecret) - : tokenEndpoint(std::move(tokenEndpoint)), clientId(std::move(clientId)), clientSecret(std::move(clientSecret)), curl(curl_easy_init()) { + : tokenEndpoint(std::move(tokenEndpoint)), clientId(std::move(clientId)), + clientSecret(std::move(clientSecret)), curl(curl_easy_init()) { if (!curl) { throw std::runtime_error("Failed to initialize CURL"); } } + OIDCClient(OIDCClient&& other) noexcept + : tokenEndpoint(std::move(tokenEndpoint)), clientId(std::move(clientId)), + clientSecret(std::move(clientSecret)), + curl(other.curl) { + other.curl = nullptr; + } + ~OIDCClient() { if (curl) { curl_easy_cleanup(curl); @@ -204,7 +212,8 @@ class OIDCClient { vaas_internals::CurlHeaders headers; headers.append("Content-Type: application/x-www-form-urlencoded"); - const std::string postFields = "grant_type=client_credentials&client_id=" + clientId + "&client_secret=" + clientSecret; + const std::string postFields = "grant_type=client_credentials&client_id=" + clientId + "&client_secret=" + + clientSecret; vaas_internals::ensureCurlOk(curl_easy_setopt(curl, CURLOPT_URL, tokenEndpoint.c_str())); vaas_internals::ensureCurlOk(curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers.raw())); @@ -214,7 +223,8 @@ class OIDCClient { const auto response_code = vaas_internals::getServerResponse(curl, jsonResponse); if (!(response_code == 200 || response_code == 401)) { - throw AuthenticationException("Server replied with unexpected HTTP response code " + std::to_string(response_code)); + throw AuthenticationException( + "Server replied with unexpected HTTP response code " + std::to_string(response_code)); } if (jsonResponse.isMember("error") || response_code != 200) { @@ -245,6 +255,7 @@ class OIDCClient { /// VaasReport contains an analysis report for a file, such as verdict information. class VaasReport { + public: const std::string sha256; enum Verdict { @@ -278,7 +289,8 @@ class VaasReport { } explicit VaasReport(const std::string& sha256, Verdict verdict) - : sha256{sha256}, verdict{verdict} {} + : sha256{sha256}, verdict{verdict} { + } }; inline std::ostream& operator<<(std::ostream& os, const VaasReport& report) { @@ -289,13 +301,22 @@ inline std::ostream& operator<<(std::ostream& os, const VaasReport& report) { /// Vaas talks to the VaaS service and provides reports for files or streams. class Vaas { public: - Vaas(std::string serverEndpoint, const std::string& tokenEndpoint, const std::string& clientId, const std::string& clientSecret) - : serverEndpoint(serverEndpoint), authenticator(tokenEndpoint, clientId, clientSecret), curl(curl_easy_init()) { + Vaas(const std::string& serverEndpoint, const std::string& tokenEndpoint, const std::string& clientId, + const std::string& clientSecret) + : serverEndpoint(serverEndpoint), authenticator(tokenEndpoint, clientId, clientSecret), + curl(curl_easy_init()) { if (!curl) { throw std::runtime_error("Failed to initialize CURL"); } } + Vaas(Vaas&& other) noexcept + : serverEndpoint(std::move(other.serverEndpoint)), // Can't actually move because it's const + authenticator(std::move(other.authenticator)), + curl(other.curl) { + other.curl = nullptr; + } + ~Vaas() { if (curl) { curl_easy_cleanup(curl); @@ -411,4 +432,4 @@ class Vaas { }; } // namespace vaas -#endif // VAAS_H \ No newline at end of file +#endif // VAAS_H diff --git a/cpp/vaas_test.cpp b/cpp/vaas_test.cpp new file mode 100644 index 00000000..01fa0c26 --- /dev/null +++ b/cpp/vaas_test.cpp @@ -0,0 +1,57 @@ +#define DOCTEST_CONFIG_IMPLEMENT +#include "vaas.h" +#include + +static char* program; + +int main(int argc, char** argv) { + program = argv[0]; + doctest::Context context; + context.applyCommandLine(argc, argv); + + int res = context.run(); // run doctest + + // important - query flags (and --exit) rely on the user doing this + if (context.shouldExit()) { + // propagate the result of the tests + return res; + } + + return 0; +} + +vaas::Vaas initVaas() { + const auto vaasUrl = std::getenv("VAAS_URL") + ? std::getenv("VAAS_URL") + : "https://upload.staging.vaas.gdatasecurity.de"; + const auto tokenUrl = std::getenv("TOKEN_URL") + ? std::getenv("TOKEN_URL") + : "https://account-staging.gdata.de/realms/vaas-staging/protocol/openid-connect/token"; + const auto clientId = std::getenv("CLIENT_ID") + ? std::getenv("CLIENT_ID") + : throw std::runtime_error("CLIENT_ID must be set"); + const auto clientSecret = std::getenv("CLIENT_SECRET") + ? std::getenv("CLIENT_SECRET") + : throw std::runtime_error("CLIENT_SECRET must be set"); + return vaas::Vaas(vaasUrl, tokenUrl, clientId, clientSecret); +} + +class VaasTestFixture { +protected: + vaas::Vaas vaas; + + VaasTestFixture() : vaas(initVaas()) { + } +}; + +TEST_CASE_FIXTURE(VaasTestFixture, "forFile_withCleanFile_returnsClean") { + auto report = vaas.forFile(program); + CHECK(report.verdict == vaas::VaasReport::Verdict::Clean); +} + +/* TODO: Currently broken +TEST_CASE_FIXTURE(VaasTestFixture, "forHash_withMaliciousFile_returnsMalicious") { + auto report = vaas.forHash("275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f"); + CHECK(report.verdict == vaas::VaasReport::Verdict::Malicious); +} +*/ \ No newline at end of file diff --git a/c++/vcpkg-configuration.json b/cpp/vcpkg-configuration.json similarity index 100% rename from c++/vcpkg-configuration.json rename to cpp/vcpkg-configuration.json diff --git a/c++/vcpkg.json b/cpp/vcpkg.json similarity index 84% rename from c++/vcpkg.json rename to cpp/vcpkg.json index a248408d..cb52b0f1 100644 --- a/c++/vcpkg.json +++ b/cpp/vcpkg.json @@ -7,7 +7,8 @@ ] }, "jsoncpp", - "openssl" + "openssl", + "doctest" ], "version": "0.0.0", "name": "vaas"