diff --git a/.docker/build.Dockerfile b/.docker/build.Dockerfile deleted file mode 100644 index 5a7f0e3b..00000000 --- a/.docker/build.Dockerfile +++ /dev/null @@ -1,40 +0,0 @@ -# Dockerfile for gvm-libs-$COMPILER-build:$VERSION - -# Define ARG we use through the build -ARG VERSION=unstable - -# Use '-slim' image for reduced image size -FROM debian:stable-slim -LABEL deprecated="This image is deprecated and may be removed soon." - -# This will make apt-get install without question -ARG DEBIAN_FRONTEND=noninteractive - -# Redefine ARG we use through the build -ARG COMPILER - -WORKDIR /source - -# Install core dependencies required for building and testing gvm-libs -RUN apt-get update && \ - apt-get install -y --no-install-recommends \ - build-essential \ - curl \ - cmake \ - pkg-config \ - gnupg \ - libglib2.0-dev \ - libgpgme-dev \ - libgnutls28-dev \ - uuid-dev \ - libssh-gcrypt-dev \ - libhiredis-dev \ - libxml2-dev \ - libpcap-dev \ - libnet1-dev \ - libldap2-dev \ - libradcli-dev \ - libpaho-mqtt-dev \ - libcgreen1-dev \ - lcov \ - && rm -rf /var/lib/apt/lists/* diff --git a/.docker/prod-oldstable.Dockerfile b/.docker/prod-oldstable.Dockerfile index 3e29f584..c2d02261 100644 --- a/.docker/prod-oldstable.Dockerfile +++ b/.docker/prod-oldstable.Dockerfile @@ -13,6 +13,7 @@ RUN apt-get update && \ cmake \ pkg-config \ gnupg \ + libcjson-dev \ libglib2.0-dev \ libgpgme-dev \ libgnutls28-dev \ @@ -28,7 +29,7 @@ RUN apt-get update && \ libcgreen1-dev \ lcov \ && rm -rf /var/lib/apt/lists/* -RUN cmake -DCMAKE_BUILD_TYPE=Release -B/build /source +RUN cmake -DCMAKE_BUILD_TYPE=Release -DOPENVASD=0 -B/build /source RUN DESTDIR=/install cmake --build /build -- install FROM debian:oldstable-slim @@ -37,10 +38,12 @@ ARG DEBIAN_FRONTEND=noninteractive RUN apt-get update && \ apt-get install -y --no-install-recommends \ + libcjson1 \ libglib2.0-0 \ libgpgme11 \ libgnutls30 \ libuuid1 \ + libjson-glib-1.0-0 \ libssh-gcrypt-4 \ libhiredis0.14 \ libxml2 \ diff --git a/.docker/prod-testing.Dockerfile b/.docker/prod-testing.Dockerfile index 79380e59..f74d9dbc 100644 --- a/.docker/prod-testing.Dockerfile +++ b/.docker/prod-testing.Dockerfile @@ -16,11 +16,12 @@ ARG DEBIAN_FRONTEND=noninteractive RUN apt-get update && \ apt-get install -y --no-install-recommends \ + libcjson1 \ libglib2.0-0 \ libgpgme11 \ libgnutls30 \ libuuid1 \ - libssh-gcrypt-4 \ + libssh-dev \ libhiredis1.1.0 \ libhiredis-dev \ libxml2 \ diff --git a/.docker/prod.Dockerfile b/.docker/prod.Dockerfile index eb804a9c..4091e1f6 100644 --- a/.docker/prod.Dockerfile +++ b/.docker/prod.Dockerfile @@ -16,9 +16,11 @@ ARG DEBIAN_FRONTEND=noninteractive RUN apt-get update && \ apt-get install -y --no-install-recommends \ + libcjson1 \ libglib2.0-0 \ libgpgme11 \ libgnutls30 \ + libjson-glib-1.0-0 \ libuuid1 \ libssh-gcrypt-4 \ libhiredis0.14 \ diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 90547ef0..3844eb25 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -2,5 +2,5 @@ * @greenbone/gvm-libs-maintainers # dev ops -.github/ @greenbone/devops @greenbone/gvm-libs-maintainers -.docker/ @greenbone/devops @greenbone/gvm-libs-maintainers +.github/ @greenbone/gvm-libs-maintainers +.docker/ @greenbone/gvm-libs-maintainers diff --git a/.github/install-dependencies.sh b/.github/install-dependencies.sh index d56a9043..d3bad760 100755 --- a/.github/install-dependencies.sh +++ b/.github/install-dependencies.sh @@ -8,11 +8,15 @@ apt-get update && \ cmake \ pkg-config \ gnupg \ + libcjson-dev \ + libcurl4-openssl-dev \ + libjson-glib-dev \ libglib2.0-dev \ libgpgme-dev \ libgnutls28-dev \ uuid-dev \ - libssh-gcrypt-dev \ + libgcrypt-dev \ + libssh-dev \ libhiredis-dev \ libxml2-dev \ libpcap-dev \ diff --git a/.github/workflows/build-container.yml b/.github/workflows/build-container.yml deleted file mode 100644 index cff10335..00000000 --- a/.github/workflows/build-container.yml +++ /dev/null @@ -1,65 +0,0 @@ -name: Build Container Image Builds - -on: - push: - branches: [ main, stable, oldstable ] - tags: ["v*"] - paths: - - .github/workflows/build-container.yml - - .docker/build.Dockerfile - pull_request: - branches: [ main, stable, oldstable ] - paths: - - .github/workflows/build-container.yml - - .docker/build.Dockerfile - workflow_dispatch: - schedule: - # rebuild image every sunday - - cron: "0 0 * * 0" - -jobs: - build: - name: Build Images - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Setup container meta information - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ github.repository }}-build - labels: | - org.opencontainers.image.vendor=Greenbone - org.opencontainers.image.base.name=debian:stable-slim - flavor: latest=false # no latest container tag for git tags - tags: | - # create container tag for git tags - type=ref,event=tag - type=ref,event=pr - # use latest for stable branch - type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'stable') }} - type=raw,value=stable,enable=${{ github.ref == format('refs/heads/{0}', 'stable') }} - type=raw,value=oldstable,enable=${{ github.ref == format('refs/heads/{0}', 'oldstable') }} - # use unstable for main branch - type=raw,value=unstable,enable={{is_default_branch}} - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Login to DockerHub - if: github.event_name != 'pull_request' - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - run: echo "Build and push ${{ steps.container.outputs.image-tags }}" - - name: Build and push - uses: docker/build-push-action@v5 - with: - context: . - push: ${{ github.event_name != 'pull_request' }} - file: .docker/build.Dockerfile - platforms: linux/amd64,linux/arm64 - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/ci-c.yml b/.github/workflows/ci-c.yml index 78cd2fe5..b94ae211 100644 --- a/.github/workflows/ci-c.yml +++ b/.github/workflows/ci-c.yml @@ -21,27 +21,42 @@ jobs: tests: name: Unit Tests runs-on: 'ubuntu-latest' - container: greenbone/gvm-libs-build:unstable + container: greenbone/gvm-libs:edge steps: + - name: Install git for Codecov uploader + run: | + apt update + apt install --no-install-recommends -y ca-certificates git + rm -rf /var/lib/apt/lists/* - uses: actions/checkout@v4 + - name: Set git safe.directory + run: git config --global --add safe.directory '*' + - run: sh .github/install-dependencies.sh - name: Configure and Compile gvm-libs run: | mkdir build cd build - cmake -DCMAKE_BUILD_TYPE=Debug -DBUILD_TESTS=1 .. + cmake -DCMAKE_BUILD_TYPE=Debug -DBUILD_TESTS=1 -DENABLE_COVERAGE=1 .. make install - name: Test gvm-libs run: | cd build make tests CTEST_OUTPUT_ON_FAILURE=1 make test + - name: Upload test coverage to Codecov + uses: codecov/codecov-action@v5 + with: + file: build/coverage/coverage.xml + token: ${{ secrets.CODECOV_TOKEN }} + flags: unittests scan-build: name: Scan-build gvm-libs with clang runs-on: 'ubuntu-latest' - container: greenbone/gvm-libs-build:unstable + container: greenbone/gvm-libs:edge steps: - uses: actions/checkout@v4 + - run: sh .github/install-dependencies.sh - name: Install clang tools run: | apt update diff --git a/.github/workflows/codeql-analysis-c.yml b/.github/workflows/codeql-analysis-c.yml index f17d8e98..71f74ccf 100644 --- a/.github/workflows/codeql-analysis-c.yml +++ b/.github/workflows/codeql-analysis-c.yml @@ -19,7 +19,7 @@ jobs: actions: read contents: read security-events: write - container: greenbone/gvm-libs-build:unstable + container: greenbone/gvm-libs:edge strategy: fail-fast: false @@ -30,6 +30,7 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - run: sh .github/install-dependencies.sh - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: diff --git a/.github/workflows/container.yml b/.github/workflows/container.yml deleted file mode 100644 index 66ee8963..00000000 --- a/.github/workflows/container.yml +++ /dev/null @@ -1,184 +0,0 @@ -name: Container Image Builds - -on: - push: - branches: [main, stable, oldstable] - tags: ["v*"] - pull_request: - branches: [main, stable, oldstable] - workflow_dispatch: - -jobs: - production: - name: Production Images - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - name: "set IS_VERSION_TAG" - run: | - echo "IS_VERSION_TAG=${{ github.ref_type == 'tag' && startsWith(github.ref_name, 'v') }}" >> $GITHUB_ENV - # set defaults - echo "IS_LATEST_TAG=false" >> $GITHUB_ENV - - name: "set IS_LATEST_TAG" - if: ( env.IS_VERSION_TAG ) - run: | - # find the latest version that is not ourself - export LATEST_VERSION=$(git tag -l | grep -v '${{ github.ref_name }}' | sort -r --version-sort) - # get major minor patch versions - IFS='.' read -r latest_major latest_minor latest_patch << EOF - $LATEST_VERSION - EOF - IFS='.' read -r tag_major tag_minor tag_patch << EOF - ${{ github.ref_name }} - EOF - # remove leading v - latest_major=$(echo $latest_major | cut -c2-) - tag_major=$(echo $tag_major | cut -c2-) - echo "$tag_major >= $latest_major" - if [[ $tag_major -ge $latest_major && ($tag_minor -ne 0 || $tag_patch -ne 0) ]]; then - # set this tag to latest and stable - echo "IS_LATEST_TAG=true" >> $GITHUB_ENV - fi - - name: "Setup meta information debian:stable" - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ github.repository }} - labels: | - org.opencontainers.image.vendor=Greenbone - org.opencontainers.image.base.name=debian:stable-slim - flavor: latest=false # no auto latest container tag for git tags - tags: | - # when IS_LATEST_TAG is set create a stable and a latest tag - type=raw,value=latest,enable=${{ env.IS_LATEST_TAG }} - type=raw,value=stable,enable=${{ env.IS_LATEST_TAG }} - # if tag version is set than create a version tags - type=semver,pattern={{version}},enable=${{ env.IS_VERSION_TAG }} - type=semver,pattern={{major}}.{{minor}},enable=${{ env.IS_VERSION_TAG }} - type=semver,pattern={{major}},enable=${{ env.IS_VERSION_TAG }} - # if we are on the main branch set edge - type=edge,branch=main - # use branch-sha otherwise for pushes to branches other then main (will not be uploaded) - type=raw,value={{branch}}-{{sha}},enable=${{ github.ref_type == 'branch' && github.event_name == 'push' && github.ref_name != 'main' }} - # use pr-$PR_ID for pull requests (will not be uploaded) - type=ref,event=pr - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Login to Docker Registry - if: github.event_name != 'pull_request' - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Build and push Container image - uses: docker/build-push-action@v5 - with: - context: . - push: ${{ github.event_name != 'pull_request' && (github.ref_type == 'tag' || github.ref_name == 'main') }} - file: .docker/prod.Dockerfile - platforms: linux/amd64,linux/arm64 - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - - - name: "Setup meta information debian:oldstable" - id: old_stable_meta - uses: docker/metadata-action@v5 - with: - images: ${{ github.repository }} - labels: | - org.opencontainers.image.vendor=Greenbone - org.opencontainers.image.base.name=debian:stable-slim - flavor: latest=false # no auto latest container tag for git tags - tags: | - # for the images provided for debian:oldstable we just provide - # oldstable on an new version or oldstable-edge when it is on main. - # oldstable-branch-sha on a branch - type=raw,value=oldstable,enable=${{ env.IS_LATEST_TAG }} - type=raw,value=oldstable-edge,enable=${{ github.ref_name == 'main' }} - type=raw,value=oldstable-{{branch}}-{{sha}},enable=${{ github.ref_type == 'branch' && github.event_name == 'push' && github.ref_name != 'main' }} - type=ref,event=pr - - name: Build and push Container image - uses: docker/build-push-action@v5 - with: - context: . - push: ${{ github.event_name != 'pull_request' && (github.ref_type == 'tag' || github.ref_name == 'main') }} - file: .docker/prod-oldstable.Dockerfile - platforms: linux/amd64,linux/arm64 - tags: ${{ steps.old_stable_meta.outputs.tags }} - labels: ${{ steps.old_stable_meta.outputs.labels }} - - - name: "Setup meta information debian:testing" - id: testing_meta - uses: docker/metadata-action@v5 - with: - images: ${{ github.repository }} - labels: | - org.opencontainers.image.vendor=Greenbone - org.opencontainers.image.base.name=debian:testing-slim - flavor: latest=false # no auto latest container tag for git tags - tags: | - # for the images provided for debian:testing we just provide - # testing on an new version or testing-edge when it is on main. - # testing-branch-sha on a branch - type=raw,value=testing,enable=${{ env.IS_LATEST_TAG }} - type=raw,value=testing-edge,enable=${{ github.ref_name == 'main' }} - type=raw,value=testing-{{branch}}-{{sha}},enable=${{ github.ref_type == 'branch' && github.event_name == 'push' && github.ref_name != 'main' }} - type=ref,event=pr - - name: Build and push Container image - uses: docker/build-push-action@v5 - with: - context: . - push: ${{ github.event_name != 'pull_request' && (github.ref_type == 'tag' || github.ref_name == 'main') }} - file: .docker/prod-testing.Dockerfile - platforms: linux/amd64,linux/arm64 - tags: ${{ steps.testing_meta.outputs.tags }} - labels: ${{ steps.testing_meta.outputs.labels }} - - # triggers projects that work with stable branches on a new stable tag - trigger-stable-projects: - needs: production - if: github.ref_type == 'tag' && startsWith(github.ref_name, 'v') - name: Trigger update container images in related projects for new tags - strategy: - fail-fast: false - matrix: - repository: ["greenbone/gvmd", "greenbone/gsad"] - runs-on: ubuntu-latest - steps: - - name: Trigger ${{ matrix.repository }} build container image build - uses: greenbone/actions/trigger-workflow@v3 - with: - token: ${{ secrets.GREENBONE_BOT_TOKEN }} - repository: ${{ matrix.repository }} - workflow: build-container.yml - ref: main - - name: Trigger ${{ matrix.repository }} container image build - uses: greenbone/actions/trigger-workflow@v3 - with: - token: ${{ secrets.GREENBONE_BOT_TOKEN }} - repository: ${{ matrix.repository }} - workflow: container.yml - ref: main - - trigger-related-projects: - needs: production - if: github.event_name != 'pull_request' - name: Trigger update container images in related projects - strategy: - fail-fast: false - matrix: - repository: - - "greenbone/openvas-scanner" - - "greenbone/boreas" - runs-on: ubuntu-latest - steps: - - name: Trigger main ${{ matrix.repository }} container image build - uses: greenbone/actions/trigger-workflow@v3 - with: - token: ${{ secrets.GREENBONE_BOT_TOKEN }} - repository: ${{ matrix.repository }} - workflow: container.yml - ref: main diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 36afcc32..1611548b 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -1,12 +1,13 @@ -name: 'Dependency Review' +name: "Dependency Review" on: [pull_request] permissions: contents: read + pull-requests: write jobs: dependency-review: runs-on: ubuntu-latest steps: - - name: 'Dependency Review' + - name: "Dependency Review" uses: greenbone/actions/dependency-review@v3 diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml new file mode 100644 index 00000000..0966a460 --- /dev/null +++ b/.github/workflows/push.yml @@ -0,0 +1,98 @@ +name: Build & Push to Greenbone Registry + +on: + push: + branches: [ main ] + tags: ["v*"] + pull_request: + branches: [ main ] + workflow_dispatch: + inputs: + ref-name: + type: string + description: "The ref to build a container image from. For example a tag v23.0.0." + required: true + +jobs: + build-push-debian-stable-container: + name: Build and Push debian:stable to Greenbone Registry + uses: greenbone/workflows/.github/workflows/container-build-push-2nd-gen.yml@main + with: + image-url: community/gvm-libs + image-labels: | + org.opencontainers.image.vendor=Greenbone + org.opencontainers.image.base.name=debian:stable-slim + ref-name: ${{ inputs.ref-name }} + secrets: inherit + + build-push-debian-oldstable-container: + name: Build and Push debian:oldstable to Greenbone Registry + uses: greenbone/workflows/.github/workflows/container-build-push-2nd-gen.yml@main + with: + build-docker-file: .docker/prod-oldstable.Dockerfile + image-url: community/gvm-libs + image-labels: | + org.opencontainers.image.vendor=Greenbone + org.opencontainers.image.base.name=debian:stable-slim + base-image-label: "oldstable" + ref-name: ${{ inputs.ref-name }} + secrets: inherit + + build-push-debian-testing-container: + name: Build and Push debian:testing to Greenbone Registry + uses: greenbone/workflows/.github/workflows/container-build-push-2nd-gen.yml@main + with: + build-docker-file: .docker/prod-testing.Dockerfile + image-url: community/gvm-libs + image-labels: | + org.opencontainers.image.vendor=Greenbone + org.opencontainers.image.base.name=debian:stable-slim + base-image-label: "testing" + ref-name: ${{ inputs.ref-name }} + secrets: inherit + + # triggers projects that work with stable branches on a new stable tag + trigger-stable-projects: + needs: build-push-debian-stable-container + if: github.ref_type == 'tag' && startsWith(github.ref_name, 'v') + name: Trigger update container images in related projects for new tags + strategy: + fail-fast: false + matrix: + repository: ["greenbone/gvmd", "greenbone/gsad"] + runs-on: ubuntu-latest + steps: + - name: Trigger ${{ matrix.repository }} build container image build + uses: greenbone/actions/trigger-workflow@v3 + with: + token: ${{ secrets.GREENBONE_BOT_TOKEN }} + repository: ${{ matrix.repository }} + workflow: build-container.yml + ref: main + - name: Trigger ${{ matrix.repository }} container image build + uses: greenbone/actions/trigger-workflow@v3 + with: + token: ${{ secrets.GREENBONE_BOT_TOKEN }} + repository: ${{ matrix.repository }} + workflow: container.yml + ref: main + + trigger-related-projects: + needs: build-push-debian-stable-container + if: github.event_name != 'pull_request' + name: Trigger update container images in related projects + strategy: + fail-fast: false + matrix: + repository: + - "greenbone/openvas-scanner" + - "greenbone/boreas" + runs-on: ubuntu-latest + steps: + - name: Trigger main ${{ matrix.repository }} container image build + uses: greenbone/actions/trigger-workflow@v3 + with: + token: ${{ secrets.GREENBONE_BOT_TOKEN }} + repository: ${{ matrix.repository }} + workflow: ${{ matrix.repository == 'greenbone/openvas-scanner' && 'control.yml' || 'container.yml' }} + ref: main diff --git a/CMakeLists.txt b/CMakeLists.txt index 9db2d75a..ea771ccb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,7 +8,7 @@ message ("-- Configuring the Greenbone Vulnerability Management Libraries...") # VERSION: Always include major, minor and patch level. project (gvm-libs - VERSION 22.9.1 + VERSION 22.15.0 LANGUAGES C) if (POLICY CMP0005) @@ -24,6 +24,7 @@ endif (NOT CMAKE_BUILD_TYPE) OPTION(BUILD_STATIC "Build static versions of the libraries" OFF) OPTION(ENABLE_COVERAGE "Enable support for coverage analysis" OFF) OPTION(BUILD_TESTS "Build tests for the libraries" OFF) +OPTION(OPENVASD "Build openvasd library" ON) if (NOT BUILD_STATIC) set (BUILD_SHARED ON) @@ -40,10 +41,11 @@ find_program (CLANG_FORMAT clang-format) if (CLANG_FORMAT) message (STATUS "Looking for clang-format... ${CLANG_FORMAT}") - add_custom_target(format COMMAND ${CLANG_FORMAT} "-i" "./base/*.c" - "./boreas/*.c" "./gmp/*.c" "./osp/*.c" "./util/*.c" - "./base/*.h" "./boreas/*.h" "./gmp/*.h" "./osp/*.h" - "./util/*.h" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + add_custom_target(format COMMAND ${CLANG_FORMAT} "-i" "./base/*.c" + "./boreas/*.c" "./gmp/*.c" "./openvasd/*.c" "./osp/*.c" + "./util/*.c" "./base/*.h" "./boreas/*.h" "./gmp/*.h" + "./openvasd/*.h" "./osp/*.h" "./util/*.h" + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") else (CLANG_FORMAT) message (STATUS "clang-format not found...") endif (CLANG_FORMAT) @@ -180,13 +182,16 @@ message ("-- Using redis socket ${REDIS_SOCKET_PATH}") message ("-- Install prefix: ${CMAKE_INSTALL_PREFIX}") if (ENABLE_COVERAGE) - set (COVERAGE_FLAGS "--coverage") + set (COVERAGE_FLAGS "--coverage -ftest-coverage -fprofile-arcs") + set (COVERAGE_DIR "${CMAKE_BINARY_DIR}/coverage") + file (MAKE_DIRECTORY ${COVERAGE_DIR}) + message ("-- Code Coverage enabled") endif (ENABLE_COVERAGE) set (HARDENING_FLAGS "-Wformat -Wformat-security -D_FORTIFY_SOURCE=2 -fstack-protector") set (LINKER_HARDENING_FLAGS "-Wl,-z,relro -Wl,-z,now") set (CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} ${COVERAGE_FLAGS}") -set (CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} ${HARDENING_FLAGS}") +set (CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} ${HARDENING_FLAGS} ${COVERAGE_FLAGS}") set ( CMAKE_C_FLAGS @@ -226,12 +231,28 @@ if (BUILD_TESTS AND NOT SKIP_SRC) add_custom_target (tests DEPENDS array-test alivedetection-test boreas_error-test boreas_io-test - cli-test cvss-test ping-test sniffer-test util-test networking-test - passwordbasedauthentication-test xmlutils-test version-test osp-test - nvti-test hosts-test) + cli-test cpeutils-test cvss-test ping-test sniffer-test util-test networking-test + passwordbasedauthentication-test xmlutils-test version-test versionutils-test + osp-test nvti-test hosts-test jsonpull-test compressutils-test) endif (BUILD_TESTS AND NOT SKIP_SRC) +# Code coverage + +if (ENABLE_COVERAGE) + add_custom_target (coverage-html + COMMAND gcovr --html-details ${COVERAGE_DIR}/coverage.html + -r ${CMAKE_SOURCE_DIR} ${CMAKE_BINARY_DIR}) + add_custom_target (coverage-xml + COMMAND gcovr --xml ${COVERAGE_DIR}/coverage.xml + -r ${CMAKE_SOURCE_DIR} ${CMAKE_BINARY_DIR}) + add_custom_target (coverage DEPENDS coverage-xml coverage-html) +endif (ENABLE_COVERAGE) + +add_custom_target (clean-coverage + COMMAND find . -name *.gcda -delete -or -name *.gcno -delete + COMMAND rm -f ${COVERAGE_DIR}/*) + ## Program if (NOT SKIP_SRC) @@ -240,6 +261,10 @@ if (NOT SKIP_SRC) add_subdirectory (util) add_subdirectory (osp) add_subdirectory (gmp) + + if (OPENVASD) + add_subdirectory (openvasd) + endif (OPENVASD) endif (NOT SKIP_SRC) ## Documentation diff --git a/INSTALL.md b/INSTALL.md index 62bfd8b2..52dbbe96 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -17,6 +17,8 @@ General build environment: * pkg-config Specific development libraries: +* libcjson >= 1.7.14 (util) +* libcurl >= 7.83.0 (openvasd) * libglib >= 2.42 (all) * libgio >= 2.42 (util) * zlib >= 1.2.8 (util) @@ -36,18 +38,21 @@ Prerequisites for building documentation: * xmltoman (optional, for building man page) Prerequisites for building tests: -* [Cgreen](https://cgreen-devs.github.io/#_installing_cgreen) (optional, for building tests) +* [Cgreen](https://cgreen-devs.github.io/cgreen/cgreen-guide-en.html#_installing_cgreen) (optional, for building tests) Install prerequisites on Debian GNU/Linux 'Bullseye' 11: apt-get install \ cmake \ pkg-config \ + libcjson-dev \ + libcurl4-openssl-dev \ libglib2.0-dev \ libgpgme-dev \ libgnutls28-dev \ uuid-dev \ - libssh-gcrypt-dev \ + libgcrypt-dev \ + libssh-dev \ libhiredis-dev \ libxml2-dev \ libpcap-dev \ diff --git a/README.md b/README.md index cddec035..a05654fb 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,6 @@ # gvm-libs [![GitHub releases](https://img.shields.io/github/release/greenbone/gvm-libs.svg)](https://github.com/greenbone/gvm-libs/releases) -[![Code Documentation Coverage](https://img.shields.io/codecov/c/github/greenbone/gvm-libs.svg?label=Doc%20Coverage&logo=codecov)](https://codecov.io/gh/greenbone/gvm-libs) [![Build and test C](https://github.com/greenbone/gvm-libs/actions/workflows/ci-c.yml/badge.svg)](https://github.com/greenbone/gvm-libs/actions/workflows/ci-c.yml) This is the libraries module for the Greenbone Community Edition. diff --git a/base/CMakeLists.txt b/base/CMakeLists.txt index 77ea724b..2e9bb1d4 100644 --- a/base/CMakeLists.txt +++ b/base/CMakeLists.txt @@ -70,6 +70,9 @@ if (BUILD_TESTS) EXCLUDE_FROM_ALL array_tests.c) + add_link_options (-g -lgcov --coverage) + add_compile_options (-g -ftest-coverage -fprofile-arcs) + add_test (array-test array-test) target_include_directories (array-test PRIVATE ${CGREEN_INCLUDE_DIRS}) diff --git a/base/cvss.c b/base/cvss.c index de1628f7..11e15f9c 100644 --- a/base/cvss.c +++ b/base/cvss.c @@ -7,10 +7,18 @@ * @file * @brief CVSS utility functions * - * This file contains utility functions for handling CVSS v2 and v3. + * This file contains utility functions for handling CVSS v2, v3 and v4. * get_cvss_score_from_base_metrics calculates the CVSS base score from a CVSS * base vector. * + * CVSS v4.0: + * + * See the CVSS v4 calculator reference implementation at + * https://github.com/FIRSTdotorg/cvss-v4-calculator and the CVSS 4.0 + * specification document at + * https://www.first.org/cvss/v4.0/specification-document + * (especially sections 7., 8.2 and 8.3). + * * CVSS v3.1: * * See equations at https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator and @@ -79,6 +87,9 @@ static double get_cvss_score_from_base_metrics_v3 (const char *); +static double +get_cvss_score_from_metrics_v4 (const char *); + /* CVSS v2. */ // clang-format off @@ -126,7 +137,7 @@ get_cvss_score_from_base_metrics_v3 (const char *); // clang-format on /** - * @brief Base metrics. + * @brief CVSS v2 Base metrics. */ enum base_metrics { @@ -199,6 +210,222 @@ static const struct impact_item impact_map[][3] = { }, }; +// CVSS 4.0 + +/** + * @brief CVSS 4.0 metrics. + */ +typedef enum +{ + // Base (11 metrics) + CVSS4_AV, /**< Attack Vector */ + CVSS4_AC, /**< Attack Complexity */ + CVSS4_AT, /**< Attack Requirements */ + CVSS4_PR, /**< Privileges Required */ + CVSS4_UI, /**< User Interaction */ + CVSS4_VC, /**< Confidentiality Impact to the Vulnerable System */ + CVSS4_VI, /**< Integrity Impact to the Vulnerable System */ + CVSS4_VA, /**< Availability Impact to the Vulnerable System */ + CVSS4_SC, /**< Confidentiality Impact to the Subsequent System */ + CVSS4_SI, /**< Integrity Impact to the Subsequent System */ + CVSS4_SA, /**< Availability Impact to the Subsequent System */ + // Threat (1 metric) + CVSS4_E, /**< Exploit Maturity */ + // Environmental (14 metrics) + CVSS4_CR, /**< Confidentiality Requirement */ + CVSS4_IR, /**< Integrity Requirement */ + CVSS4_AR, /**< Availability Requirement */ + CVSS4_MAV, /**< Modified Attack Vector */ + CVSS4_MAC, /**< Modified Attack Complexity */ + CVSS4_MAT, /**< Modified Attack Requirements */ + CVSS4_MPR, /**< Modified Privileges Required */ + CVSS4_MUI, /**< Modified User Interaction */ + CVSS4_MVC, /**< Modified Confidentiality Impact to the Vulnerable System */ + CVSS4_MVI, /**< Modified Integrity Impact to the Vulnerable System */ + CVSS4_MVA, /**< Modified Availability Impact to the Vulnerable System */ + CVSS4_MSC, /**< Modified Confidentiality Impact to the Subsequent System */ + CVSS4_MSI, /**< Modified Integrity Impact to the Subsequent System */ + CVSS4_MSA, /**< Modified Availability Impact to the Subsequent System */ + // Supplemental (6 metrics) + CVSS4_S, /**< Safety */ + CVSS4_AU, /**< Automatable */ + CVSS4_R, /**< Recovery */ + CVSS4_V, /**< Value Density */ + CVSS4_RE, /**< Vulnerability Response Effort */ + CVSS4_U, /**< Provider Urgency */ + // Maximum number + CVSS4_METRICS_MAX, /**< Maximum number of metrics */ +} cvss4_metric_t; + +/** + * @brief Blank simplified CVSS 4.0 metrics string + */ +#define CVSS_METRICS_STR_BLANK "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" + +/** + * @brief Blank simplified CVSS 4.0 macrovector string + */ +#define CVSS_MACROVECTOR_BLANK "XXXXXX" + +/** + * @brief String to enum mapping and allowed values for a CVSS 4.0 metric. + * + * This assumes all allowed values are single characters. + * The Provider Urgency metric can be longer, so it needs special handling + * only using the first letter. + */ +typedef struct +{ + const char *metric_str; /**< The metric abbreviation string */ + const cvss4_metric_t metric; /**< The metric enum value */ + const char *values; /**< String of characters allowed as values */ +} cvss4_metric_def_t; + +/** + * @brief String to enum mappings and allowed values for CVSS 4.0 metrics. + * + * Notes: + * - The Provider Urgency metric can be longer than one character, + * so it needs special handling. + * - The orginal specification only lists the value S (Safety) for the + * modified metrics MSI and MSA, but the calculator reference implementation + * also uses it for the unmodified ones, SI and SA. + */ +static cvss4_metric_def_t cvss4_metric_defs[] = { + // Base (11 metrics) + {"AV", CVSS4_AV, "NALP"}, + {"AC", CVSS4_AC, "LH"}, + {"AT", CVSS4_AT, "NP"}, + {"PR", CVSS4_PR, "NLH"}, + {"UI", CVSS4_UI, "NPA"}, + {"VC", CVSS4_VC, "HLN"}, + {"VI", CVSS4_VI, "HLN"}, + {"VA", CVSS4_VA, "HLN"}, + {"SC", CVSS4_SC, "HLN"}, + {"SI", CVSS4_SI, "HLNS"}, + {"SA", CVSS4_SA, "HLNS"}, + // Threat (1 metric) + {"E", CVSS4_E, "XAPU"}, + // Environmental (14 metrics) + {"CR", CVSS4_CR, "XHML"}, + {"IR", CVSS4_IR, "XHML"}, + {"AR", CVSS4_AR, "XHML"}, + {"MAV", CVSS4_MAV, "XNALP"}, + {"MAC", CVSS4_MAC, "XLH"}, + {"MAT", CVSS4_MAT, "XNP"}, + {"MPR", CVSS4_MPR, "XNLH"}, + {"MUI", CVSS4_MUI, "XNPA"}, + {"MVC", CVSS4_MVC, "XHLN"}, + {"MVI", CVSS4_MVI, "XHLN"}, + {"MVA", CVSS4_MVA, "XHLN"}, + {"MSC", CVSS4_MSC, "XHLN"}, + {"MSI", CVSS4_MSI, "XHLNS"}, + {"MSA", CVSS4_MSA, "XHLNS"}, + // Supplemental (6 metrics) + {"S", CVSS4_S, "XNP"}, + {"AU", CVSS4_AU, "XNY"}, + {"R", CVSS4_R, "XAUI"}, + {"V", CVSS4_V, "XDC"}, + {"RE", CVSS4_RE, "XLMH"}, + {"U", CVSS4_U, "XCGAR"}, // Abbreviated to first letters + // Max number / array terminator + {NULL, CVSS4_METRICS_MAX, NULL}}; + +/** + * @brief Key-Value mappings of CVSS 4.0 macrovectors to scores. + */ +typedef struct +{ + const char *vector; + double score; +} cvss4_macrovector_mapping_t; + +/** + * @brief CVSS 4.0 macrovector mappings + * + * This list has been generated from the lookup table in the + * FIRST CVSS calculator reference implementation at + * https://github.com/FIRSTdotorg/cvss-v4-calculator/blob/main/cvss_lookup.js + */ +static const cvss4_macrovector_mapping_t cvss4_macrovector_mappings[] = { + {"000000", 10}, {"000001", 9.9}, {"000010", 9.8}, {"000011", 9.5}, + {"000020", 9.5}, {"000021", 9.2}, {"000100", 10}, {"000101", 9.6}, + {"000110", 9.3}, {"000111", 8.7}, {"000120", 9.1}, {"000121", 8.1}, + {"000200", 9.3}, {"000201", 9}, {"000210", 8.9}, {"000211", 8}, + {"000220", 8.1}, {"000221", 6.8}, {"001000", 9.8}, {"001001", 9.5}, + {"001010", 9.5}, {"001011", 9.2}, {"001020", 9}, {"001021", 8.4}, + {"001100", 9.3}, {"001101", 9.2}, {"001110", 8.9}, {"001111", 8.1}, + {"001120", 8.1}, {"001121", 6.5}, {"001200", 8.8}, {"001201", 8}, + {"001210", 7.8}, {"001211", 7}, {"001220", 6.9}, {"001221", 4.8}, + {"002001", 9.2}, {"002011", 8.2}, {"002021", 7.2}, {"002101", 7.9}, + {"002111", 6.9}, {"002121", 5}, {"002201", 6.9}, {"002211", 5.5}, + {"002221", 2.7}, {"010000", 9.9}, {"010001", 9.7}, {"010010", 9.5}, + {"010011", 9.2}, {"010020", 9.2}, {"010021", 8.5}, {"010100", 9.5}, + {"010101", 9.1}, {"010110", 9}, {"010111", 8.3}, {"010120", 8.4}, + {"010121", 7.1}, {"010200", 9.2}, {"010201", 8.1}, {"010210", 8.2}, + {"010211", 7.1}, {"010220", 7.2}, {"010221", 5.3}, {"011000", 9.5}, + {"011001", 9.3}, {"011010", 9.2}, {"011011", 8.5}, {"011020", 8.5}, + {"011021", 7.3}, {"011100", 9.2}, {"011101", 8.2}, {"011110", 8}, + {"011111", 7.2}, {"011120", 7}, {"011121", 5.9}, {"011200", 8.4}, + {"011201", 7}, {"011210", 7.1}, {"011211", 5.2}, {"011220", 5}, + {"011221", 3}, {"012001", 8.6}, {"012011", 7.5}, {"012021", 5.2}, + {"012101", 7.1}, {"012111", 5.2}, {"012121", 2.9}, {"012201", 6.3}, + {"012211", 2.9}, {"012221", 1.7}, {"100000", 9.8}, {"100001", 9.5}, + {"100010", 9.4}, {"100011", 8.7}, {"100020", 9.1}, {"100021", 8.1}, + {"100100", 9.4}, {"100101", 8.9}, {"100110", 8.6}, {"100111", 7.4}, + {"100120", 7.7}, {"100121", 6.4}, {"100200", 8.7}, {"100201", 7.5}, + {"100210", 7.4}, {"100211", 6.3}, {"100220", 6.3}, {"100221", 4.9}, + {"101000", 9.4}, {"101001", 8.9}, {"101010", 8.8}, {"101011", 7.7}, + {"101020", 7.6}, {"101021", 6.7}, {"101100", 8.6}, {"101101", 7.6}, + {"101110", 7.4}, {"101111", 5.8}, {"101120", 5.9}, {"101121", 5}, + {"101200", 7.2}, {"101201", 5.7}, {"101210", 5.7}, {"101211", 5.2}, + {"101220", 5.2}, {"101221", 2.5}, {"102001", 8.3}, {"102011", 7}, + {"102021", 5.4}, {"102101", 6.5}, {"102111", 5.8}, {"102121", 2.6}, + {"102201", 5.3}, {"102211", 2.1}, {"102221", 1.3}, {"110000", 9.5}, + {"110001", 9}, {"110010", 8.8}, {"110011", 7.6}, {"110020", 7.6}, + {"110021", 7}, {"110100", 9}, {"110101", 7.7}, {"110110", 7.5}, + {"110111", 6.2}, {"110120", 6.1}, {"110121", 5.3}, {"110200", 7.7}, + {"110201", 6.6}, {"110210", 6.8}, {"110211", 5.9}, {"110220", 5.2}, + {"110221", 3}, {"111000", 8.9}, {"111001", 7.8}, {"111010", 7.6}, + {"111011", 6.7}, {"111020", 6.2}, {"111021", 5.8}, {"111100", 7.4}, + {"111101", 5.9}, {"111110", 5.7}, {"111111", 5.7}, {"111120", 4.7}, + {"111121", 2.3}, {"111200", 6.1}, {"111201", 5.2}, {"111210", 5.7}, + {"111211", 2.9}, {"111220", 2.4}, {"111221", 1.6}, {"112001", 7.1}, + {"112011", 5.9}, {"112021", 3}, {"112101", 5.8}, {"112111", 2.6}, + {"112121", 1.5}, {"112201", 2.3}, {"112211", 1.3}, {"112221", 0.6}, + {"200000", 9.3}, {"200001", 8.7}, {"200010", 8.6}, {"200011", 7.2}, + {"200020", 7.5}, {"200021", 5.8}, {"200100", 8.6}, {"200101", 7.4}, + {"200110", 7.4}, {"200111", 6.1}, {"200120", 5.6}, {"200121", 3.4}, + {"200200", 7}, {"200201", 5.4}, {"200210", 5.2}, {"200211", 4}, + {"200220", 4}, {"200221", 2.2}, {"201000", 8.5}, {"201001", 7.5}, + {"201010", 7.4}, {"201011", 5.5}, {"201020", 6.2}, {"201021", 5.1}, + {"201100", 7.2}, {"201101", 5.7}, {"201110", 5.5}, {"201111", 4.1}, + {"201120", 4.6}, {"201121", 1.9}, {"201200", 5.3}, {"201201", 3.6}, + {"201210", 3.4}, {"201211", 1.9}, {"201220", 1.9}, {"201221", 0.8}, + {"202001", 6.4}, {"202011", 5.1}, {"202021", 2}, {"202101", 4.7}, + {"202111", 2.1}, {"202121", 1.1}, {"202201", 2.4}, {"202211", 0.9}, + {"202221", 0.4}, {"210000", 8.8}, {"210001", 7.5}, {"210010", 7.3}, + {"210011", 5.3}, {"210020", 6}, {"210021", 5}, {"210100", 7.3}, + {"210101", 5.5}, {"210110", 5.9}, {"210111", 4}, {"210120", 4.1}, + {"210121", 2}, {"210200", 5.4}, {"210201", 4.3}, {"210210", 4.5}, + {"210211", 2.2}, {"210220", 2}, {"210221", 1.1}, {"211000", 7.5}, + {"211001", 5.5}, {"211010", 5.8}, {"211011", 4.5}, {"211020", 4}, + {"211021", 2.1}, {"211100", 6.1}, {"211101", 5.1}, {"211110", 4.8}, + {"211111", 1.8}, {"211120", 2}, {"211121", 0.9}, {"211200", 4.6}, + {"211201", 1.8}, {"211210", 1.7}, {"211211", 0.7}, {"211220", 0.8}, + {"211221", 0.2}, {"212001", 5.3}, {"212011", 2.4}, {"212021", 1.4}, + {"212101", 2.4}, {"212111", 1.2}, {"212121", 0.5}, {"212201", 1}, + {"212211", 0.3}, {"212221", 0.1}, {NULL, 0.0}}; + +/** + * @brief Hashtable for quick lookup of CVSS macrovector scores. + * + * Macrovector scores should be looked up with cvss4_macrovector_score + * which ensures the table is initialized and returns the scores as + * double values instead of pointers. + */ +static GHashTable *cvss4_macrovector_table = NULL; + /** * @brief Determine base metric enumeration from a string. * @@ -367,6 +594,8 @@ get_cvss_score_from_base_metrics (const char *cvss_str) || g_str_has_prefix (cvss_str, "CVSS:3.0/")) return get_cvss_score_from_base_metrics_v3 (cvss_str + strlen ("CVSS:3.X/")); + if (g_str_has_prefix (cvss_str, "CVSS:4.0/")) + return get_cvss_score_from_metrics_v4 (cvss_str + strlen ("CVSS:4.X/")); memset (&cvss, 0x00, sizeof (struct cvss)); @@ -603,3 +832,1052 @@ get_cvss_score_from_base_metrics_v3 (const char *cvss_str) return roundup (base); } + +/** + * @brief Initialize the CVSS 4.0 macrovector lookup table. + */ +static void +cvss4_init_macrovector_table () +{ + if (cvss4_macrovector_table) + return; + + int index = 0; + cvss4_macrovector_table = g_hash_table_new (g_str_hash, g_str_equal); + while (cvss4_macrovector_mappings[index].vector != NULL) + { + g_hash_table_insert (cvss4_macrovector_table, + (gpointer) cvss4_macrovector_mappings[index].vector, + (gpointer) &cvss4_macrovector_mappings[index].score); + + index++; + } +} + +/** + * @brief Get the CVSS 4.0 score for a given macrovector string. + * + * @param[in] vector The macrovector to look up. + * + * @return The score of the given vector or -1.0 if the macrovector is invalid. + */ +static inline double +cvss4_macrovector_score (const char *vector) +{ + double *score_ptr; + + cvss4_init_macrovector_table (); + score_ptr = g_hash_table_lookup (cvss4_macrovector_table, vector); + if (score_ptr) + return *score_ptr; + return -1.0; +} + +/** + * @brief Get the effective value of a metric in a simplified CVSS4 vector. + * + * As this only returns the first character, the Provider Urgency metric + * (CVSS4_U) needs special handling to get the full string. + * + * @param[in] simplified_vec The simplified vector string to get value from. + * @param[in] metric The metric to get the value of. + * + * @return The metric value as a single character. + */ +static char +cvss4_m (const char *simplified_vec, cvss4_metric_t metric) +{ + char selected = simplified_vec[metric]; + + // If E=X it will default to the worst case i.e. E=A + if (metric == CVSS4_E && selected == 'X') + return 'A'; + + // If CR=X, IR=X or AR=X they will default to the worst case + // i.e. CR=H, IR=H and AR=H + if ((metric == CVSS4_CR || metric == CVSS4_IR || metric == CVSS4_AR) + && selected == 'X') + return 'H'; + + // All other environmental metrics just overwrite base score values, + // so if they’re not defined just use the base score value. + if (metric >= CVSS4_AV && metric <= CVSS4_SA) + { + char modified_selected = simplified_vec[metric - CVSS4_AV + CVSS4_MAV]; + if (modified_selected != 'X') + return modified_selected; + } + + return selected; +} + +/** + * @brief Simplify CVSS 4.0 base vector so metrics can be indexed by enum. + * + * The vector is simplified to a strictly ordered character array with + * each character index corresponding to the cvss4_base_metrics enum value + * and using 'X' for undefined metric values. + * + * This relies on all allowed values being single characters, or having + * unique first characters in case of the Provider Urgency metric. + * + * @param[in] cvss_str The original vector without the prefix "CVSS:4.0/". + * + * @return A simplified vector string as described above or NULL on error. + */ +static gchar * +simplify_cvss4_vector (const char *cvss_str) +{ + gchar **split_cvss_str, **split_cvss_point; + gboolean valid = TRUE; + gchar *vec = NULL; + cvss4_metric_t metric; + + if (cvss_str == NULL || strcmp (cvss_str, "") == 0) + return NULL; + + vec = g_strdup (CVSS_METRICS_STR_BLANK); + + split_cvss_str = g_strsplit (cvss_str, "/", -1); + split_cvss_point = split_cvss_str; + while (valid && *split_cvss_point) + { + if (strcmp (*split_cvss_point, "") == 0) + { + split_cvss_point++; + continue; + } + + gchar **split_component = g_strsplit (*split_cvss_point, ":", 2); + const gchar *metric_str = split_component[0], *value = split_component[1]; + + valid = FALSE; + + if (value == NULL) + { + g_debug ("%s: value for metric %s missing", __func__, metric_str); + break; + } + else if (strcasecmp (metric_str, "U") == 0) + { + // Special case for the Provider Urgency metric + if (strcasecmp (value, "Red") && strcasecmp (value, "Amber") + && strcasecmp (value, "Green") && strcasecmp (value, "Clear") + && strcasecmp (value, "X")) + { + g_debug ("%s: value for metric %s must be one of" + " 'Red', 'Amber', 'Green', 'Clear', 'X'", + __func__, metric_str); + break; + } + else + valid = TRUE; + } + else if (strlen (value) != 1) + { + g_debug ("%s: value for metric %s must be 1 character", __func__, + metric_str); + break; + } + + cvss4_metric_def_t *metric_def = &cvss4_metric_defs[0]; + while (metric_def->metric_str) + { + if (strcasecmp (metric_str, metric_def->metric_str) == 0) + { + char value_char = g_ascii_toupper (value[0]); + + // Reject duplicate metrics + if (vec[metric_def->metric] != 'X') + { + g_debug ("%s: duplicate metric %s", __func__, metric_str); + break; + } + + // Set the metric in the simplified vector + if (strchr (metric_def->values, value_char)) + { + valid = TRUE; + vec[metric_def->metric] = value_char; + } + else + { + g_debug ("%s: invalid metric: %s:%c", __func__, metric_str, + value_char); + } + break; + } + metric_def++; + } + + split_cvss_point++; + g_strfreev (split_component); + } + g_strfreev (split_cvss_str); + + for (metric = CVSS4_AV; valid && metric <= CVSS4_SA; metric++) + { + if (vec[metric] == 'X') + { + g_debug ("%s: mandatory metric %s is undefined", __func__, + cvss4_metric_defs[metric].metric_str); + valid = FALSE; + } + } + + if (!valid) + { + g_debug ("%s: vector %s is invalid", __func__, cvss_str); + g_free (vec); + return NULL; + } + + return vec; +} + +/** + * @brief Expands a simplified CVSS 4.0 vector into its full string form + * + * @param[in] vec The simplified vector to expand + * + * @return The full vector, including the "CVSS:4.0/" prefix + */ +static gchar * +cvss4_vector_expand (const char *vec) +{ + cvss4_metric_t metric; + GString *str = g_string_new ("CVSS:4.0"); + for (metric = 0; metric < CVSS4_METRICS_MAX; metric++) + { + const char *expanded_value; + if (vec[metric] == 'X') + continue; + cvss4_metric_def_t def = cvss4_metric_defs[metric]; + if (metric == CVSS4_U) + { + switch (vec[metric]) + { + case 'R': + expanded_value = "Red"; + break; + case 'A': + expanded_value = "Amber"; + break; + case 'G': + expanded_value = "Green"; + break; + case 'C': + expanded_value = "Clear"; + break; + default: + expanded_value = NULL; + } + } + else + expanded_value = NULL; + + if (expanded_value) + g_string_append_printf (str, "/%s:%s", def.metric_str, expanded_value); + else + g_string_append_printf (str, "/%s:%c", def.metric_str, vec[metric]); + } + return g_string_free (str, FALSE); +} + +/** + * @brief Calculate CVSS 4.0 macrovector from a simplified vector. + * + * @param[in] vec The simplified vector to get the macrovector of + * + * @return The macrovector. + */ +static inline gchar * +cvss4_macrovector (const char *vec) +{ + gchar *macrovector; + if (vec == NULL) + return NULL; + + macrovector = g_strdup (CVSS_MACROVECTOR_BLANK); + + // EQ1: 0-AV:N and PR:N and UI:N + // 1-(AV:N or PR:N or UI:N) and not (AV:N and PR:N and UI:N) and not AV:P + // 2-AV:P or not(AV:N or PR:N or UI:N) + char av = cvss4_m (vec, CVSS4_AV); + char pr = cvss4_m (vec, CVSS4_PR); + char ui = cvss4_m (vec, CVSS4_UI); + + if (av == 'N' && pr == 'N' && ui == 'N') + macrovector[0] = '0'; + else if ((av == 'N' || pr == 'N' || ui == 'N') && !(av == 'P')) + macrovector[0] = '1'; + else + macrovector[0] = '2'; + + // EQ2: 0-(AC:L and AT:N) + // 1-(not(AC:L and AT:N)) + char ac = cvss4_m (vec, CVSS4_AC); + char at = cvss4_m (vec, CVSS4_AT); + + if (ac == 'L' && at == 'N') + macrovector[1] = '0'; + else + macrovector[1] = '1'; + + // EQ3: 0-(VC:H and VI:H) + // 1-(not(VC:H and VI:H) and (VC:H or VI:H or VA:H)) + // 2-not (VC:H or VI:H or VA:H) + char vc = cvss4_m (vec, CVSS4_VC); + char vi = cvss4_m (vec, CVSS4_VI); + char va = cvss4_m (vec, CVSS4_VA); + + if (vc == 'H' && vi == 'H') + macrovector[2] = '0'; + else if (vc == 'H' || vi == 'H' || va == 'H') + macrovector[2] = '1'; + else + macrovector[2] = '2'; + + // EQ4: 0-(MSI:S or MSA:S) + // 1-not (MSI:S or MSA:S) and (SC:H or SI:H or SA:H) + // 2-not (MSI:S or MSA:S) and not (SC:H or SI:H or SA:H) + // + // "Effective" SI and SA are the same as MSI and MSA for the purposes of + // checking for the "Safety" value. + char sc = cvss4_m (vec, CVSS4_SC); + char si = cvss4_m (vec, CVSS4_SI); + char sa = cvss4_m (vec, CVSS4_SA); + if (si == 'S' || sa == 'S') + macrovector[3] = '0'; + else if (sc == 'H' || si == 'H' || sa == 'H') + macrovector[3] = '1'; + else + macrovector[3] = '2'; + + // EQ5: 0-E:A + // 1-E:P + // 2-E:U + char e = cvss4_m (vec, CVSS4_E); + if (e == 'A') + macrovector[4] = '0'; + else if (e == 'P') + macrovector[4] = '1'; + else + macrovector[4] = '2'; + + // EQ6: 0-(CR:H and VC:H) or (IR:H and VI:H) or (AR:H and VA:H) + // 1-not[(CR:H and VC:H) or (IR:H and VI:H) or (AR:H and VA:H)] + char cr = cvss4_m (vec, CVSS4_CR); + char ir = cvss4_m (vec, CVSS4_IR); + char ar = cvss4_m (vec, CVSS4_AR); + if ((cr == 'H' && vc == 'H') || (ir == 'H' && vi == 'H') + || (ar == 'H' && va == 'H')) + macrovector[5] = '0'; + else + macrovector[5] = '1'; + + return macrovector; +} + +/** + * @brief Calulate the maximal scoring differences from a CVSS 4.0 macrovector. + * + * @param[in] macrovector + * @param[out] available_distance_eq1 Maximal scoring diff. for EQ1 + * @param[out] available_distance_eq2 Maximal scoring diff. for EQ2 + * @param[out] available_distance_eq3eq6 Maximal scoring diff. for EQ3 and EQ6 + * @param[out] available_distance_eq4 Maximal scoring diff. for EQ4 + * @param[out] available_distance_eq5 Maximal scoring diff. for EQ5 + */ +static void +cvss4_maximal_scoring_differences (const char *macrovector, + double *available_distance_eq1, + double *available_distance_eq2, + double *available_distance_eq3eq6, + double *available_distance_eq4, + double *available_distance_eq5) +{ + double value = cvss4_macrovector_score (macrovector); + double score_eq1_next_lower_macro, score_eq2_next_lower_macro; + double score_eq3eq6_next_lower_macro; + double score_eq4_next_lower_macro, score_eq5_next_lower_macro; + + // Next lower macrovector for EQ1 only exists if EQ1 is 0 or 1. + if (macrovector[0] <= '1') + { + gchar *eq1_next_lower_macro = g_strdup (macrovector); + eq1_next_lower_macro[0]++; + score_eq1_next_lower_macro = + cvss4_macrovector_score (eq1_next_lower_macro); + g_free (eq1_next_lower_macro); + } + else + score_eq1_next_lower_macro = -1.0; + + // Next lower macrovector for EQ2 only exists if EQ2 is 0. + if (macrovector[1] == '0') + { + gchar *eq2_next_lower_macro = g_strdup (macrovector); + eq2_next_lower_macro[1]++; + score_eq2_next_lower_macro = + cvss4_macrovector_score (eq2_next_lower_macro); + } + else + score_eq2_next_lower_macro = -1.0; + + // Next lower macrovector for EQ3. + if ((macrovector[2] == '0' || macrovector[2] == '1') && macrovector[5] == '1') + { + gchar *eq3eq6_next_lower_macro = g_strdup (macrovector); + eq3eq6_next_lower_macro[2]++; + score_eq3eq6_next_lower_macro = + cvss4_macrovector_score (eq3eq6_next_lower_macro); + g_free (eq3eq6_next_lower_macro); + } + else if (macrovector[2] == '1' && macrovector[5] == '0') + { + gchar *eq3eq6_next_lower_macro = g_strdup (macrovector); + eq3eq6_next_lower_macro[5]++; + score_eq3eq6_next_lower_macro = + cvss4_macrovector_score (eq3eq6_next_lower_macro); + g_free (eq3eq6_next_lower_macro); + } + else if (macrovector[2] == '0' && macrovector[5] == '0') + { + gchar *eq3eq6_next_lower_macro_left = g_strdup (macrovector); + eq3eq6_next_lower_macro_left[5]++; + gchar *eq3eq6_next_lower_macro_right = g_strdup (macrovector); + eq3eq6_next_lower_macro_right[2]++; + double score_eq3eq6_next_lower_macro_left = + cvss4_macrovector_score (eq3eq6_next_lower_macro_left); + double score_eq3eq6_next_lower_macro_right = + cvss4_macrovector_score (eq3eq6_next_lower_macro_right); + + if (score_eq3eq6_next_lower_macro_left + > score_eq3eq6_next_lower_macro_right) + score_eq3eq6_next_lower_macro = score_eq3eq6_next_lower_macro_left; + else + score_eq3eq6_next_lower_macro = score_eq3eq6_next_lower_macro_right; + + g_free (eq3eq6_next_lower_macro_left); + g_free (eq3eq6_next_lower_macro_right); + } + else + score_eq3eq6_next_lower_macro = -1.0; + + // Next lower macrovector for EQ4 only exists if EQ4 is 0 or 1. + if (macrovector[3] <= '1') + { + gchar *eq4_next_lower_macro = g_strdup (macrovector); + eq4_next_lower_macro[3]++; + score_eq4_next_lower_macro = + cvss4_macrovector_score (eq4_next_lower_macro); + g_free (eq4_next_lower_macro); + } + else + score_eq4_next_lower_macro = -1.0; + + // Next lower macrovector for EQ5 only exists if EQ5 is 0 or 1. + if (macrovector[4] <= '1') + { + gchar *eq5_next_lower_macro = g_strdup (macrovector); + eq5_next_lower_macro[4]++; + score_eq5_next_lower_macro = + cvss4_macrovector_score (eq5_next_lower_macro); + g_free (eq5_next_lower_macro); + } + else + score_eq5_next_lower_macro = -1.0; + + if (value != -1.0) + { + *available_distance_eq1 = score_eq1_next_lower_macro != -1.0 + ? value - score_eq1_next_lower_macro + : -1.0; + *available_distance_eq2 = score_eq2_next_lower_macro != -1.0 + ? value - score_eq2_next_lower_macro + : -1.0; + *available_distance_eq3eq6 = score_eq3eq6_next_lower_macro != -1.0 + ? value - score_eq3eq6_next_lower_macro + : -1.0; + *available_distance_eq4 = score_eq4_next_lower_macro != -1.0 + ? value - score_eq4_next_lower_macro + : -1.0; + *available_distance_eq5 = score_eq5_next_lower_macro != -1.0 + ? value - score_eq5_next_lower_macro + : -1.0; + } + else + { + *available_distance_eq1 = -1.0; + *available_distance_eq2 = -1.0; + *available_distance_eq3eq6 = -1.0; + *available_distance_eq4 = -1.0; + *available_distance_eq5 = -1.0; + } +} + +/** + * @brief Composes a list of max vectors for the given CVSS 4.0 macrovector. + * + * @param[in] macrovector The macrovector to get the max vectors of. + * + * @return NULL-terminated array of vectors in simplified form. + */ +static gchar ** +cvss4_max_vectors (const char *macrovector) +{ + const char **eq1_maxes, **eq2_maxes, **eq3eq6_maxes; + const char *eq4_max, *eq5_max; + gchar **ret; + + // EQ1 + static const char *eq1_maxes_0[] = {"AV:N/PR:N/UI:N/", NULL}; + static const char *eq1_maxes_1[] = {"AV:A/PR:N/UI:N/", "AV:N/PR:L/UI:N/", + "AV:N/PR:N/UI:P/", NULL}; + static const char *eq1_maxes_2[] = {"AV:P/PR:N/UI:N/", "AV:A/PR:L/UI:P/", + NULL}; + if (macrovector[0] == '0') + eq1_maxes = eq1_maxes_0; + else if (macrovector[0] == '1') + eq1_maxes = eq1_maxes_1; + else + eq1_maxes = eq1_maxes_2; + + // EQ2 + static const char *eq2_maxes_0[] = {"AC:L/AT:N/", NULL}; + static const char *eq2_maxes_1[] = {"AC:H/AT:N/", "AC:L/AT:P/", NULL}; + if (macrovector[1] == '0') + eq2_maxes = eq2_maxes_0; + else + eq2_maxes = eq2_maxes_1; + + // EQ3+EQ6 + static const char *eq3eq6_maxes_00[] = {"VC:H/VI:H/VA:H/CR:H/IR:H/AR:H/", + NULL}; + static const char *eq3eq6_maxes_01[] = { + "VC:H/VI:H/VA:L/CR:M/IR:M/AR:H/", "VC:H/VI:H/VA:H/CR:M/IR:M/AR:M/", NULL}; + static const char *eq3eq6_maxes_10[] = { + "VC:L/VI:H/VA:H/CR:H/IR:H/AR:H/", "VC:H/VI:L/VA:H/CR:H/IR:H/AR:H/", NULL}; + static const char *eq3eq6_maxes_11[] = { + "VC:L/VI:H/VA:L/CR:H/IR:M/AR:H/", "VC:L/VI:H/VA:H/CR:H/IR:M/AR:M/", + "VC:H/VI:L/VA:H/CR:M/IR:H/AR:M/", "VC:H/VI:L/VA:L/CR:M/IR:H/AR:H/", + "VC:L/VI:L/VA:H/CR:H/IR:H/AR:M/", NULL}; + static const char *eq3eq6_maxes_21[] = {"VC:L/VI:L/VA:L/CR:H/IR:H/AR:H/", + NULL}; + if ((macrovector[2] == '0')) + { + if (macrovector[5] == '0') + eq3eq6_maxes = eq3eq6_maxes_00; + else + eq3eq6_maxes = eq3eq6_maxes_01; + } + else if ((macrovector[2] == '1')) + { + if (macrovector[5] == '0') + eq3eq6_maxes = eq3eq6_maxes_10; + else + eq3eq6_maxes = eq3eq6_maxes_11; + } + else + eq3eq6_maxes = eq3eq6_maxes_21; + + // EQ4 + if (macrovector[3] == '0') + eq4_max = "SC:H/SI:S/SA:S/"; + else if (macrovector[3] == '1') + eq4_max = "SC:H/SI:H/SA:H/"; + else + eq4_max = "SC:L/SI:L/SA:L/"; + + // EQ5 + if (macrovector[4] == '0') + eq5_max = "E:A/"; + else if (macrovector[4] == '1') + eq5_max = "E:P/"; + else + eq5_max = "E:U/"; + + GPtrArray *max_vectors = g_ptr_array_new (); + const char **eq1_max, **eq2_max, **eq3eq6_max; + for (eq1_max = eq1_maxes; *eq1_max != NULL; eq1_max++) + { + for (eq2_max = eq2_maxes; *eq2_max != NULL; eq2_max++) + { + for (eq3eq6_max = eq3eq6_maxes; *eq3eq6_max != NULL; eq3eq6_max++) + { + gchar *full_vector = + g_strdup_printf ("%s%s%s%s%s", *eq1_max, *eq2_max, *eq3eq6_max, + eq4_max, eq5_max); + gchar *vector = simplify_cvss4_vector (full_vector); + if (vector == NULL) + g_warning ("%s: generated vector %s is invalid", __func__, + full_vector); + else + g_ptr_array_add (max_vectors, vector); + g_free (full_vector); + } + } + } + + g_ptr_array_add (max_vectors, NULL); + + ret = (gchar **) max_vectors->pdata; + g_ptr_array_free (max_vectors, FALSE); + return ret; +} + +/** + * @brief Get the index of a CVSS 4.0 metric value for severity distances. + * + * @param[in] metric The metric to check. + * @param[in] value The value of the given metric. + * + * @return The index value + */ +static double +cvss4_metric_level (cvss4_metric_t metric, char value) +{ + switch (metric) + { + case CVSS4_AV: + switch (value) + { + case 'N': + return 0.0; + case 'A': + return 0.1; + case 'L': + return 0.2; + case 'P': + return 0.3; + default: + return -99.0; + } + break; + case CVSS4_PR: + switch (value) + { + case 'N': + return 0.0; + case 'L': + return 0.1; + case 'H': + return 0.2; + default: + return -99.0; + } + break; + case CVSS4_UI: + switch (value) + { + case 'N': + return 0.0; + case 'P': + return 0.1; + case 'A': + return 0.2; + default: + return -99.0; + } + break; + case CVSS4_AC: + switch (value) + { + case 'L': + return 0.0; + case 'H': + return 0.1; + default: + return -99.0; + } + break; + case CVSS4_AT: + switch (value) + { + case 'N': + return 0.0; + case 'P': + return 0.1; + default: + return -99.0; + } + break; + case CVSS4_VC: + case CVSS4_VI: + case CVSS4_VA: + switch (value) + { + case 'H': + return 0.0; + case 'L': + return 0.1; + case 'N': + return 0.2; + default: + return -99.0; + } + break; + case CVSS4_SC: + case CVSS4_SI: + case CVSS4_SA: + switch (value) + { + case 'S': + return 0.0; + case 'H': + return 0.1; + case 'L': + return 0.2; + case 'N': + return 0.3; + default: + return -99.0; + } + break; + case CVSS4_CR: + case CVSS4_IR: + case CVSS4_AR: + switch (value) + { + case 'H': + return 0.0; + case 'M': + return 0.1; + case 'L': + return 0.2; + default: + return -99.0; + } + break; + + // The Exploit Maturity metric is included in the reference implementation + // but never used + /* + case CVSS4_E: + switch (value) + { + case 'A': return 0.0; + case 'P': return 0.1; + case 'U': return 0.2; + } + break; + */ + default: + return -99.0; + } +} + +/** + * @brief Calculate severity distance for a metric in two CVSS 4.0 vectors. + * + * @param[in] metric The metric to calculate severity distance for. + * @param[in] vec The vector to be scored in simplified form. + * @param[in] max_vec The max vector to subtract in simplified form. + * + * @return The severity distance. + */ +static inline double +cvss4_severity_distance (cvss4_metric_t metric, const char *vec, + const char *max_vec) +{ + return cvss4_metric_level (metric, cvss4_m (vec, metric)) + - cvss4_metric_level (metric, max_vec[metric]); +} + +/** + * @brief Calculate current severity distances for given CVSS 4.0 vector + * + * @param[in] vec The vector in simplified form + * @param[in] macrovector Corresponding macrovector + * @param[out] current_severity_distance_eq1 Distance for EQ1 + * @param[out] current_severity_distance_eq2 Distance for EQ2 + * @param[out] current_severity_distance_eq3eq6 Distance for EQ3 and EQ6 + * @param[out] current_severity_distance_eq4 Distance for EQ4 + * @param[out] current_severity_distance_eq5 Distance for EQ5 + */ +static void +cvss4_current_severity_distances (const char *vec, const char *macrovector, + double *current_severity_distance_eq1, + double *current_severity_distance_eq2, + double *current_severity_distance_eq3eq6, + double *current_severity_distance_eq4, + double *current_severity_distance_eq5) +{ + double severity_distance_AV, severity_distance_PR, severity_distance_UI; + double severity_distance_AC, severity_distance_AT; + double severity_distance_VC, severity_distance_VI, severity_distance_VA; + double severity_distance_SC, severity_distance_SI, severity_distance_SA; + double severity_distance_CR, severity_distance_IR, severity_distance_AR; + + severity_distance_AV = severity_distance_PR = severity_distance_UI = -99.0; + severity_distance_AC = severity_distance_AT = -99.0; + severity_distance_VC = severity_distance_VI = severity_distance_VA = -99.0; + severity_distance_SC = severity_distance_SI = severity_distance_SA = -99.0; + severity_distance_CR = severity_distance_IR = severity_distance_AR = -99.0; + + char **max_vectors, **max_vec; + max_vectors = cvss4_max_vectors (macrovector); + for (max_vec = max_vectors; *max_vec != NULL; max_vec++) + { + severity_distance_AV = cvss4_severity_distance (CVSS4_AV, vec, *max_vec); + severity_distance_PR = cvss4_severity_distance (CVSS4_PR, vec, *max_vec); + severity_distance_UI = cvss4_severity_distance (CVSS4_UI, vec, *max_vec); + + severity_distance_AC = cvss4_severity_distance (CVSS4_AC, vec, *max_vec); + severity_distance_AT = cvss4_severity_distance (CVSS4_AT, vec, *max_vec); + + severity_distance_VC = cvss4_severity_distance (CVSS4_VC, vec, *max_vec); + severity_distance_VI = cvss4_severity_distance (CVSS4_VI, vec, *max_vec); + severity_distance_VA = cvss4_severity_distance (CVSS4_VA, vec, *max_vec); + + severity_distance_SC = cvss4_severity_distance (CVSS4_SC, vec, *max_vec); + severity_distance_SI = cvss4_severity_distance (CVSS4_SI, vec, *max_vec); + severity_distance_SA = cvss4_severity_distance (CVSS4_SA, vec, *max_vec); + + severity_distance_CR = cvss4_severity_distance (CVSS4_CR, vec, *max_vec); + severity_distance_IR = cvss4_severity_distance (CVSS4_IR, vec, *max_vec); + severity_distance_AR = cvss4_severity_distance (CVSS4_AR, vec, *max_vec); + + if (severity_distance_AV < 0.0 || severity_distance_PR < 0.0 + || severity_distance_UI < 0.0 || severity_distance_AC < 0.0 + || severity_distance_AT < 0.0 || severity_distance_VC < 0.0 + || severity_distance_VI < 0.0 || severity_distance_VA < 0.0 + || severity_distance_SC < 0.0 || severity_distance_SI < 0.0 + || severity_distance_SA < 0.0 || severity_distance_CR < 0.0 + || severity_distance_IR < 0.0 || severity_distance_AR < 0.0) + continue; + + g_debug ("%s AV:%0.1f PR:%0.1f UI:%0.1f |" + " AC:%0.1f AT:%0.1f |" + " VC:%0.1f VI:%0.1f VA:%0.1f |" + " SC:%0.1f SI:%0.1f SA:%0.1f |" + " CR:%0.1f IR:%0.1f AR:%0.1f", + __func__, severity_distance_AV, severity_distance_PR, + severity_distance_UI, severity_distance_AC, severity_distance_AT, + severity_distance_VC, severity_distance_VI, severity_distance_VA, + severity_distance_SC, severity_distance_SI, severity_distance_SA, + severity_distance_CR, severity_distance_IR, + severity_distance_AR); + break; + } + + gchar *max_vec_expanded = cvss4_vector_expand (*max_vec); + g_debug ("%s: max_vec: %s", __func__, max_vec_expanded); + g_free (max_vec_expanded); + g_strfreev (max_vectors); + + *current_severity_distance_eq1 = + severity_distance_AV + severity_distance_PR + severity_distance_UI; + *current_severity_distance_eq2 = severity_distance_AC + severity_distance_AT; + *current_severity_distance_eq3eq6 = + severity_distance_VC + severity_distance_VI + severity_distance_VA + + severity_distance_CR + severity_distance_IR + severity_distance_AR; + *current_severity_distance_eq4 = + severity_distance_SC + severity_distance_SI + severity_distance_SA; + *current_severity_distance_eq5 = 0.0; +} + +/** + * @brief Get the max severity values for a CVSS 4.0 macrovector + * + * The values are the MaxSeverity values already multiplied by 0.1 + * + * @param[in] macrovector The macrovector to get the max severity values for + * @param[out] max_severity_eq1 Max severity for EQ1 + * @param[out] max_severity_eq2 Max severity for EQ2 + * @param[out] max_severity_eq3eq6 Max severity for EQ3 and EQ6 + * @param[out] max_severity_eq4 Max severity for EQ4 + */ +static void +cvss4_max_severities (const char *macrovector, double *max_severity_eq1, + double *max_severity_eq2, double *max_severity_eq3eq6, + double *max_severity_eq4) +{ + switch (macrovector[0]) + { + case '0': + *max_severity_eq1 = 0.1; + break; + case '1': + *max_severity_eq1 = 0.4; + break; + case '2': + *max_severity_eq1 = 0.5; + break; + default: + *max_severity_eq1 = -99.0; + } + + switch (macrovector[1]) + { + case '0': + *max_severity_eq2 = 0.1; + break; + case '1': + *max_severity_eq2 = 0.2; + break; + default: + *max_severity_eq2 = -99.0; + } + + switch (macrovector[2]) + { + case '0': + if (macrovector[5] == '0') + *max_severity_eq3eq6 = 0.7; + else + *max_severity_eq3eq6 = 0.6; + break; + case '1': + *max_severity_eq3eq6 = 0.8; + break; + case '2': + *max_severity_eq3eq6 = 1.0; + break; + default: + *max_severity_eq3eq6 = -99.0; + } + + switch (macrovector[3]) + { + case '0': + *max_severity_eq4 = 0.6; + break; + case '1': + *max_severity_eq4 = 0.5; + break; + case '2': + *max_severity_eq4 = 0.4; + break; + default: + *max_severity_eq4 = -99.0; + } +} + +/** + * @brief Calculate CVSS 4.0 Score. + * + * @param cvss_str Vector from which to compute score, without prefix. + * + * @return CVSS score, or -1 on error. + */ +static double +get_cvss_score_from_metrics_v4 (const char *cvss_str) +{ + char *vec = NULL; + char *macrovector = NULL; + + double available_distance_eq1, available_distance_eq2; + double available_distance_eq3eq6; + double available_distance_eq4, available_distance_eq5; + + double current_severity_distance_eq1, current_severity_distance_eq2; + double current_severity_distance_eq3eq6; + double current_severity_distance_eq4, current_severity_distance_eq5; + + double max_severity_eq1, max_severity_eq2, max_severity_eq3eq6; + double max_severity_eq4; + + double mean_distance, value; + + int n_existing_lower = 0; + + // Convert vector to simplified, enum-indexed string + g_debug ("%s: CVSS string: %s", __func__, cvss_str); + vec = simplify_cvss4_vector (cvss_str); + g_debug ("%s: simplified vector: %s", __func__, vec); + if (vec == NULL) + return -1.0; + + // Calculate macrovector + macrovector = cvss4_macrovector (vec); + value = cvss4_macrovector_score (macrovector); + g_debug ("%s: macrovector: %s, value: %0.1f", __func__, macrovector, value); + if (macrovector == NULL || value == -1.0) + { + g_free (vec); + return -1.0; + } + + // Calculate maximum distances + cvss4_maximal_scoring_differences ( + macrovector, &available_distance_eq1, &available_distance_eq2, + &available_distance_eq3eq6, &available_distance_eq4, + &available_distance_eq5); + g_debug ("%s: maximal scoring diffs:" + " EQ1:%0.1f EQ2:%0.1f EQ3+EQ6:%0.1f EQ5:%0.1f EQ6:%0.1f", + __func__, available_distance_eq1, available_distance_eq2, + available_distance_eq3eq6, available_distance_eq4, + available_distance_eq5); + + // Calculate current severity distances + cvss4_current_severity_distances ( + vec, macrovector, ¤t_severity_distance_eq1, + ¤t_severity_distance_eq2, ¤t_severity_distance_eq3eq6, + ¤t_severity_distance_eq4, ¤t_severity_distance_eq5); + + g_debug ("%s: current severity distances:" + "EQ1:%0.1f EQ2:%0.1f EQ3+EQ6:%0.1f EQ4:%0.1f EQ5:%0.1f", + __func__, current_severity_distance_eq1, + current_severity_distance_eq2, current_severity_distance_eq3eq6, + current_severity_distance_eq4, current_severity_distance_eq5); + + // Get MaxSeverity + cvss4_max_severities (macrovector, &max_severity_eq1, &max_severity_eq2, + &max_severity_eq3eq6, &max_severity_eq4); + + g_free (vec); + g_free (macrovector); + + // Calculate mean distances + mean_distance = 0.0; + if (available_distance_eq1 >= 0.0) + { + n_existing_lower++; + double percent_to_next_severity = + (current_severity_distance_eq1) / max_severity_eq1; + mean_distance += (available_distance_eq1 * percent_to_next_severity); + } + + if (available_distance_eq2 >= 0.0) + { + n_existing_lower++; + double percent_to_next_severity = + (current_severity_distance_eq2) / max_severity_eq2; + mean_distance += (available_distance_eq2 * percent_to_next_severity); + } + + if (available_distance_eq3eq6 >= 0.0) + { + n_existing_lower++; + double percent_to_next_severity = + (current_severity_distance_eq3eq6) / max_severity_eq3eq6; + mean_distance += (available_distance_eq3eq6 * percent_to_next_severity); + } + + if (available_distance_eq4 >= 0.0) + { + n_existing_lower++; + double percent_to_next_severity = + (current_severity_distance_eq4) / max_severity_eq4; + mean_distance += (available_distance_eq4 * percent_to_next_severity); + } + + if (available_distance_eq5 >= 0.0) + { + // For EQ5 the percentage is always 0 + n_existing_lower++; + } + + mean_distance = mean_distance / n_existing_lower; + + // Get and adjust macrovector score + value = value - mean_distance; + if (value < 0.0) + value = 0.0; + else if (value > 10.0) + value = 10.0; + + return round (value * 10.0) / 10.0; +} diff --git a/base/cvss_tests.c b/base/cvss_tests.c index 8060b359..caad53c2 100644 --- a/base/cvss_tests.c +++ b/base/cvss_tests.c @@ -77,6 +77,75 @@ Ensure (cvss, get_cvss_score_from_base_metrics_succeeds_v3) CHECK ("CVSS:3.1/av:n/ac:l/pr:n/ui:n/s:u/c:h/i:l/a:n", 8.2); } +Ensure (cvss, get_cvss_score_from_base_metrics_succeeds_v4) +{ + CHECK ("CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H", + 10.0); + + /* Trailing separator. */ + CHECK ("CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H/", + 10.0); + + /* We support any case in metrics. */ + CHECK ("CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H", + 10.0); + + /* Test various base vectors */ + CHECK ("CVSS:4.0/AV:N/AC:H/AT:N/PR:L/UI:A/VC:H/VI:L/VA:N/SC:N/SI:L/SA:H", + 6.9); + + CHECK ("CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:P/VC:L/VI:L/VA:L/SC:H/SI:H/SA:H", + 5.8); + + CHECK ("CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:P/VC:L/VI:L/VA:L/SC:H/SI:H/SA:H/" + "MSI:S", + 7.0); + + CHECK ("CVSS:4.0/AV:P/AC:H/AT:P/PR:H/UI:A/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N", + 1.0); + + CHECK ("CVSS:4.0/AV:L/AC:H/AT:N/PR:L/UI:A/VC:L/VI:L/VA:H/SC:N/SI:N/SA:N", + 4.4); + + /* Test cases for picking one of two macrovector scores + * when EQ3 and EQ6 of the macrovector are 0*/ + + CHECK ("CVSS:4.0/AV:N/AC:H/AT:P/PR:H/UI:A/VC:H/VI:H/VA:L/SC:N/SI:N/SA:N", + 7.1); + + CHECK ("CVSS:4.0/AV:A/AC:H/AT:P/PR:H/UI:A/VC:H/VI:H/VA:L/SC:N/SI:N/SA:N", + 5.3); + + /* Test vectors with Requirements metric */ + CHECK ("CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:L/VA:N/SC:L/SI:L/SA:L/" + "CR:M", + 8.0); + + CHECK ("CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:L/VA:N/SC:L/SI:L/SA:L/" + "IR:L", + 8.7); + + CHECK ("CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:L/SC:N/SI:N/SA:N/" + "CR:M/IR:L/AR:L", + 8.9); + + /* Test vectors with Exploit Maturity metric */ + CHECK ("CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:L/VA:N/SC:L/SI:L/SA:L/E:P", + 7.8); + + CHECK ("CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:L/VA:N/SC:L/SI:L/SA:L/E:U", + 6.7); + + /* Provider Urgency is a special case with longer values */ + CHECK ("CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H/" + "U:Amber", + 10.0); + + CHECK ("CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H/" + "U:X", + 10.0); +} + Ensure (cvss, get_cvss_score_from_base_metrics_fails) { CHECK ("", -1.0); @@ -113,6 +182,52 @@ Ensure (cvss, get_cvss_score_from_base_metrics_fails) CHECK ("cvss:3.0/AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H", -1.0); } +Ensure (cvss, get_cvss_score_from_base_metrics_fails_v4) +{ + /* No metrics given */ + CHECK ("CVSS:4.0", -1.0); + + /* Metric name is invalid */ + CHECK ("CVSS:4.0/AXXXX:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H", + -1.0); + + /* Metric name is missing */ + CHECK ("CVSS:4.0/:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H", -1.0); + + /* Metric value is invalid */ + CHECK ("CVSS:4.0/AV:Y/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H", + -1.0); + + /* Metric value is too long */ + CHECK ("CVSS:4.0/AV:XYZ/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H", + -1.0); + + /* Metric value is missing */ + CHECK ("CVSS:4.0/AV:/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H", + -1.0); + + CHECK ("CVSS:4.0/AV/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H", -1.0); + + /* Duplicate Metric */ + CHECK ("CVSS:4.0/AV:N/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H", + -1.0); + + /* Missing mandatory metrics */ + CHECK ("CVSS:4.0/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H", -1.0); + + CHECK ("CVSS:4.0/AV:N/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H", -1.0); + + CHECK ("CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H", -1.0); + + /* Version must be uppercase. */ + CHECK ("cvss:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H", + -1.0); + + /* Invalid Provider Urgency */ + CHECK ("CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H/U:R", + -1.0); +} + Ensure (cvss, get_cvss_score_from_base_metrics_all_in_feed_match) { /* Every distinct CVSSv2 vector and score shipped in the feed nvdcve files. */ @@ -1752,6 +1867,10 @@ main (int argc, char **argv) add_test_with_context (suite, cvss, get_cvss_score_from_base_metrics_fails); add_test_with_context (suite, cvss, get_cvss_score_from_base_metrics_succeeds_v3); + add_test_with_context (suite, cvss, + get_cvss_score_from_base_metrics_succeeds_v4); + add_test_with_context (suite, cvss, + get_cvss_score_from_base_metrics_fails_v4); add_test_with_context (suite, cvss, get_cvss_score_from_base_metrics_all_in_feed_match); diff --git a/base/logging.c b/base/logging.c index 290d4a26..36223c05 100644 --- a/base/logging.c +++ b/base/logging.c @@ -34,6 +34,13 @@ */ #define G_LOG_DOMAIN "libgvm base" +/** + * @brief Timezone to use for logs. + * + * NULL means to use the current timezone. + */ +static gchar *log_tz = NULL; + /** * @struct gvm_logging_t * @brief Logging stores the parameters loaded from a log configuration @@ -69,7 +76,14 @@ get_time (gchar *time_fmt) { time_t now; struct tm ts; - gchar buf[80]; + gchar buf[80], *original_tz; + + if (log_tz) + { + original_tz = getenv ("TZ") ? g_strdup (getenv ("TZ")) : NULL; + setenv ("TZ", log_tz, 1); + tzset (); + } /* Get the current time. */ now = time (NULL); @@ -78,6 +92,19 @@ get_time (gchar *time_fmt) localtime_r (&now, &ts); strftime (buf, sizeof (buf), time_fmt, &ts); + if (log_tz) + { + /* Revert to stored TZ. */ + if (original_tz) + { + setenv ("TZ", original_tz, 1); + g_free (original_tz); + tzset (); + } + else + unsetenv ("TZ"); + } + return g_strdup_printf ("%s", buf); } @@ -873,6 +900,21 @@ check_log_file (gvm_logging_t *log_domain_entry) return 0; } +/** + * @brief Set the log timezone. + * + * This is the timezone used for dates in log messages. If NULL then + * the current timezone is used. + * + * @param tz Timezone. + */ +void +set_log_tz (const gchar *tz) +{ + g_free (log_tz); + log_tz = tz ? g_strdup (tz) : NULL; +} + /** * @brief Sets up routing of logdomains to log handlers. * diff --git a/base/logging.h b/base/logging.h index 5919e798..729a30ed 100644 --- a/base/logging.h +++ b/base/logging.h @@ -54,4 +54,7 @@ get_log_reference (void); void free_log_reference (void); +void +set_log_tz (const gchar *); + #endif /* not _GVM_LOGGING_H */ diff --git a/base/nvti.c b/base/nvti.c index 5a9df616..3032509a 100644 --- a/base/nvti.c +++ b/base/nvti.c @@ -70,8 +70,7 @@ typedef struct vtref * * @param ref_text The optional text accompanying a reference. * - * @return NULL in case the memory could not be allocated. - * Else a vtref structure which needs to be + * @return A vtref structure which needs to be * released using @ref vtref_free . */ vtref_t * @@ -174,8 +173,7 @@ typedef struct vtseverity * @param[in] score The score to be set. * @param[in] value The value corresponding to the type. * - * @return NULL in case the memory could not be allocated. - * Else a vtref structure which needs to be + * @return A vtref structure which needs to be * released using @ref vtref_free . */ vtseverity_t * @@ -458,8 +456,7 @@ typedef struct nvtpref * * @param dflt The default to be set. A copy will created of this. * - * @return NULL in case the memory could not be allocated. - * Else a nvtpref structure which needs to be + * @return An nvtpref structure which needs to be * released using @ref nvtpref_free . */ nvtpref_t * @@ -554,8 +551,7 @@ nvtpref_default (const nvtpref_t *np) /** * @brief Create a new (empty) nvti structure. * - * @return NULL in case the memory could not be allocated. - * Else an empty nvti structure which needs to be + * @return An empty nvti structure which needs to be * released using @ref nvti_free . * The whole struct is initialized with 0's. */ diff --git a/doc/Doxyfile.in b/doc/Doxyfile.in index d58e5aca..624b6f31 100644 --- a/doc/Doxyfile.in +++ b/doc/Doxyfile.in @@ -755,6 +755,7 @@ WARN_LOGFILE = INPUT = @CMAKE_SOURCE_DIR@/base \ @CMAKE_SOURCE_DIR@/gmp \ + @CMAKE_SOURCE_DIR@/openvasd \ @CMAKE_SOURCE_DIR@/osp \ @CMAKE_SOURCE_DIR@/util diff --git a/openvasd/CMakeLists.txt b/openvasd/CMakeLists.txt new file mode 100644 index 00000000..88e9bd27 --- /dev/null +++ b/openvasd/CMakeLists.txt @@ -0,0 +1,67 @@ +# SPDX-FileCopyrightText: 2015-2023 Greenbone AG +# +# SPDX-License-Identifier: GPL-2.0-or-later + +## Library + +include (FindPkgConfig) + +if (NOT PKG_CONFIG_FOUND) + message(FATAL_ERROR "pkg-config executable not found. Aborting.") +endif (NOT PKG_CONFIG_FOUND) + +## Dependency checks + +pkg_check_modules (GLIB REQUIRED glib-2.0>=2.42) +pkg_check_modules (CURL REQUIRED libcurl>=7.83.0) + +# for json parsing we need cJSON +pkg_check_modules (CJSON REQUIRED libcjson>=1.7.14) + + +include_directories (${GLIB_INCLUDE_DIRS} ${GLIB_JSON_INCLUDE_DIRS} + ${CURL_INCLUDE_DIRS}) + +set (FILES openvasd.c vtparser.c) +set (HEADERS openvasd.h) + +if (BUILD_STATIC) + add_library (gvm_openvasd_static STATIC ${FILES}) + set_target_properties (gvm_openvasd_static PROPERTIES OUTPUT_NAME "gvm_openvasd") + set_target_properties (gvm_openvasd_static PROPERTIES CLEAN_DIRECT_OUTPUT 1) + set_target_properties (gvm_openvasd_static PROPERTIES PUBLIC_HEADER "${HEADERS}") +endif (BUILD_STATIC) + +if (BUILD_SHARED) + add_library (gvm_openvasd_shared SHARED ${FILES}) + set_target_properties (gvm_openvasd_shared PROPERTIES OUTPUT_NAME "gvm_openvasd") + set_target_properties (gvm_openvasd_shared PROPERTIES CLEAN_DIRECT_OUTPUT 1) + set_target_properties (gvm_openvasd_shared PROPERTIES SOVERSION "${PROJECT_VERSION_MAJOR}") + set_target_properties (gvm_openvasd_shared PROPERTIES VERSION "${CPACK_PACKAGE_VERSION}") + set_target_properties (gvm_openvasd_shared PROPERTIES PUBLIC_HEADER "${HEADERS}") + + target_link_libraries (gvm_openvasd_shared LINK_PRIVATE ${GLIB_LDFLAGS} ${GLIB_JSON_LDFLAGS} + ${CURL_LDFLAGS} ${LINKER_HARDENING_FLAGS}) +endif (BUILD_SHARED) + +## Install +configure_file (libgvm_openvasd.pc.in ${CMAKE_BINARY_DIR}/libgvm_openvasd.pc @ONLY) + +install (FILES ${CMAKE_BINARY_DIR}/libgvm_openvasd.pc + DESTINATION ${LIBDIR}/pkgconfig) + +if (BUILD_STATIC) + install (TARGETS gvm_openvasd_static + RUNTIME DESTINATION ${BINDIR} + ARCHIVE DESTINATION ${LIBDIR} + PUBLIC_HEADER DESTINATION "${INCLUDEDIR}/gvm/openvasd") +endif (BUILD_STATIC) +if (BUILD_SHARED) + install (TARGETS gvm_openvasd_shared + RUNTIME DESTINATION ${BINDIR} + LIBRARY DESTINATION ${LIBDIR} + ARCHIVE DESTINATION ${LIBDIR} + PUBLIC_HEADER DESTINATION "${INCLUDEDIR}/gvm/openvasd") +endif (BUILD_SHARED) + +## End diff --git a/openvasd/libgvm_openvasd.pc.in b/openvasd/libgvm_openvasd.pc.in new file mode 100644 index 00000000..8c09403a --- /dev/null +++ b/openvasd/libgvm_openvasd.pc.in @@ -0,0 +1,11 @@ +prefix=@CMAKE_INSTALL_PREFIX@ +exec_prefix=@EXEC_PREFIX@ +libdir=@LIBDIR@ +includedir=@INCLUDEDIR@ + +Name: gvmlibs-openvasd +Description: Greenbone Vulnerability Management Library openvasd +Version: @LIBGVMCONFIG_VERSION@ +Requires.private: glib-2.0 >= 2.42.0 +Cflags: -I${includedir} -I${includedir}/gvm +Libs: -L${libdir} -lgvm_openvasd diff --git a/openvasd/openvasd.c b/openvasd/openvasd.c new file mode 100644 index 00000000..a8ed0e48 --- /dev/null +++ b/openvasd/openvasd.c @@ -0,0 +1,2334 @@ +/* SPDX-FileCopyrightText: 2024 Greenbone AG + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +/** + * @file + * @brief API for Openvas Daemon communication. + */ + +#include "openvasd.h" + +#include "../base/array.h" +#include "../base/networking.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#undef G_LOG_DOMAIN +/** + * @brief GLib log domain. + */ +#define G_LOG_DOMAIN "libgvm ovd" + +#define RESP_CODE_ERR -1 +#define RESP_CODE_OK 0 + +/** + * @brief Wrapps a CURLM * handler and the custom header. + */ +typedef struct openvasd_curlm +{ + CURLM *h; + struct curl_slist *customheader; +} openvasd_curlm_t; + +/** @brief Define a string struct for storing the response + * and the curl handler + */ +struct openvasd_string +{ + gchar *ptr; + size_t len; + openvasd_curlm_t *curl_hnd; +}; + +typedef struct openvasd_string *openvasd_vt_stream_t; + +/** + * @brief Struct holding the data for connecting with Openvasd. + */ +struct openvasd_connector +{ + gchar *ca_cert; /**< Path to the directory holding the CA certificate. */ + gchar *cert; /**< Client certificate. */ + gchar *key; /**< Client key. */ + gchar *apikey; /**< API key for authentication. */ + gchar *server; /**< original openvasd server URL. */ + gchar *host; /**< server hostname. */ + gchar *scan_id; /**< Scan ID. */ + int port; /**< server port. */ + openvasd_vt_stream_t stream_resp; /** For response */ +}; + +/** + * @brief Struct holding options for Openvasd parameters. + */ +struct openvasd_param +{ + gchar *id; /**< Parameter id. */ + gchar *name; /**< Parameter name. */ + gchar *defval; /**< Default value. */ + gchar *description; /**< Description. */ + gchar *type; /**< Parameter type. */ + int mandatory; /**< If mandatory. */ +}; + +/** + * @brief Struct credential information for Openvasd. + */ +struct openvasd_credential +{ + gchar *type; /**< Credential type */ + gchar *service; /**< Service the credential is for */ + gchar *port; /**< Port the credential is for */ + GHashTable *auth_data; /**< Authentication data (username, password, etc.)*/ +}; + +/** + * @brief Struct holding target information. + */ +struct openvasd_target +{ + gchar *scan_id; /** Scan ID */ + GSList *credentials; /** Credentials to use in the scan */ + gchar *exclude_hosts; /** String defining one or many hosts to exclude */ + gchar *hosts; /** String defining one or many hosts to scan */ + gchar *ports; /** String defining the ports to scan */ + gchar *finished_hosts; /** String defining hosts to exclude as finished */ + gboolean icmp; /** Alive test method icmp */ + gboolean tcp_syn; /** Alive test method tcp_syn */ + gboolean tcp_ack; /** Alive test method tcp_ack */ + gboolean arp; /** Alive test method arp */ + gboolean consider_alive; /** Alive test method consider alive */ + int reverse_lookup_unify; /** Value defining reverse_lookup_unify opt */ + int reverse_lookup_only; /** Value defining reverse_lookup_only opt */ +}; + +/** + * @brief Struct holding vt information + */ +struct openvasd_vt_single +{ + gchar *vt_id; + GHashTable *vt_values; +}; + +/** + * @brief Request methods + */ +enum openvas_request_method +{ + POST, + GET, + HEAD, + DELETE, +}; + +typedef enum openvas_request_method openvasd_req_method_t; + +/** + * @brief Allocate openvasd curl handler + * + * @return Openvasd curl handler. + */ +static openvasd_curlm_t * +openvasd_curlm_handler_new (void) +{ + return (openvasd_curlm_t *) g_malloc0 (sizeof (struct openvasd_curlm)); +} + +/** + * @brief Cleanup an openvasd curl handler + * + * @param h Openvasd curl handler to clean + */ +static void +openvasd_curlm_handler_close (openvasd_curlm_t *h) +{ + int queued = 0; + + /* when an easy handle has completed, remove it */ + CURLMsg *msg = curl_multi_info_read (h->h, &queued); + if (msg) + { + if (msg->msg == CURLMSG_DONE) + { + curl_multi_remove_handle (h->h, msg->easy_handle); + curl_easy_cleanup (msg->easy_handle); + curl_slist_free_all (h->customheader); + curl_multi_cleanup (h->h); + return; + } + g_warning ("%s: Not possible to clean up the curl handler", __func__); + } +} + +/** @brief Allocate the vt stream struct to hold the response + * and the curlm handler + * + * @return The vt stream struct. Must be free with openvasd_vt_stream_new(). + */ +static openvasd_vt_stream_t +openvasd_vt_stream_new (void) +{ + openvasd_vt_stream_t s; + s = g_malloc0 (sizeof (struct openvasd_string)); + s->len = 0; + s->ptr = g_malloc0 (s->len + 1); + s->curl_hnd = openvasd_curlm_handler_new (); + return s; +} + +/** @brief Cleanup the string struct to hold the response and the + * curl multiperform handler + * + * @param s The string struct to be freed + */ +static void +openvasd_vt_stream_free (openvasd_vt_stream_t s) +{ + if (s == NULL) + return; + + g_free (s->ptr); + if (s->curl_hnd) + openvasd_curlm_handler_close (s->curl_hnd); + + g_free (s); +} + +/** @brief Reinitialize the string struct to hold the response + * + * @param s The string struct to be reset + */ +static void +openvasd_vt_stream_reset (openvasd_vt_stream_t s) +{ + if (s) + { + g_free (s->ptr); + s->len = 0; + s->ptr = g_malloc0 (s->len + 1); + } +} + +/** @brief Initialize an openvasd connector. + * + * @return An an openvasd connector struct. It must be freed + * with openvasd_connector_free() + */ +openvasd_connector_t +openvasd_connector_new (void) +{ + openvasd_connector_t connector; + openvasd_vt_stream_t stream; + + connector = g_malloc0 (sizeof (struct openvasd_connector)); + stream = openvasd_vt_stream_new (); + connector->stream_resp = stream; + + return connector; +} + +/** @brief Build a openvasd connector + * + * Receive option name and value to build the openvasd connector + * + * @param conn struct holding the openvasd connector information + * @param opt option to set + * @param val value to set + * + * @return Return OK on success, otherwise error; + */ +openvasd_error_t +openvasd_connector_builder (openvasd_connector_t conn, openvasd_conn_opt_t opt, + const void *val) +{ + if (conn == NULL) + conn = openvasd_connector_new (); + + if (opt < OPENVASD_CA_CERT || opt > OPENVASD_PORT) + return OPENVASD_INVALID_OPT; + + if (val == NULL) + return OPENVASD_INVALID_VALUE; + + switch (opt) + { + case OPENVASD_CA_CERT: + conn->ca_cert = g_strdup ((char *) val); + break; + case OPENVASD_CERT: + conn->cert = g_strdup ((char *) val); + break; + case OPENVASD_KEY: + conn->key = g_strdup ((char *) val); + break; + case OPENVASD_API_KEY: + conn->apikey = g_strdup ((char *) val); + break; + case OPENVASD_SERVER: + conn->server = g_strdup ((char *) val); + break; + case OPENVASD_HOST: + conn->host = g_strdup ((char *) val); + break; + case OPENVASD_SCAN_ID: + conn->scan_id = g_strdup ((const gchar *) val); + break; + case OPENVASD_PORT: + default: + conn->port = *((int *) val); + break; + }; + + return OPENVASD_OK; +} + +/** @brief Build a openvasd connector + * + * Receive option name and value to build the openvasd connector + * + * @param conn struct holding the openvasd connector information + * + * @return Return OPENVASD_OK + */ +openvasd_error_t +openvasd_connector_free (openvasd_connector_t conn) +{ + if (conn == NULL) + return OPENVASD_OK; + + g_free (conn->ca_cert); + g_free (conn->cert); + g_free (conn->key); + g_free (conn->apikey); + g_free (conn->server); + g_free (conn->host); + g_free (conn->scan_id); + openvasd_vt_stream_free (conn->stream_resp); + g_free (conn); + conn = NULL; + + return OPENVASD_OK; +} + +/** + * @brief Free an openvasd response struct + * + * @param resp Response to be freed + */ +void +openvasd_response_cleanup (openvasd_resp_t resp) +{ + if (resp == NULL) + return; + + g_free (resp->body); + g_free (resp->header); + g_free (resp); + resp = NULL; +} + +/** @brief Call back function to stored the response. + * + * The function signature is the necessary to work with + * libcurl. It stores the response in s. It reallocate memory if necessary. + */ +static size_t +response_callback_fn (void *ptr, size_t size, size_t nmemb, void *struct_string) +{ + openvasd_vt_stream_t s = struct_string; + size_t new_len = s->len + size * nmemb; + gchar *ptr_aux = g_realloc (s->ptr, new_len + 1); + s->ptr = ptr_aux; + memcpy (s->ptr + s->len, ptr, size * nmemb); + s->ptr[new_len] = '\0'; + s->len = new_len; + + return size * nmemb; +} + +static struct curl_slist * +init_customheader (const gchar *apikey, gboolean contenttype) +{ + struct curl_slist *customheader = NULL; + struct curl_slist *temp = NULL; + + // Set API KEY + if (apikey) + { + GString *xapikey; + xapikey = g_string_new ("X-API-KEY: "); + g_string_append (xapikey, apikey); + temp = curl_slist_append (customheader, xapikey->str); + if (!temp) + g_warning ("%s: Not possible to set API-KEY", __func__); + else + customheader = temp; + g_string_free (xapikey, TRUE); + } + // SET Content type + if (contenttype) + { + temp = curl_slist_append (customheader, "Content-Type: application/json"); + if (!temp) + g_warning ("%s: Not possible to set Content-Type", __func__); + else + customheader = temp; + } + + return customheader; +} + +/** @brief Create a CURL handler + * + * @param conn struct holding the openvasd connector information + * @param method request method (e.g. GET) + * @param path Path to the resource (e.g. /vts) + * @param data String containing the request body in json format (scan + * action, scan config) + * @param customheader A CURL slist with custom headers. It is set in the + * handler and must be free after use with curl_slist_free_all(). + * @param resp Structure holding the body response, filled by the + * callback function + * @param err On error, this variable is filled with an error message + * in json format. + * + * @return a CURL handler, or NULL on error. + */ +static CURL * +handler (openvasd_connector_t conn, openvasd_req_method_t method, gchar *path, + gchar *data, struct curl_slist *customheader, gchar **err) +{ + CURL *curl; + GString *url = NULL; + + if (!conn) + { + *err = g_strdup ("{\"error\": \"Missing openvasd connector\"}"); + g_warning ("%s: Missing openvasd connector", __func__); + return NULL; + } + + if ((curl = curl_easy_init ()) == NULL) + { + *err = + g_strdup ("{\"error\": \"Not possible to initialize curl library\"}"); + g_warning ("%s: Not possible to initialize curl library", __func__); + return NULL; + } + + url = g_string_new (g_strdup (conn->server)); + + if (conn->port > 0 && conn->port < 65535) + { + char buf[6]; + g_snprintf (buf, sizeof (buf), ":%d", conn->port); + g_string_append (url, buf); + } + + if (path != NULL && path[0] != '\0') + g_string_append (url, path); + + // Set URL + g_debug ("%s: URL: %s", __func__, url->str); + if (curl_easy_setopt (curl, CURLOPT_URL, url->str) != CURLE_OK) + { + g_string_free (url, TRUE); + g_warning ("%s: Not possible to set the URL", __func__); + curl_easy_cleanup (curl); + *err = g_strdup ("{\"error\": \"Not possible to set URL\"}"); + return NULL; + } + g_string_free (url, TRUE); + + // Server verification + if (conn->ca_cert != NULL) + { + struct curl_blob blob; + blob.data = conn->ca_cert; + blob.len = strlen (conn->ca_cert); + blob.flags = CURL_BLOB_COPY; + + curl_easy_setopt (curl, CURLOPT_SSL_VERIFYPEER, 1L); + curl_easy_setopt (curl, CURLOPT_SSL_VERIFYHOST, 1L); + if (curl_easy_setopt (curl, CURLOPT_CAINFO_BLOB, &blob) != CURLE_OK) + { + g_warning ("%s: Not possible to set the CA certificate", __func__); + curl_easy_cleanup (curl); + *err = + g_strdup ("{\"error\": \"Not possible to set CA certificate\"}"); + return NULL; + } + } + else + { + // Accept an insecure connection. Don't verify the server certificate + curl_easy_setopt (curl, CURLOPT_SSL_VERIFYPEER, 0L); + curl_easy_setopt (curl, CURLOPT_SSL_VERIFYHOST, 0L); + curl_easy_setopt (curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2); + g_debug ("%s: Server certificate verification disabled.", __func__); + } + + // Client certificate + if (conn->cert != NULL && conn->key != NULL) + { + struct curl_blob blob; + blob.data = conn->cert; + blob.len = strlen (conn->cert); + blob.flags = CURL_BLOB_COPY; + + if (curl_easy_setopt (curl, CURLOPT_SSLCERT_BLOB, &blob) != CURLE_OK) + { + g_warning ("%s: Not possible to set the Client certificate", + __func__); + curl_easy_cleanup (curl); + *err = g_strdup ( + "{\"error\": \"Not possible to set Client certificate\"}"); + return NULL; + } + blob.data = conn->key; + blob.len = strlen (conn->key); + blob.flags = CURL_BLOB_COPY; + + if (curl_easy_setopt (curl, CURLOPT_SSLKEY_BLOB, &blob) != CURLE_OK) + { + g_warning ("%s: Not possible to set the Client private key", + __func__); + curl_easy_cleanup (curl); + *err = g_strdup ( + "{\"error\": \"Not possible to set Client private key\"}"); + return NULL; + } + } + + switch (method) + { + case POST: + if (data != NULL && data[0] != '\0') + { + // Set body + curl_easy_setopt (curl, CURLOPT_POSTFIELDS, data); + curl_easy_setopt (curl, CURLOPT_POSTFIELDSIZE, strlen (data)); + } + break; + case GET: + curl_easy_setopt (curl, CURLOPT_HTTPGET, 1L); + break; + case DELETE: + curl_easy_setopt (curl, CURLOPT_CUSTOMREQUEST, "DELETE"); + break; + default: + curl_easy_setopt (curl, CURLOPT_CUSTOMREQUEST, "HEAD"); + break; + }; + + if (customheader != NULL) + curl_easy_setopt (curl, CURLOPT_HTTPHEADER, customheader); + + // Init the struct where the response is stored and set the callback function + curl_easy_setopt (curl, CURLOPT_WRITEFUNCTION, response_callback_fn); + curl_easy_setopt (curl, CURLOPT_WRITEDATA, conn->stream_resp); + + return curl; +} + +/** @brief Send request + * + * @param curl The CURL handler to perform an request. + * @param header_name If this field is set, is looked in the header and + * its value is returned inside the response. + * @param response The response struct to be filled with the response + * code and the header value. + * + * @return Return struct containing the http response code and the response + * body. In case of error the struct is filled with code RESP_CODE_ERR (-1) and + * a message. NULL on memory related error. Response must be free()'ed by the + * caller with openvasd_response_free() + */ +static openvasd_resp_t +openvasd_send_request (CURL *curl, const gchar *header_name, + openvasd_resp_t response) +{ + long http_code = RESP_CODE_ERR; + + int ret = CURLE_OK; + if ((ret = curl_easy_perform (curl)) != CURLE_OK) + { + g_warning ("%s: Error sending request: %d", __func__, ret); + curl_easy_cleanup (curl); + response->code = http_code; + response->body = g_strdup ("{\"error\": \"Error sending request\"}"); + return response; + } + + curl_easy_getinfo (curl, CURLINFO_RESPONSE_CODE, &http_code); + if (header_name) + { + struct curl_header *hname; + curl_easy_header (curl, header_name, 0, CURLH_HEADER, -1, &hname); + response->header = g_strdup (hname->value); + } + curl_easy_cleanup (curl); + response->code = http_code; + + return response; +} + +/** + * @brief Request HEAD + * + * @param conn Connector struct with the data necessary for the connection + * + * @return Response containing the header information + */ +openvasd_resp_t +openvasd_get_version (openvasd_connector_t conn) +{ + gchar *err = NULL; + CURL *hnd = NULL; + openvasd_resp_t response = NULL; + struct curl_slist *customheader = NULL; + + response = g_malloc0 (sizeof (struct openvasd_response)); + + customheader = init_customheader (conn->apikey, FALSE); + hnd = handler (conn, HEAD, "/", NULL, customheader, &err); + if (hnd == NULL) + { + curl_slist_free_all (customheader); + response->code = RESP_CODE_ERR; + response->body = err; + openvasd_reset_vt_stream (conn); + return response; + } + + openvasd_send_request (hnd, NULL, response); + curl_slist_free_all (customheader); + if (response->code != RESP_CODE_ERR) + response->body = g_strdup (openvasd_vt_stream_str (conn)); + + openvasd_reset_vt_stream (conn); + return response; +} + +/** + * @brief Initialized an curl multiperform handler which allows fetch feed + * metadata chunk by chunk. + * + * @param conn Connector struct with the data necessary for the connection + * @param mhnd The curl multiperform handler. It the caller doesn't provide + * it initialized, it will be initialized. The caller has to free it with + * openvasd_curlm_handler_close(). + * @param resp The stringstream struct for the write callback function. + * + * @return The response. + */ +openvasd_resp_t +openvasd_get_vt_stream_init (openvasd_connector_t conn) +{ + GString *path; + openvasd_resp_t response = NULL; + gchar *err = NULL; + CURL *hnd = NULL; + CURLM *h = NULL; + struct curl_slist *customheader = NULL; + + response = g_malloc0 (sizeof (struct openvasd_response)); + + path = g_string_new ("/vts?information=1"); + customheader = init_customheader (conn->apikey, FALSE); + hnd = handler (conn, GET, path->str, NULL, customheader, &err); + if (hnd == NULL) + { + curl_slist_free_all (customheader); + g_string_free (path, TRUE); + response->code = RESP_CODE_ERR; + response->body = err; + return response; + } + g_string_free (path, TRUE); + + h = curl_multi_init (); + curl_multi_add_handle (h, hnd); + + conn->stream_resp->curl_hnd->h = h; + conn->stream_resp->curl_hnd->customheader = customheader; + + response->code = RESP_CODE_OK; + return response; +} + +void +openvasd_reset_vt_stream (openvasd_connector_t conn) +{ + openvasd_vt_stream_reset (conn->stream_resp); +} + +gchar * +openvasd_vt_stream_str (openvasd_connector_t conn) +{ + return conn->stream_resp->ptr; +} + +size_t +openvasd_vt_stream_len (openvasd_connector_t conn) +{ + return conn->stream_resp->len; +} + +/** + * @brief Get a new feed metadata chunk. + * + * This function must be call until the + * return value is 0, meaning there is no more data to fetch. + * + * @param mhnd Curl multiperfom for requesting the feed metadata + * + * @return greather than 0 if the handler is still getting data. 0 if the + * transmision finished. -1 on error + */ +int +openvasd_get_vt_stream (openvasd_connector_t conn) +{ + static int running = 0; + CURLM *h = conn->stream_resp->curl_hnd->h; + if (!(h)) + { + return -1; + } + + CURLMcode mc = curl_multi_perform (h, &running); + if (!mc && running) + /* wait for activity, timeout or "nothing" */ + mc = curl_multi_poll (h, NULL, 0, 5000, NULL); + if (mc != CURLM_OK) + { + g_warning ("%s: error on curl_multi_poll(): %d\n", __func__, mc); + return -1; + } + + return running; +} + +/** + * @brief Get VT's metadata + * + * @param conn Connector struct with the data necessary for the connection + * + * @return Response Struct containing the feed metadata in json format in the + * body. + */ +openvasd_resp_t +openvasd_get_vts (openvasd_connector_t conn) +{ + GString *path; + openvasd_resp_t response = NULL; + gchar *err = NULL; + CURL *hnd = NULL; + struct curl_slist *customheader = NULL; + + response = g_malloc0 (sizeof (struct openvasd_response)); + + path = g_string_new ("/vts?information=1"); + customheader = init_customheader (conn->apikey, FALSE); + hnd = handler (conn, GET, path->str, NULL, customheader, &err); + if (hnd == NULL) + { + curl_slist_free_all (customheader); + g_string_free (path, TRUE); + response->code = RESP_CODE_ERR; + response->body = err; + return response; + } + g_string_free (path, TRUE); + + openvasd_send_request (hnd, NULL, response); + curl_slist_free_all (customheader); + if (response->code != RESP_CODE_ERR) + response->body = g_strdup (openvasd_vt_stream_str (conn)); + + openvasd_reset_vt_stream (conn); + return response; +} + +/** + * @Brief Get VT's metadata + * + * @param conn Connector struct with the data necessary for the connection + * @param data String containing the scan config in JSON format. + * + * @return Response Struct containing the resonse. + */ +openvasd_resp_t +openvasd_start_scan (openvasd_connector_t conn, gchar *data) +{ + openvasd_resp_t response = NULL; + cJSON *parser = NULL; + GString *path; + gchar *err = NULL; + CURL *hnd = NULL; + struct curl_slist *customheader = NULL; + + response = g_malloc0 (sizeof (struct openvasd_response)); + + customheader = init_customheader (conn->apikey, TRUE); + hnd = handler (conn, POST, "/scans", data, customheader, &err); + if (hnd == NULL) + { + curl_slist_free_all (customheader); + response->code = RESP_CODE_ERR; + response->body = err; + return response; + } + + openvasd_send_request (hnd, NULL, response); + curl_slist_free_all (customheader); + if (response->code == RESP_CODE_ERR) + { + response->code = RESP_CODE_ERR; + if (response->body == NULL) + response->body = + g_strdup ("{\"error\": \"Storing scan configuration\"}"); + g_warning ("%s: Error storing scan configuration ", __func__); + openvasd_reset_vt_stream (conn); + return response; + } + + // Get the Scan ID + parser = cJSON_Parse (openvasd_vt_stream_str (conn)); + if (!parser) + { + const gchar *error_ptr = cJSON_GetErrorPtr (); + if (error_ptr != NULL) + { + response->body = g_strdup_printf ("{\"error\": \"%s\"}", error_ptr); + g_warning ("%s: %s", __func__, error_ptr); + } + else + { + response->body = g_strdup ( + "{\"error\": \"Parsing json string to get the scan ID\"}"); + g_warning ("%s: Parsing json string to get the scan ID", __func__); + } + response->code = RESP_CODE_ERR; + cJSON_Delete (parser); + openvasd_reset_vt_stream (conn); + return response; + } + + conn->scan_id = g_strdup (cJSON_GetStringValue (parser)); + + // Start the scan + path = g_string_new ("/scans"); + if (conn->scan_id != NULL && conn->scan_id[0] != '\0') + { + g_string_append (path, "/"); + g_string_append (path, conn->scan_id); + } + else + { + response->code = RESP_CODE_ERR; + response->body = g_strdup ("{\"error\": \"Missing scan ID\"}"); + g_string_free (path, TRUE); + g_warning ("%s: Missing scan ID", __func__); + cJSON_Delete (parser); + return response; + } + + openvasd_reset_vt_stream (conn); + customheader = init_customheader (conn->apikey, TRUE); + hnd = handler (conn, POST, path->str, "{\"action\": \"start\"}", customheader, + &err); + if (hnd == NULL) + { + curl_slist_free_all (customheader); + g_string_free (path, TRUE); + response->code = RESP_CODE_ERR; + response->body = err; + return response; + } + g_string_free (path, TRUE); + + openvasd_send_request (hnd, NULL, response); + curl_slist_free_all (customheader); + if (response->code == RESP_CODE_ERR) + { + response->code = RESP_CODE_ERR; + if (response->body == NULL) + response->body = g_strdup ("{\"error\": \"Starting the scan.\"}"); + g_warning ("%s: Error starting the scan.", __func__); + return response; + } + + cJSON_Delete (parser); + response->body = g_strdup (openvasd_vt_stream_str (conn)); + openvasd_reset_vt_stream (conn); + return response; +} + +openvasd_resp_t +openvasd_stop_scan (openvasd_connector_t conn) +{ + openvasd_resp_t response = NULL; + GString *path; + gchar *err = NULL; + CURL *hnd = NULL; + struct curl_slist *customheader = NULL; + + response = g_malloc0 (sizeof (struct openvasd_response)); + + // Stop the scan + path = g_string_new ("/scans"); + if (conn->scan_id != NULL && conn->scan_id[0] != '\0') + { + g_string_append (path, "/"); + g_string_append (path, conn->scan_id); + } + else + { + response->code = RESP_CODE_ERR; + response->body = g_strdup ("{\"error\": \"Missing scan ID\"}"); + g_string_free (path, TRUE); + g_warning ("%s: Missing scan ID", __func__); + return response; + } + + customheader = init_customheader (conn->apikey, TRUE); + hnd = handler (conn, POST, path->str, "{\"action\": \"stop\"}", customheader, + &err); + if (hnd == NULL) + { + curl_slist_free_all (customheader); + g_string_free (path, TRUE); + response->code = RESP_CODE_ERR; + response->body = err; + return response; + } + g_string_free (path, TRUE); + + openvasd_send_request (hnd, NULL, response); + curl_slist_free_all (customheader); + if (response->code != RESP_CODE_ERR) + response->body = g_strdup (openvasd_vt_stream_str (conn)); + + openvasd_reset_vt_stream (conn); + return response; +} + +openvasd_resp_t +openvasd_get_scan_results (openvasd_connector_t conn, long first, long last) +{ + openvasd_resp_t response = NULL; + GString *path = NULL; + gchar *err = NULL; + CURL *hnd = NULL; + struct curl_slist *customheader = NULL; + + response = g_malloc0 (sizeof (struct openvasd_response)); + + path = g_string_new ("/scans"); + if (conn->scan_id != NULL && conn->scan_id[0] != '\0') + { + g_string_append (path, "/"); + g_string_append (path, conn->scan_id); + if (last > first) + g_string_append_printf (path, "/results?range%ld-%ld", first, last); + else if (last < first) + g_string_append_printf (path, "/results?range=%ld", first); + else + g_string_append (path, "/results"); + } + else + { + response->code = RESP_CODE_ERR; + response->body = g_strdup ("{\"error\": \"Missing scan ID\"}"); + g_string_free (path, TRUE); + g_warning ("%s: Missing scan ID", __func__); + return response; + } + + customheader = init_customheader (conn->apikey, FALSE); + hnd = handler (conn, GET, path->str, NULL, customheader, &err); + if (hnd == NULL) + { + curl_slist_free_all (customheader); + g_string_free (path, TRUE); + response->code = RESP_CODE_ERR; + response->body = err; + return response; + } + g_string_free (path, TRUE); + + openvasd_send_request (hnd, NULL, response); + curl_slist_free_all (customheader); + if (response->code != RESP_CODE_ERR) + response->body = g_strdup (openvasd_vt_stream_str (conn)); + else if (response->code == RESP_CODE_ERR) + { + g_warning ("%s: Not possible to get scan results", __func__); + response->body = + g_strdup ("{\"error\": \"Not possible to get scan results\"}"); + } + + openvasd_reset_vt_stream (conn); + return response; +} + +openvasd_result_t +openvasd_result_new (unsigned long id, gchar *type, gchar *ip_address, + gchar *hostname, gchar *oid, int port, gchar *protocol, + gchar *message, gchar *detail_name, gchar *detail_value, + gchar *detail_source_type, gchar *detail_source_name, + gchar *detail_source_description) +{ + openvasd_result_t result = g_malloc0 (sizeof (struct openvasd_result)); + + result->id = id; + result->type = g_strdup (type); + result->ip_address = g_strdup (ip_address); + result->hostname = g_strdup (hostname); + result->oid = g_strdup (oid); + result->port = port; + result->protocol = g_strdup (protocol); + result->message = g_strdup (message); + result->detail_name = g_strdup (detail_name); + result->detail_value = g_strdup (detail_value); + result->detail_source_name = g_strdup (detail_source_name); + result->detail_source_type = g_strdup (detail_source_type); + result->detail_source_description = g_strdup (detail_source_description); + + return result; +} + +char * +openvasd_get_result_member_str (openvasd_result_t result, + openvasd_result_member_string_t member) +{ + if (!result) + return NULL; + switch (member) + { + case TYPE: + return result->type; + + case IP_ADDRESS: + return result->ip_address; + case HOSTNAME: + return result->hostname; + case OID: + return result->oid; + case PROTOCOL: + return result->protocol; + case MESSAGE: + return result->message; + case DETAIL_NAME: + return result->detail_name; + case DETAIL_VALUE: + return result->detail_value; + case DETAIL_SOURCE_NAME: + return result->detail_source_name; + case DETAIL_SOURCE_TYPE: + return result->detail_source_type; + case DETAIL_SOURCE_DESCRIPTION: + return result->detail_source_description; + default: + return NULL; + } +} + +int +openvasd_get_result_member_int (openvasd_result_t result, + openvasd_result_member_int_t member) +{ + if (!result) + return -1; + + switch (member) + { + case ID: + return result->id; + case PORT: + return result->port; + default: + return -1; + } +} + +void +openvasd_result_free (openvasd_result_t result) +{ + if (result == NULL) + return; + + g_free (result->type); + g_free (result->ip_address); + g_free (result->hostname); + g_free (result->oid); + g_free (result->protocol); + g_free (result->message); + g_free (result->detail_name); + g_free (result->detail_value); + g_free (result->detail_source_name); + g_free (result->detail_source_type); + g_free (result->detail_source_description); + g_free (result); + result = NULL; +} + +int +openvasd_parsed_results (openvasd_connector_t conn, unsigned long first, + unsigned long last, GSList **results) +{ + cJSON *parser = NULL; + cJSON *result_obj = NULL; + const gchar *err = NULL; + openvasd_resp_t resp = NULL; + openvasd_result_t result = NULL; + unsigned long id = 0; + gchar *type = NULL; + gchar *ip_address = NULL; + gchar *hostname = NULL; + gchar *oid = NULL; + int port = 0; + gchar *protocol = NULL; + gchar *message = NULL; + gchar *detail_name = NULL; + gchar *detail_value = NULL; + gchar *detail_source_type = NULL; + gchar *detail_source_name = NULL; + gchar *detail_source_description = NULL; + int ret = -1; + + resp = openvasd_get_scan_results (conn, first, last); + + if (resp->code != 200) + return resp->code; + + if ((parser = cJSON_Parse (resp->body)) == NULL) + { + err = cJSON_GetErrorPtr (); + goto res_cleanup; + } + if (!cJSON_IsArray (parser)) + { + // No results. No information. + goto res_cleanup; + } + + cJSON_ArrayForEach (result_obj, parser) + { + cJSON *item = NULL; + if (!cJSON_IsObject (result_obj)) + // error + goto res_cleanup; + + if ((item = cJSON_GetObjectItem (result_obj, "id")) != NULL + && cJSON_IsNumber (item)) + id = item->valuedouble; + + if ((item = cJSON_GetObjectItem (result_obj, "type")) != NULL + && cJSON_IsString (item)) + type = g_strdup (item->valuestring); + + if ((item = cJSON_GetObjectItem (result_obj, "ip_address")) != NULL + && cJSON_IsString (item)) + ip_address = g_strdup (item->valuestring); + + if ((item = cJSON_GetObjectItem (result_obj, "hostname")) != NULL + && cJSON_IsString (item)) + hostname = g_strdup (item->valuestring); + + if ((item = cJSON_GetObjectItem (result_obj, "oid")) != NULL + && cJSON_IsString (item)) + oid = g_strdup (item->valuestring); + + if ((item = cJSON_GetObjectItem (result_obj, "port")) != NULL + && cJSON_IsNumber (item)) + port = item->valueint; + + if ((item = cJSON_GetObjectItem (result_obj, "protocol")) != NULL + && cJSON_IsString (item)) + protocol = g_strdup (item->valuestring); + + if ((item = cJSON_GetObjectItem (result_obj, "message")) != NULL + && cJSON_IsString (item)) + message = g_strdup (item->valuestring); + + if ((item = cJSON_GetObjectItem (result_obj, "detail")) != NULL + && cJSON_IsObject (item)) + { + cJSON *detail_obj = NULL; + + if ((detail_obj = cJSON_GetObjectItem (item, "name")) != NULL + && cJSON_IsString (detail_obj)) + detail_name = g_strdup (detail_obj->valuestring); + + if ((detail_obj = cJSON_GetObjectItem (item, "value")) != NULL + && cJSON_IsString (detail_obj)) + detail_value = g_strdup (detail_obj->valuestring); + + cJSON *source_obj = NULL; + if ((source_obj = cJSON_GetObjectItem (detail_obj, "type")) != NULL + && cJSON_IsObject (source_obj)) + detail_source_type = g_strdup (detail_obj->valuestring); + + if ((source_obj = cJSON_GetObjectItem (detail_obj, "name")) != NULL + && cJSON_IsString (source_obj)) + detail_source_name = g_strdup (detail_obj->valuestring); + + if ((source_obj = cJSON_GetObjectItem (detail_obj, "description")) + != NULL + && cJSON_IsString (source_obj)) + detail_source_description = g_strdup (detail_obj->valuestring); + } + + result = openvasd_result_new (id, type, ip_address, hostname, oid, port, + protocol, message, detail_name, detail_value, + detail_source_type, detail_source_name, + detail_source_description); + + *results = g_slist_append (*results, result); + ret = resp->code; + } + +res_cleanup: + openvasd_response_cleanup (resp); + if (err != NULL) + { + g_warning ("%s: Unable to parse scan results. Reason: %s", __func__, err); + } + cJSON_Delete (parser); + + return ret; +} + +openvasd_resp_t +openvasd_get_scan_status (openvasd_connector_t conn) +{ + openvasd_resp_t response = NULL; + GString *path = NULL; + gchar *err = NULL; + CURL *hnd = NULL; + struct curl_slist *customheader = NULL; + + response = g_malloc0 (sizeof (struct openvasd_response)); + + path = g_string_new ("/scans"); + if (conn->scan_id != NULL && conn->scan_id[0] != '\0') + { + g_string_append (path, "/"); + g_string_append (path, conn->scan_id); + g_string_append (path, "/status"); + } + else + { + response->code = RESP_CODE_ERR; + response->body = g_strdup ("{\"error\": \"Missing scan ID\"}"); + g_string_free (path, TRUE); + g_warning ("%s: Missing scan ID", __func__); + return response; + } + + customheader = init_customheader (conn->apikey, FALSE); + hnd = handler (conn, GET, path->str, NULL, customheader, &err); + if (hnd == NULL) + { + curl_slist_free_all (customheader); + g_string_free (path, TRUE); + response->code = RESP_CODE_ERR; + response->body = err; + return response; + } + g_string_free (path, TRUE); + + openvasd_send_request (hnd, NULL, response); + curl_slist_free_all (customheader); + if (response->code != RESP_CODE_ERR) + response->body = g_strdup (openvasd_vt_stream_str (conn)); + else if (response->code == RESP_CODE_ERR) + { + response->body = + g_strdup ("{\"error\": \"Not possible to get scan status\"}"); + g_warning ("%s: Not possible to get scan status", __func__); + } + + openvasd_reset_vt_stream (conn); + return response; +} + +/** @brief Get the value from an object or error. + * + * @return 0 on success, -1 on error. + */ +static int +get_member_value_or_fail (cJSON *reader, const gchar *member) +{ + cJSON *item = NULL; + if ((item = cJSON_GetObjectItem (reader, member)) == NULL + && cJSON_IsNumber (item)) + return -1; + + return item->valueint; +} + +static int +openvasd_get_scan_progress_ext (openvasd_connector_t conn, + openvasd_resp_t response) +{ + cJSON *parser; + cJSON *reader = NULL; + const gchar *err = NULL; + int all = 0, excluded = 0, dead = 0, alive = 0, queued = 0, finished = 0; + int running_hosts_progress_sum = 0; + + openvasd_resp_t resp; + int progress = -1; + + if (!response && !conn) + return -1; + + if (response == NULL) + resp = openvasd_get_scan_status (conn); + else + resp = response; + + if (resp->code == 404) + return -2; + else if (resp->code != 200) + return -1; + + parser = cJSON_Parse (resp->body); + if (!parser) + { + err = cJSON_GetErrorPtr (); + goto cleanup; + } + + if ((reader = cJSON_GetObjectItem (parser, "host_info")) == NULL) + { + goto cleanup; + } + if (!cJSON_IsObject (reader)) + { + // Scan still not started. No information. + progress = 0; + goto cleanup; + } + + // read general hosts count + all = get_member_value_or_fail (reader, "all"); + excluded = get_member_value_or_fail (reader, "excluded"); + dead = get_member_value_or_fail (reader, "dead"); + alive = get_member_value_or_fail (reader, "alive"); + queued = get_member_value_or_fail (reader, "queued"); + finished = get_member_value_or_fail (reader, "finished"); + + // read progress of single running hosts + cJSON *scanning = NULL; + if ((scanning = cJSON_GetObjectItem (reader, "scanning")) != NULL + && cJSON_IsObject (scanning)) + { + cJSON *host = scanning->child; + while (host) + { + running_hosts_progress_sum += cJSON_GetNumberValue (host); + host = host->next; + } + + } // end scanning + // end host_info + + if (all < 0 || excluded < 0 || dead < 0 || alive < 0 || queued < 0 + || finished < 0) + { + goto cleanup; + } + + if ((all + finished - dead) > 0) + progress = (running_hosts_progress_sum + 100 * (alive + finished)) + / (all + finished - dead); + else + progress = 100; + +cleanup: + if (err != NULL) + g_warning ("%s: Unable to parse scan status. Reason: %s", __func__, err); + cJSON_Delete (parser); + + return progress; +} + +int +openvasd_get_scan_progress (openvasd_connector_t conn) +{ + return openvasd_get_scan_progress_ext (conn, NULL); +} + +static openvasd_status_t +get_status_code_from_openvas (const gchar *status_val) +{ + openvasd_status_t status_code = OPENVASD_SCAN_STATUS_ERROR; + + if (g_strcmp0 (status_val, "stored") == 0) + status_code = OPENVASD_SCAN_STATUS_STORED; + else if (g_strcmp0 (status_val, "requested") == 0) + status_code = OPENVASD_SCAN_STATUS_REQUESTED; + else if (g_strcmp0 (status_val, "running") == 0) + status_code = OPENVASD_SCAN_STATUS_RUNNING; + else if (g_strcmp0 (status_val, "stopped") == 0) + status_code = OPENVASD_SCAN_STATUS_STOPPED; + else if (g_strcmp0 (status_val, "succeeded") == 0) + status_code = OPENVASD_SCAN_STATUS_SUCCEEDED; + else if (g_strcmp0 (status_val, "interrupted") == 0) + status_code = OPENVASD_SCAN_STATUS_FAILED; + + return status_code; +} + +/** @brief Return a struct with the general scan status + * + * @param conn Openvasd connector data + * + * @return The data in a struct. The struct must be freed + * by the caller. + */ +openvasd_scan_status_t +openvasd_parsed_scan_status (openvasd_connector_t conn) +{ + cJSON *parser = NULL; + cJSON *status = NULL; + openvasd_resp_t resp = NULL; + gchar *status_val = NULL; + time_t start_time = 0, end_time = 0; + int progress = -1; + openvasd_status_t status_code = OPENVASD_SCAN_STATUS_ERROR; + openvasd_scan_status_t status_info; + + resp = openvasd_get_scan_status (conn); + + status_info = g_malloc0 (sizeof (struct openvasd_scan_status)); + if (resp->code != 200) + { + status_info->status = status_code; + status_info->response_code = resp->code; + openvasd_response_cleanup (resp); + return status_info; + } + if ((parser = cJSON_Parse (resp->body)) == NULL) + goto status_cleanup; + + if ((status = cJSON_GetObjectItem (parser, "status")) == NULL + || !cJSON_IsString (status)) + goto status_cleanup; + status_val = g_strdup (status->valuestring); + + if ((status = cJSON_GetObjectItem (parser, "start_time")) != NULL + && !cJSON_IsNumber (status)) + start_time = status->valuedouble; + + if ((status = cJSON_GetObjectItem (parser, "end_time")) != NULL + && !cJSON_IsNumber (status)) + end_time = status->valuedouble; + + progress = openvasd_get_scan_progress_ext (NULL, resp); + +status_cleanup: + openvasd_response_cleanup (resp); + cJSON_Delete (parser); + + status_code = get_status_code_from_openvas (status_val); + g_free (status_val); + + status_info->status = status_code; + status_info->end_time = end_time; + status_info->start_time = start_time; + status_info->progress = progress; + + return status_info; +} + +openvasd_resp_t +openvasd_delete_scan (openvasd_connector_t conn) +{ + openvasd_resp_t response = NULL; + GString *path; + gchar *err = NULL; + CURL *hnd = NULL; + struct curl_slist *customheader = NULL; + + response = g_malloc0 (sizeof (struct openvasd_response)); + + // Stop the scan + path = g_string_new ("/scans"); + if (conn->scan_id != NULL && conn->scan_id[0] != '\0') + { + g_string_append (path, "/"); + g_string_append (path, conn->scan_id); + } + else + { + response->code = RESP_CODE_ERR; + response->body = g_strdup ("{\"error\": \"Missing scan ID\"}"); + g_string_free (path, TRUE); + g_warning ("%s: Missing scan ID", __func__); + return response; + } + + customheader = init_customheader (conn->apikey, FALSE); + hnd = handler (conn, DELETE, path->str, NULL, customheader, &err); + if (hnd == NULL) + { + curl_slist_free_all (customheader); + g_string_free (path, TRUE); + response->code = RESP_CODE_ERR; + response->body = err; + return response; + } + g_string_free (path, TRUE); + + openvasd_send_request (hnd, NULL, response); + curl_slist_free_all (customheader); + if (response->code != RESP_CODE_ERR) + response->body = g_strdup (openvasd_vt_stream_str (conn)); + else if (response->code == RESP_CODE_ERR) + { + response->body = + g_strdup ("{\"error\": \"Not possible to delete scan.\"}"); + g_warning ("%s: Not possible to delete scan", __func__); + } + + openvasd_reset_vt_stream (conn); + return response; +} + +openvasd_resp_t +openvasd_get_health_alive (openvasd_connector_t conn) +{ + openvasd_resp_t response = NULL; + gchar *err = NULL; + CURL *hnd = NULL; + struct curl_slist *customheader = NULL; + + response = g_malloc0 (sizeof (struct openvasd_response)); + + customheader = init_customheader (conn->apikey, FALSE); + hnd = handler (conn, GET, "/health/alive", NULL, customheader, &err); + if (hnd == NULL) + { + curl_slist_free_all (customheader); + response->code = RESP_CODE_ERR; + response->body = err; + return response; + } + + openvasd_send_request (hnd, NULL, response); + curl_slist_free_all (customheader); + if (response->code != RESP_CODE_ERR) + response->body = g_strdup (openvasd_vt_stream_str (conn)); + else if (response->code == RESP_CODE_ERR) + { + response->body = + g_strdup ("{\"error\": \"Not possible to get health information.\"}"); + g_warning ("%s: Not possible to get health information", __func__); + } + + openvasd_reset_vt_stream (conn); + return response; +} + +openvasd_resp_t +openvasd_get_health_ready (openvasd_connector_t conn) +{ + openvasd_resp_t response = NULL; + gchar *err = NULL; + CURL *hnd = NULL; + struct curl_slist *customheader = NULL; + + response = g_malloc0 (sizeof (struct openvasd_response)); + + customheader = init_customheader (conn->apikey, FALSE); + hnd = handler (conn, GET, "/health/ready", NULL, customheader, &err); + if (hnd == NULL) + { + response->code = RESP_CODE_ERR; + response->body = err; + return response; + } + + openvasd_send_request (hnd, "feed-version", response); + curl_slist_free_all (customheader); + if (response->code != RESP_CODE_ERR) + response->body = g_strdup (openvasd_vt_stream_str (conn)); + else if (response->code == RESP_CODE_ERR) + { + response->body = + g_strdup ("{\"error\": \"Not possible to get health information.\"}"); + g_warning ("%s: Not possible to get health information", __func__); + } + + openvasd_reset_vt_stream (conn); + return response; +} + +openvasd_resp_t +openvasd_get_health_started (openvasd_connector_t conn) +{ + openvasd_resp_t response = NULL; + gchar *err = NULL; + CURL *hnd = NULL; + struct curl_slist *customheader = NULL; + + response = g_malloc0 (sizeof (struct openvasd_response)); + + customheader = init_customheader (conn->apikey, FALSE); + hnd = handler (conn, GET, "/health/started", NULL, customheader, &err); + if (hnd == NULL) + { + curl_slist_free_all (customheader); + response->code = RESP_CODE_ERR; + response->body = err; + return response; + } + + openvasd_send_request (hnd, NULL, response); + curl_slist_free_all (customheader); + if (response->code != RESP_CODE_ERR) + response->body = g_strdup (openvasd_vt_stream_str (conn)); + else if (response->code == RESP_CODE_ERR) + { + response->body = + g_strdup ("{\"error\": \"Not possible to get health information.\"}"); + g_warning ("%s: Not possible to get health information", __func__); + } + + openvasd_reset_vt_stream (conn); + return response; +} + +openvasd_resp_t +openvasd_get_scan_preferences (openvasd_connector_t conn) +{ + openvasd_resp_t response = NULL; + gchar *err = NULL; + CURL *hnd = NULL; + struct curl_slist *customheader = NULL; + + response = g_malloc0 (sizeof (struct openvasd_response)); + + customheader = init_customheader (conn->apikey, FALSE); + if ((hnd = + handler (conn, GET, "/scans/preferences", NULL, customheader, &err)) + == NULL) + { + curl_slist_free_all (customheader); + response->code = RESP_CODE_ERR; + response->body = err; + return response; + } + + openvasd_send_request (hnd, NULL, response); + curl_slist_free_all (customheader); + if (response->code != RESP_CODE_ERR) + response->body = g_strdup (openvasd_vt_stream_str (conn)); + else if (response->code == RESP_CODE_ERR) + { + response->body = + g_strdup ("{\"error\": \"Not possible to get scans preferences.\"}"); + g_warning ("%s: Not possible to get scans_preferences", __func__); + } + + openvasd_reset_vt_stream (conn); + return response; +} + +/** + * @brief Create a new Openvasd parameter. + * + * @return New Openvasd parameter. + */ +static openvasd_param_t * +openvasd_param_new (char *id, gchar *name, gchar *defval, gchar *description, + gchar *type, int mandatory) +{ + openvasd_param_t *param = g_malloc0 (sizeof (openvasd_param_t)); + + param->id = id; + param->defval = defval; + param->description = description; + param->name = name; + param->mandatory = mandatory; + param->type = type; + return param; +} + +/** + * @brief Free an Openvasd parameter. + * + * @param param Openvasd parameter to destroy. + */ +void +openvasd_param_free (openvasd_param_t *param) +{ + if (!param) + return; + g_free (param->id); + g_free (param->name); + g_free (param->defval); + g_free (param->description); + g_free (param->type); +} + +/** + * @brief Get the parameter id + * + * @param param Openvasd parameter + */ +char * +openvasd_param_id (openvasd_param_t *param) +{ + if (!param) + return NULL; + + return param->id; +} + +/** + * @brief Get the parameter default + * + * @param param Openvasd parameter + */ +char * +openvasd_param_name (openvasd_param_t *param) +{ + if (!param) + return NULL; + + return param->defval; +} + +/** + * @brief Get the parameter description + * + * @param param Openvasd parameter + */ +char * +openvasd_param_desc (openvasd_param_t *param) +{ + if (!param) + return NULL; + + return param->description; +} + +/** + * @brief Get the parameter type + * + * @param param Openvasd parameter + */ +char * +openvasd_param_type (openvasd_param_t *param) +{ + if (!param) + return NULL; + + return param->type; +} + +/** + * @brief Get the parameter default + * + * @param param Openvasd parameter + */ +char * +openvasd_param_default (openvasd_param_t *param) +{ + if (!param) + return NULL; + + return param->defval; +} + +/** + * @brief If the parameter is mandatory + * + * @param param Openvasd parameter + */ +int +openvasd_param_mandatory (openvasd_param_t *param) +{ + if (!param) + return 0; + + return param->mandatory; +} + +int +openvasd_parsed_scans_preferences (openvasd_connector_t conn, GSList **params) +{ + openvasd_resp_t resp = NULL; + cJSON *parser; + cJSON *param_obj = NULL; + int err = 0; + + resp = openvasd_get_scan_preferences (conn); + + if (resp->code != 200) + return -1; + + // No results. No information. + if ((parser = cJSON_Parse (resp->body)) == NULL || !cJSON_IsArray (parser)) + { + err = 1; + goto prefs_cleanup; + } + + cJSON_ArrayForEach (param_obj, parser) + { + const gchar *id = NULL, *name = NULL, *desc = NULL; + gchar *defval = NULL, *param_type = NULL; + openvasd_param_t *param = NULL; + int val, mandatory = 0; + char buf[6]; + cJSON *item = NULL; + if ((item = cJSON_GetObjectItem (param_obj, "id")) != NULL + && cJSON_IsString (item)) + id = g_strdup (item->valuestring); + + if ((item = cJSON_GetObjectItem (param_obj, "name")) != NULL + && cJSON_IsString (item)) + name = g_strdup (item->valuestring); + + if ((item = cJSON_GetObjectItem (param_obj, "description")) != NULL + && cJSON_IsString (item)) + desc = g_strdup (item->valuestring); + + if ((item = cJSON_GetObjectItem (param_obj, "default")) != NULL) + { + if (cJSON_IsNumber (item)) + { + val = item->valueint; + g_snprintf (buf, sizeof (buf), "%d", val); + defval = g_strdup (buf); + param_type = g_strdup ("integer"); + } + else if (cJSON_IsString (item)) + { + defval = g_strdup (item->valuestring); + param_type = g_strdup ("string"); + } + else if (cJSON_IsBool (item)) + { + if (cJSON_IsTrue (item)) + defval = g_strdup ("yes"); + else + defval = g_strdup ("no"); + param_type = g_strdup ("boolean"); + } + else + { + g_warning ("%s: Unable to parse scan preferences.", __func__); + g_free (defval); + g_free (param_type); + continue; + } + } + + param = + openvasd_param_new (g_strdup (id), g_strdup (name), g_strdup (defval), + g_strdup (desc), g_strdup (param_type), mandatory); + g_free (defval); + g_free (param_type); + *params = g_slist_append (*params, param); + } + +prefs_cleanup: + openvasd_response_cleanup (resp); + cJSON_Delete (parser); + if (err) + { + g_warning ("%s: Unable to parse scan preferences.", __func__); + return -1; + } + + return 0; +} + +// Scan config builder +static void +add_port_to_scan_json (gpointer range, gpointer p_array) +{ + range_t *ports = range; + + cJSON *port = cJSON_CreateObject (); + if (ports->type == 1) + cJSON_AddStringToObject (port, "protocol", "udp"); + else + cJSON_AddStringToObject (port, "protocol", "tcp"); + + cJSON *ranges_array = cJSON_CreateArray (); + cJSON *range_obj = cJSON_CreateObject (); + cJSON_AddNumberToObject (range_obj, "start", ports->start); + + if (ports->end > ports->start && ports->end < 65535) + cJSON_AddNumberToObject (range_obj, "end", ports->end); + else + cJSON_AddNumberToObject (range_obj, "end", ports->start); + cJSON_AddItemToArray (ranges_array, range_obj); + cJSON_AddItemToObject (port, "range", ranges_array); + cJSON_AddItemToArray ((cJSON *) p_array, port); +} + +static void +add_credential_to_scan_json (gpointer credentials, gpointer cred_array) +{ + GHashTableIter auth_data_iter; + gchar *auth_data_name, *auth_data_value; + cJSON *cred_obj = NULL; + + openvasd_credential_t *cred = credentials; + + cred_obj = cJSON_CreateObject (); + cJSON_AddStringToObject (cred_obj, "service", cred->service); + + if (cred->port) + { + cJSON_AddNumberToObject (cred_obj, "port", atoi (cred->port)); + } + + cJSON *cred_type_obj = cJSON_CreateObject (); + g_hash_table_iter_init (&auth_data_iter, cred->auth_data); + while (g_hash_table_iter_next (&auth_data_iter, (gpointer *) &auth_data_name, + (gpointer *) &auth_data_value)) + cJSON_AddStringToObject (cred_type_obj, auth_data_name, auth_data_value); + cJSON_AddItemToObject (cred_obj, cred->type, cred_type_obj); + + cJSON_AddItemToArray ((cJSON *) cred_array, cred_obj); +} + +static void +add_scan_preferences_to_scan_json (gpointer key, gpointer val, + gpointer scan_prefs_array) +{ + cJSON *pref_obj = cJSON_CreateObject (); + cJSON_AddStringToObject (pref_obj, "id", key); + cJSON_AddStringToObject (pref_obj, "value", val); + cJSON_AddItemToArray (scan_prefs_array, pref_obj); +} + +static void +add_vts_to_scan_json (gpointer single_vt, gpointer vts_array) +{ + GHashTableIter vt_data_iter; + gchar *vt_param_id, *vt_param_value; + + openvasd_vt_single_t *vt = single_vt; + + cJSON *vt_obj = cJSON_CreateObject (); + + cJSON_AddStringToObject (vt_obj, "oid", vt->vt_id); + + if (g_hash_table_size (vt->vt_values)) + { + cJSON *params_array = cJSON_CreateArray (); + + g_hash_table_iter_init (&vt_data_iter, vt->vt_values); + while (g_hash_table_iter_next (&vt_data_iter, (gpointer *) &vt_param_id, + (gpointer *) &vt_param_value)) + { + cJSON *param_obj = cJSON_CreateObject (); + cJSON_AddNumberToObject (param_obj, "id", atoi (vt_param_id)); + cJSON_AddStringToObject (param_obj, "value", vt_param_value); + cJSON_AddItemToArray (params_array, param_obj); + } + cJSON_AddItemToObject (vt_obj, "parameters", params_array); + } + cJSON_AddItemToArray (vts_array, vt_obj); +} + +/** + * @brief Build a json object with data necessary to start a scan + * + * JSON result consists of scan_id, message type, host ip, + * hostname, port, together with proto, OID, result message and uri. + * + * @param target target + * @param scan_preferences Scan preferences to be added to the scan config + * @param vts VTS collection to be added to the scan config. + * + * @return JSON string on success. Must be freed by caller. NULL on error. + */ +char * +openvasd_build_scan_config_json (openvasd_target_t *target, + GHashTable *scan_preferences, GSList *vts) +{ + cJSON *scan_obj = NULL; + cJSON *target_obj = NULL; + cJSON *hosts_array = NULL; + cJSON *exclude_hosts_array = NULL; + cJSON *finished_hosts_array = NULL; + gchar *json_str = NULL; + + /* Build the message in json format to be published. */ + scan_obj = cJSON_CreateObject (); + + if (target->scan_id && target->scan_id[0] != '\0') + cJSON_AddStringToObject (scan_obj, "scan_id", target->scan_id); + + // begin target + target_obj = cJSON_CreateObject (); + + // hosts + hosts_array = cJSON_CreateArray (); + gchar **hosts_list = g_strsplit (target->hosts, ",", 0); + for (int i = 0; hosts_list[i] != NULL; i++) + { + cJSON *host_item = NULL; + host_item = cJSON_CreateString (hosts_list[i]); + cJSON_AddItemToArray (hosts_array, host_item); + } + g_strfreev (hosts_list); + cJSON_AddItemToObject (target_obj, "hosts", hosts_array); + + // exclude hosts + if (target->exclude_hosts && target->exclude_hosts[0] != '\0') + { + exclude_hosts_array = cJSON_CreateArray (); + gchar **exclude_hosts_list = g_strsplit (target->exclude_hosts, ",", 0); + for (int i = 0; exclude_hosts_list[i] != NULL; i++) + { + cJSON *exclude_host_item = NULL; + exclude_host_item = cJSON_CreateString (exclude_hosts_list[i]); + cJSON_AddItemToArray (exclude_hosts_array, exclude_host_item); + } + g_strfreev (exclude_hosts_list); + cJSON_AddItemToObject (target_obj, "excluded_hosts", exclude_hosts_array); + } + + // finished hosts + if (target->finished_hosts && target->finished_hosts[0] != '\0') + { + finished_hosts_array = cJSON_CreateArray (); + gchar **finished_hosts_list = g_strsplit (target->finished_hosts, ",", 0); + for (int i = 0; finished_hosts_list[i] != NULL; i++) + { + cJSON *finished_host_item = NULL; + finished_host_item = cJSON_CreateString (finished_hosts_list[i]); + cJSON_AddItemToArray (finished_hosts_array, finished_host_item); + } + g_strfreev (hosts_list); + cJSON_AddItemToObject (target_obj, "finished_hosts", + finished_hosts_array); + } + + // ports + if (target->ports && target->ports[0] != '\0') + { + cJSON *ports_array = cJSON_CreateArray (); + array_t *ports = port_range_ranges (target->ports); + g_ptr_array_foreach (ports, add_port_to_scan_json, ports_array); + array_free (ports); + cJSON_AddItemToObject (target_obj, "ports", ports_array); + } + + // credentials + cJSON *credentials = cJSON_CreateArray (); + g_slist_foreach (target->credentials, add_credential_to_scan_json, + credentials); + cJSON_AddItemToObject (target_obj, "credentials", credentials); + + // reverse lookup + if (target->reverse_lookup_unify) + cJSON_AddBoolToObject (target_obj, "reverse_lookup_unify", cJSON_True); + else + cJSON_AddBoolToObject (target_obj, "reverse_lookup_unify", cJSON_False); + + if (target->reverse_lookup_only) + cJSON_AddBoolToObject (target_obj, "reverse_lookup_only", cJSON_True); + else + cJSON_AddBoolToObject (target_obj, "reverse_lookup_only", cJSON_False); + + // alive test methods + cJSON *alive_test_methods = cJSON_CreateArray (); + if (target->arp) + cJSON_AddItemToArray (alive_test_methods, cJSON_CreateString ("arp")); + if (target->tcp_ack) + cJSON_AddItemToArray (alive_test_methods, cJSON_CreateString ("tcp_ack")); + if (target->tcp_syn) + cJSON_AddItemToArray (alive_test_methods, cJSON_CreateString ("tcp_syn")); + if (target->consider_alive) + cJSON_AddItemToArray (alive_test_methods, + cJSON_CreateString ("consider_alive")); + if (target->icmp) + cJSON_AddItemToArray (alive_test_methods, cJSON_CreateString ("icmp")); + cJSON_AddItemToObject (target_obj, "alive_test_methods", alive_test_methods); + + cJSON_AddItemToObject (scan_obj, "target", target_obj); + + // Begin Scan Preferences + cJSON *scan_prefs_array = cJSON_CreateArray (); + g_hash_table_foreach (scan_preferences, add_scan_preferences_to_scan_json, + scan_prefs_array); + cJSON_AddItemToObject (scan_obj, "scan_preferences", scan_prefs_array); + + // Begin VTs + cJSON *vts_array = cJSON_CreateArray (); + g_slist_foreach (vts, add_vts_to_scan_json, vts_array); + cJSON_AddItemToObject (scan_obj, "vts", vts_array); + + json_str = cJSON_Print (scan_obj); + cJSON_Delete (scan_obj); + if (json_str == NULL) + g_warning ("%s: Error while creating JSON.", __func__); + + return json_str; +} + +/** + * @brief Allocate and initialize a new Openvasd credential. + * + * @param type The credential type. + * @param service The service the credential is for. + * @param port The port. + * + * @return New openvasd credential. + */ +openvasd_credential_t * +openvasd_credential_new (const gchar *type, const gchar *service, + const gchar *port) +{ + openvasd_credential_t *new_credential; + + new_credential = g_malloc0 (sizeof (openvasd_credential_t)); + + new_credential->type = type ? g_strdup (type) : NULL; + new_credential->service = service ? g_strdup (service) : NULL; + new_credential->port = port ? g_strdup (port) : NULL; + new_credential->auth_data = + g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); + + return new_credential; +} + +/** + * @brief Free an Openvasd credential. + * + * @param credential The credential to free. + */ +void +openvasd_credential_free (openvasd_credential_t *credential) +{ + if (!credential) + return; + + g_free (credential->type); + g_free (credential->service); + g_free (credential->port); + g_hash_table_destroy (credential->auth_data); + g_free (credential); +} + +/** + * @brief Get authentication data from an Openvasd credential. + * + * @param credential The credential to get the data from. + * @param name The name of the data item to get. + * @param value The authentication data or NULL to unset. + */ +void +openvasd_credential_set_auth_data (openvasd_credential_t *credential, + const gchar *name, const gchar *value) +{ + if (credential == NULL || name == NULL) + return; + + if (g_regex_match_simple ("^[[:alpha:]][[:alnum:]_]*$", name, 0, 0)) + { + if (value) + g_hash_table_replace (credential->auth_data, g_strdup (name), + g_strdup (value)); + else + g_hash_table_remove (credential->auth_data, name); + } + else + { + g_warning ("%s: Invalid auth data name: %s", __func__, name); + } +} + +/** + * @brief Create a new Openvasd target. + * + * @param scanid Scan ID. + * @param hosts The hostnames of the target. + * @param ports The ports of the target. + * @param exclude_hosts The excluded hosts of the target. + * @param reverse_lookup_unify Lookup flag. + * @param reverse_lookup_only Lookup flag. + * + * @return The newly allocated openvasd_target_t. + */ +openvasd_target_t * +openvasd_target_new (const gchar *scanid, const gchar *hosts, + const gchar *ports, const gchar *exclude_hosts, + int reverse_lookup_unify, int reverse_lookup_only) +{ + openvasd_target_t *new_target; + new_target = g_malloc0 (sizeof (openvasd_target_t)); + + if (scanid && *scanid) + new_target->scan_id = g_strdup (scanid); + + new_target->exclude_hosts = exclude_hosts ? g_strdup (exclude_hosts) : NULL; + new_target->finished_hosts = NULL; + new_target->hosts = hosts ? g_strdup (hosts) : NULL; + new_target->ports = ports ? g_strdup (ports) : NULL; + new_target->reverse_lookup_unify = + reverse_lookup_unify ? reverse_lookup_unify : 0; + new_target->reverse_lookup_only = + reverse_lookup_only ? reverse_lookup_only : 0; + + return new_target; +} + +/** + * @brief Set the finished hosts of an Openvasd target. + * + * @param target The Openvasd target to modify. + * @param finished_hosts The hostnames to consider finished. + */ +void +openvasd_target_set_finished_hosts (openvasd_target_t *target, + const gchar *finished_hosts) +{ + g_free (target->finished_hosts); + target->finished_hosts = finished_hosts ? g_strdup (finished_hosts) : NULL; +} + +/** + * @brief Free an Openvasd target, including all added credentials. + * + * @param target The Openvasd target to free. + */ +void +openvasd_target_free (openvasd_target_t *target) +{ + if (!target) + return; + + g_slist_free_full (target->credentials, + (GDestroyNotify) openvasd_credential_free); + g_free (target->exclude_hosts); + g_free (target->finished_hosts); + g_free (target->scan_id); + g_free (target->hosts); + g_free (target->ports); + g_free (target); + target = NULL; +} + +/** + * @brief Add alive test methods to Openvasd target. + * + * @param target The Openvasd target to add the methods to. + * @param icmp Use ICMP ping. + * @param tcp_syn Use TCP-SYN ping. + * @param tcp_ack Use TCP-ACK ping. + * @param arp Use ARP ping. + * @param consider_alive Consider host to be alive. + */ +void +openvasd_target_add_alive_test_methods (openvasd_target_t *target, + gboolean icmp, gboolean tcp_syn, + gboolean tcp_ack, gboolean arp, + gboolean consider_alive) +{ + if (!target) + return; + + target->icmp = icmp; + target->tcp_syn = tcp_syn; + target->tcp_ack = tcp_ack; + target->arp = arp; + target->consider_alive = consider_alive; +} + +/** + * @brief Add a credential to an Openvasd target. + * + * @param target The Openvasd target to add the credential to. + * @param credential The credential to add. Will be freed with target. + */ +void +openvasd_target_add_credential (openvasd_target_t *target, + openvasd_credential_t *credential) +{ + if (!target || !credential) + return; + + target->credentials = g_slist_prepend (target->credentials, credential); +} + +/** + * @brief Create a new single Openvasd VT. + * + * @param vt_id The id of the VT. + * + * @return The newly allocated single VT. + */ +openvasd_vt_single_t * +openvasd_vt_single_new (const gchar *vt_id) +{ + openvasd_vt_single_t *new_vt_single; + new_vt_single = g_malloc0 (sizeof (openvasd_vt_single_t)); + + new_vt_single->vt_id = vt_id ? g_strdup (vt_id) : NULL; + new_vt_single->vt_values = + g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); + + return new_vt_single; +} + +/** + * @brief Free a single Openvasd VT, including all preference values. + * + * @param vt_single The Openvasd VT to free. + */ +void +openvasd_vt_single_free (openvasd_vt_single_t *vt_single) +{ + if (!vt_single) + return; + + g_hash_table_destroy (vt_single->vt_values); + + g_free (vt_single->vt_id); + g_free (vt_single); +} + +/** + * @brief Add a preference value to an Openvasd VT. + * + * This creates a copy of the name and value. + * + * @param vt_single The VT to add the preference to. + * @param name The name / identifier of the preference. + * @param value The value of the preference. + */ +void +openvasd_vt_single_add_value (openvasd_vt_single_t *vt_single, + const gchar *name, const gchar *value) +{ + g_hash_table_replace (vt_single->vt_values, g_strdup (name), + g_strdup (value)); +} diff --git a/openvasd/openvasd.h b/openvasd/openvasd.h new file mode 100644 index 00000000..d15cfcdc --- /dev/null +++ b/openvasd/openvasd.h @@ -0,0 +1,275 @@ +/* SPDX-FileCopyrightText: 2024 Greenbone AG + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +/** + * @file + * @brief API for Openvas Daemon communication. + */ + +#ifndef _GVM_OPENVASD_H +#define _GVM_OPENVASD_H + +#include "../base/nvti.h" +#include "../util/jsonpull.h" + +#include +#include +#include + +/** @brief Struct to hold an scan result */ +struct openvasd_result +{ + unsigned long id; + gchar *type; + gchar *ip_address; + gchar *hostname; + gchar *oid; + int port; + gchar *protocol; + gchar *message; + gchar *detail_name; + gchar *detail_value; + gchar *detail_source_type; + gchar *detail_source_name; + gchar *detail_source_description; +}; + +/** @brief Openvasd Errors */ +enum OPENVASD_ERROR +{ + OPENVASD_INVALID_OPT, + OPENVASD_NOT_INITIALIZED, + OPENVASD_INVALID_VALUE, + OPENVASD_ERROR, + OPENVASD_OK, +}; + +/** @brief Openvasd options for the connector builder */ +enum OPENVASD_CONNECTOR_OPTS +{ + OPENVASD_CA_CERT, + OPENVASD_CERT, + OPENVASD_KEY, + OPENVASD_API_KEY, + OPENVASD_SERVER, + OPENVASD_HOST, + OPENVASD_SCAN_ID, + OPENVASD_PORT, +}; + +enum OPENVASD_RESULT_MEMBER_STRING +{ + TYPE, + IP_ADDRESS, + HOSTNAME, + OID, + PROTOCOL, + MESSAGE, + DETAIL_NAME, + DETAIL_VALUE, + DETAIL_SOURCE_NAME, + DETAIL_SOURCE_TYPE, + DETAIL_SOURCE_DESCRIPTION, +}; + +enum OPENVASD_RESULT_MEMBER_INT +{ + ID, + PORT, +}; + +/** + * @brief Openvasd scan status. + */ +typedef enum +{ + OPENVASD_SCAN_STATUS_ERROR = -2, /**< Error status. */ + OPENVASD_SCAN_STATUS_FAILED = -1, /**< Failed status. */ + OPENVASD_SCAN_STATUS_STORED, /**< Stored status */ + OPENVASD_SCAN_STATUS_REQUESTED, /**< Queued status */ + OPENVASD_SCAN_STATUS_RUNNING, /**< Running status. */ + OPENVASD_SCAN_STATUS_STOPPED, /**< Stopped status. */ + OPENVASD_SCAN_STATUS_SUCCEEDED, /**< Succeeded status */ +} openvasd_status_t; + +struct openvasd_response +{ + long code; /**< HTTP code response. */ + gchar *body; /**< String containing the body response. */ + gchar *header; /**< A header value. */ +}; + +struct openvasd_scan_status +{ + time_t start_time; + time_t end_time; + int progress; + openvasd_status_t status; + long response_code; +}; + +typedef struct openvasd_response *openvasd_resp_t; + +typedef enum OPENVASD_RESULT_MEMBER_INT openvasd_result_member_int_t; + +typedef enum OPENVASD_RESULT_MEMBER_STRING openvasd_result_member_string_t; + +typedef enum OPENVASD_CONNECTOR_OPTS openvasd_conn_opt_t; + +typedef enum OPENVASD_ERROR openvasd_error_t; + +typedef struct openvasd_result *openvasd_result_t; + +typedef struct openvasd_connector *openvasd_connector_t; + +typedef struct openvasd_scan_status *openvasd_scan_status_t; + +// Functions to build/free request data +openvasd_connector_t +openvasd_connector_new (void); + +openvasd_error_t +openvasd_connector_builder (openvasd_connector_t, openvasd_conn_opt_t, + const void *); + +openvasd_error_t openvasd_connector_free (openvasd_connector_t); + +void openvasd_response_cleanup (openvasd_resp_t); + +// Requests +openvasd_resp_t openvasd_get_version (openvasd_connector_t); + +openvasd_resp_t openvasd_get_vts (openvasd_connector_t); + +openvasd_resp_t +openvasd_start_scan (openvasd_connector_t, gchar *); + +openvasd_resp_t openvasd_stop_scan (openvasd_connector_t); + +openvasd_resp_t openvasd_delete_scan (openvasd_connector_t); + +openvasd_resp_t +openvasd_get_scan_results (openvasd_connector_t, long, long); + +openvasd_result_t +openvasd_result_new (unsigned long, gchar *, gchar *, gchar *, gchar *, int, + gchar *, gchar *, gchar *, gchar *, gchar *, gchar *, + gchar *); + +void openvasd_result_free (openvasd_result_t); + +char *openvasd_get_result_member_str (openvasd_result_t, + openvasd_result_member_string_t); + +int openvasd_get_result_member_int (openvasd_result_t, + openvasd_result_member_int_t); + +int +openvasd_parsed_results (openvasd_connector_t, unsigned long, unsigned long, + GSList **); + +openvasd_resp_t openvasd_get_scan_status (openvasd_connector_t); + +openvasd_scan_status_t openvasd_parsed_scan_status (openvasd_connector_t); + +int openvasd_get_scan_progress (openvasd_connector_t); + +openvasd_resp_t openvasd_get_health_alive (openvasd_connector_t); + +openvasd_resp_t openvasd_get_health_ready (openvasd_connector_t); + +openvasd_resp_t openvasd_get_health_started (openvasd_connector_t); + +/* Scanner preferences */ + +typedef struct openvasd_param openvasd_param_t; + +openvasd_resp_t openvasd_get_scan_preferences (openvasd_connector_t); + +int +openvasd_parsed_scans_preferences (openvasd_connector_t, GSList **); + +void +openvasd_param_free (openvasd_param_t *); + +char * +openvasd_param_id (openvasd_param_t *); + +char * +openvasd_param_name (openvasd_param_t *); + +char * +openvasd_param_desc (openvasd_param_t *); + +int +openvasd_param_mandatory (openvasd_param_t *); + +char * +openvasd_param_type (openvasd_param_t *); + +char * +openvasd_param_default (openvasd_param_t *); + +/* Target builder */ +typedef struct openvasd_target openvasd_target_t; + +typedef struct openvasd_vt_single openvasd_vt_single_t; + +typedef struct openvasd_credential openvasd_credential_t; + +openvasd_target_t * +openvasd_target_new (const gchar *, const gchar *, const gchar *, const gchar *, + int, int); + +void +openvasd_target_set_finished_hosts (openvasd_target_t *, const gchar *); + +void +openvasd_target_add_alive_test_methods (openvasd_target_t *, gboolean, gboolean, + gboolean, gboolean, gboolean); + +void +openvasd_target_free (openvasd_target_t *); + +openvasd_credential_t * +openvasd_credential_new (const gchar *, const gchar *, const gchar *); + +void +openvasd_credential_set_auth_data (openvasd_credential_t *, const gchar *, + const gchar *); +void +openvasd_credential_free (openvasd_credential_t *); + +void +openvasd_target_add_credential (openvasd_target_t *, openvasd_credential_t *); + +openvasd_vt_single_t * +openvasd_vt_single_new (const gchar *); + +void +openvasd_vt_single_free (openvasd_vt_single_t *); + +void +openvasd_vt_single_add_value (openvasd_vt_single_t *, const gchar *, + const gchar *); + +char * +openvasd_build_scan_config_json (openvasd_target_t *, GHashTable *, GSList *); + +/* VT stream */ +openvasd_resp_t openvasd_get_vt_stream_init (openvasd_connector_t); + +int openvasd_get_vt_stream (openvasd_connector_t); + +void openvasd_reset_vt_stream (openvasd_connector_t); + +char *openvasd_vt_stream_str (openvasd_connector_t); + +size_t openvasd_vt_stream_len (openvasd_connector_t); + +nvti_t * +openvasd_parse_vt (gvm_json_pull_parser_t *, gvm_json_pull_event_t *); + +#endif diff --git a/openvasd/vtparser.c b/openvasd/vtparser.c new file mode 100644 index 00000000..2007598e --- /dev/null +++ b/openvasd/vtparser.c @@ -0,0 +1,405 @@ +/* SPDX-FileCopyrightText: 2024 Greenbone AG + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +/** + * @file + * @brief Simple JSON reader. + */ + +#define _GNU_SOURCE /* See feature_test_macros(7) */ +#define _FILE_OFFSET_BITS 64 +#include "../base/cvss.h" +#include "../util/jsonpull.h" +#include "openvasd.h" + +#include +#include +#include +#include + +/** + * @brief VT categories + */ +typedef enum +{ + ACT_INIT = 0, + ACT_SCANNER, + ACT_SETTINGS, + ACT_GATHER_INFO, + ACT_ATTACK, + ACT_MIXED_ATTACK, + ACT_DESTRUCTIVE_ATTACK, + ACT_DENIAL, + ACT_KILL_HOST, + ACT_FLOOD, + ACT_END, +} nvt_category; + +/** + * @brief Get the VT category type given the category as string + * + * @param cat The category as string. + * + * @return Integer representing the category type. + */ +static int +get_category_from_name (const gchar *cat) +{ + if (!g_strcmp0 (cat, "init")) + return ACT_INIT; + else if (!g_strcmp0 (cat, "scanner")) + return ACT_SCANNER; + else if (!g_strcmp0 (cat, "settings")) + return ACT_SETTINGS; + else if (!g_strcmp0 (cat, "gather_info")) + return ACT_GATHER_INFO; + else if (!g_strcmp0 (cat, "attack")) + return ACT_ATTACK; + else if (!g_strcmp0 (cat, "mixed_attack")) + return ACT_MIXED_ATTACK; + else if (!g_strcmp0 (cat, "destructive_attack")) + return ACT_DESTRUCTIVE_ATTACK; + else if (!g_strcmp0 (cat, "denial")) + return ACT_DENIAL; + else if (!g_strcmp0 (cat, "kill_host")) + return ACT_KILL_HOST; + else if (!g_strcmp0 (cat, "flood")) + return ACT_FLOOD; + else if (!g_strcmp0 (cat, "end")) + return ACT_END; + + return -1; +} + +static void +add_tags_to_nvt (nvti_t *nvt, cJSON *tag_obj) +{ + if (cJSON_IsObject (tag_obj)) + { + cJSON *item = NULL; + if ((item = cJSON_GetObjectItem (tag_obj, "affected")) != NULL + && cJSON_IsString (item)) + nvti_set_affected (nvt, item->valuestring); + + if ((item = cJSON_GetObjectItem (tag_obj, "creation_date")) != NULL + && cJSON_IsNumber (item)) + nvti_set_creation_time (nvt, item->valuedouble); + + if ((item = cJSON_GetObjectItem (tag_obj, "last_modification")) != NULL + && cJSON_IsNumber (item)) + nvti_set_modification_time (nvt, item->valuedouble); + + if ((item = cJSON_GetObjectItem (tag_obj, "insight")) != NULL + && cJSON_IsString (item)) + nvti_set_insight (nvt, item->valuestring); + + if ((item = cJSON_GetObjectItem (tag_obj, "impact")) != NULL + && cJSON_IsString (item)) + nvti_set_impact (nvt, item->valuestring); + + if ((item = cJSON_GetObjectItem (tag_obj, "qod")) != NULL + && cJSON_IsString (item)) + nvti_set_qod (nvt, item->valuestring); + + if ((item = cJSON_GetObjectItem (tag_obj, "qod_type")) != NULL + && cJSON_IsString (item)) + nvti_set_qod_type (nvt, item->valuestring); + + if ((item = cJSON_GetObjectItem (tag_obj, "solution")) != NULL + && cJSON_IsString (item)) + { + nvti_set_solution (nvt, item->valuestring); + + if ((item = cJSON_GetObjectItem (tag_obj, "solution_type")) != NULL + && cJSON_IsString (item)) + nvti_set_solution_type (nvt, item->valuestring); + else + g_debug ("%s: SOLUTION: missing type for OID: %s", __func__, + nvti_oid (nvt)); + if ((item = cJSON_GetObjectItem (tag_obj, "solution_method")) != NULL + && cJSON_IsString (item)) + nvti_set_solution_method (nvt, item->valuestring); + } + + if ((item = cJSON_GetObjectItem (tag_obj, "summary")) != NULL + && cJSON_IsString (item)) + nvti_set_summary (nvt, item->valuestring); + + if ((item = cJSON_GetObjectItem (tag_obj, "vuldetect")) != NULL + && cJSON_IsString (item)) + nvti_set_detection (nvt, item->valuestring); + + // Parse severity + gchar *severity_vector = NULL; + + if ((item = cJSON_GetObjectItem (tag_obj, "severity_vector")) != NULL + && cJSON_IsString (item)) + severity_vector = item->valuestring; + + if (!severity_vector) + { + if ((item = cJSON_GetObjectItem (tag_obj, "cvss_base_vector")) != NULL + && cJSON_IsString (item)) + severity_vector = item->valuestring; + } + + if (severity_vector) + { + gchar *severity_origin = NULL, *severity_type = NULL; + gchar *cvss_base; + + time_t severity_date = 0; + double cvss_base_dbl; + + if (g_strrstr (severity_vector, "CVSS:3")) + severity_type = g_strdup ("cvss_base_v3"); + else + severity_type = g_strdup ("cvss_base_v2"); + + cvss_base_dbl = get_cvss_score_from_base_metrics (severity_vector); + + if ((item = cJSON_GetObjectItem (tag_obj, "severity_date")) != NULL + && cJSON_IsNumber (item)) + severity_date = item->valuedouble; + + if ((item = cJSON_GetObjectItem (tag_obj, "severity_origin")) != NULL + && cJSON_IsString (item)) + severity_origin = item->valuestring; + + nvti_add_vtseverity ( + nvt, vtseverity_new (severity_type, severity_origin, severity_date, + cvss_base_dbl, severity_vector)); + + nvti_add_tag (nvt, "cvss_base_vector", severity_vector); + + cvss_base = g_strdup_printf ( + "%.1f", get_cvss_score_from_base_metrics (severity_vector)); + nvti_set_cvss_base (nvt, cvss_base); + + g_free (cvss_base); + g_free (severity_type); + // end parsing severity + } + else + { + g_warning ("%s: SEVERITY missing value element", __func__); + nvti_free (nvt); + nvt = NULL; + } + } // end tag +} + +static void +parse_references (nvti_t *nvt, cJSON *vt_obj) +{ + cJSON *item = NULL; + if ((item = cJSON_GetObjectItem (vt_obj, "references")) != NULL + && cJSON_IsArray (item)) + { + cJSON *ref_obj; + cJSON *ref_item; + cJSON_ArrayForEach (ref_obj, item) + { + gchar *id, *class; + + if (!cJSON_IsObject (ref_obj)) + { + g_debug ("%s: Error reading VT/REFS reference object", __func__); + continue; + } + + if ((ref_item = cJSON_GetObjectItem (ref_obj, "class")) != NULL + && cJSON_IsString (ref_item)) + { + class = ref_item->valuestring; + if ((ref_item = cJSON_GetObjectItem (ref_obj, "id")) == NULL + && !cJSON_IsString (ref_item)) + { + g_warning ("%s: REF missing ID attribute", __func__); + continue; + } + + id = ref_item->valuestring; + nvti_add_vtref (nvt, vtref_new (class, id, NULL)); + } + else + { + g_warning ("%s: REF missing class attribute", __func__); + continue; + } + } + } // end references +} + +static void +add_preferences_to_nvt (nvti_t *nvt, cJSON *vt_obj) +{ + cJSON *item = NULL; + if ((item = cJSON_GetObjectItem (vt_obj, "preferences")) != NULL) + { + if (!cJSON_IsArray (item)) + g_debug ("%s: Error reading VT/REFS array", __func__); + else + { + cJSON *prefs_obj = NULL; + cJSON *prefs_item = NULL; + + cJSON_ArrayForEach (prefs_obj, item) + { + gchar *class, *name, *default_val; + int id; + if (!cJSON_IsObject (prefs_obj)) + { + g_debug ("%s: Error reading VT/PREFS preference object", + __func__); + continue; + } + + if ((prefs_item = cJSON_GetObjectItem (prefs_obj, "class")) == NULL + || !cJSON_IsString (prefs_item)) + { + g_warning ("%s: PREF missing class attribute", __func__); + continue; + } + class = prefs_item->valuestring; + + if ((prefs_item = cJSON_GetObjectItem (prefs_obj, "id")) == NULL + || !cJSON_IsNumber (prefs_item)) + { + g_warning ("%s: PREF missing id attribute", __func__); + continue; + } + id = prefs_item->valueint; + + if ((prefs_item = cJSON_GetObjectItem (prefs_obj, "name")) == NULL + || !cJSON_IsString (prefs_item)) + { + g_warning ("%s: PREF missing name attribute", __func__); + continue; + } + name = prefs_item->valuestring; + + if ((prefs_item = cJSON_GetObjectItem (prefs_obj, "default")) + == NULL + || !cJSON_IsString (prefs_item)) + { + g_warning ("%s: PREF missing default attribute", __func__); + continue; + } + default_val = prefs_item->valuestring; + + nvti_add_pref (nvt, nvtpref_new (id, name, class, default_val)); + } // end each prefs + } // end prefs array + } // end preferences +} + +/** + * @brief Parse a VT element given in json format. + * + * @param parser Json pull parser. + * @param event Json pull event. + * + * @return nvti structure containing the VT metadata, NULL otherwise. + * The nvti struct must be freed with nvti_free() by the caller. + */ +nvti_t * +openvasd_parse_vt (gvm_json_pull_parser_t *parser, gvm_json_pull_event_t *event) +{ + nvti_t *nvt = NULL; + cJSON *vt_obj = NULL; + cJSON *item = NULL; + gchar *error_message = NULL; + + gvm_json_pull_parser_next (parser, event); + + // Handle start/end of json array + gchar *path = gvm_json_path_to_string (event->path); + if (!g_strcmp0 (path, "$") && event->type == GVM_JSON_PULL_EVENT_ARRAY_START) + { + gvm_json_pull_parser_next (parser, event); + g_debug ("%s: Start parsing feed", __func__); + } + else if (!g_strcmp0 (path, "$") + && (event->type == GVM_JSON_PULL_EVENT_ARRAY_END + || event->type == GVM_JSON_PULL_EVENT_EOF)) + { + g_debug ("%s: Finish parsing feed", __func__); + g_free (path); + return NULL; + } + g_free (path); + + // It is an NVT object + if (event->type != GVM_JSON_PULL_EVENT_OBJECT_START) + { + g_warning ("%s: Error reading VT object", __func__); + return NULL; + } + + vt_obj = gvm_json_pull_expand_container (parser, &error_message); + if (!cJSON_IsObject (vt_obj)) + { + g_free (error_message); + cJSON_Delete (vt_obj); + return NULL; + } + g_free (error_message); + + nvt = nvti_new (); + + if ((item = cJSON_GetObjectItem (vt_obj, "oid")) != NULL + && cJSON_IsString (item)) + nvti_set_oid (nvt, item->valuestring); + else + { + g_warning ("%s: VT missing OID", __func__); + cJSON_Delete (vt_obj); + nvti_free (nvt); + return NULL; + } + + if ((item = cJSON_GetObjectItem (vt_obj, "name")) != NULL + && cJSON_IsString (item)) + nvti_set_name (nvt, item->valuestring); + else + { + g_warning ("%s: VT missing NAME", __func__); + cJSON_Delete (vt_obj); + nvti_free (nvt); + return NULL; + } + + if ((item = cJSON_GetObjectItem (vt_obj, "family")) != NULL + && cJSON_IsString (item)) + nvti_set_family (nvt, item->valuestring); + else + { + g_warning ("%s: VT missing FAMILY", __func__); + cJSON_Delete (vt_obj); + nvti_free (nvt); + return NULL; + } + + if ((item = cJSON_GetObjectItem (vt_obj, "category")) != NULL + && cJSON_IsString (item)) + nvti_set_category (nvt, get_category_from_name (item->valuestring)); + else + { + g_warning ("%s: VT missing CATEGORY", __func__); + cJSON_Delete (vt_obj); + nvti_free (nvt); + return NULL; + } + + cJSON *tag_obj = cJSON_GetObjectItem (vt_obj, "tag"); + if (tag_obj) + add_tags_to_nvt (nvt, tag_obj); + + parse_references (nvt, vt_obj); + add_preferences_to_nvt (nvt, vt_obj); + cJSON_Delete (vt_obj); + return nvt; +} diff --git a/util/CMakeLists.txt b/util/CMakeLists.txt index 4e8378b2..36ba0077 100644 --- a/util/CMakeLists.txt +++ b/util/CMakeLists.txt @@ -42,6 +42,9 @@ pkg_check_modules (GPGME REQUIRED gpgme>=1.7.0) # for serverutils we need libgcrypt pkg_check_modules (GCRYPT REQUIRED libgcrypt) +# for json parsing we need cJSON +pkg_check_modules (CJSON REQUIRED libcjson>=1.7.14) + # for mqtt find_library(LIBPAHO paho-mqtt3c) message (STATUS "Looking for paho-mqtt3c ... ${LIBPAHO}") @@ -109,13 +112,13 @@ endif (BUILD_WITH_LDAP) include_directories (${GLIB_INCLUDE_DIRS} ${GPGME_INCLUDE_DIRS} ${GCRYPT_INCLUDE_DIRS} ${LIBXML2_INCLUDE_DIRS}) -set (FILES passwordbasedauthentication.c compressutils.c fileutils.c gpgmeutils.c kb.c ldaputils.c - nvticache.c mqtt.c radiusutils.c serverutils.c sshutils.c uuidutils.c +set (FILES cpeutils.c passwordbasedauthentication.c compressutils.c fileutils.c gpgmeutils.c jsonpull.c kb.c + ldaputils.c nvticache.c mqtt.c radiusutils.c serverutils.c sshutils.c uuidutils.c versionutils.c xmlutils.c) -set (HEADERS passwordbasedauthentication.h authutils.h compressutils.h fileutils.h gpgmeutils.h kb.h - ldaputils.h nvticache.h mqtt.h radiusutils.h serverutils.h sshutils.h - uuidutils.h xmlutils.h) +set (HEADERS cpeutils.h passwordbasedauthentication.h authutils.h compressutils.h fileutils.h gpgmeutils.h + jsonpull.h kb.h ldaputils.h nvticache.h mqtt.h radiusutils.h serverutils.h sshutils.h + uuidutils.h versionutils.h xmlutils.h) if (BUILD_STATIC) add_library (gvm_util_static STATIC ${FILES}) @@ -137,13 +140,29 @@ if (BUILD_SHARED) ${RADIUS_LDFLAGS} ${LIBSSH_LDFLAGS} ${GNUTLS_LDFLAGS} ${GCRYPT_LDFLAGS} ${LDAP_LDFLAGS} ${REDIS_LDFLAGS} ${LIBXML2_LDFLAGS} ${UUID_LDFLAGS} - ${LINKER_HARDENING_FLAGS} ${CRYPT_LDFLAGS}) + ${LINKER_HARDENING_FLAGS} ${CRYPT_LDFLAGS} + ${CJSON_LDFLAGS}) endif (BUILD_SHARED) ## Tests if (BUILD_TESTS) + add_executable (jsonpull-test + EXCLUDE_FROM_ALL + jsonpull_tests.c) + + add_test (jsonpull-test jsonpull-test) + + target_include_directories (jsonpull-test PRIVATE ${CGREEN_INCLUDE_DIRS}) + + target_link_libraries (jsonpull-test ${CGREEN_LIBRARIES} + ${GLIB_LDFLAGS} ${CJSON_LDFLAGS}) + + add_custom_target (tests-jsonpull + DEPENDS jsonpull-test) + + add_executable (passwordbasedauthentication-test EXCLUDE_FROM_ALL passwordbasedauthentication_tests.c) @@ -161,6 +180,60 @@ if (BUILD_TESTS) add_custom_target (tests-passwordbasedauthentication DEPENDS passwordbasedauthentication-test) + add_executable (compressutils-test + EXCLUDE_FROM_ALL + compressutils_tests.c) + + add_test (compressutils-test compressutils-test) + + target_include_directories (compressutils-test PRIVATE ${CGREEN_INCLUDE_DIRS}) + + target_link_libraries (compressutils-test ${CGREEN_LIBRARIES} + ${GLIB_LDFLAGS} ${GIO_LDFLAGS} ${GPGME_LDFLAGS} ${ZLIB_LDFLAGS} + ${RADIUS_LDFLAGS} ${LIBSSH_LDFLAGS} ${GNUTLS_LDFLAGS} + ${GCRYPT_LDFLAGS} ${LDAP_LDFLAGS} ${REDIS_LDFLAGS} + ${LIBXML2_LDFLAGS} ${UUID_LDFLAGS} + ${LINKER_HARDENING_FLAGS}) + + add_custom_target (tests-compressutils + DEPENDS compressutils-test) + + add_executable (cpeutils-test + EXCLUDE_FROM_ALL + cpeutils_tests.c) + + add_test (cpeutils-test cpeutils-test) + + target_include_directories (cpeutils-test PRIVATE ${CGREEN_INCLUDE_DIRS}) + + target_link_libraries (cpeutils-test ${CGREEN_LIBRARIES} + ${GLIB_LDFLAGS} ${GIO_LDFLAGS} ${GPGME_LDFLAGS} ${ZLIB_LDFLAGS} + ${RADIUS_LDFLAGS} ${LIBSSH_LDFLAGS} ${GNUTLS_LDFLAGS} + ${GCRYPT_LDFLAGS} ${LDAP_LDFLAGS} ${REDIS_LDFLAGS} + ${LIBXML2_LDFLAGS} ${UUID_LDFLAGS} + ${LINKER_HARDENING_FLAGS}) + + add_custom_target (tests-cpeutils + DEPENDS cpeutils-test) + + add_executable (versionutils-test + EXCLUDE_FROM_ALL + versionutils_tests.c) + + add_test (versionutils-test versionutils-test) + + target_include_directories (versionutils-test PRIVATE ${CGREEN_INCLUDE_DIRS}) + + target_link_libraries (versionutils-test ${CGREEN_LIBRARIES} + ${GLIB_LDFLAGS} ${GIO_LDFLAGS} ${GPGME_LDFLAGS} ${ZLIB_LDFLAGS} + ${RADIUS_LDFLAGS} ${LIBSSH_LDFLAGS} ${GNUTLS_LDFLAGS} + ${GCRYPT_LDFLAGS} ${LDAP_LDFLAGS} ${REDIS_LDFLAGS} + ${LIBXML2_LDFLAGS} ${UUID_LDFLAGS} + ${LINKER_HARDENING_FLAGS}) + + add_custom_target (tests-versionutils + DEPENDS versionutils-test) + add_executable (xmlutils-test EXCLUDE_FROM_ALL xmlutils_tests.c) @@ -171,10 +244,10 @@ if (BUILD_TESTS) target_link_libraries (xmlutils-test ${CGREEN_LIBRARIES} ${GLIB_LDFLAGS} ${GIO_LDFLAGS} ${GPGME_LDFLAGS} ${ZLIB_LDFLAGS} - ${RADIUS_LDFLAGS} ${LIBSSH_LDFLAGS} ${GNUTLS_LDFLAGS} - ${GCRYPT_LDFLAGS} ${LDAP_LDFLAGS} ${REDIS_LDFLAGS} - ${LIBXML2_LDFLAGS} ${UUID_LDFLAGS} - ${LINKER_HARDENING_FLAGS}) + ${RADIUS_LDFLAGS} ${LIBSSH_LDFLAGS} ${GNUTLS_LDFLAGS} + ${GCRYPT_LDFLAGS} ${LDAP_LDFLAGS} ${REDIS_LDFLAGS} + ${LIBXML2_LDFLAGS} ${UUID_LDFLAGS} + ${LINKER_HARDENING_FLAGS}) add_custom_target (tests-xmlutils DEPENDS xmlutils-test) diff --git a/util/authutils.c b/util/authutils.c index 706b89c7..a73ff1bd 100644 --- a/util/authutils.c +++ b/util/authutils.c @@ -80,6 +80,25 @@ auth_method_name (auth_method_t method) return authentication_methods[method]; } +/** + * @brief Check if name is a valid auth method name. + * + * @param name Name of auth method. + * + * @return 1 if valid, else 0. + */ +int +auth_method_name_valid (const gchar *name) +{ + int i; + for (i = 0; i < 1000; i++) + if (authentication_methods[i] == NULL) + break; + else if (strcmp (authentication_methods[i], name) == 0) + return 1; + return 0; +} + /** * @brief Initializes Gcrypt. * diff --git a/util/authutils.h b/util/authutils.h index 19b15e18..0d3661cf 100644 --- a/util/authutils.h +++ b/util/authutils.h @@ -32,6 +32,9 @@ typedef enum authentication_method auth_method_t; const gchar *auth_method_name (auth_method_t); +int +auth_method_name_valid (const gchar *); + int gvm_auth_init (void); diff --git a/util/compressutils.c b/util/compressutils.c index a75f928d..0a64d4e2 100644 --- a/util/compressutils.c +++ b/util/compressutils.c @@ -15,6 +15,8 @@ #define ZLIB_CONST #endif +#define _GNU_SOURCE + #include "compressutils.h" #include /* for g_free, g_malloc0 */ @@ -237,3 +239,100 @@ gvm_compress_gzipheader (const void *src, unsigned long srclen, } } } + +/** + * @brief Read decompressed data from a gzip file. + * + * @param[in] cookie The gzFile to read from. + * @param[in] buffer The buffer to output decompressed data to. + * @param[in] buffer_size The size of the buffer. + * + * @return The number of bytes read into the buffer. + */ +static ssize_t +gz_file_read (void *cookie, char *buffer, size_t buffer_size) +{ + gzFile gz_file = cookie; + + return gzread (gz_file, buffer, buffer_size); +} + +/** + * @brief Close a gzip file. + * + * @param[in] cookie The gzFile to close. + * + * @return 0 on success, other values on error (see gzclose() from zlib). + */ +static int +gz_file_close (void *cookie) +{ + gzFile gz_file = cookie; + + return gzclose (gz_file); + ; +} + +/** + * @brief Opens a gzip file as a FILE* stream for reading and decompression. + * + * @param[in] path Path to the gzip file to open. + * + * @return The FILE* on success, NULL otherwise. + */ +FILE * +gvm_gzip_open_file_reader (const char *path) +{ + static cookie_io_functions_t io_functions = { + .read = gz_file_read, + .write = NULL, + .seek = NULL, + .close = gz_file_close, + }; + + if (path == NULL) + { + return NULL; + } + + gzFile gz_file = gzopen (path, "r"); + if (gz_file == NULL) + { + return NULL; + } + + FILE *file = fopencookie (gz_file, "r", io_functions); + return file; +} + +/** + * @brief Opens a gzip file as a FILE* stream for reading and decompression. + * + * @param[in] fd File descriptor of the gzip file to open. + * + * @return The FILE* on success, NULL otherwise. + */ +FILE * +gvm_gzip_open_file_reader_fd (int fd) +{ + static cookie_io_functions_t io_functions = { + .read = gz_file_read, + .write = NULL, + .seek = NULL, + .close = gz_file_close, + }; + + if (fd < 0) + { + return NULL; + } + + gzFile gz_file = gzdopen (fd, "r"); + if (gz_file == NULL) + { + return NULL; + } + + FILE *file = fopencookie (gz_file, "r", io_functions); + return file; +} diff --git a/util/compressutils.h b/util/compressutils.h index 039f0a56..f5debd75 100644 --- a/util/compressutils.h +++ b/util/compressutils.h @@ -11,6 +11,8 @@ #ifndef _GVM_COMPRESSUTILS_H #define _GVM_COMPRESSUTILS_H +#include + void * gvm_compress (const void *, unsigned long, unsigned long *); @@ -20,4 +22,10 @@ gvm_compress_gzipheader (const void *, unsigned long, unsigned long *); void * gvm_uncompress (const void *, unsigned long, unsigned long *); +FILE * +gvm_gzip_open_file_reader (const char *); + +FILE * +gvm_gzip_open_file_reader_fd (int); + #endif /* not _GVM_COMPRESSUTILS_H */ diff --git a/util/compressutils_tests.c b/util/compressutils_tests.c new file mode 100644 index 00000000..a9446be3 --- /dev/null +++ b/util/compressutils_tests.c @@ -0,0 +1,126 @@ +/* SPDX-FileCopyrightText: 2019-2023 Greenbone AG + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "compressutils.c" + +#include +#include +#include + +Describe (compressutils); +BeforeEach (compressutils) +{ +} + +AfterEach (compressutils) +{ +} + +Ensure (compressutils, can_compress_and_uncompress_without_header) +{ + const char *testdata = "TEST-12345-12345-TEST"; + + unsigned long compressed_len; + char *compressed = + gvm_compress (testdata, strlen (testdata) + 1, &compressed_len); + assert_that (compressed_len, is_greater_than (0)); + assert_that (compressed, is_not_null); + assert_that (compressed, is_not_equal_to_string (testdata)); + + unsigned long uncompressed_len; + char *uncompressed = + gvm_uncompress (compressed, compressed_len, &uncompressed_len); + assert_that (uncompressed_len, is_equal_to (strlen (testdata) + 1)); + assert_that (uncompressed, is_equal_to_string (testdata)); +} + +Ensure (compressutils, can_compress_and_uncompress_with_header) +{ + const char *testdata = "TEST-12345-12345-TEST"; + + unsigned long compressed_len; + char *compressed = + gvm_compress_gzipheader (testdata, strlen (testdata) + 1, &compressed_len); + assert_that (compressed_len, is_greater_than (0)); + assert_that (compressed, is_not_null); + assert_that (compressed, is_not_equal_to_string (testdata)); + // Check for gzip magic number and deflate compression mode byte + assert_that (compressed[0], is_equal_to ((char) 0x1f)); + assert_that (compressed[1], is_equal_to ((char) 0x8b)); + assert_that (compressed[2], is_equal_to (8)); + + unsigned long uncompressed_len; + char *uncompressed = + gvm_uncompress (compressed, compressed_len, &uncompressed_len); + assert_that (uncompressed_len, is_equal_to (strlen (testdata) + 1)); + assert_that (uncompressed, is_equal_to_string (testdata)); +} + +Ensure (compressutils, can_uncompress_using_reader) +{ + const char *testdata = "TEST-12345-12345-TEST"; + unsigned long compressed_len; + char *compressed = + gvm_compress_gzipheader (testdata, strlen (testdata) + 1, &compressed_len); + + char compressed_filename[35] = "/tmp/gvm_gzip_test_XXXXXX"; + int compressed_fd = mkstemp (compressed_filename); + write (compressed_fd, compressed, compressed_len); + close (compressed_fd); + + FILE *stream = gvm_gzip_open_file_reader (compressed_filename); + assert_that (stream, is_not_null); + + gchar *uncompressed = g_malloc0 (30); + fread (uncompressed, 1, 30, stream); + assert_that (uncompressed, is_equal_to_string (testdata)); + + assert_that (fclose (stream), is_equal_to (0)); +} + +Ensure (compressutils, can_uncompress_using_fd_reader) +{ + const char *testdata = "TEST-12345-12345-TEST"; + unsigned long compressed_len; + char *compressed = + gvm_compress_gzipheader (testdata, strlen (testdata) + 1, &compressed_len); + + char compressed_filename[35] = "/tmp/gvm_gzip_test_XXXXXX"; + int compressed_fd = mkstemp (compressed_filename); + write (compressed_fd, compressed, compressed_len); + close (compressed_fd); + + compressed_fd = open (compressed_filename, O_RDONLY); + + FILE *stream = gvm_gzip_open_file_reader_fd (compressed_fd); + assert_that (stream, is_not_null); + + gchar *uncompressed = g_malloc0 (30); + fread (uncompressed, 1, 30, stream); + assert_that (uncompressed, is_equal_to_string (testdata)); + + assert_that (fclose (stream), is_equal_to (0)); +} + +/* Test suite. */ +int +main (int argc, char **argv) +{ + TestSuite *suite; + + suite = create_test_suite (); + + add_test_with_context (suite, compressutils, + can_compress_and_uncompress_without_header); + add_test_with_context (suite, compressutils, + can_compress_and_uncompress_with_header); + add_test_with_context (suite, compressutils, can_uncompress_using_reader); + add_test_with_context (suite, compressutils, can_uncompress_using_fd_reader); + + if (argc > 1) + return run_single_test (suite, argv[1], create_text_reporter ()); + + return run_test_suite (suite, create_text_reporter ()); +} diff --git a/util/cpeutils.c b/util/cpeutils.c new file mode 100644 index 00000000..24d53634 --- /dev/null +++ b/util/cpeutils.c @@ -0,0 +1,1631 @@ +/* SPDX-FileCopyrightText: 2009-2024 Greenbone AG + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +/** + * @file + * @brief Functions to convert different CPE notations into each other. + * + * This library provides functions to read the CPE 2.2 URI binding of a + * CPE or the CPE 2.3 formatted string binding of a CPE into a CPE struct + * that corresponds to the WFN naming of a CPE. Further functions to convert + * the CPE struct into the different bindings are provided. + * This file also contains a function that checks if one CPE (represented in a + * CPE struct) is a match for an other CPE (also represented in a CPE struct). + */ + +#include "cpeutils.h" + +#include +#include +#include +#include +#include + +#undef G_LOG_DOMAIN +/** + * @brief GLib logging domain. + */ +#define G_LOG_DOMAIN "libgvm util" + +static enum set_relation +compare_component (const char *, const char *); + +static enum set_relation +compare_strings (const char *, const char *); + +static int +count_escapes (const char *, int, int); + +static gboolean +is_even_wildcards (const char *, int); + +static gboolean +has_wildcards (const char *); + +static int +index_of (const char *, const char *, int); + +static gboolean +is_string (const char *); + +static char * +get_uri_component (const char *, int); + +static char * +decode_uri_component (const char *); + +static void +unpack_sixth_uri_component (const char *, cpe_struct_t *); + +static char * +get_fs_component (const char *, int); + +static char * +unbind_fs_component (char *); + +static char * +add_quoting (const char *); + +static char * +bind_cpe_component_for_uri (const char *); + +static char * +transform_for_uri (const char *); + +static char * +pack_sixth_uri_component (const cpe_struct_t *); + +static char * +bind_cpe_component_for_fs (const char *); + +static char * +process_quoted_chars (const char *); + +static void +trim_pct (char *); + +static void +get_code (char *, const char *); + +/** + * @brief Convert a URI CPE to a formatted string CPE. + * + * @param[in] uri_cpe A CPE v2.2-conformant URI. + * + * @return A formatted string CPE. + */ +char * +uri_cpe_to_fs_cpe (const char *uri_cpe) +{ + cpe_struct_t cpe; + char *fs_cpe; + + cpe_struct_init (&cpe); + uri_cpe_to_cpe_struct (uri_cpe, &cpe); + fs_cpe = cpe_struct_to_fs_cpe (&cpe); + cpe_struct_free (&cpe); + return (fs_cpe); +} + +/** + * @brief Convert a URI CPE to a formatted string product. + * + * @param[in] uri_cpe A CPE v2.2-conformant URI. + * + * @return A formatted string product. + */ +char * +uri_cpe_to_fs_product (const char *uri_cpe) +{ + cpe_struct_t cpe; + char *fs_cpe; + + cpe_struct_init (&cpe); + uri_cpe_to_cpe_struct (uri_cpe, &cpe); + fs_cpe = cpe_struct_to_fs_product (&cpe); + cpe_struct_free (&cpe); + return (fs_cpe); +} + +/** + * @brief Convert a formatted string CPE to a URI CPE. + * + * @param[in] fs_cpe A formatted string CPE. + * + * @return A CPE v2.2-conformant URI. + */ +char * +fs_cpe_to_uri_cpe (const char *fs_cpe) +{ + cpe_struct_t cpe; + char *uri_cpe; + + cpe_struct_init (&cpe); + fs_cpe_to_cpe_struct (fs_cpe, &cpe); + uri_cpe = cpe_struct_to_uri_cpe (&cpe); + cpe_struct_free (&cpe); + return (uri_cpe); +} + +/** + * @brief Convert a formatted string CPE to an URI product. + * + * @param[in] fs_cpe A formatted string CPE. + * + * @return An URI product. + */ +char * +fs_cpe_to_uri_product (const char *fs_cpe) +{ + cpe_struct_t cpe; + char *uri_cpe; + + cpe_struct_init (&cpe); + fs_cpe_to_cpe_struct (fs_cpe, &cpe); + uri_cpe = cpe_struct_to_uri_product (&cpe); + cpe_struct_free (&cpe); + return (uri_cpe); +} + +/** + * @brief Read a URI CPE into the CPE struct. + * + * @param[in] uri_cpe A CPE v2.2-conformant URI. + * + * @param[out] cpe Pointer to the filled CPE struct. + */ +void +uri_cpe_to_cpe_struct (const char *uri_cpe, cpe_struct_t *cpe) +{ + char *uri_component; + + uri_component = get_uri_component (uri_cpe, 1); + cpe->part = decode_uri_component (uri_component); + g_free (uri_component); + uri_component = get_uri_component (uri_cpe, 2); + cpe->vendor = decode_uri_component (uri_component); + g_free (uri_component); + uri_component = get_uri_component (uri_cpe, 3); + cpe->product = decode_uri_component (uri_component); + g_free (uri_component); + uri_component = get_uri_component (uri_cpe, 4); + cpe->version = decode_uri_component (uri_component); + g_free (uri_component); + uri_component = get_uri_component (uri_cpe, 5); + cpe->update = decode_uri_component (uri_component); + g_free (uri_component); + uri_component = get_uri_component (uri_cpe, 6); + if (strcmp (uri_component, "") == 0 || strcmp (uri_component, "-") == 0 + || *uri_component != '~') + cpe->edition = decode_uri_component (uri_component); + else + unpack_sixth_uri_component (uri_component, cpe); + g_free (uri_component); + + uri_component = get_uri_component (uri_cpe, 7); + cpe->language = decode_uri_component (uri_component); + g_free (uri_component); +} + +/** + * @brief Convert a CPE struct into a URI CPE. + * + * @param[in] cpe A pointer to the CPE struct. + * + * @return A CPE v2.2-conformant URI. + */ +char * +cpe_struct_to_uri_cpe (const cpe_struct_t *cpe) +{ + GString *uri_cpe; + char *bind_cpe_component; + uri_cpe = g_string_new ("cpe:/"); + + bind_cpe_component = bind_cpe_component_for_uri (cpe->part); + if (bind_cpe_component) + { + g_string_append (uri_cpe, bind_cpe_component); + g_string_append_c (uri_cpe, ':'); + g_free (bind_cpe_component); + } + bind_cpe_component = bind_cpe_component_for_uri (cpe->vendor); + if (bind_cpe_component) + { + g_string_append (uri_cpe, bind_cpe_component); + g_string_append_c (uri_cpe, ':'); + g_free (bind_cpe_component); + } + bind_cpe_component = bind_cpe_component_for_uri (cpe->product); + if (bind_cpe_component) + { + g_string_append (uri_cpe, bind_cpe_component); + g_string_append_c (uri_cpe, ':'); + g_free (bind_cpe_component); + } + bind_cpe_component = bind_cpe_component_for_uri (cpe->version); + if (bind_cpe_component) + { + g_string_append (uri_cpe, bind_cpe_component); + g_string_append_c (uri_cpe, ':'); + g_free (bind_cpe_component); + } + bind_cpe_component = bind_cpe_component_for_uri (cpe->update); + if (bind_cpe_component) + { + g_string_append (uri_cpe, bind_cpe_component); + g_string_append_c (uri_cpe, ':'); + g_free (bind_cpe_component); + } + bind_cpe_component = pack_sixth_uri_component (cpe); + if (bind_cpe_component) + { + g_string_append (uri_cpe, bind_cpe_component); + g_string_append_c (uri_cpe, ':'); + g_free (bind_cpe_component); + } + bind_cpe_component = bind_cpe_component_for_uri (cpe->language); + if (bind_cpe_component) + { + g_string_append (uri_cpe, bind_cpe_component); + g_string_append_c (uri_cpe, ':'); + g_free (bind_cpe_component); + } + + char *result = g_string_free (uri_cpe, FALSE); + trim_pct (result); + return (result); +} + +/** + * @brief Convert a CPE struct into a URI product. + * + * @param[in] cpe A pointer to the CPE struct. + * + * @return A CPE v2.2-conformant URI product. + */ +char * +cpe_struct_to_uri_product (const cpe_struct_t *cpe) +{ + GString *uri_cpe; + char *bind_cpe_component; + uri_cpe = g_string_new ("cpe:/"); + + bind_cpe_component = bind_cpe_component_for_uri (cpe->part); + if (bind_cpe_component) + { + g_string_append (uri_cpe, bind_cpe_component); + g_string_append_c (uri_cpe, ':'); + g_free (bind_cpe_component); + } + bind_cpe_component = bind_cpe_component_for_uri (cpe->vendor); + if (bind_cpe_component) + { + g_string_append (uri_cpe, bind_cpe_component); + g_string_append_c (uri_cpe, ':'); + g_free (bind_cpe_component); + } + bind_cpe_component = bind_cpe_component_for_uri (cpe->product); + if (bind_cpe_component) + { + g_string_append (uri_cpe, bind_cpe_component); + g_string_append_c (uri_cpe, ':'); + g_free (bind_cpe_component); + } + + char *result = g_string_free (uri_cpe, FALSE); + trim_pct (result); + return (result); +} + +/** + * @brief Get the version from an uri cpe. + * + * @param[in] uri_cpe The uri cpe to get the version from. + * + * @return The version of the uri cpe. + */ +char * +get_version_from_uri_cpe (const char *uri_cpe) +{ + char *version = get_uri_component (uri_cpe, 4); + char *decoded_version = decode_uri_component (version); + g_free (version); + return decoded_version; +} + +/** + * @brief Read a formatted string CPE into the CPE struct. + * + * @param[in] fs_cpe A formatted string CPE. + * + * @param[out] cpe Pointer to the filled CPE struct. + */ +void +fs_cpe_to_cpe_struct (const char *fs_cpe, cpe_struct_t *cpe) +{ + char *fs_component; + + fs_component = get_fs_component (fs_cpe, 2); + cpe->part = unbind_fs_component (fs_component); + fs_component = get_fs_component (fs_cpe, 3); + cpe->vendor = unbind_fs_component (fs_component); + fs_component = get_fs_component (fs_cpe, 4); + cpe->product = unbind_fs_component (fs_component); + fs_component = get_fs_component (fs_cpe, 5); + cpe->version = unbind_fs_component (fs_component); + fs_component = get_fs_component (fs_cpe, 6); + cpe->update = unbind_fs_component (fs_component); + fs_component = get_fs_component (fs_cpe, 7); + cpe->edition = unbind_fs_component (fs_component); + fs_component = get_fs_component (fs_cpe, 8); + cpe->language = unbind_fs_component (fs_component); + fs_component = get_fs_component (fs_cpe, 9); + cpe->sw_edition = unbind_fs_component (fs_component); + fs_component = get_fs_component (fs_cpe, 10); + cpe->target_sw = unbind_fs_component (fs_component); + fs_component = get_fs_component (fs_cpe, 11); + cpe->target_hw = unbind_fs_component (fs_component); + fs_component = get_fs_component (fs_cpe, 12); + cpe->other = unbind_fs_component (fs_component); +} + +/** + * @brief Convert a CPE struct into a formatted string CPE. + * + * @param[in] cpe A pointer to the CPE struct. + * + * @return A formatted string CPE. + */ +char * +cpe_struct_to_fs_cpe (const cpe_struct_t *cpe) +{ + GString *fs_cpe; + char *bind_cpe_component; + + fs_cpe = g_string_new ("cpe:2.3:"); + + bind_cpe_component = bind_cpe_component_for_fs (cpe->part); + if (bind_cpe_component) + { + g_string_append (fs_cpe, bind_cpe_component); + g_string_append_c (fs_cpe, ':'); + g_free (bind_cpe_component); + } + bind_cpe_component = bind_cpe_component_for_fs (cpe->vendor); + if (bind_cpe_component) + { + g_string_append (fs_cpe, bind_cpe_component); + g_string_append_c (fs_cpe, ':'); + g_free (bind_cpe_component); + } + bind_cpe_component = bind_cpe_component_for_fs (cpe->product); + if (bind_cpe_component) + { + g_string_append (fs_cpe, bind_cpe_component); + g_string_append_c (fs_cpe, ':'); + g_free (bind_cpe_component); + } + bind_cpe_component = bind_cpe_component_for_fs (cpe->version); + if (bind_cpe_component) + { + g_string_append (fs_cpe, bind_cpe_component); + g_string_append_c (fs_cpe, ':'); + g_free (bind_cpe_component); + } + bind_cpe_component = bind_cpe_component_for_fs (cpe->update); + if (bind_cpe_component) + { + g_string_append (fs_cpe, bind_cpe_component); + g_string_append_c (fs_cpe, ':'); + g_free (bind_cpe_component); + } + bind_cpe_component = bind_cpe_component_for_fs (cpe->edition); + if (bind_cpe_component) + { + g_string_append (fs_cpe, bind_cpe_component); + g_string_append_c (fs_cpe, ':'); + g_free (bind_cpe_component); + } + bind_cpe_component = bind_cpe_component_for_fs (cpe->language); + if (bind_cpe_component) + { + g_string_append (fs_cpe, bind_cpe_component); + g_string_append_c (fs_cpe, ':'); + g_free (bind_cpe_component); + } + bind_cpe_component = bind_cpe_component_for_fs (cpe->sw_edition); + if (bind_cpe_component) + { + g_string_append (fs_cpe, bind_cpe_component); + g_string_append_c (fs_cpe, ':'); + g_free (bind_cpe_component); + } + bind_cpe_component = bind_cpe_component_for_fs (cpe->target_sw); + if (bind_cpe_component) + { + g_string_append (fs_cpe, bind_cpe_component); + g_string_append_c (fs_cpe, ':'); + g_free (bind_cpe_component); + } + bind_cpe_component = bind_cpe_component_for_fs (cpe->target_hw); + if (bind_cpe_component) + { + g_string_append (fs_cpe, bind_cpe_component); + g_string_append_c (fs_cpe, ':'); + g_free (bind_cpe_component); + } + bind_cpe_component = bind_cpe_component_for_fs (cpe->other); + if (bind_cpe_component) + { + g_string_append (fs_cpe, bind_cpe_component); + g_free (bind_cpe_component); + } + return (g_string_free (fs_cpe, FALSE)); +} + +/** + * @brief Convert a CPE struct into a formatted string product. + * + * @param[in] cpe A pointer to the CPE struct. + * + * @return A formatted string product. + */ +char * +cpe_struct_to_fs_product (const cpe_struct_t *cpe) +{ + GString *fs_cpe; + char *bind_cpe_component; + + fs_cpe = g_string_new ("cpe:2.3:"); + + bind_cpe_component = bind_cpe_component_for_fs (cpe->part); + if (bind_cpe_component) + { + g_string_append (fs_cpe, bind_cpe_component); + g_string_append_c (fs_cpe, ':'); + g_free (bind_cpe_component); + } + bind_cpe_component = bind_cpe_component_for_fs (cpe->vendor); + if (bind_cpe_component) + { + g_string_append (fs_cpe, bind_cpe_component); + g_string_append_c (fs_cpe, ':'); + g_free (bind_cpe_component); + } + bind_cpe_component = bind_cpe_component_for_fs (cpe->product); + if (bind_cpe_component) + { + g_string_append (fs_cpe, bind_cpe_component); + g_string_append_c (fs_cpe, ':'); + g_free (bind_cpe_component); + } + return (g_string_free (fs_cpe, FALSE)); +} + +/** + * @brief Get the indexth component of a URI CPE. + * + * @param[in] uri_cpe The URI CPE. + * @param[in] index The number of the component to get. + * + * @return The indexth component of the URI CPE. + */ +static char * +get_uri_component (const char *uri_cpe, int index) +{ + char *component = NULL; + char *c; + char *component_start, *component_end; + + if (!uri_cpe) + return NULL; + + c = (char *) uri_cpe; + + /* find start of component */ + for (int i = 0; *c != '\0' && i < index; c++) + { + if (*c == ':') + i++; + } + + if (index == 1 && *c != '\0') + c++; + + component_start = c; + + /* find end of component */ + if (*component_start == '\0') + component_end = component_start; + else + { + for (c = component_start; *c != '\0' && *c != ':'; c++) + ; + } + + component_end = c; + + if (component_start >= component_end || component_end == 0) + component = (char *) g_strdup (""); + else + component = g_strndup (component_start, component_end - component_start); + + return component; +} + +/** + * @brief Decode a component of a URI CPE. + * + * @param[in] component The component to decode. + * + * @return The decoded component of the URI CPE. + */ +static char * +decode_uri_component (const char *component) +{ + GString *decoded_component; + char *escapes = "!\"#$%&'()*+,/:;<=>?@[\\]^`{|}~"; + char *tmp_component; + char code_a[4], code_b[4], code_c[4]; + long unsigned int index; + gboolean embedded; + + if (!component) + return (NULL); + + if (strcmp (component, "") == 0 || strcmp (component, " ") == 0) + { + return (g_strdup ("ANY")); + } + if (strcmp (component, "-") == 0) + { + return (g_strdup ("NA")); + } + + tmp_component = g_strdup (component); + + /* set all characters to lowercase */ + char *c = tmp_component; + for (; *c; c++) + *c = tolower (*c); + + index = 0; + embedded = FALSE; + decoded_component = g_string_sized_new (2 * strlen (component)); + + char l; + char *unescaped; + while (index < strlen (tmp_component)) + { + l = *(tmp_component + index); + + if (l == '.' || l == '-' || l == '~') + { + g_string_append_c (decoded_component, '\\'); + g_string_append_c (decoded_component, l); + index++; + embedded = TRUE; + continue; + } + if (l != '%') + { + g_string_append_c (decoded_component, l); + index++; + embedded = TRUE; + continue; + } + + get_code (code_a, tmp_component + index); + + if (strcmp (code_a, "%01") == 0) + { + if (index >= 3) + get_code (code_b, tmp_component + index - 3); + else + code_b[0] = '0'; + if (strlen (tmp_component) >= index + 6) + get_code (code_c, tmp_component + index + 3); + else + code_c[0] = '0'; + if ((index == 0 || index == strlen (tmp_component) - 3) + || (!embedded && strcmp (code_b, "%01")) + || (embedded && strcmp (code_c, "%01"))) + { + g_string_append_c (decoded_component, '?'); + index = index + 3; + continue; + } + else + { + g_string_free (decoded_component, TRUE); + g_free (tmp_component); + return (NULL); + } + } + + if (strcmp (code_a, "%02") == 0) + { + if (index == 0 || index == strlen (tmp_component) - 3) + { + g_string_append_c (decoded_component, '*'); + index = index + 3; + continue; + } + else + { + g_string_free (decoded_component, TRUE); + g_free (tmp_component); + return (NULL); + } + } + + unescaped = g_uri_unescape_string (code_a, NULL); + if (unescaped && strchr (escapes, *unescaped)) + { + g_string_append_c (decoded_component, '\\'); + g_string_append (decoded_component, unescaped); + g_free (unescaped); + } + else if (unescaped) + { + g_string_append (decoded_component, unescaped); + g_free (unescaped); + } + else + { + g_string_free (decoded_component, TRUE); + g_free (tmp_component); + return (NULL); + } + index = index + 3; + embedded = TRUE; + } + + g_free (tmp_component); + return (g_string_free (decoded_component, FALSE)); +} + +/** + * @brief Unpack the sixth component of a URI CPE. + * + * @param[in] component The component to unpack. + * + * @param[out] cpe Pointer to the CPE struct where the unpacked and + * decoded values of the component are stored. + */ +static void +unpack_sixth_uri_component (const char *component, cpe_struct_t *cpe) +{ + const char *start = component + 1; + const char *end; + + char *edition, *sw_edition, *target_sw, *target_hw, *other; + + end = strchr (start, '~'); + if (start >= end || end == NULL) + edition = strdup (""); + else + edition = g_strndup (start, end - start); + + if (end != NULL) + { + start = end + 1; + end = strchr (start, '~'); + if (start >= end || end == NULL) + sw_edition = strdup (""); + else + sw_edition = g_strndup (start, end - start); + } + else + sw_edition = strdup (""); + + if (end != NULL) + { + start = end + 1; + end = strchr (start, '~'); + if (start >= end || end == NULL) + target_sw = strdup (""); + else + target_sw = g_strndup (start, end - start); + } + else + target_sw = strdup (""); + + if (end != NULL) + { + start = end + 1; + end = strchr (start, '~'); + if (start >= end || end == NULL) + target_hw = strdup (""); + else + target_hw = g_strndup (start, end - start); + } + else + target_hw = strdup (""); + + if (end != NULL) + { + start = end + 1; + end = component + strlen (component); + if (start >= end) + other = strdup (""); + else + other = g_strndup (start, end - start); + } + else + other = strdup (""); + + cpe->edition = decode_uri_component (edition); + g_free (edition); + cpe->sw_edition = decode_uri_component (sw_edition); + g_free (sw_edition); + cpe->target_sw = decode_uri_component (target_sw); + g_free (target_sw); + cpe->target_hw = decode_uri_component (target_hw); + g_free (target_hw); + cpe->other = decode_uri_component (other); + g_free (other); +} + +/** + * @brief Get the indexth component of a formatted string CPE. + * + * @param[in] fs_cpe The formatted string CPE. + * @param[in] index The number of the component to get. + * + * @return The indexth component of the formatted string CPE. + */ +static char * +get_fs_component (const char *fs_cpe, int index) +{ + char *component = NULL; + char *c; + char *component_start, *component_end; + gboolean escaped; + + if (!fs_cpe) + return NULL; + + if (*fs_cpe == '\0') + return ((char *) g_strdup ("")); + + c = (char *) fs_cpe; + + /* find start of component */ + escaped = FALSE; + if (index == 0) + component_start = c; + else + { + for (int i = 0; *c != '\0' && i < index; c++) + { + if (*c == ':' && !escaped) + i++; + else if (*c == '\\' && !escaped) + escaped = TRUE; + else + escaped = FALSE; + } + component_start = c; + } + + /* find end of component */ + escaped = FALSE; + if (*component_start == '\0') + component_end = component_start; + else + { + for (c = component_start; *c != '\0'; c++) + { + if (*c == ':' && !escaped) + break; + if (*c == '\\' && !escaped) + escaped = TRUE; + else + escaped = FALSE; + } + } + + component_end = c; + + if (component_start >= component_end || component_end == NULL) + component = (char *) g_strdup (""); + else + component = g_strndup (component_start, component_end - component_start); + + return component; +} + +/** + * @brief Unbind a formatted string CPE component. + * + * @param[in] component The component to unbind. + * + * @return The unbound component of the formatted string CPE. + */ +static char * +unbind_fs_component (char *component) +{ + char *unbound_component; + + if (strcmp (component, "*") == 0) + { + g_free (component); + return ((char *) g_strdup ("ANY")); + } + if (strcmp (component, "-") == 0) + { + g_free (component); + return ((char *) g_strdup ("NA")); + } + + unbound_component = add_quoting (component); + g_free (component); + return (unbound_component); +} + +/** + * @brief Handle the quoting for an unbind formatted string CPE component. + * + * @param[in] component The component to add the quotings to. + * + * @return The component of the formatted string CPE with all necessary + * quotes added. + */ +static char * +add_quoting (const char *component) +{ + GString *quoted_component; + char *tmp_component; + char *c; + gboolean embedded; + + if (!component) + return (NULL); + + quoted_component = g_string_sized_new (2 * strlen (component)); + tmp_component = (char *) g_strdup (component); + embedded = FALSE; + + /* set all characters to lowercase */ + for (c = tmp_component; *c; c++) + *c = tolower (*c); + + c = tmp_component; + while (*c != '\0') + { + if (g_ascii_isalnum (*c) || *c == '_') + { + g_string_append_c (quoted_component, *c); + c++; + embedded = TRUE; + continue; + } + if (*c == '\\') + { + c++; + if (*c != '\0') + { + g_string_append_c (quoted_component, '\\'); + g_string_append_c (quoted_component, *c); + embedded = TRUE; + c++; + continue; + } + } + if (*c == '*') + { + if ((c == tmp_component) + || (c == tmp_component + strlen (tmp_component - 1))) + { + g_string_append_c (quoted_component, *c); + c++; + embedded = TRUE; + continue; + } + else + { + g_free (tmp_component); + return (NULL); + } + } + if (*c == '?') + { + if ((c == tmp_component) + || (c == tmp_component + strlen (tmp_component - 1)) + || (!embedded && (c > tmp_component) && (*(c - 1) == '?')) + || (embedded && *(c + 1) == '?')) + { + g_string_append_c (quoted_component, *c); + c++; + embedded = FALSE; + continue; + } + else + { + g_free (tmp_component); + return (NULL); + } + } + g_string_append_c (quoted_component, '\\'); + g_string_append_c (quoted_component, *c); + c++; + embedded = TRUE; + } + g_free (tmp_component); + return (g_string_free (quoted_component, FALSE)); +} + +/** + * @brief Bind a CPE component for a URI CPE. + * + * @param[in] component The component to bind. + * + * @return The bound component for the URI CPE. + */ +static char * +bind_cpe_component_for_uri (const char *component) +{ + if (!component) + return (g_strdup ("")); + if (strcmp (component, "") == 0) + return (g_strdup ("")); + if (strcmp (component, "ANY") == 0) + return (g_strdup ("")); + if (strcmp (component, "NA") == 0) + return (g_strdup ("-")); + return (transform_for_uri (component)); +} + +/** + * @brief Transform a CPE component for a URI CPE. + * + * @param[in] component The component to transform. + * + * @return The transformed component for the URI CPE. + */ +static char * +transform_for_uri (const char *component) +{ + GString *result; + char *tmp_component; + char *c; + + if (!component) + return (g_strdup ("")); + if (strcmp (component, "") == 0) + return (g_strdup ("")); + + tmp_component = g_strdup (component); + + /* set all characters to lowercase */ + for (c = tmp_component; *c; c++) + *c = tolower (*c); + + result = g_string_new (""); + c = tmp_component; + + while (*c) + { + if ((g_ascii_isalnum (*c) || *c == '_') && *c != '-') + { + g_string_append_c (result, *c); + c++; + continue; + } + if (*c == '\\') + { + c++; + if (*c != '\0') + { + char to_escape[2]; + char *escaped; + to_escape[0] = *c; + to_escape[1] = '\0'; + escaped = g_uri_escape_string (to_escape, NULL, FALSE); + g_string_append (result, escaped); + g_free (escaped); + c++; + } + continue; + } + if (*c == '?') + g_string_append (result, "%01"); + if (*c == '*') + g_string_append (result, "%02"); + c++; + } + g_free (tmp_component); + return (g_string_free (result, FALSE)); +} + +/** + * @brief Pack the sixth component of a URI CPE. + * + * @param[in] component The CPE struct with the components to pack into the + * sixth component of a URI CPE. + * + * @return The packed component for the URI CPE. + */ +static char * +pack_sixth_uri_component (const cpe_struct_t *cpe) +{ + if ((cpe->sw_edition == NULL || strcmp (cpe->sw_edition, "") == 0) + && (cpe->target_sw == NULL || strcmp (cpe->target_sw, "") == 0) + && (cpe->target_hw == NULL || strcmp (cpe->target_hw, "") == 0) + && (cpe->other == NULL || strcmp (cpe->other, "") == 0)) + { + if (strcmp (cpe->edition, "ANY") == 0) + return (g_strdup ("")); + if (strcmp (cpe->edition, "NA") == 0) + return (g_strdup ("-")); + return (g_strdup (cpe->edition)); + } + + char *edition = bind_cpe_component_for_uri (cpe->edition); + char *sw_edition = bind_cpe_component_for_uri (cpe->sw_edition); + char *target_sw = bind_cpe_component_for_uri (cpe->target_sw); + char *target_hw = bind_cpe_component_for_uri (cpe->target_hw); + char *other = bind_cpe_component_for_uri (cpe->other); + GString *component; + component = g_string_new (""); + if (!((!sw_edition || strcmp (sw_edition, "") == 0) + && (!target_sw || strcmp (target_sw, "") == 0) + && (!target_hw || strcmp (target_hw, "") == 0) + && (!other || strcmp (other, "") == 0))) + g_string_append_printf (component, "~%s~%s~%s~%s~%s", edition, sw_edition, + target_sw, target_hw, other); + else if (edition) + g_string_append (component, edition); + + if (edition) + g_free (edition); + if (sw_edition) + g_free (sw_edition); + if (target_sw) + g_free (target_sw); + if (target_hw) + g_free (target_hw); + if (other) + g_free (other); + return (g_string_free (component, FALSE)); +} + +/** + * @brief Bind a CPE component for a formatted string CPE. + * + * @param[in] component The component to bind. + * + * @return The bound component for the formatted string CPE. + */ +static char * +bind_cpe_component_for_fs (const char *component) +{ + if (!component) + return (g_strdup ("*")); + if (strcmp (component, "") == 0) + return (g_strdup ("*")); + if (strcmp (component, "ANY") == 0) + return (g_strdup ("*")); + if (strcmp (component, "NA") == 0) + return (g_strdup ("-")); + return (process_quoted_chars (component)); +} + +/** + * @brief Process the quoted characters of a CPE component for + * a formatted string CPE. + * + * @param[in] component The component to process. + * + * @return The processed component for the formatted string CPE. + */ +static char * +process_quoted_chars (const char *component) +{ + if (!component) + return (g_strdup ("")); + if (strcmp (component, "") == 0) + return (g_strdup ("")); + + GString *fs_component; + fs_component = g_string_new (""); + char *c = (char *) component; + char next_c; + + while (*c) + { + if (*c != '\\') + { + g_string_append_c (fs_component, *c); + c++; + } + else + { + next_c = *(c + 1); + if (next_c == '.' || next_c == '-' || next_c == '_') + { + g_string_append_c (fs_component, next_c); + c += 2; + } + else if (next_c) + { + g_string_append_c (fs_component, '\\'); + g_string_append_c (fs_component, next_c); + c += 2; + } + } + } + return (g_string_free (fs_component, FALSE)); +} + +/** + * @brief Initialize a CPE struct. + * + * @param[in/out] cpe The pointer to the CPE to initialize. + */ +void +cpe_struct_init (cpe_struct_t *cpe) +{ + cpe->part = NULL; + cpe->vendor = NULL; + cpe->product = NULL; + cpe->version = NULL; + cpe->update = NULL; + cpe->edition = NULL; + cpe->sw_edition = NULL; + cpe->target_sw = NULL; + cpe->target_hw = NULL; + cpe->other = NULL; + cpe->language = NULL; + + /* to keep the compiler satisfied */ + cpe->part = cpe->part; +} + +/** + * @brief Free a CPE struct. + * + * @param[in/out] cpe The CPE to be freed. + */ +void +cpe_struct_free (cpe_struct_t *cpe) +{ + if (!cpe) + return; + if (cpe->part) + g_free (cpe->part); + if (cpe->vendor) + g_free (cpe->vendor); + if (cpe->product) + g_free (cpe->product); + if (cpe->version) + g_free (cpe->version); + if (cpe->update) + g_free (cpe->update); + if (cpe->edition) + g_free (cpe->edition); + if (cpe->sw_edition) + g_free (cpe->sw_edition); + if (cpe->target_sw) + g_free (cpe->target_sw); + if (cpe->target_hw) + g_free (cpe->target_hw); + if (cpe->other) + g_free (cpe->other); + if (cpe->language) + g_free (cpe->language); +} + +/** + * @brief Cut of trailing ':' signs. + * + * @param[in/out] str The string to be processed. + */ +static void +trim_pct (char *str) +{ + char *c; + + if (!str) + return; + c = str + strlen (str) - 1; + while (c >= str) + { + if (*c == ':') + { + *c = '\0'; + c--; + } + else + break; + } +} + +/** + * @brief Get the percent code from the start of a string. + * + * @param[in] str The string to get the code from. + * @param[out] code The percent code. + */ +static void +get_code (char *code, const char *str) +{ + code[0] = *str; + code[1] = *(str + 1); + code[2] = *(str + 2); + code[3] = '\0'; +} + +/** + * @brief Returns if source is a match for target. That means + * that source is a superset of target. + * + * @param[in] source The cpe_struct that represents a set of CPEs. + * @param[in] target The cpe_struct that represents a single CPE or + * or a set of CPEs that is checked if it is a + * subset of source meaning that it is matched by + * source. + * + * @return Returns if source is a match for target. + */ +gboolean +cpe_struct_match (cpe_struct_t *source, cpe_struct_t *target) +{ + enum set_relation relation; + + relation = compare_component (source->part, target->part); + if (relation != SUPERSET && relation != EQUAL) + return (FALSE); + relation = compare_component (source->vendor, target->vendor); + if (relation != SUPERSET && relation != EQUAL) + return (FALSE); + relation = compare_component (source->product, target->product); + if (relation != SUPERSET && relation != EQUAL) + return (FALSE); + relation = compare_component (source->version, target->version); + if (relation != SUPERSET && relation != EQUAL) + return (FALSE); + relation = compare_component (source->update, target->update); + if (relation != SUPERSET && relation != EQUAL) + return (FALSE); + relation = compare_component (source->edition, target->edition); + if (relation != SUPERSET && relation != EQUAL) + return (FALSE); + relation = compare_component (source->sw_edition, target->sw_edition); + if (relation != SUPERSET && relation != EQUAL) + return (FALSE); + relation = compare_component (source->target_sw, target->target_sw); + if (relation != SUPERSET && relation != EQUAL) + return (FALSE); + relation = compare_component (source->target_hw, target->target_hw); + if (relation != SUPERSET && relation != EQUAL) + return (FALSE); + relation = compare_component (source->other, target->other); + if (relation != SUPERSET && relation != EQUAL) + return (FALSE); + relation = compare_component (source->language, target->language); + if (relation != SUPERSET && relation != EQUAL) + return (FALSE); + + return (TRUE); +} + +/** + * @brief Returns if the part behind the version of source is a match + * for that part of target. That means, that source is a superset + * of target if also the first part matches. + * + * @param[in] source The cpe_struct that represents a set of CPEs. + * @param[in] target The cpe_struct that represents a single CPE or + * or a set of CPEs that is checked if it is a + * subset of source meaning that it is matched by + * source. + * + * @return Returns if source is a match for target. + */ +gboolean +cpe_struct_match_tail (cpe_struct_t *source, cpe_struct_t *target) +{ + enum set_relation relation; + + relation = compare_component (source->update, target->update); + if (relation != SUPERSET && relation != EQUAL) + return (FALSE); + relation = compare_component (source->edition, target->edition); + if (relation != SUPERSET && relation != EQUAL) + return (FALSE); + relation = compare_component (source->sw_edition, target->sw_edition); + if (relation != SUPERSET && relation != EQUAL) + return (FALSE); + relation = compare_component (source->target_sw, target->target_sw); + if (relation != SUPERSET && relation != EQUAL) + return (FALSE); + relation = compare_component (source->target_hw, target->target_hw); + if (relation != SUPERSET && relation != EQUAL) + return (FALSE); + relation = compare_component (source->other, target->other); + if (relation != SUPERSET && relation != EQUAL) + return (FALSE); + relation = compare_component (source->language, target->language); + if (relation != SUPERSET && relation != EQUAL) + return (FALSE); + + return (TRUE); +} + +/** + * @brief Returns if the component "source" is a match for the component + * "target". That means that source is a superset of target. + * + * @param[in] source The component of a cpe_struct. + * @param[in] target The component of a cpe_struct that is checked if it + * is a subset of source meaning that it is matched by + * source. + * + * @return Returns if source is a match for target. + */ +static enum set_relation +compare_component (const char *source, const char *target) +{ + enum set_relation result; + char *source_cpy, *target_cpy; + char *c; + + if (source) + source_cpy = g_strdup (source); + else + source_cpy = g_strdup ("ANY"); + if (target) + target_cpy = g_strdup (target); + else + target_cpy = g_strdup ("ANY"); + + if (is_string (source_cpy)) + { + /* set all characters to lowercase */ + for (c = source_cpy; *c; c++) + *c = tolower (*c); + } + if (is_string (target_cpy)) + { + /* set all characters to lowercase */ + for (c = target_cpy; *c; c++) + *c = tolower (*c); + } + if (is_string (target_cpy) && has_wildcards (target_cpy)) + { + g_free (source_cpy); + g_free (target_cpy); + return (UNDEFINED); + } + if (strcmp (source_cpy, target_cpy) == 0) + { + g_free (source_cpy); + g_free (target_cpy); + return (EQUAL); + } + if (strcmp (source_cpy, "ANY") == 0) + { + g_free (source_cpy); + g_free (target_cpy); + return (SUPERSET); + } + if (strcmp (target_cpy, "ANY") == 0) + { + g_free (source_cpy); + g_free (target_cpy); + return (SUBSET); + } + if (strcmp (target_cpy, "NA") == 0 || strcmp (source_cpy, "NA") == 0) + { + g_free (source_cpy); + g_free (target_cpy); + return (DISJOINT); + } + + result = compare_strings (source_cpy, target_cpy); + g_free (source_cpy); + g_free (target_cpy); + return (result); +} + +/** + * @brief Returns if the string of a component "source" is a match for the + * the string of a component "target". That means that source + * represents a superset of target. + * + * @param[in] source The string of a component of a cpe_struct. + * @param[in] target The string of a component of a cpe_struct that is + * checked if it represents a subset of source meaning + * that it is matched by source. + * + * @return Returns if source is a match for target. + */ +static enum set_relation +compare_strings (const char *source, const char *target) +{ + int start = 0; + int end = strlen (source); + int begins = 0; + int ends = 0; + + char *sub_source; + + if (*source == '*') + { + start = 1; + begins = -1; + } + else + { + while (start < (int) strlen (source) && *(source + start) == '?') + { + start++; + begins++; + } + } + if (*(source + end - 1) == '*' && is_even_wildcards (source, end - 1)) + { + end--; + ends = -1; + } + else + { + while (end > 0 && *(source + end - 1) == '?' + && is_even_wildcards (source, end - 1)) + { + end--; + ends++; + } + } + + sub_source = g_strndup (source + start, end - start); + int index = -1; + int escapes = 0; + int leftover = strlen (target); + + while (leftover > 0) + { + index = index_of (target, sub_source, index + 1); + if (index == -1) + break; + escapes = count_escapes (target, 0, index); + if (index > 0 && begins != -1 && begins < (index - escapes)) + break; + escapes = count_escapes (target, index + 1, strlen (target)); + leftover = strlen (target) - index - escapes - strlen (sub_source); + if (leftover > 0 && (ends != -1 && leftover > ends)) + continue; + g_free (sub_source); + return SUPERSET; + } + g_free (sub_source); + return DISJOINT; +} + +/** + * @brief Counts the number of unescaped escape signs ("\") in a specified + * part of a string. + * + * @param[in] str The string to be examined. + * @param[in] start The start position in the string where the examination + * begins. + * @param[in] end The end position in the string where the examination + * ends. + * + * @return Returns the number of unescaped escape signs in the specified + * part of the string. + */ +static int +count_escapes (const char *str, int start, int end) +{ + int result = 0; + gboolean active = FALSE; + + for (int i = 0; i < end && *(str + i) != '\0'; i++) + { + active = (!active && *(str + i) == '\\'); + if (active && i >= start) + result++; + } + return (result); +} + +/** + * @brief Returns true if an even number of escape (backslash) characters + * precede the character at the index "index" in string "str". + * + * @param[in] str The string to be examined. + * @param[in] index The index where the examination starts. + * + * @return Returns if an even number of escape characters precede the + * character at index "index". + */ +static gboolean +is_even_wildcards (const char *str, int index) +{ + int result = 0; + + while (index > 0 && *(str + index - 1) == '\\') + { + index--; + result++; + } + return ((result % 2) == 0); +} + +/** + * @brief Returns if a given string contains wildcards ("*" or "?"). + * + * @param[in] str The string to be examined. + * + * @return Returns TRUE if the string contains wildcards. FALSE otherwise. + */ +static gboolean +has_wildcards (const char *str) +{ + char *c = (char *) str; + gboolean active = FALSE; + + while (*c != '\0') + { + if (!active && (*c == '?' || *c == '*')) + return TRUE; + + if (!active && *c == '\\') + active = TRUE; + else + active = FALSE; + + c++; + } + return FALSE; +} + +/** + * @brief Searches the string "str" for the first occurrence of the string + * "sub_str", starting at the offset "offset" in "str". + * + * @param[in] str The string to be examined. + * @param[in] sub_str The string to be searched for in "str". + * @param[in] offset The offset where to start the search in "str". + * + * @return Returns the index where the string "sub_str" starts in "str", if + * the string "sub_str" was found, -1 otherwise. + */ +static int +index_of (const char *str, const char *sub_str, int offset) +{ + char *start; + char *begin_substr; + + if (offset > (int) strlen (str)) + return (-1); + + start = (char *) str + offset; + begin_substr = strstr (start, sub_str); + if (begin_substr == NULL) + return (-1); + return (begin_substr - str); +} + +/** + * @brief Returns if a string is an ordinary string and does not represent + * one of the logical values "ANY" or "NA". + * + * @param[in] str The string to be examined. + * + * @return Returns TRUE if the string "str" does not represent one of the + * logical values "ANY" or "NA". Returns FALSE otherwise. + */ +static gboolean +is_string (const char *str) +{ + if (!str) + return TRUE; + + return (strcmp (str, "ANY") && strcmp (str, "NA")); +} diff --git a/util/cpeutils.h b/util/cpeutils.h new file mode 100644 index 00000000..e53398a6 --- /dev/null +++ b/util/cpeutils.h @@ -0,0 +1,93 @@ +/* SPDX-FileCopyrightText: 2009-2024 Greenbone AG + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +/** + * @file + * @brief Headers for CPE utils. + */ + +#ifndef _GVM_CPEUTILS_H +#define _GVM_CPEUTILS_H + +#include +#include + +/** + * @brief XML context. + * + * This structure is used to represent the WFN naming of a CPE. + */ +typedef struct +{ + char *part; + char *vendor; + char *product; + char *version; + char *update; + char *edition; + char *sw_edition; + char *target_sw; + char *target_hw; + char *other; + char *language; +} cpe_struct_t; + +char * +uri_cpe_to_fs_cpe (const char *); + +char * +uri_cpe_to_fs_product (const char *); + +char * +fs_cpe_to_uri_cpe (const char *); + +char * +fs_cpe_to_uri_product (const char *); + +void +uri_cpe_to_cpe_struct (const char *, cpe_struct_t *); + +char * +cpe_struct_to_uri_cpe (const cpe_struct_t *); + +char * +cpe_struct_to_uri_product (const cpe_struct_t *); + +char * +get_version_from_uri_cpe (const char *); + +void +fs_cpe_to_cpe_struct (const char *, cpe_struct_t *); + +char * +cpe_struct_to_fs_cpe (const cpe_struct_t *); + +char * +cpe_struct_to_fs_product (const cpe_struct_t *); + +void +cpe_struct_init (cpe_struct_t *); + +void +cpe_struct_free (cpe_struct_t *); + +gboolean +cpe_struct_match (cpe_struct_t *, cpe_struct_t *); + +gboolean +cpe_struct_match_tail (cpe_struct_t *, cpe_struct_t *); + +enum set_relation +{ + DISJOINT, + EQUAL, + SUBSET, + SUPERSET, + UNDEFINED +}; + +#define CPE_COMPONENT_IS_ANY(component) (component[0] == 'A') + +#endif diff --git a/util/cpeutils_tests.c b/util/cpeutils_tests.c new file mode 100644 index 00000000..6e4d2cb9 --- /dev/null +++ b/util/cpeutils_tests.c @@ -0,0 +1,304 @@ +/* SPDX-FileCopyrightText: 2019-2023 Greenbone AG + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "cpeutils.c" + +#include +#include + +Describe (cpeutils); +BeforeEach (cpeutils) +{ +} + +AfterEach (cpeutils) +{ +} + +/* parse_entity */ + +Ensure (cpeutils, uri_cpe_to_cpe_struct) +{ + cpe_struct_t cpe; + char *uri_cpe; + + uri_cpe = "cpe:/a:microsoft:internet_explorer:8.0.6001:beta"; + cpe_struct_init (&cpe); + uri_cpe_to_cpe_struct (uri_cpe, &cpe); + assert_that (cpe.part, is_equal_to_string ("a")); + assert_that (cpe.vendor, is_equal_to_string ("microsoft")); + assert_that (cpe.product, is_equal_to_string ("internet_explorer")); + assert_that (cpe.version, is_equal_to_string ("8\\.0\\.6001")); + assert_that (cpe.update, is_equal_to_string ("beta")); + assert_that (cpe.edition, is_equal_to_string ("ANY")); + assert_that (cpe.language, is_equal_to_string ("ANY")); + cpe_struct_free (&cpe); + + uri_cpe = "cpe:/a:microsoft:internet_explorer:8.%2a:sp%3f"; + cpe_struct_init (&cpe); + uri_cpe_to_cpe_struct (uri_cpe, &cpe); + assert_that (cpe.part, is_equal_to_string ("a")); + assert_that (cpe.vendor, is_equal_to_string ("microsoft")); + assert_that (cpe.product, is_equal_to_string ("internet_explorer")); + assert_that (cpe.version, is_equal_to_string ("8\\.\\*")); + assert_that (cpe.update, is_equal_to_string ("sp\\?")); + assert_that (cpe.edition, is_equal_to_string ("ANY")); + assert_that (cpe.language, is_equal_to_string ("ANY")); + cpe_struct_free (&cpe); + + uri_cpe = "cpe:/a:hp:insight_diagnostics:7.4.0.1570::~~online~win2003~x64~"; + cpe_struct_init (&cpe); + uri_cpe_to_cpe_struct (uri_cpe, &cpe); + assert_that (cpe.part, is_equal_to_string ("a")); + assert_that (cpe.vendor, is_equal_to_string ("hp")); + assert_that (cpe.product, is_equal_to_string ("insight_diagnostics")); + assert_that (cpe.version, is_equal_to_string ("7\\.4\\.0\\.1570")); + assert_that (cpe.update, is_equal_to_string ("ANY")); + assert_that (cpe.edition, is_equal_to_string ("ANY")); + assert_that (cpe.sw_edition, is_equal_to_string ("online")); + assert_that (cpe.target_sw, is_equal_to_string ("win2003")); + assert_that (cpe.target_hw, is_equal_to_string ("x64")); + assert_that (cpe.other, is_equal_to_string ("ANY")); + assert_that (cpe.language, is_equal_to_string ("ANY")); + cpe_struct_free (&cpe); + + uri_cpe = + "cpe:/a:hp:insight_diagnostics:7.4.0.1570::~~online~win2003~x64~other"; + cpe_struct_init (&cpe); + uri_cpe_to_cpe_struct (uri_cpe, &cpe); + assert_that (cpe.part, is_equal_to_string ("a")); + assert_that (cpe.vendor, is_equal_to_string ("hp")); + assert_that (cpe.product, is_equal_to_string ("insight_diagnostics")); + assert_that (cpe.version, is_equal_to_string ("7\\.4\\.0\\.1570")); + assert_that (cpe.update, is_equal_to_string ("ANY")); + assert_that (cpe.edition, is_equal_to_string ("ANY")); + assert_that (cpe.sw_edition, is_equal_to_string ("online")); + assert_that (cpe.target_sw, is_equal_to_string ("win2003")); + assert_that (cpe.target_hw, is_equal_to_string ("x64")); + assert_that (cpe.other, is_equal_to_string ("other")); + assert_that (cpe.language, is_equal_to_string ("ANY")); + cpe_struct_free (&cpe); + + uri_cpe = + "cpe:/" + "a:hp:insight_diagnostics:7.4.0.1570::~~online~win2003~x64~other:english"; + cpe_struct_init (&cpe); + uri_cpe_to_cpe_struct (uri_cpe, &cpe); + assert_that (cpe.part, is_equal_to_string ("a")); + assert_that (cpe.vendor, is_equal_to_string ("hp")); + assert_that (cpe.product, is_equal_to_string ("insight_diagnostics")); + assert_that (cpe.version, is_equal_to_string ("7\\.4\\.0\\.1570")); + assert_that (cpe.update, is_equal_to_string ("ANY")); + assert_that (cpe.edition, is_equal_to_string ("ANY")); + assert_that (cpe.sw_edition, is_equal_to_string ("online")); + assert_that (cpe.target_sw, is_equal_to_string ("win2003")); + assert_that (cpe.target_hw, is_equal_to_string ("x64")); + assert_that (cpe.other, is_equal_to_string ("other")); + assert_that (cpe.language, is_equal_to_string ("english")); + cpe_struct_free (&cpe); + + uri_cpe = "This is a ~:SIGNAL:~ test."; + cpe_struct_init (&cpe); + uri_cpe_to_cpe_struct (uri_cpe, &cpe); + cpe_struct_free (&cpe); +} + +Ensure (cpeutils, fs_cpe_to_cpe_struct) +{ + cpe_struct_t cpe; + char *fs_cpe; + + fs_cpe = "cpe:2.3:a:microsoft:internet_explorer:8.0.6001:beta:*:*:*:*:*:*"; + cpe_struct_init (&cpe); + fs_cpe_to_cpe_struct (fs_cpe, &cpe); + assert_that (cpe.part, is_equal_to_string ("a")); + assert_that (cpe.vendor, is_equal_to_string ("microsoft")); + assert_that (cpe.product, is_equal_to_string ("internet_explorer")); + assert_that (cpe.version, is_equal_to_string ("8\\.0\\.6001")); + assert_that (cpe.update, is_equal_to_string ("beta")); + assert_that (cpe.edition, is_equal_to_string ("ANY")); + assert_that (cpe.language, is_equal_to_string ("ANY")); + assert_that (cpe.sw_edition, is_equal_to_string ("ANY")); + assert_that (cpe.target_sw, is_equal_to_string ("ANY")); + assert_that (cpe.target_hw, is_equal_to_string ("ANY")); + assert_that (cpe.other, is_equal_to_string ("ANY")); + cpe_struct_free (&cpe); + + fs_cpe = "This is a ~:SIGNAL:~ test."; + cpe_struct_init (&cpe); + fs_cpe_to_cpe_struct (fs_cpe, &cpe); + cpe_struct_free (&cpe); +} + +Ensure (cpeutils, cpe_struct_to_uri_cpe) +{ + cpe_struct_t cpe; + char *uri_cpe; + + cpe_struct_init (&cpe); + cpe.part = "a"; + cpe.vendor = "microsoft"; + cpe.product = "internet_explorer"; + cpe.version = "8\\.0\\.6001"; + cpe.update = "beta"; + cpe.edition = "ANY"; + + uri_cpe = cpe_struct_to_uri_cpe (&cpe); + assert_that (uri_cpe, is_equal_to_string ( + "cpe:/a:microsoft:internet_explorer:8.0.6001:beta")); + g_free (uri_cpe); +} + +Ensure (cpeutils, cpe_struct_to_fs_cpe) +{ + cpe_struct_t cpe; + char *fs_cpe; + + cpe_struct_init (&cpe); + cpe.part = "a"; + cpe.vendor = "microsoft"; + cpe.product = "internet_explorer"; + cpe.version = "8\\.0\\.6001"; + cpe.update = "beta"; + cpe.edition = "ANY"; + + fs_cpe = cpe_struct_to_fs_cpe (&cpe); + assert_that ( + fs_cpe, + is_equal_to_string ( + "cpe:2.3:a:microsoft:internet_explorer:8.0.6001:beta:*:*:*:*:*:*")); + g_free (fs_cpe); +} + +Ensure (cpeutils, uri_cpe_to_fs_cpe) +{ + char *uri_cpe = "cpe:/a:microsoft:internet_explorer:8.0.6001:beta"; + char *fs_cpe = uri_cpe_to_fs_cpe (uri_cpe); + assert_that ( + fs_cpe, + is_equal_to_string ( + "cpe:2.3:a:microsoft:internet_explorer:8.0.6001:beta:*:*:*:*:*:*")); + g_free (fs_cpe); + + uri_cpe = "cpe:/a:hp:insight_diagnostics:7.4.0.1570:-:~~online~win2003~x64~"; + fs_cpe = uri_cpe_to_fs_cpe (uri_cpe); + assert_that (fs_cpe, + is_equal_to_string ("cpe:2.3:a:hp:insight_diagnostics:7.4.0." + "1570:-:*:*:online:win2003:x64:*")); + g_free (fs_cpe); + + uri_cpe = "This is a ~:SIGNAL:~ test."; + fs_cpe = uri_cpe_to_fs_cpe (uri_cpe); + g_free (fs_cpe); +} + +Ensure (cpeutils, fs_cpe_to_uri_cpe) +{ + char *fs_cpe = + "cpe:2.3:a:microsoft:internet_explorer:8.0.6001:beta:*:*:*:*:*:*"; + char *uri_cpe = fs_cpe_to_uri_cpe (fs_cpe); + assert_that (uri_cpe, is_equal_to_string ( + "cpe:/a:microsoft:internet_explorer:8.0.6001:beta")); + g_free (uri_cpe); + + fs_cpe = + "cpe:2.3:a:hp:insight_diagnostics:7.4.0.1570:-:*:*:online:win2003:x64:*"; + uri_cpe = fs_cpe_to_uri_cpe (fs_cpe); + assert_that ( + uri_cpe, + is_equal_to_string ( + "cpe:/a:hp:insight_diagnostics:7.4.0.1570:-:~~online~win2003~x64~")); + g_free (uri_cpe); + + fs_cpe = + "cpe:2.3:a:hp:insight_diagnostics:7\\:4.0.1570:-:*:*:online:win2003:x64:*"; + uri_cpe = fs_cpe_to_uri_cpe (fs_cpe); + assert_that ( + uri_cpe, + is_equal_to_string ( + "cpe:/a:hp:insight_diagnostics:7%3A4.0.1570:-:~~online~win2003~x64~")); + g_free (uri_cpe); + + fs_cpe = + "cpe:2.3:a:hp:insight_diagnostics:7.4.0.1570:-:*:*:online:win\\:2003:x64:*"; + uri_cpe = fs_cpe_to_uri_cpe (fs_cpe); + assert_that ( + uri_cpe, + is_equal_to_string ( + "cpe:/a:hp:insight_diagnostics:7.4.0.1570:-:~~online~win%3A2003~x64~")); + g_free (uri_cpe); + + fs_cpe = "cpe:2.3:a:hp:insight_diagnostics:7.4.0.1570:-:*:*:online:win\\:\\:" + "2003:x64:*"; + uri_cpe = fs_cpe_to_uri_cpe (fs_cpe); + assert_that ( + uri_cpe, + is_equal_to_string ( + "cpe:/" + "a:hp:insight_diagnostics:7.4.0.1570:-:~~online~win%3A%3A2003~x64~")); + g_free (uri_cpe); + + fs_cpe = "cpe:2.3:a:hp:insight_diagnostics:7.4.0.1570:-:*:*:online:" + "win2003\\\\:x64:*"; + uri_cpe = fs_cpe_to_uri_cpe (fs_cpe); + assert_that ( + uri_cpe, + is_equal_to_string ( + "cpe:/a:hp:insight_diagnostics:7.4.0.1570:-:~~online~win2003%5C~x64~")); + g_free (uri_cpe); + + fs_cpe = "This is a ~:SIGNAL:~ test."; + uri_cpe = fs_cpe_to_uri_cpe (fs_cpe); + g_free (uri_cpe); +} + +Ensure (cpeutils, cpe_struct_match) +{ + cpe_struct_t cpe1, cpe2; + char *fs_cpe1, *fs_cpe2; + + fs_cpe1 = "cpe:2.3:a:microsoft:internet_explorer:8.0.6001:beta:*:*:*:*:*:*"; + cpe_struct_init (&cpe1); + fs_cpe_to_cpe_struct (fs_cpe1, &cpe1); + assert_that (cpe_struct_match (&cpe1, &cpe1), is_equal_to (TRUE)); + + fs_cpe2 = "cpe:2.3:a:microsoft:internet_explorer:*:beta:*:*:*:*:*:*"; + cpe_struct_init (&cpe2); + fs_cpe_to_cpe_struct (fs_cpe2, &cpe2); + assert_that (cpe_struct_match (&cpe2, &cpe1), is_equal_to (TRUE)); + + assert_that (cpe_struct_match (&cpe1, &cpe2), is_equal_to (FALSE)); + + fs_cpe2 = "cpe:2.3:a:microsoft:internet_explorer:*:-:*:*:*:*:*:*"; + cpe_struct_free (&cpe2); + cpe_struct_init (&cpe2); + fs_cpe_to_cpe_struct (fs_cpe2, &cpe2); + assert_that (cpe_struct_match (&cpe2, &cpe1), is_equal_to (FALSE)); + + cpe_struct_free (&cpe1); + cpe_struct_free (&cpe2); +} + +/* Test suite. */ +int +main (int argc, char **argv) +{ + TestSuite *suite; + + suite = create_test_suite (); + + add_test_with_context (suite, cpeutils, uri_cpe_to_cpe_struct); + add_test_with_context (suite, cpeutils, fs_cpe_to_cpe_struct); + add_test_with_context (suite, cpeutils, cpe_struct_to_uri_cpe); + add_test_with_context (suite, cpeutils, cpe_struct_to_fs_cpe); + add_test_with_context (suite, cpeutils, uri_cpe_to_fs_cpe); + add_test_with_context (suite, cpeutils, fs_cpe_to_uri_cpe); + add_test_with_context (suite, cpeutils, cpe_struct_match); + + if (argc > 1) + return run_single_test (suite, argv[1], create_text_reporter ()); + + return run_test_suite (suite, create_text_reporter ()); +} diff --git a/util/jsonpull.c b/util/jsonpull.c new file mode 100644 index 00000000..12df83b5 --- /dev/null +++ b/util/jsonpull.c @@ -0,0 +1,965 @@ +/* SPDX-FileCopyrightText: 2024 Greenbone AG + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "jsonpull.h" + +#include + +#define GVM_JSON_CHAR_EOF -1 ///< End of file +#define GVM_JSON_CHAR_ERROR -2 ///< Error reading file +#define GVM_JSON_CHAR_UNDEFINED -3 ///< Undefined state + +/** + * @brief Escapes a string according to the JSON or JSONPath standard + * + * @param[in] string The string to escape + * @param[in] single_quote Whether to escape single quotes + * + * @return The escaped string + */ +gchar * +gvm_json_string_escape (const char *string, gboolean single_quote) +{ + gchar *point; + if (string == NULL) + return NULL; + + GString *escaped = g_string_sized_new (strlen (string)); + for (point = (char *) string; *point != 0; point++) + { + unsigned char character = *point; + + if ((character > 31) && (character != '\\') + && (single_quote ? (character != '\'') : (character != '\"'))) + { + g_string_append_c (escaped, character); + } + else + { + g_string_append_c (escaped, '\\'); + switch (*point) + { + case '\\': + case '\'': + case '\"': + g_string_append_c (escaped, *point); + break; + case '\b': + g_string_append_c (escaped, 'b'); + break; + case '\f': + g_string_append_c (escaped, 'f'); + break; + case '\n': + g_string_append_c (escaped, 'n'); + break; + case '\r': + g_string_append_c (escaped, 'r'); + break; + case '\t': + g_string_append_c (escaped, 't'); + break; + default: + g_string_append_printf (escaped, "u%04x", character); + } + } + } + return g_string_free (escaped, FALSE); +} + +/** + * @brief Creates a new JSON path element. + * + * @param[in] parent_type Type of the parent (array, object, none/root) + * @param[in] depth The depth in the document tree + * + * @return The newly allocated path element + */ +gvm_json_path_elem_t * +gvm_json_pull_path_elem_new (gvm_json_pull_container_type_t parent_type, + int depth) +{ + gvm_json_path_elem_t *new_elem = g_malloc0 (sizeof (gvm_json_path_elem_t)); + new_elem->parent_type = parent_type; + new_elem->depth = depth; + return new_elem; +} + +/** + * @brief Frees a JSON path element. + * + * @param[in] elem The element to free + */ +void +gvm_json_pull_path_elem_free (gvm_json_path_elem_t *elem) +{ + g_free (elem->key); + g_free (elem); +} + +/** + * @brief Initializes a JSON pull event data structure. + * + * @param[in] event The event structure to initialize + */ +void +gvm_json_pull_event_init (gvm_json_pull_event_t *event) +{ + memset (event, 0, sizeof (gvm_json_pull_event_t)); +} + +/** + * @brief Frees all data of JSON pull event data structure. + * + * @param[in] event The event structure to clean up + */ +void +gvm_json_pull_event_cleanup (gvm_json_pull_event_t *event) +{ + cJSON_free (event->value); + if (event->error_message) + g_free (event->error_message); + memset (event, 0, sizeof (gvm_json_pull_event_t)); +} + +/** + * @brief Initializes a JSON pull parser. + * + * @param[in] parser The parser data structure to initialize + * @param[in] input_stream The JSON input stream + * @param[in] parse_buffer_limit Maximum buffer size for parsing values + * @param[in] read_buffer_size Buffer size for reading from the stream + */ +void +gvm_json_pull_parser_init_full (gvm_json_pull_parser_t *parser, + FILE *input_stream, size_t parse_buffer_limit, + size_t read_buffer_size) +{ + assert (parser); + assert (input_stream); + memset (parser, 0, sizeof (gvm_json_pull_parser_t)); + + if (parse_buffer_limit <= 0) + parse_buffer_limit = GVM_JSON_PULL_PARSE_BUFFER_LIMIT; + + if (read_buffer_size <= 0) + read_buffer_size = GVM_JSON_PULL_READ_BUFFER_SIZE; + + parser->input_stream = input_stream; + parser->path = g_queue_new (); + parser->expect = GVM_JSON_PULL_EXPECT_VALUE; + parser->parse_buffer_limit = parse_buffer_limit; + parser->parse_buffer = g_string_new (""); + parser->read_buffer_size = read_buffer_size; + parser->read_buffer = g_malloc0 (read_buffer_size); + parser->last_read_char = GVM_JSON_CHAR_UNDEFINED; +} + +/** + * @brief Initializes a JSON pull parser with default buffer sizes. + * + * @param[in] parser The parser data structure to initialize + * @param[in] input_stream The JSON input stream + */ +void +gvm_json_pull_parser_init (gvm_json_pull_parser_t *parser, FILE *input_stream) +{ + gvm_json_pull_parser_init_full (parser, input_stream, 0, 0); +} + +/** + * @brief Frees the data of a JSON pull parser. + * + * @param[in] parser The parser data structure to free the data of + */ +void +gvm_json_pull_parser_cleanup (gvm_json_pull_parser_t *parser) +{ + assert (parser); + g_queue_free_full (parser->path, + (GDestroyNotify) gvm_json_pull_path_elem_free); + g_string_free (parser->parse_buffer, TRUE); + g_free (parser->read_buffer); + memset (parser, 0, sizeof (gvm_json_pull_parser_t)); +} + +/** + * @brief Generates message for an error that occurred reading the JSON stream. + * + * @return The newly allocated error message + */ +static gchar * +gvm_json_read_stream_error_str () +{ + return g_strdup_printf ("error reading JSON stream: %s", strerror (errno)); +} + +/** + * @brief Checks if the parse buffer limit of a JSON pull parser is reached. + * + * @param[in] value_type The value type to include in the error message + * @param[in] parser The parser to check the parse buffer of + * @param[in] event Event data for error status and message if needed + * + * @return 0 if buffer size is okay, 1 if limit was reached + */ +static int +gvm_json_pull_check_parse_buffer_size (const char *value_type, + gvm_json_pull_parser_t *parser, + gvm_json_pull_event_t *event) +{ + if (parser->parse_buffer->len >= parser->parse_buffer_limit) + { + event->error_message = + g_strdup_printf ("%s exceeds size limit of %zu bytes", value_type, + parser->parse_buffer_limit); + event->type = GVM_JSON_PULL_EVENT_ERROR; + return 1; + } + return 0; +} + +/** + * @brief Reads the next character in a pull parser input stream. + * + * @param[in] parser The parser to read the next character from + * + * @return The character code, GVM_JSON_CHAR_ERROR or GVM_JSON_CHAR_EOF. + */ +static int +gvm_json_pull_parser_next_char (gvm_json_pull_parser_t *parser) +{ + parser->read_pos++; + if (parser->read_pos < parser->last_read_size) + { + parser->last_read_char = + (unsigned char) parser->read_buffer[parser->read_pos]; + return parser->last_read_char; + } + else + { + parser->read_pos = 0; + parser->last_read_size = fread ( + parser->read_buffer, 1, parser->read_buffer_size, parser->input_stream); + if (ferror (parser->input_stream)) + parser->last_read_char = GVM_JSON_CHAR_ERROR; + else if (parser->last_read_size <= 0) + parser->last_read_char = GVM_JSON_CHAR_EOF; + else + parser->last_read_char = + (unsigned char) parser->read_buffer[parser->read_pos]; + return parser->last_read_char; + } +} + +/** + * @brief Tries to parse the buffer content of a JSON pull parser. + * + * @param[in] parser The parser to use the parse buffer of + * @param[in] event Event set error of if necessary + * @param[in] value_name Name of the value for error message if needed + * @param[in] validate_func Function for validating the parsed value + * @param[out] cjson_value Return of the parsed cJSON object on success + * + * @return 0 success, 1 error + */ +static int +gvm_json_pull_parse_buffered (gvm_json_pull_parser_t *parser, + gvm_json_pull_event_t *event, + const char *value_name, + cJSON_bool (*validate_func) (const cJSON *const), + cJSON **cjson_value) +{ + cJSON *parsed_value = cJSON_Parse (parser->parse_buffer->str); + *cjson_value = NULL; + if (validate_func (parsed_value) == 0) + { + event->type = GVM_JSON_PULL_EVENT_ERROR; + event->error_message = g_strdup_printf ("error parsing %s", value_name); + cJSON_free (parsed_value); + return 1; + } + *cjson_value = parsed_value; + return 0; +} + +/** + * @brief Handles error or EOF after reading a character in JSON pull parser. + * + * @param[in] parser Parser to get the last read character from + * @param[in] event Event data to set EOF or error status in + * @param[in] allow_eof Whether to allow EOF, generate error on EOF if FALSE + */ +static void +gvm_json_pull_handle_read_end (gvm_json_pull_parser_t *parser, + gvm_json_pull_event_t *event, gboolean allow_eof) +{ + if (parser->last_read_char == GVM_JSON_CHAR_ERROR) + { + event->error_message = gvm_json_read_stream_error_str (); + event->type = GVM_JSON_PULL_EVENT_ERROR; + } + else if (allow_eof) + event->type = GVM_JSON_PULL_EVENT_EOF; + else + { + event->error_message = g_strdup ("unexpected EOF"); + event->type = GVM_JSON_PULL_EVENT_ERROR; + } +} + +/** + * @brief Skips whitespaces in the input stream of a JSON pull parser + * + * The parser will be at the first non-whitespace character on success. + * + * @param[in] parser Parser to skip the whitespaces in + * @param[in] event Event data to set EOF or error status in + * @param[in] allow_eof Whether to allow EOF, generate error on EOF if FALSE + * + * @return 1 if EOF was reached or an error occurred, 0 otherwise + */ +static int +gvm_json_pull_skip_space (gvm_json_pull_parser_t *parser, + gvm_json_pull_event_t *event, gboolean allow_eof) +{ + while (g_ascii_isspace (parser->last_read_char)) + gvm_json_pull_parser_next_char (parser); + if (parser->last_read_char < 0) + { + gvm_json_pull_handle_read_end (parser, event, allow_eof); + return 1; + } + return 0; +} + +/** + * @brief Parses a string in a JSON pull parser. + * + * The parser is expected to be at the opening quote mark and will be at the + * character after the closing quote mark on success. + * + * @param[in] parser Parser to handle the string value in + * @param[in] event Event data to set EOF or error status in + * @param[out] cjson_value The cJSON value for the string on success + * + * @return 1 if an error occurred (including EOF), 0 otherwise + */ +static int +gvm_json_pull_parse_string (gvm_json_pull_parser_t *parser, + gvm_json_pull_event_t *event, cJSON **cjson_value) +{ + gboolean escape_next_char = FALSE; + g_string_truncate (parser->parse_buffer, 0); + g_string_append_c (parser->parse_buffer, '"'); + while (gvm_json_pull_parser_next_char (parser) >= 0) + { + if (gvm_json_pull_check_parse_buffer_size ("string", parser, event)) + return 1; + g_string_append_c (parser->parse_buffer, parser->last_read_char); + if (escape_next_char) + escape_next_char = FALSE; + else if (parser->last_read_char == '\\') + escape_next_char = TRUE; + else if (parser->last_read_char == '"') + break; + } + + if (parser->last_read_char < 0) + { + gvm_json_pull_handle_read_end (parser, event, FALSE); + return 1; + } + + gvm_json_pull_parser_next_char (parser); + + return gvm_json_pull_parse_buffered (parser, event, "string", cJSON_IsString, + cjson_value); +} + +/** + * @brief Parses a number in a JSON pull parser. + * + * The parser is expected to be at the first character of the number and will + * be at the first non-number character on success. + * + * @param[in] parser Parser to handle the number value in + * @param[in] event Event data to set EOF or error status in + * @param[out] cjson_value The cJSON value for the number on success. + * + * @return 1 if an error occurred, 0 otherwise + */ +static int +gvm_json_pull_parse_number (gvm_json_pull_parser_t *parser, + gvm_json_pull_event_t *event, cJSON **cjson_value) +{ + g_string_truncate (parser->parse_buffer, 0); + g_string_append_c (parser->parse_buffer, parser->last_read_char); + while (gvm_json_pull_parser_next_char (parser) >= 0) + { + if (gvm_json_pull_check_parse_buffer_size ("number", parser, event)) + return 1; + if (g_ascii_isdigit (parser->last_read_char) + || parser->last_read_char == '.' || parser->last_read_char == 'e' + || parser->last_read_char == '-' || parser->last_read_char == '+') + g_string_append_c (parser->parse_buffer, parser->last_read_char); + else + break; + } + + if (parser->last_read_char == GVM_JSON_CHAR_ERROR) + { + event->error_message = gvm_json_read_stream_error_str (); + event->type = GVM_JSON_PULL_EVENT_ERROR; + return 1; + } + + return gvm_json_pull_parse_buffered (parser, event, "number", cJSON_IsNumber, + cjson_value); +} + +/** + * @brief Parses a keyword value in a JSON pull parser. + * + * The parser is expected to be at the first character of the keyword and will + * be at the first character after the keyword on success. + * + * @param[in] parser Parser to handle the keyword value in + * @param[in] event Event data to set EOF or error status in + * @param[in] keyword The expected keyword, e.g. "null", "true", "false". + * + * @return 1 if an error occurred, 0 otherwise + */ +static int +gvm_json_pull_parse_keyword (gvm_json_pull_parser_t *parser, + gvm_json_pull_event_t *event, const char *keyword) +{ + for (size_t i = 0; i < strlen (keyword); i++) + { + if (parser->last_read_char < 0) + { + gvm_json_pull_handle_read_end (parser, event, FALSE); + return 1; + } + else if (parser->last_read_char != keyword[i]) + { + event->type = GVM_JSON_PULL_EVENT_ERROR; + event->error_message = + g_strdup_printf ("misspelled keyword '%s'", keyword); + return 1; + } + gvm_json_pull_parser_next_char (parser); + } + return 0; +} + +/** + * @brief Updates the expectation for a JSON pull parser according to the path. + * + * @param[in] parser The parser to update. + */ +static void +parse_value_next_expect (gvm_json_pull_parser_t *parser) +{ + if (parser->path->length) + parser->expect = GVM_JSON_PULL_EXPECT_COMMA; + else + parser->expect = GVM_JSON_PULL_EXPECT_EOF; +} + +/** + * @brief Handles the case that an object key is expected in a JSON pull parser. + * + * This will continue the parsing until the value is expected, the end of the + * current object was reached or an error occurred. + * + * @param[in] parser Parser to process + * @param[in] event Event data to set error or end of object status in + * + * @return 1 if an error occurred, 0 otherwise + */ +static int +gvm_json_pull_parse_key (gvm_json_pull_parser_t *parser, + gvm_json_pull_event_t *event) +{ + if (gvm_json_pull_skip_space (parser, event, FALSE)) + return 1; + + cJSON *key_cjson = NULL; + gchar *key_str; + gvm_json_path_elem_t *path_elem; + + switch (parser->last_read_char) + { + case '"': + if (gvm_json_pull_parse_string (parser, event, &key_cjson)) + return 1; + key_str = g_strdup (key_cjson->valuestring); + cJSON_free (key_cjson); + + // Expect colon: + if (gvm_json_pull_skip_space (parser, event, FALSE)) + { + g_free (key_str); + return 1; + } + if (parser->last_read_char != ':') + { + event->type = GVM_JSON_PULL_EVENT_ERROR; + event->error_message = g_strdup_printf ("expected colon"); + g_free (key_str); + return 1; + } + gvm_json_pull_parser_next_char (parser); + + path_elem = g_queue_peek_tail (parser->path); + g_free (path_elem->key); + path_elem->key = key_str; + parser->expect = GVM_JSON_PULL_EXPECT_VALUE; + + break; + case '}': + event->type = GVM_JSON_PULL_EVENT_OBJECT_END; + event->value = NULL; + gvm_json_pull_path_elem_free (g_queue_pop_tail (parser->path)); + parse_value_next_expect (parser); + gvm_json_pull_parser_next_char (parser); + break; + case ']': + event->type = GVM_JSON_PULL_EVENT_ERROR; + event->error_message = g_strdup ("unexpected closing square bracket"); + return 1; + default: + event->type = GVM_JSON_PULL_EVENT_ERROR; + event->error_message = g_strdup ("unexpected character"); + return 1; + } + + return 0; +} + +/** + * @brief Handles the case that a comma is expected in a JSON pull parser. + * + * This will continue the parsing until a comma or the end of the + * current array/object was reached or an error occurred. + * + * @param[in] parser Parser to process + * @param[in] event Event data to set error or end of object status in + * + * @return 1 if an error occurred, 0 otherwise + */ +static int +gvm_json_pull_parse_comma (gvm_json_pull_parser_t *parser, + gvm_json_pull_event_t *event) +{ + if (gvm_json_pull_skip_space (parser, event, FALSE)) + return 1; + + gvm_json_path_elem_t *path_elem = NULL; + switch (parser->last_read_char) + { + case ',': + path_elem = g_queue_peek_tail (parser->path); + path_elem->index++; + if (path_elem->parent_type == GVM_JSON_PULL_CONTAINER_OBJECT) + parser->expect = GVM_JSON_PULL_EXPECT_KEY; + else + parser->expect = GVM_JSON_PULL_EXPECT_VALUE; + gvm_json_pull_parser_next_char (parser); + break; + case ']': + path_elem = g_queue_peek_tail (parser->path); + if (path_elem == NULL + || path_elem->parent_type != GVM_JSON_PULL_CONTAINER_ARRAY) + { + event->type = GVM_JSON_PULL_EVENT_ERROR; + event->error_message = g_strdup ("unexpected closing square bracket"); + return 1; + } + event->type = GVM_JSON_PULL_EVENT_ARRAY_END; + event->value = NULL; + gvm_json_pull_path_elem_free (g_queue_pop_tail (parser->path)); + parse_value_next_expect (parser); + gvm_json_pull_parser_next_char (parser); + break; + case '}': + path_elem = g_queue_peek_tail (parser->path); + if (path_elem == NULL + || path_elem->parent_type != GVM_JSON_PULL_CONTAINER_OBJECT) + { + event->type = GVM_JSON_PULL_EVENT_ERROR; + event->error_message = g_strdup ("unexpected closing curly brace"); + return 1; + } + event->type = GVM_JSON_PULL_EVENT_OBJECT_END; + event->value = NULL; + gvm_json_pull_path_elem_free (g_queue_pop_tail (parser->path)); + parse_value_next_expect (parser); + gvm_json_pull_parser_next_char (parser); + break; + default: + event->error_message = g_strdup ("expected comma or end of container"); + event->type = GVM_JSON_PULL_EVENT_ERROR; + return 1; + } + return 0; +} + +/** + * @brief Handles the case that a value is expected in a JSON pull parser. + * + * This will continue the parsing until a value or the end of the + * current array/object was parsed or an error occurred. + * + * @param[in] parser Parser to process + * @param[in] event Event data to set error or end of object status in + * + * @return 1 if an error occurred, 0 otherwise + */ +static int +gvm_json_pull_parse_value (gvm_json_pull_parser_t *parser, + gvm_json_pull_event_t *event) +{ + if (gvm_json_pull_skip_space (parser, event, FALSE)) + return 1; + + cJSON *cjson_value = NULL; + gvm_json_path_elem_t *path_elem = NULL; + + switch (parser->last_read_char) + { + case '"': + if (gvm_json_pull_parse_string (parser, event, &cjson_value)) + return 1; + event->type = GVM_JSON_PULL_EVENT_STRING; + event->value = cjson_value; + parse_value_next_expect (parser); + break; + case 'n': + if (gvm_json_pull_parse_keyword (parser, event, "null")) + return 1; + event->type = GVM_JSON_PULL_EVENT_NULL; + event->value = cJSON_CreateNull (); + parse_value_next_expect (parser); + break; + case 'f': + if (gvm_json_pull_parse_keyword (parser, event, "false")) + return 1; + event->type = GVM_JSON_PULL_EVENT_BOOLEAN; + event->value = cJSON_CreateFalse (); + parse_value_next_expect (parser); + break; + case 't': + if (gvm_json_pull_parse_keyword (parser, event, "true")) + return 1; + event->type = GVM_JSON_PULL_EVENT_BOOLEAN; + event->value = cJSON_CreateTrue (); + parse_value_next_expect (parser); + break; + case '[': + event->type = GVM_JSON_PULL_EVENT_ARRAY_START; + event->value = NULL; + parser->path_add = gvm_json_pull_path_elem_new ( + GVM_JSON_PULL_CONTAINER_ARRAY, parser->path->length); + parser->expect = GVM_JSON_PULL_EXPECT_VALUE; + gvm_json_pull_parser_next_char (parser); + break; + case ']': + path_elem = g_queue_peek_tail (parser->path); + if (path_elem == NULL + || path_elem->parent_type != GVM_JSON_PULL_CONTAINER_ARRAY) + { + event->type = GVM_JSON_PULL_EVENT_ERROR; + event->error_message = g_strdup ("unexpected closing square bracket"); + return 1; + } + event->type = GVM_JSON_PULL_EVENT_ARRAY_END; + event->value = NULL; + gvm_json_pull_path_elem_free (g_queue_pop_tail (parser->path)); + parse_value_next_expect (parser); + gvm_json_pull_parser_next_char (parser); + break; + case '{': + event->type = GVM_JSON_PULL_EVENT_OBJECT_START; + event->value = NULL; + parser->path_add = gvm_json_pull_path_elem_new ( + GVM_JSON_PULL_CONTAINER_OBJECT, parser->path->length); + parser->expect = GVM_JSON_PULL_EXPECT_KEY; + gvm_json_pull_parser_next_char (parser); + break; + case '}': + event->type = GVM_JSON_PULL_EVENT_ERROR; + event->error_message = g_strdup ("unexpected closing curly brace"); + return 1; + break; + default: + if (g_ascii_isdigit (parser->last_read_char) + || parser->last_read_char == '-') + { + if (gvm_json_pull_parse_number (parser, event, &cjson_value)) + return 1; + event->type = GVM_JSON_PULL_EVENT_NUMBER; + event->value = cjson_value; + parse_value_next_expect (parser); + } + else + { + event->type = GVM_JSON_PULL_EVENT_ERROR; + event->error_message = g_strdup ("unexpected character"); + return 1; + } + } + return 0; +} + +/** + * @brief Get the next event from a JSON pull parser. + * + * Note: This invalidates previous event data like the cJSON value. + * + * @param[in] parser The JSON pull parser to process until the next event + * @param[in] event Structure to store event data in. + */ +void +gvm_json_pull_parser_next (gvm_json_pull_parser_t *parser, + gvm_json_pull_event_t *event) +{ + assert (parser); + assert (event); + + gvm_json_pull_event_cleanup (event); + if (parser->last_read_char == GVM_JSON_CHAR_UNDEFINED) + { + // Handle first read of the stream + if (gvm_json_pull_parser_next_char (parser) < 0) + { + gvm_json_pull_handle_read_end (parser, event, TRUE); + return; + } + } + + event->path = parser->path; + + // Delayed addition to path after a container start element + if (parser->path_add) + { + g_queue_push_tail (parser->path, parser->path_add); + parser->path_add = NULL; + } + + // Check for expected end of file + if (parser->expect == GVM_JSON_PULL_EXPECT_EOF) + { + gvm_json_pull_skip_space (parser, event, TRUE); + + if (parser->last_read_char == GVM_JSON_CHAR_ERROR) + { + event->type = GVM_JSON_PULL_EVENT_ERROR; + event->error_message = gvm_json_read_stream_error_str (); + } + else if (parser->last_read_char != GVM_JSON_CHAR_EOF) + { + event->type = GVM_JSON_PULL_EVENT_ERROR; + event->error_message = g_strdup_printf ( + "unexpected character at end of file (%d)", parser->last_read_char); + return; + } + return; + } + + if (parser->expect == GVM_JSON_PULL_EXPECT_COMMA) + { + if (gvm_json_pull_parse_comma (parser, event)) + return; + } + + if (parser->expect == GVM_JSON_PULL_EXPECT_KEY) + { + if (gvm_json_pull_parse_key (parser, event)) + return; + } + + if (parser->expect == GVM_JSON_PULL_EXPECT_VALUE) + { + gvm_json_pull_parse_value (parser, event); + } +} + +/** + * @brief Expands the current array or object of a JSON pull parser. + * + * This should be called after an array or object start event. + * + * @param[in] parser Parser to get the current container element from + * @param[out] error_message Error message output + * + * @return The expanded container as a cJSON object if successful, else NULL + */ +cJSON * +gvm_json_pull_expand_container (gvm_json_pull_parser_t *parser, + gchar **error_message) +{ + gvm_json_path_elem_t *path_tail = NULL; + + int start_depth; + gboolean in_string, escape_next_char, in_expanded_container; + cJSON *expanded; + + g_string_truncate (parser->parse_buffer, 0); + + if (error_message) + *error_message = NULL; + + // require "path_add" to only allow expansion at start of container + if (parser->path_add) + { + path_tail = parser->path_add; + g_queue_push_tail (parser->path, path_tail); + parser->path_add = NULL; + } + + if (path_tail && path_tail->parent_type == GVM_JSON_PULL_CONTAINER_ARRAY) + g_string_append_c (parser->parse_buffer, '['); + else if (path_tail + && path_tail->parent_type == GVM_JSON_PULL_CONTAINER_OBJECT) + g_string_append_c (parser->parse_buffer, '{'); + else + { + if (error_message) + *error_message = + g_strdup ("can only expand after array or object start"); + return NULL; + } + + start_depth = path_tail->depth; + in_string = escape_next_char = FALSE; + in_expanded_container = TRUE; + + while (parser->last_read_char >= 0 && in_expanded_container) + { + if (parser->parse_buffer->len >= parser->parse_buffer_limit) + { + if (error_message) + *error_message = + g_strdup_printf ("container exceeds size limit of %zu bytes", + parser->parse_buffer_limit); + return NULL; + } + + g_string_append_c (parser->parse_buffer, parser->last_read_char); + + if (escape_next_char) + { + escape_next_char = FALSE; + } + else if (in_string) + { + escape_next_char = (parser->last_read_char == '\\'); + in_string = (parser->last_read_char != '"'); + } + else + { + switch (parser->last_read_char) + { + case '"': + in_string = TRUE; + break; + case '[': + path_tail = gvm_json_pull_path_elem_new ( + GVM_JSON_PULL_CONTAINER_ARRAY, parser->path->length); + g_queue_push_tail (parser->path, path_tail); + break; + case '{': + path_tail = gvm_json_pull_path_elem_new ( + GVM_JSON_PULL_CONTAINER_OBJECT, parser->path->length); + g_queue_push_tail (parser->path, path_tail); + break; + case ']': + path_tail = g_queue_pop_tail (parser->path); + if (path_tail->parent_type != GVM_JSON_PULL_CONTAINER_ARRAY) + { + if (error_message) + *error_message = + g_strdup ("unexpected closing square bracket"); + return NULL; + } + if (path_tail->depth == start_depth) + in_expanded_container = FALSE; + break; + case '}': + path_tail = g_queue_pop_tail (parser->path); + if (path_tail->parent_type != GVM_JSON_PULL_CONTAINER_OBJECT) + { + if (error_message) + *error_message = + g_strdup ("unexpected closing curly brace"); + return NULL; + } + if (path_tail->depth == start_depth) + in_expanded_container = FALSE; + break; + } + } + gvm_json_pull_parser_next_char (parser); + } + + if (parser->last_read_char == GVM_JSON_CHAR_ERROR) + { + if (error_message) + *error_message = gvm_json_read_stream_error_str (); + return NULL; + } + else if (in_expanded_container && parser->last_read_char == GVM_JSON_CHAR_EOF) + { + if (error_message) + *error_message = g_strdup ("unexpected EOF"); + return NULL; + } + + expanded = cJSON_Parse (parser->parse_buffer->str); + g_string_truncate (parser->parse_buffer, 0); + parse_value_next_expect (parser); + + if (expanded == NULL && error_message) + *error_message = g_strdup ("could not parse expanded container"); + + return expanded; +} + +/** + * @brief Appends a string path element to a JSONPath string. + * + * @param[in] path_elem The path element to append + * @param[in] path_string The path string to append to + */ +static void +gvm_json_path_string_add_elem (gvm_json_path_elem_t *path_elem, + GString *path_string) +{ + if (path_elem->parent_type == GVM_JSON_PULL_CONTAINER_OBJECT) + { + gchar *escaped_key = gvm_json_string_escape (path_elem->key, TRUE); + g_string_append_printf (path_string, "['%s']", escaped_key); + g_free (escaped_key); + } + else + g_string_append_printf (path_string, "[%d]", path_elem->index); +} + +/** + * @brief Converts a path as used by a JSON pull parser to a JSONPath string. + * + * @param[in] path The path to convert + * + * @return Newly allocated string of the path in JSONPath bracket notation + */ +gchar * +gvm_json_path_to_string (GQueue *path) +{ + GString *path_string = g_string_new ("$"); + g_queue_foreach (path, (GFunc) gvm_json_path_string_add_elem, path_string); + return g_string_free (path_string, FALSE); +} diff --git a/util/jsonpull.h b/util/jsonpull.h new file mode 100644 index 00000000..50d36578 --- /dev/null +++ b/util/jsonpull.h @@ -0,0 +1,134 @@ +/* SPDX-FileCopyrightText: 2024 Greenbone AG + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#ifndef _GVM_JSONPULL_H +#define _GVM_JSONPULL_H + +#define _GNU_SOURCE + +#include +#include +#include + +/** + * @brief Type of container the parser is currently in + */ +typedef enum +{ + GVM_JSON_PULL_CONTAINER_NONE = 0, ///< No container / document root + GVM_JSON_PULL_CONTAINER_ARRAY, ///< Array + GVM_JSON_PULL_CONTAINER_OBJECT, ///< Object +} gvm_json_pull_container_type_t; + +/** + * @brief Path element types for the JSON pull parser. + */ +typedef struct gvm_json_path_elem +{ + gvm_json_pull_container_type_t parent_type; ///< parent container type + int index; ///< Index of the element within the parent + char *key; ///< Key if element is in an object + int depth; ///< Number of ancestor elements +} gvm_json_path_elem_t; + +/** + * @brief Event types for the JSON pull parser + */ +typedef enum +{ + GVM_JSON_PULL_EVENT_UNDEFINED = 0, + GVM_JSON_PULL_EVENT_ARRAY_START, + GVM_JSON_PULL_EVENT_ARRAY_END, + GVM_JSON_PULL_EVENT_OBJECT_START, + GVM_JSON_PULL_EVENT_OBJECT_END, + GVM_JSON_PULL_EVENT_STRING, + GVM_JSON_PULL_EVENT_NUMBER, + GVM_JSON_PULL_EVENT_BOOLEAN, + GVM_JSON_PULL_EVENT_NULL, + GVM_JSON_PULL_EVENT_EOF, + GVM_JSON_PULL_EVENT_ERROR, +} gvm_json_pull_event_type_t; + +/** + * @brief Event generated by the JSON pull parser. + */ +typedef struct +{ + gvm_json_pull_event_type_t type; ///< Type of event + GQueue *path; ///< Path to the event value + cJSON *value; ///< Value for non-container value events + gchar *error_message; ///< Error message, NULL on success +} gvm_json_pull_event_t; + +/** + * @brief Expected token state for the JSON pull parser + */ +typedef enum +{ + GVM_JSON_PULL_EXPECT_UNDEFINED = 0, ///< Undefined state + GVM_JSON_PULL_EXPECT_VALUE, ///< Expect start of a value + GVM_JSON_PULL_EXPECT_KEY, ///< Expect start of a key + GVM_JSON_PULL_EXPECT_COMMA, ///< Expect comma or container end brace + GVM_JSON_PULL_EXPECT_EOF ///< Expect end of file +} gvm_json_pull_expect_t; + +#define GVM_JSON_PULL_PARSE_BUFFER_LIMIT 10485760 + +#define GVM_JSON_PULL_READ_BUFFER_SIZE 4096 + +/** + * @brief A json pull parser + */ +typedef struct +{ + GQueue *path; ///< Path to the current value + gvm_json_path_elem_t *path_add; ///< Path elem to add in next step + gvm_json_pull_expect_t expect; ///< Current expected token + int keyword_pos; ///< Position in a keyword like "true" or "null" + FILE *input_stream; ///< Input stream + char *read_buffer; ///< Stream reading buffer + size_t read_buffer_size; ///< Size of the stream reading buffer + size_t last_read_size; ///< Size of last stream read + int last_read_char; ///< Character last read from stream + size_t read_pos; ///< Position in current read + GString *parse_buffer; ///< Buffer for parsing values and object keys + size_t parse_buffer_limit; ///< Maximum parse buffer size +} gvm_json_pull_parser_t; + +gchar * +gvm_json_string_escape (const char *, gboolean); + +gvm_json_path_elem_t * +gvm_json_pull_path_elem_new (gvm_json_pull_container_type_t, int); + +void +gvm_json_pull_path_elem_free (gvm_json_path_elem_t *); + +void +gvm_json_pull_event_init (gvm_json_pull_event_t *); + +void +gvm_json_pull_event_cleanup (gvm_json_pull_event_t *); + +void +gvm_json_pull_parser_init_full (gvm_json_pull_parser_t *, FILE *, size_t, + size_t); + +void +gvm_json_pull_parser_init (gvm_json_pull_parser_t *, FILE *); + +void +gvm_json_pull_parser_cleanup (gvm_json_pull_parser_t *); + +void +gvm_json_pull_parser_next (gvm_json_pull_parser_t *, gvm_json_pull_event_t *); + +cJSON * +gvm_json_pull_expand_container (gvm_json_pull_parser_t *, gchar **); + +gchar * +gvm_json_path_to_string (GQueue *path); + +#endif /* _GVM_JSONPULL_H */ diff --git a/util/jsonpull_tests.c b/util/jsonpull_tests.c new file mode 100644 index 00000000..444f5a66 --- /dev/null +++ b/util/jsonpull_tests.c @@ -0,0 +1,1225 @@ +/* SPDX-FileCopyrightText: 2019-2023 Greenbone AG + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "jsonpull.c" + +#include +#include +#include + +Describe (jsonpull); +BeforeEach (jsonpull) +{ +} +AfterEach (jsonpull) +{ +} + +/* + * Helper function to open a string as a read-only stream. + */ +static inline FILE * +fstropen_r (const char *str) +{ + return fmemopen ((void *) str, strlen (str), "r"); +} + +static ssize_t +read_with_error_on_eof (void *stream_cookie, char *buf, size_t size) +{ + FILE *stream = stream_cookie; + ssize_t ret = fread (buf, 1, size, stream); + if (ret <= 0) + { + errno = EIO; + return -1; + } + else + return ret; +} + +#define INIT_JSON_PARSER(json_string) \ + gvm_json_pull_event_t event; \ + gvm_json_pull_parser_t parser; \ + FILE *jsonstream; \ + jsonstream = fstropen_r (json_string); \ + gvm_json_pull_event_init (&event); \ + gvm_json_pull_parser_init_full (&parser, jsonstream, 100, 4); + +#define INIT_READ_ERROR_JSON_PARSER(json_string) \ + gvm_json_pull_event_t event; \ + gvm_json_pull_parser_t parser; \ + FILE *jsonstream = fstropen_r (json_string); \ + cookie_io_functions_t io_functions = {.read = read_with_error_on_eof, \ + .write = NULL, \ + .seek = NULL, \ + .close = NULL}; \ + FILE *errorstream = fopencookie (jsonstream, "r", io_functions); \ + gvm_json_pull_event_init (&event); \ + gvm_json_pull_parser_init_full (&parser, errorstream, 100, 4); + +#define CLEANUP_JSON_PARSER \ + gvm_json_pull_event_cleanup (&event); \ + gvm_json_pull_parser_cleanup (&parser); \ + fclose (jsonstream); + +#define CHECK_PATH_EQUALS(expected_path_str) \ + path_str = gvm_json_path_to_string (event.path); \ + assert_that (path_str, is_equal_to_string (expected_path_str)); \ + g_free (path_str); + +#define JSON_READ_ERROR "error reading JSON stream: Input/output error" + +Ensure (jsonpull, can_json_escape_strings) +{ + const char *unescaped_string = "\"'Abc\\\b\f\n\r\t\001Äöü'\""; + const char *escaped_string_dq = "\\\"'Abc\\\\\\b\\f\\n\\r\\t\\u0001Äöü'\\\""; + const char *escaped_string_sq = "\"\\'Abc\\\\\\b\\f\\n\\r\\t\\u0001Äöü\\'\""; + + gchar *escaped_string = NULL; + escaped_string = gvm_json_string_escape (NULL, FALSE); + assert_that (escaped_string, is_null); + + escaped_string = gvm_json_string_escape (unescaped_string, FALSE); + assert_that (escaped_string, is_equal_to_string (escaped_string_dq)); + g_free (escaped_string); + + escaped_string = gvm_json_string_escape (unescaped_string, TRUE); + assert_that (escaped_string, is_equal_to_string (escaped_string_sq)); + g_free (escaped_string); +} + +Ensure (jsonpull, can_init_parser_with_defaults) +{ + gvm_json_pull_parser_t parser; + FILE *strfile = fstropen_r ("[]"); + + gvm_json_pull_parser_init (&parser, strfile); + assert_that (parser.input_stream, is_equal_to (strfile)); + assert_that (parser.parse_buffer_limit, + is_equal_to (GVM_JSON_PULL_PARSE_BUFFER_LIMIT)); + assert_that (parser.read_buffer_size, + is_equal_to (GVM_JSON_PULL_READ_BUFFER_SIZE)); +} + +Ensure (jsonpull, can_parse_false) +{ + INIT_JSON_PARSER ("false"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_BOOLEAN)); + assert_that (cJSON_IsBool (event.value), is_true); + assert_that (cJSON_IsFalse (event.value), is_true); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_EOF)); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, can_parse_true) +{ + INIT_JSON_PARSER ("true"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_BOOLEAN)); + assert_that (cJSON_IsBool (event.value), is_true); + assert_that (cJSON_IsTrue (event.value), is_true); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_EOF)); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, can_parse_null) +{ + INIT_JSON_PARSER ("null"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_NULL)); + assert_that (cJSON_IsNull (event.value), is_true); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_EOF)); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, can_parse_empty_strings) +{ + INIT_JSON_PARSER ("\"\""); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_STRING)); + assert_that (event.value->valuestring, is_equal_to_string ("")); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_EOF)); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, can_parse_strings_with_content) +{ + INIT_JSON_PARSER ("\n\"123\\tXYZ\\nÄöü\"\n"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_STRING)); + assert_that (event.value->valuestring, is_equal_to_string ("123\tXYZ\nÄöü")); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_EOF)); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, can_parse_integer_numbers) +{ + INIT_JSON_PARSER ("-0987"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_NUMBER)); + assert_that (event.value->valueint, is_equal_to (-987)); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_EOF)); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, can_parse_floating_point_numbers) +{ + INIT_JSON_PARSER ("\t\n 1.2345e+4\n"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_NUMBER)); + assert_that (event.value->valuedouble, is_equal_to (1.2345e+4)); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_EOF)); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, can_parse_empty_arrays) +{ + INIT_JSON_PARSER ("[ ]"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_START)); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_END)); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_EOF)); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, can_parse_single_elem_arrays) +{ + gchar *path_str; + INIT_JSON_PARSER ("[ 123 ]"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_START)); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_NUMBER)); + assert_that (event.value->valueint, is_equal_to (123)); + CHECK_PATH_EQUALS ("$[0]"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_END)); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_EOF)); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, can_parse_multiple_elem_arrays) +{ + gchar *path_str; + INIT_JSON_PARSER ("[123, \"ABC\", null]"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_START)); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_NUMBER)); + assert_that (event.value->valueint, is_equal_to (123)); + CHECK_PATH_EQUALS ("$[0]"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_STRING)); + assert_that (event.value->valuestring, is_equal_to_string ("ABC")); + CHECK_PATH_EQUALS ("$[1]"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_NULL)); + CHECK_PATH_EQUALS ("$[2]"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_END)); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_EOF)); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, can_parse_empty_objects) +{ + INIT_JSON_PARSER ("{ }"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_START)); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_END)); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_EOF)); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, can_parse_single_elem_objects) +{ + gchar *path_str; + INIT_JSON_PARSER ("{ \"keyA\": \"valueA\" }"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_START)); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_STRING)); + assert_that (event.value->valuestring, is_equal_to_string ("valueA")); + CHECK_PATH_EQUALS ("$['keyA']"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_END)); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_EOF)); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, can_parse_multiple_elem_objects) +{ + gchar *path_str; + INIT_JSON_PARSER ("{ \"keyA\": \"valueA\", \"keyB\":12345 }"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_START)); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_STRING)); + assert_that (event.value->valuestring, is_equal_to_string ("valueA")); + CHECK_PATH_EQUALS ("$['keyA']"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_NUMBER)); + assert_that (event.value->valueint, is_equal_to (12345)); + CHECK_PATH_EQUALS ("$['keyB']"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_END)); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_EOF)); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, can_parse_nested_containers) +{ + gchar *path_str; + INIT_JSON_PARSER ("[{\"A\":null, \"B\":{\"C\": [1,2]}, \"D\":\"3\"}, [4]]"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_START)); + CHECK_PATH_EQUALS ("$"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_START)); + CHECK_PATH_EQUALS ("$[0]"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_NULL)); + CHECK_PATH_EQUALS ("$[0]['A']"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_START)); + CHECK_PATH_EQUALS ("$[0]['B']"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_START)); + CHECK_PATH_EQUALS ("$[0]['B']['C']"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_NUMBER)); + assert_that (event.value->valueint, is_equal_to (1)); + CHECK_PATH_EQUALS ("$[0]['B']['C'][0]"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_NUMBER)); + assert_that (event.value->valueint, is_equal_to (2)); + CHECK_PATH_EQUALS ("$[0]['B']['C'][1]"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_END)); + CHECK_PATH_EQUALS ("$[0]['B']['C']"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_END)); + CHECK_PATH_EQUALS ("$[0]['B']"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_STRING)); + assert_that (event.value->valuestring, is_equal_to_string ("3")); + CHECK_PATH_EQUALS ("$[0]['D']"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_END)); + CHECK_PATH_EQUALS ("$[0]"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_START)); + CHECK_PATH_EQUALS ("$[1]"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_NUMBER)); + assert_that (event.value->valueint, is_equal_to (4)); + CHECK_PATH_EQUALS ("$[1][0]"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_END)); + CHECK_PATH_EQUALS ("$[1]"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_END)); + CHECK_PATH_EQUALS ("$"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_EOF)); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, can_expand_arrays) +{ + gchar *path_str, *error_message; + cJSON *expanded, *child; + INIT_JSON_PARSER ("[[], [1], [2, [3]], [\"A\", \"\\\"B]\"]]"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_START)); + CHECK_PATH_EQUALS ("$"); + + // empty array + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_START)); + CHECK_PATH_EQUALS ("$[0]"); + expanded = gvm_json_pull_expand_container (&parser, &error_message); + assert_that (error_message, is_equal_to_string (NULL)); + assert_that (expanded, is_not_null); + assert_that (cJSON_IsArray (expanded), is_true); + assert_that (expanded->child, is_null); + cJSON_free (expanded); + + // single-element array + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_START)); + CHECK_PATH_EQUALS ("$[1]"); + expanded = gvm_json_pull_expand_container (&parser, &error_message); + assert_that (error_message, is_null); + assert_that (expanded, is_not_null); + assert_that (cJSON_IsArray (expanded), is_true); + child = expanded->child; + assert_that (child, is_not_null); + assert_that (cJSON_IsNumber (child), is_true); + assert_that (child->valueint, is_equal_to (1)); + child = child->next; + assert_that (child, is_null); + cJSON_free (expanded); + + // multi-element array + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_START)); + CHECK_PATH_EQUALS ("$[2]"); + expanded = gvm_json_pull_expand_container (&parser, &error_message); + assert_that (error_message, is_null); + assert_that (expanded, is_not_null); + assert_that (cJSON_IsArray (expanded), is_true); + child = expanded->child; + assert_that (child, is_not_null); + assert_that (cJSON_IsNumber (child), is_true); + assert_that (child->valueint, is_equal_to (2)); + child = child->next; + assert_that (child, is_not_null); + assert_that (cJSON_IsArray (child), is_true); + assert_that (child->child->valueint, is_equal_to (3)); + cJSON_free (expanded); + + // string array + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_START)); + CHECK_PATH_EQUALS ("$[3]"); + expanded = gvm_json_pull_expand_container (&parser, &error_message); + assert_that (error_message, is_null); + assert_that (expanded, is_not_null); + assert_that (cJSON_IsArray (expanded), is_true); + child = expanded->child; + assert_that (child, is_not_null); + assert_that (cJSON_IsString (child), is_true); + assert_that (child->valuestring, is_equal_to_string ("A")); + child = child->next; + assert_that (child, is_not_null); + assert_that (cJSON_IsString (child), is_true); + assert_that (child->valuestring, is_equal_to_string ("\"B]")); + cJSON_free (expanded); + + // array end and EOF + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_END)); + CHECK_PATH_EQUALS ("$"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_EOF)); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, can_expand_objects) +{ + gchar *path_str, *error_message; + cJSON *expanded, *child; + INIT_JSON_PARSER ( + "{\"A\":{}, \"B\": {\"C\": \"\\\"D}\", \"E\":123, \"F\":{}}}"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_START)); + CHECK_PATH_EQUALS ("$"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_START)); + CHECK_PATH_EQUALS ("$['A']"); + expanded = gvm_json_pull_expand_container (&parser, &error_message); + assert_that (error_message, is_null); + assert_that (cJSON_IsObject (expanded), is_true); + assert_that (expanded->child, is_null); + cJSON_free (expanded); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_START)); + CHECK_PATH_EQUALS ("$['B']"); + expanded = gvm_json_pull_expand_container (&parser, &error_message); + assert_that (error_message, is_null); + assert_that (cJSON_IsObject (expanded), is_true); + child = expanded->child; + assert_that (child, is_not_null); + assert_that (cJSON_IsString (child), is_true); + assert_that (child->string, is_equal_to_string ("C")); + assert_that (child->valuestring, is_equal_to_string ("\"D}")); + child = child->next; + assert_that (child, is_not_null); + assert_that (cJSON_IsNumber (child), is_true); + assert_that (child->string, is_equal_to_string ("E")); + assert_that (child->valueint, is_equal_to (123)); + child = child->next; + assert_that (child, is_not_null); + assert_that (cJSON_IsObject (child), is_true); + assert_that (child->string, is_equal_to_string ("F")); + assert_that (child->child, is_null); + cJSON_free (expanded); + + // object end and EOF + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_END)); + CHECK_PATH_EQUALS ("$"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_EOF)); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_read_error) +{ + INIT_READ_ERROR_JSON_PARSER ("123"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, is_equal_to_string (JSON_READ_ERROR)); + gvm_json_pull_parser_cleanup (&parser); + gvm_json_pull_event_cleanup (&event); + fclose (jsonstream); +} + +Ensure (jsonpull, fails_for_misspelled_true) +{ + INIT_JSON_PARSER ("trxyz"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, + is_equal_to_string ("misspelled keyword 'true'")); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_incomplete_true) +{ + INIT_JSON_PARSER ("tru"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, is_equal_to_string ("unexpected EOF")); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_misspelled_false) +{ + INIT_JSON_PARSER ("falxyz"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, + is_equal_to_string ("misspelled keyword 'false'")); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_misspelled_null) +{ + INIT_JSON_PARSER ("nulx"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, + is_equal_to_string ("misspelled keyword 'null'")); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_string_read_error) +{ + INIT_READ_ERROR_JSON_PARSER ("\"ABCDEFG\""); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, is_equal_to_string (JSON_READ_ERROR)); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_string_eof) +{ + INIT_JSON_PARSER ("\"no closing quote here"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, is_equal_to_string ("unexpected EOF")); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_overlong_string) +{ + INIT_JSON_PARSER ("\"This should be too long for a small parse buffer\""); + parser.parse_buffer_limit = 10; + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, + is_equal_to_string ("string exceeds size limit of 10 bytes")); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_invalid_string) +{ + INIT_JSON_PARSER ("\"This has an invalid escape sequence: \\x\""); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, + is_equal_to_string ("error parsing string")); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_number_read_error) +{ + INIT_READ_ERROR_JSON_PARSER ("12345.123456789"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, is_equal_to_string (JSON_READ_ERROR)); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_overlong_number) +{ + INIT_READ_ERROR_JSON_PARSER ("12345.123456789"); + parser.parse_buffer_limit = 10; + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, + is_equal_to_string ("number exceeds size limit of 10 bytes")); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_invalid_number) +{ + INIT_JSON_PARSER ("-+e"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, + is_equal_to_string ("error parsing number")); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_array_eof) +{ + INIT_JSON_PARSER ("["); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_START)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, is_equal_to_string ("unexpected EOF")); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_array_eof_after_value) +{ + INIT_JSON_PARSER ("[123"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_START)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_NUMBER)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, is_equal_to_string ("unexpected EOF")); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_array_eof_after_comma) +{ + INIT_JSON_PARSER ("[123,"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_START)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_NUMBER)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, is_equal_to_string ("unexpected EOF")); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_array_read_error) +{ + INIT_READ_ERROR_JSON_PARSER ("[ "); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_START)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, is_equal_to_string (JSON_READ_ERROR)); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_invalid_array_bracket) +{ + INIT_JSON_PARSER ("[}"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_START)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, + is_equal_to_string ("unexpected closing curly brace")); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_invalid_array_bracket_after_value) +{ + INIT_JSON_PARSER ("[123}"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_START)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_NUMBER)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, + is_equal_to_string ("unexpected closing curly brace")); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_invalid_array_other_char) +{ + INIT_JSON_PARSER ("[!"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_START)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, + is_equal_to_string ("unexpected character")); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_invalid_array_other_char_after_value) +{ + INIT_JSON_PARSER ("[123!"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_START)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_NUMBER)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, + is_equal_to_string ("expected comma or end of container")); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_object_key_eof) +{ + INIT_JSON_PARSER ("{"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_START)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, is_equal_to_string ("unexpected EOF")); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_object_key_read_error) +{ + INIT_READ_ERROR_JSON_PARSER ("{ "); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_START)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, is_equal_to_string (JSON_READ_ERROR)); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_object_key_invalid_string) +{ + INIT_JSON_PARSER ("{\"invalid escape:\\x\": 123}"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_START)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, + is_equal_to_string ("error parsing string")); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_invalid_object_key_bracket) +{ + INIT_JSON_PARSER ("{]"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_START)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, + is_equal_to_string ("unexpected closing square bracket")); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_invalid_object_key_other_char) +{ + INIT_JSON_PARSER ("{!"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_START)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, + is_equal_to_string ("unexpected character")); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_object_colon_eof) +{ + INIT_JSON_PARSER ("{\"A\" "); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_START)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, is_equal_to_string ("unexpected EOF")); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_object_colon_read_error) +{ + INIT_READ_ERROR_JSON_PARSER ("{\"A\" "); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_START)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, is_equal_to_string (JSON_READ_ERROR)); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_object_colon_other_char) +{ + INIT_JSON_PARSER ("{\"A\"!"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_START)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, is_equal_to_string ("expected colon")); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_object_value_eof) +{ + INIT_JSON_PARSER ("{\"A\": "); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_START)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, is_equal_to_string ("unexpected EOF")); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_object_value_read_error) +{ + INIT_READ_ERROR_JSON_PARSER ("{\"A\": "); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_START)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, is_equal_to_string (JSON_READ_ERROR)); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_object_value_curly_brace) +{ + INIT_JSON_PARSER ("{\"A\": }"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_START)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, + is_equal_to_string ("unexpected closing curly brace")); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_object_value_square_bracket) +{ + INIT_JSON_PARSER ("{\"A\": ]"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_START)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, + is_equal_to_string ("unexpected closing square bracket")); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_object_eof_after_value) +{ + INIT_JSON_PARSER ("{\"A\": 123"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_START)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_NUMBER)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, is_equal_to_string ("unexpected EOF")); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_object_square_bracket_after_value) +{ + INIT_JSON_PARSER ("{\"A\": 123 ]"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_START)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_NUMBER)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, + is_equal_to_string ("unexpected closing square bracket")); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_object_eof_after_comma) +{ + INIT_JSON_PARSER ("{\"A\": 123, "); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_START)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_NUMBER)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, is_equal_to_string ("unexpected EOF")); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_read_error_after_doc_end) +{ + INIT_READ_ERROR_JSON_PARSER ("123 "); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_NUMBER)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, is_equal_to_string (JSON_READ_ERROR)); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_content_after_doc_end) +{ + INIT_JSON_PARSER ("123 456"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_NUMBER)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, + is_equal_to_string ("unexpected character at end of file (52)")); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_expand_before_container) +{ + cJSON *cjson_value; + gchar *error_message; + INIT_JSON_PARSER ("[]"); + + cjson_value = gvm_json_pull_expand_container (&parser, &error_message); + assert_that (cjson_value, is_null); + assert_that (error_message, is_equal_to_string ("can only expand after" + " array or object start")); + + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_expand_after_value) +{ + cJSON *cjson_value; + gchar *error_message; + INIT_JSON_PARSER ("[123, 456]"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_START)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_NUMBER)); + + cjson_value = gvm_json_pull_expand_container (&parser, &error_message); + assert_that (cjson_value, is_null); + assert_that (error_message, is_equal_to_string ("can only expand after" + " array or object start")); + + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_expand_invalid_content) +{ + cJSON *cjson_value; + gchar *error_message; + INIT_JSON_PARSER ("[invalid content]"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_START)); + + cjson_value = gvm_json_pull_expand_container (&parser, &error_message); + assert_that (cjson_value, is_null); + assert_that (error_message, + is_equal_to_string ("could not parse expanded container")); + + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_expand_overlong) +{ + cJSON *cjson_value; + gchar *error_message; + INIT_JSON_PARSER ("[1234567890.123456780]"); + parser.parse_buffer_limit = 10; + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_START)); + + cjson_value = gvm_json_pull_expand_container (&parser, &error_message); + assert_that (cjson_value, is_null); + assert_that (error_message, + is_equal_to_string ("container exceeds size limit of 10 bytes")); + + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_expand_unexpected_curly_brace) +{ + cJSON *cjson_value; + gchar *error_message; + INIT_JSON_PARSER ("[ 123 }"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_START)); + + cjson_value = gvm_json_pull_expand_container (&parser, &error_message); + assert_that (cjson_value, is_null); + assert_that (error_message, + is_equal_to_string ("unexpected closing curly brace")); + + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_expand_unexpected_square_bracket) +{ + cJSON *cjson_value; + gchar *error_message; + INIT_JSON_PARSER ("{ \"A\": 123 ]"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_START)); + + cjson_value = gvm_json_pull_expand_container (&parser, &error_message); + assert_that (cjson_value, is_null); + assert_that (error_message, + is_equal_to_string ("unexpected closing square bracket")); + + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_expand_eof) +{ + cJSON *cjson_value; + gchar *error_message; + INIT_JSON_PARSER ("[ 123"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_START)); + + cjson_value = gvm_json_pull_expand_container (&parser, &error_message); + assert_that (cjson_value, is_null); + assert_that (error_message, is_equal_to_string ("unexpected EOF")); + + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_expand_read_error) +{ + cJSON *cjson_value; + gchar *error_message; + INIT_READ_ERROR_JSON_PARSER ("[ 123 "); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_START)); + + cjson_value = gvm_json_pull_expand_container (&parser, &error_message); + assert_that (cjson_value, is_null); + assert_that (error_message, is_equal_to_string (JSON_READ_ERROR)); + + CLEANUP_JSON_PARSER; +} + +int +main (int argc, char **argv) +{ + TestSuite *suite; + + suite = create_test_suite (); + + add_test_with_context (suite, jsonpull, can_json_escape_strings); + + add_test_with_context (suite, jsonpull, can_init_parser_with_defaults); + + add_test_with_context (suite, jsonpull, can_parse_false); + add_test_with_context (suite, jsonpull, can_parse_true); + add_test_with_context (suite, jsonpull, can_parse_null); + + add_test_with_context (suite, jsonpull, can_parse_empty_strings); + add_test_with_context (suite, jsonpull, can_parse_strings_with_content); + + add_test_with_context (suite, jsonpull, can_parse_integer_numbers); + add_test_with_context (suite, jsonpull, can_parse_floating_point_numbers); + + add_test_with_context (suite, jsonpull, can_parse_empty_arrays); + add_test_with_context (suite, jsonpull, can_parse_single_elem_arrays); + add_test_with_context (suite, jsonpull, can_parse_multiple_elem_arrays); + + add_test_with_context (suite, jsonpull, can_parse_empty_objects); + add_test_with_context (suite, jsonpull, can_parse_single_elem_objects); + add_test_with_context (suite, jsonpull, can_parse_multiple_elem_objects); + add_test_with_context (suite, jsonpull, can_parse_nested_containers); + add_test_with_context (suite, jsonpull, can_expand_arrays); + add_test_with_context (suite, jsonpull, can_expand_objects); + + add_test_with_context (suite, jsonpull, fails_for_read_error); + + add_test_with_context (suite, jsonpull, fails_for_misspelled_true); + add_test_with_context (suite, jsonpull, fails_for_incomplete_true); + add_test_with_context (suite, jsonpull, fails_for_misspelled_false); + add_test_with_context (suite, jsonpull, fails_for_misspelled_null); + + add_test_with_context (suite, jsonpull, fails_for_string_eof); + add_test_with_context (suite, jsonpull, fails_for_string_read_error); + add_test_with_context (suite, jsonpull, fails_for_overlong_string); + add_test_with_context (suite, jsonpull, fails_for_invalid_string); + + add_test_with_context (suite, jsonpull, fails_for_number_read_error); + add_test_with_context (suite, jsonpull, fails_for_overlong_number); + add_test_with_context (suite, jsonpull, fails_for_invalid_number); + + add_test_with_context (suite, jsonpull, fails_for_array_eof); + add_test_with_context (suite, jsonpull, fails_for_array_eof_after_value); + add_test_with_context (suite, jsonpull, fails_for_array_eof_after_comma); + add_test_with_context (suite, jsonpull, fails_for_array_read_error); + add_test_with_context (suite, jsonpull, fails_for_invalid_array_bracket); + add_test_with_context (suite, jsonpull, + fails_for_invalid_array_bracket_after_value); + add_test_with_context (suite, jsonpull, fails_for_invalid_array_other_char); + add_test_with_context (suite, jsonpull, + fails_for_invalid_array_other_char_after_value); + + add_test_with_context (suite, jsonpull, fails_for_object_key_eof); + add_test_with_context (suite, jsonpull, fails_for_object_key_read_error); + add_test_with_context (suite, jsonpull, fails_for_object_key_invalid_string); + add_test_with_context (suite, jsonpull, fails_for_invalid_object_key_bracket); + add_test_with_context (suite, jsonpull, + fails_for_invalid_object_key_other_char); + + add_test_with_context (suite, jsonpull, fails_for_object_colon_eof); + add_test_with_context (suite, jsonpull, fails_for_object_colon_read_error); + add_test_with_context (suite, jsonpull, fails_for_object_colon_other_char); + + add_test_with_context (suite, jsonpull, fails_for_object_value_eof); + add_test_with_context (suite, jsonpull, fails_for_object_value_read_error); + add_test_with_context (suite, jsonpull, fails_for_object_value_curly_brace); + add_test_with_context (suite, jsonpull, + fails_for_object_value_square_bracket); + add_test_with_context (suite, jsonpull, fails_for_object_eof_after_value); + add_test_with_context (suite, jsonpull, fails_for_object_eof_after_comma); + add_test_with_context (suite, jsonpull, + fails_for_object_square_bracket_after_value); + + add_test_with_context (suite, jsonpull, fails_for_read_error_after_doc_end); + add_test_with_context (suite, jsonpull, fails_for_content_after_doc_end); + + add_test_with_context (suite, jsonpull, fails_for_expand_before_container); + add_test_with_context (suite, jsonpull, fails_for_expand_after_value); + add_test_with_context (suite, jsonpull, fails_for_expand_invalid_content); + add_test_with_context (suite, jsonpull, fails_for_expand_overlong); + add_test_with_context (suite, jsonpull, + fails_for_expand_unexpected_curly_brace); + add_test_with_context (suite, jsonpull, + fails_for_expand_unexpected_square_bracket); + add_test_with_context (suite, jsonpull, fails_for_expand_read_error); + add_test_with_context (suite, jsonpull, fails_for_expand_eof); + + if (argc > 1) + return run_single_test (suite, argv[1], create_text_reporter ()); + return run_test_suite (suite, create_text_reporter ()); +} diff --git a/util/kb.h b/util/kb.h index a67b7a58..fccb36b6 100644 --- a/util/kb.h +++ b/util/kb.h @@ -73,7 +73,7 @@ struct kb_item { char *v_str; /**< Hold an str value for this kb item. */ int v_int; /**< Hold an int value for this kb item. */ - }; /**< Value of this knowledge base item. */ + }; /**< Value of this knowledge base item. */ size_t len; /**< Length of string. */ struct kb_item *next; /**< Next item in list. */ diff --git a/util/sshutils.c b/util/sshutils.c index c590bb34..c285895d 100644 --- a/util/sshutils.c +++ b/util/sshutils.c @@ -76,6 +76,8 @@ gvm_ssh_public_from_private (const char *private_key, const char *passphrase) const char *type; int ret; + if (private_key == NULL) + return NULL; decrypted_priv = gvm_ssh_pkcs8_decrypt (private_key, passphrase); ret = ssh_pki_import_privkey_base64 (decrypted_priv ? decrypted_priv : private_key, diff --git a/util/versionutils.c b/util/versionutils.c new file mode 100644 index 00000000..d0a2eb60 --- /dev/null +++ b/util/versionutils.c @@ -0,0 +1,363 @@ +/* SPDX-FileCopyrightText: 2009-2024 Greenbone AG + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +/** + * @file + * @brief Functions to handle version numbers / version strings. + * + * Up to now this library provides a function to compare two version numbers / + * two version strings to decide which version is the newer one. + */ + +#include "versionutils.h" + +#include +#include +#include +#include +#include + +#undef G_LOG_DOMAIN +/** + * @brief GLib logging domain. + */ +#define G_LOG_DOMAIN "libgvm util" + +static gchar * +prepare_version_string (const char *); + +static int +get_release_state (const char *, int); + +static char * +get_part (const char *, int); + +static gboolean +is_text (const char *); + +static char * +str_cpy (char *, int); + +/** + * @brief Compare two version strings representing a software version + * to decide which version is newer. + * + * @param[in] version1 The first version string to compare. + * @param[in] version2 The second version string to compare. + * + * @return Returns a value > 0 if version1 is newer than version2. + * Returns 0 if version1 is the same than version2. + * Returns a value between -1 and -4 if version2 is newer + * than version1. + * Returns -5 if the result is undefined. + */ +int +cmp_versions (const char *version1, const char *version2) +{ + char *ver1, *ver2; + char *part1, *part2; + int index1 = 0, index2 = 0; + int release_state1 = 0, release_state2 = 0; + int rs1, rs2; + + ver1 = prepare_version_string (version1); + ver2 = prepare_version_string (version2); + + if (ver1 == NULL || ver2 == NULL) + { + g_free (ver1); + g_free (ver2); + return (-5); + } + if (strcmp (ver1, ver2) == 0) + { + g_free (ver1); + g_free (ver2); + return (0); + } + + if ((release_state1 = get_release_state (ver1, index1))) + index1++; + if ((release_state2 = get_release_state (ver2, index2))) + index2++; + + part1 = get_part (ver1, index1); + part2 = get_part (ver2, index2); + while (part1 && part2) + { + if (strcmp (part1, part2) == 0) + { + index1++; + index2++; + g_free (part1); + g_free (part2); + part1 = get_part (ver1, index1); + part2 = get_part (ver2, index2); + continue; + } + else + break; + } + + if (part1 == NULL && part2 == NULL) + return (release_state2 - release_state1); + + if (is_text (part1) || is_text (part2)) + { + if (part1) + g_free (part1); + if (part2) + g_free (part2); + return (-5); // undefined + } + + rs1 = get_release_state (ver1, index1); + rs2 = get_release_state (ver2, index2); + + if ((rs1 && release_state1) || (rs2 && release_state2)) + return (-5); // undefined + + if (part1 == NULL) + { + g_free (part2); + if (rs2) + return (rs2 - release_state1); + else + return (-1); + } + + if (part2 == NULL) + { + g_free (part1); + if (rs1) + return (release_state2 - rs1); + else + return (1); + } + + int ret = -5; + + if (rs1 && rs2) + ret = rs2 - rs1; + + if (rs1) + ret = -1; + + if (rs2) + ret = 1; + + if (!rs1 && !rs2 && atoi (part1) < atoi (part2)) + ret = -1; + + if (!rs1 && !rs2 && atoi (part1) == atoi (part2)) + ret = 0; + + if (!rs1 && !rs2 && atoi (part1) > atoi (part2)) + ret = 1; + + g_free (part1); + g_free (part2); + g_free (ver1); + g_free (ver2); + return (ret); +} + +/** + * @brief Prepare the version string for comparison. + * + * @param[in] version The version string to generate the prepared + * version string from. + * + * @return Returns a prepared copy of the version string version. + */ +static gchar * +prepare_version_string (const char *version) +{ + char prep_version[2048]; + char *ver; + int index_v, index_pv; + gboolean is_digit; + + if (!version) + return (NULL); + + if (strlen (version) > 1024) + return (NULL); + + ver = g_strdup (version); + + /* set all characters to lowercase */ + char *c = ver; + for (; *c; c++) + *c = tolower (*c); + + index_v = index_pv = 0; + + is_digit = g_ascii_isdigit (ver[0]); + + while (index_v < (int) strlen (ver) && index_pv < 2047) + { + if (ver[index_v] == '\\') + { + index_v++; + continue; + } + + if (ver[index_v] == '_' || ver[index_v] == '-' || ver[index_v] == '+' + || ver[index_v] == ':' || ver[index_v] == '.') + { + if (index_pv > 0 && prep_version[index_pv - 1] != '.') + { + prep_version[index_pv] = '.'; + index_pv++; + } + index_v++; + continue; + } + + if (is_digit != g_ascii_isdigit (ver[index_v])) + { + is_digit = !is_digit; + if (index_pv > 0 && prep_version[index_pv - 1] != '.') + { + prep_version[index_pv] = '.'; + index_pv++; + } + } + + if (ver[index_v] == 'r') + { + if (strstr (ver + index_v, "releasecandidate") == ver + index_v) + { + prep_version[index_pv] = 'r'; + prep_version[index_pv + 1] = 'c'; + index_pv += 2; + index_v += 16; + continue; + } + if ((strstr (ver + index_v, "release-candidate") == ver + index_v) + || (strstr (ver + index_v, "release_candidate") == ver + index_v)) + { + prep_version[index_pv] = 'r'; + prep_version[index_pv + 1] = 'c'; + index_pv += 2; + index_v += 17; + continue; + } + } + + prep_version[index_pv] = ver[index_v]; + index_v++; + index_pv++; + } + + prep_version[index_pv] = '\0'; + g_free (ver); + return (g_strdup (prep_version)); +} + +/** + * @brief Gets the release state of a specified part of the version string + * if any. + * + * @param[in] version The version string to get the release state from. + * @param[in] index The part of the version string to check. + * + * @return Returns 0 if there is no release state, returns 4 if the release + * state is "development" (dev), returns 3 if the state is "alpha", + * 2 if the state is beta and 1 if the state is release candidate (rc). + */ +static int +get_release_state (const char *version, int index) +{ + char *part; + int rel_stat = 0; + + part = get_part (version, index); + + if (part == NULL) + return (0); + + if (strcmp (part, "dev") == 0 || strcmp (part, "development") == 0) + rel_stat = 4; + if (strcmp (part, "alpha") == 0) + rel_stat = 3; + if (strcmp (part, "beta") == 0) + rel_stat = 2; + if (strcmp (part, "rc") == 0) + rel_stat = 1; + + g_free (part); + return (rel_stat); +} + +/** + * @brief Gets the part of the version string that is specified by index. + * + * @param[in] version The version string to get the part from. + * @param[in] index The part of the version string to return. + * + * @return Returns a copy of the specified part of the version string. + */ +static char * +get_part (const char *version, int index) +{ + int dot_count = 0; + int begin, end; + + for (begin = 0; begin < (int) strlen (version) && dot_count < index; begin++) + { + if (version[begin] == '.') + dot_count++; + } + + if (begin == (int) strlen (version)) + return (NULL); + + for (end = begin + 1; end < (int) strlen (version) && version[end] != '.'; + end++) + ; + + return (str_cpy ((char *) (version + begin), end - begin)); +} + +/** + * @brief Checks if a given part of the version string is plain text. + * + * @param[in] part The part of the version string to check. + * + * @return Returns TRUE if part contains only plain text, FALSE otherwise. + */ +static gboolean +is_text (const char *part) +{ + if (!part) + return (FALSE); + if (strcmp (part, "dev") == 0 || strcmp (part, "alpha") == 0 + || strcmp (part, "beta") == 0 || strcmp (part, "rc") == 0) + return (FALSE); + if (g_ascii_isdigit (*part)) + return (FALSE); + return (TRUE); +} + +/** + * @brief Copy size characters of a string to an newly allocated new string. + * + * @param[in] src The string the first size characters are to be copied + * from. + * @param[in] size The number of characters to copy. + * + * @return The copy of the first size characters of src as a new string. + */ +static char * +str_cpy (char *source, int size) +{ + char *result; + result = (char *) g_malloc (size + 1); + memset (result, 0, size + 1); + strncpy (result, source, size); + return (result); +} diff --git a/util/versionutils.h b/util/versionutils.h new file mode 100644 index 00000000..d093189c --- /dev/null +++ b/util/versionutils.h @@ -0,0 +1,20 @@ +/* SPDX-FileCopyrightText: 2009-2024 Greenbone AG + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +/** + * @file + * @brief Headers for version utils. + */ + +#ifndef _GVM_VERSIONUTILS_H +#define _GVM_VERSIONUTILS_H + +#include +#include + +int +cmp_versions (const char *, const char *); + +#endif diff --git a/util/versionutils_tests.c b/util/versionutils_tests.c new file mode 100644 index 00000000..c7e0502b --- /dev/null +++ b/util/versionutils_tests.c @@ -0,0 +1,88 @@ +/* SPDX-FileCopyrightText: 2019-2023 Greenbone AG + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "versionutils.c" + +#include +#include + +Describe (versionutils); +BeforeEach (versionutils) +{ +} + +AfterEach (versionutils) +{ +} + +/* parse_entity */ + +Ensure (versionutils, cmp_versions) +{ + char *version1, *version2; + int result; + + version1 = "test"; + version2 = "test-1"; + result = cmp_versions (version1, version2); + assert_that (result, is_less_than (0)); + assert_that (result, is_greater_than (-5)); + + version1 = "beta-test-2"; + version2 = "test_1"; + result = cmp_versions (version1, version2); + assert_that (result, is_greater_than (0)); + + version1 = "beta-test-2"; + version2 = "test-2.beta"; + result = cmp_versions (version1, version2); + assert_that (result, is_equal_to (0)); + + version1 = "test-2.beta"; + version2 = "test-2.a"; + result = cmp_versions (version1, version2); + assert_that (result, is_equal_to (-5)); + + version1 = "test-2.beta"; + version2 = "test-2.1"; + result = cmp_versions (version1, version2); + assert_that (result, is_equal_to (-1)); + + version1 = "test-2.release_candidate"; + version2 = "test-2"; + result = cmp_versions (version1, version2); + assert_that (result, is_equal_to (-1)); + + version1 = "test-2.release_candidate2"; + version2 = "test-2.release_candidate1"; + result = cmp_versions (version1, version2); + assert_that (result, is_greater_than (0)); + + version1 = "test-2.release_candidatea"; + version2 = "test-2.release_candidateb"; + result = cmp_versions (version1, version2); + assert_that (result, is_equal_to (-5)); + + version1 = "2024-06-24"; + version2 = "2024-06-23"; + result = cmp_versions (version1, version2); + assert_that (result, is_greater_than (0)); +} + +/* Test suite. */ +int +main (int argc, char **argv) +{ + TestSuite *suite; + + suite = create_test_suite (); + + add_test_with_context (suite, versionutils, cmp_versions); + + if (argc > 1) + return run_single_test (suite, argv[1], create_text_reporter ()); + + return run_test_suite (suite, create_text_reporter ()); +} diff --git a/util/xmlutils.c b/util/xmlutils.c index e6edba5f..fbcf4f48 100644 --- a/util/xmlutils.c +++ b/util/xmlutils.c @@ -3030,7 +3030,7 @@ xml_file_iterator_next (xml_file_iterator_t iterator, gchar **error) { if (error) { - xmlErrorPtr xml_error; + const xmlError *xml_error; xml_error = xmlCtxtGetLastError (iterator->parser_ctxt); *error = g_strdup_printf ("error parsing XML" " (line %d column %d): %s",