diff --git a/.bouncer.yaml b/.bouncer.yaml new file mode 100644 index 00000000..02b64cec --- /dev/null +++ b/.bouncer.yaml @@ -0,0 +1,49 @@ +permit: + - BSD.* + - MIT.* + - Apache.* + - MPL.* + - ISC + - CC0-1.0 +ignore-packages: + # packageurl-go is released under the MIT license located in the root of the repo at /mit.LICENSE + - github.com/anchore/packageurl-go + + # tools-golang is released under the Apache License, version 2.0 (Apache-2.0) + # https://github.com/spdx/tools-golang/blob/main/LICENSE.code + - github.com/spdx/tools-golang + + # from: https://github.com/xi2/xz/blob/master/LICENSE + # All these files have been put into the public domain. + # You can do whatever you want with these files. + - github.com/xi2/xz + + # from: https://gitlab.com/cznic/sqlite/-/blob/v1.15.4/LICENSE + # This is a BSD-3-Clause license + - modernc.org/libc + - modernc.org/libc/errno + - modernc.org/libc/fcntl + - modernc.org/libc/fts + - modernc.org/libc/grp + - modernc.org/libc/langinfo + - modernc.org/libc/limits + - modernc.org/libc/netdb + - modernc.org/libc/netinet/in + - modernc.org/libc/poll + - modernc.org/libc/pthread + - modernc.org/libc/pwd + - modernc.org/libc/signal + - modernc.org/libc/stdio + - modernc.org/libc/stdlib + - modernc.org/libc/sys/socket + - modernc.org/libc/sys/stat + - modernc.org/libc/sys/types + - modernc.org/libc/termios + - modernc.org/libc/time + - modernc.org/libc/unistd + - modernc.org/libc/utime + - modernc.org/libc/uuid/uuid + - modernc.org/libc/wctype + - modernc.org/mathutil + - modernc.org/memory + diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..0c09b404 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +publish/test-fixtures/** filter=lfs diff=lfs merge=lfs -text +test/acceptance/test-fixtures/result/** filter=lfs diff=lfs merge=lfs -text diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..bcd163cf --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,20 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +**Please provide a set of steps on how to reproduce the issue** + +**What happened**: + +**What you expected to happen**: + +**Anything else we need to know?**: + +**Environment**: +- Output of `grype-db version`: +- OS (e.g: `cat /etc/os-release` or similar): diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 58b188eb..edd71d50 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,11 +1,6 @@ contact_links: - - name: Help & Support Slack Channel ⛑️ - # link to toolbox-help channel - url: https://anchorecommunity.slack.com/archives/C019BUXV7R6 - about: Ask a questions and get answers - - - name: Developer Slack Channel 💬 - # link to toolbox-dev channel - url: https://anchorecommunity.slack.com/archives/C0190NF9TSM - about: Participate in design discussions and feature development \ No newline at end of file + - name: Join the Slack community 💬 + # link to our community Slack registration page + url: https://anchore.com/slack + about: 'Come chat with us! Ask for help, join our software development efforts, or just give us feedback!' diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..d07c5f15 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,15 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: enhancement +assignees: '' + +--- + +**What would you like to be added**: + +**Why is this needed**: + +**Additional context**: + diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..eb2c6595 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: daily + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: daily diff --git a/.github/scripts/aggregate-all-provider-cache.py b/.github/scripts/aggregate-all-provider-cache.py new file mode 100755 index 00000000..453462d2 --- /dev/null +++ b/.github/scripts/aggregate-all-provider-cache.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python3 +import yaml +import sys +import subprocess + +with open(".grype-db.yaml") as f: + providers = [x["name"] for x in yaml.safe_load(f.read()).get("provider", {}).get("configs", [])] + +print(f"providers: {providers}") +for provider in providers: + subprocess.run(f"make download-provider-cache provider={provider}", shell=True, check=True, stdout=sys.stdout, stderr=sys.stderr) diff --git a/.github/scripts/ci-check.sh b/.github/scripts/ci-check.sh new file mode 100755 index 00000000..b507c327 --- /dev/null +++ b/.github/scripts/ci-check.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +red=$(tput setaf 1) +bold=$(tput bold) +normal=$(tput sgr0) + +# assert we are running in CI (or die!) +if [[ -z "$CI" ]]; then + echo "${bold}${red}This script should ONLY be run in CI. Exiting...${normal}" + exit 1 +fi diff --git a/.github/scripts/goreleaser-install.sh b/.github/scripts/goreleaser-install.sh new file mode 100755 index 00000000..0a5497d2 --- /dev/null +++ b/.github/scripts/goreleaser-install.sh @@ -0,0 +1,398 @@ +#!/bin/sh +set -e +# Code generated by godownloader on 2019-12-25T12:47:14Z. DO NOT EDIT. +# + +usage() { + this=$1 + cat </dev/null +} +echoerr() { + echo "$@" 1>&2 +} +log_prefix() { + echo "$0" +} +_logp=6 +log_set_priority() { + _logp="$1" +} +log_priority() { + if test -z "$1"; then + echo "$_logp" + return + fi + [ "$1" -le "$_logp" ] +} +log_tag() { + case $1 in + 0) echo "emerg" ;; + 1) echo "alert" ;; + 2) echo "crit" ;; + 3) echo "err" ;; + 4) echo "warning" ;; + 5) echo "notice" ;; + 6) echo "info" ;; + 7) echo "debug" ;; + *) echo "$1" ;; + esac +} +log_debug() { + log_priority 7 || return 0 + echoerr "$(log_prefix)" "$(log_tag 7)" "$@" +} +log_info() { + log_priority 6 || return 0 + echoerr "$(log_prefix)" "$(log_tag 6)" "$@" +} +log_err() { + log_priority 3 || return 0 + echoerr "$(log_prefix)" "$(log_tag 3)" "$@" +} +log_crit() { + log_priority 2 || return 0 + echoerr "$(log_prefix)" "$(log_tag 2)" "$@" +} +uname_os() { + os=$(uname -s | tr '[:upper:]' '[:lower:]') + case "$os" in + cygwin_nt*) os="windows" ;; + mingw*) os="windows" ;; + msys_nt*) os="windows" ;; + esac + echo "$os" +} +uname_arch() { + arch=$(uname -m) + case $arch in + x86_64) arch="amd64" ;; + x86) arch="386" ;; + i686) arch="386" ;; + i386) arch="386" ;; + aarch64) arch="arm64" ;; + armv5*) arch="armv5" ;; + armv6*) arch="armv6" ;; + armv7*) arch="armv7" ;; + esac + echo ${arch} +} +uname_os_check() { + os=$(uname_os) + case "$os" in + darwin) return 0 ;; + dragonfly) return 0 ;; + freebsd) return 0 ;; + linux) return 0 ;; + android) return 0 ;; + nacl) return 0 ;; + netbsd) return 0 ;; + openbsd) return 0 ;; + plan9) return 0 ;; + solaris) return 0 ;; + windows) return 0 ;; + esac + log_crit "uname_os_check '$(uname -s)' got converted to '$os' which is not a GOOS value. Please file bug at https://github.com/client9/shlib" + return 1 +} +uname_arch_check() { + arch=$(uname_arch) + case "$arch" in + 386) return 0 ;; + amd64) return 0 ;; + arm64) return 0 ;; + armv5) return 0 ;; + armv6) return 0 ;; + armv7) return 0 ;; + ppc64) return 0 ;; + ppc64le) return 0 ;; + mips) return 0 ;; + mipsle) return 0 ;; + mips64) return 0 ;; + mips64le) return 0 ;; + s390x) return 0 ;; + amd64p32) return 0 ;; + esac + log_crit "uname_arch_check '$(uname -m)' got converted to '$arch' which is not a GOARCH value. Please file bug report at https://github.com/client9/shlib" + return 1 +} +untar() { + tarball=$1 + case "${tarball}" in + *.tar.gz | *.tgz) tar --no-same-owner -xzf "${tarball}" ;; + *.tar) tar --no-same-owner -xf "${tarball}" ;; + *.zip) unzip "${tarball}" ;; + *) + log_err "untar unknown archive format for ${tarball}" + return 1 + ;; + esac +} +http_download_curl() { + local_file=$1 + source_url=$2 + header=$3 + if [ -z "$header" ]; then + code=$(curl -w '%{http_code}' -sL -o "$local_file" "$source_url") + else + code=$(curl -w '%{http_code}' -sL -H "$header" -o "$local_file" "$source_url") + fi + if [ "$code" != "200" ]; then + log_debug "http_download_curl received HTTP status $code" + return 1 + fi + return 0 +} +http_download_wget() { + local_file=$1 + source_url=$2 + header=$3 + if [ -z "$header" ]; then + wget -q -O "$local_file" "$source_url" + else + wget -q --header "$header" -O "$local_file" "$source_url" + fi +} +http_download() { + log_debug "http_download $2" + if is_command curl; then + http_download_curl "$@" + return + elif is_command wget; then + http_download_wget "$@" + return + fi + log_crit "http_download unable to find wget or curl" + return 1 +} +http_copy() { + tmp=$(mktemp) + http_download "${tmp}" "$1" "$2" || return 1 + body=$(cat "$tmp") + rm -f "${tmp}" + echo "$body" +} +github_release() { + owner_repo=$1 + version=$2 + test -z "$version" && version="latest" + giturl="https://github.com/${owner_repo}/releases/${version}" + json=$(http_copy "$giturl" "Accept:application/json") + test -z "$json" && return 1 + version=$(echo "$json" | tr -s '\n' ' ' | sed 's/.*"tag_name":"//' | sed 's/".*//') + test -z "$version" && return 1 + echo "$version" +} +hash_sha256() { + TARGET=${1:-/dev/stdin} + if is_command gsha256sum; then + hash=$(gsha256sum "$TARGET") || return 1 + echo "$hash" | cut -d ' ' -f 1 + elif is_command sha256sum; then + hash=$(sha256sum "$TARGET") || return 1 + echo "$hash" | cut -d ' ' -f 1 + elif is_command shasum; then + hash=$(shasum -a 256 "$TARGET" 2>/dev/null) || return 1 + echo "$hash" | cut -d ' ' -f 1 + elif is_command openssl; then + hash=$(openssl -dst openssl dgst -sha256 "$TARGET") || return 1 + echo "$hash" | cut -d ' ' -f a + else + log_crit "hash_sha256 unable to find command to compute sha-256 hash" + return 1 + fi +} +hash_sha256_verify() { + TARGET=$1 + checksums=$2 + if [ -z "$checksums" ]; then + log_err "hash_sha256_verify checksum file not specified in arg2" + return 1 + fi + BASENAME=${TARGET##*/} + want=$(grep "${BASENAME}$" "${checksums}" 2>/dev/null | tr '\t' ' ' | cut -d ' ' -f 1) + if [ -z "$want" ]; then + log_err "hash_sha256_verify unable to find checksum for '${TARGET}' in '${checksums}'" + return 1 + fi + got=$(hash_sha256 "$TARGET") + if [ "$want" != "$got" ]; then + log_err "hash_sha256_verify checksum for '$TARGET' did not verify ${want} vs $got" + return 1 + fi +} +cat /dev/null < /dev/null + +NEXT_VERSION=$(cat VERSION) + +if [[ "$NEXT_VERSION" == "" || "${NEXT_VERSION}" == "(Unreleased)" ]]; then + echo "Could not determine the next version to release. Exiting..." + exit 1 +fi + +while true; do + read -p "${bold}Do you want to trigger a release for version '${NEXT_VERSION}'?${normal} [y/n] " yn + case $yn in + [Yy]* ) echo; break;; + [Nn]* ) echo; echo "Cancelling release..."; exit;; + * ) echo "Please answer yes or no.";; + esac +done + +echo "${bold}Kicking off release for ${NEXT_VERSION}${normal}..." +echo +gh workflow run release.yaml -f version=${NEXT_VERSION} + +echo +echo "${bold}Waiting for release to start...${normal}" +sleep 10 + +set +e + +echo "${bold}Head to the release workflow to monitor the release:${normal} $(gh run list --workflow=release.yaml --limit=1 --json url --jq '.[].url')" +id=$(gh run list --workflow=release.yaml --limit=1 --json databaseId --jq '.[].databaseId') +gh run watch $id --exit-status || (echo ; echo "${bold}Logs of failed step:${normal}" && GH_PAGER="" gh run view $id --log-failed) diff --git a/.github/workflows/daily-data-sync.yaml b/.github/workflows/daily-data-sync.yaml new file mode 100644 index 00000000..a236f7d9 --- /dev/null +++ b/.github/workflows/daily-data-sync.yaml @@ -0,0 +1,142 @@ +name: 'Daily Data Sync' +on: + # allow for kicking off DB builds manually + workflow_dispatch: + + + # run 7 AM (UTC) daily + schedule: + - cron: '0 7 * * *' +env: + GO_VERSION: "1.20.x" + CGO_ENABLED: "0" + GO_CACHE_KEY: d41d8cd98f00 + SLACK_NOTIFICATIONS: true + +jobs: + discover-providers: + name: "Discover vulnerability providers" + runs-on: ubuntu-20.04 + outputs: + providers: ${{ steps.read-providers.outputs.providers }} + steps: + - uses: actions/checkout@v3 + + - name: Read configured providers + id: read-providers + # TODO: honor CI overrides + run: | + content=`make show-providers` + echo "providers=$content" >> $GITHUB_OUTPUT + + update-provider: + name: "Update provider" + needs: discover-providers + runs-on: ubuntu-20.04 + # set the permissions granted to the github token to publish to ghcr.io + permissions: + contents: read + packages: write + strategy: + matrix: + provider: ${{fromJson(needs.discover-providers.outputs.providers)}} + fail-fast: false + steps: + - uses: actions/setup-go@v3 + with: + go-version: ${{ env.GO_VERSION }} + + - uses: actions/checkout@v3 + + - name: Restore go cache + id: go-cache + uses: actions/cache@v3 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }}-${{ env.GO_CACHE_KEY }} + restore-keys: | + ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ env.GO_CACHE_KEY }}- + + - name: Restore tool cache + id: tool-cache + uses: actions/cache@v3 + with: + path: ${{ github.workspace }}/.tmp + key: ${{ runner.os }}-tool-${{ hashFiles('Makefile') }}-${{ env.GO_CACHE_KEY }} + + - name: (cache-miss) Bootstrap all project dependencies + if: steps.tool-cache.outputs.cache-hit != 'true' || steps.go-cache.outputs.cache-hit != 'true' + run: make bootstrap + + - name: Login to ghcr.io + run: | + echo ${{ secrets.GITHUB_TOKEN }} | oras login ghcr.io --username ${{ github.actor }} --password-stdin + + - name: Download the existing provider state + run: bash -c "make download-provider-cache provider=${{ matrix.provider }} date=latest || true" + + - name: Update the provider + run: make refresh-provider-cache provider=${{ matrix.provider }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload the provider workspace state + run: make upload-provider-cache provider=${{ matrix.provider }} + +# - uses: 8398a7/action-slack@v3 +# with: +# status: ${{ job.status }} +# fields: workflow,eventName +# text: Pulling the feed data has failed +# env: +# SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TOOLBOX_WEBHOOK_URL }} +# if: ${{ failure() && env.SLACK_NOTIFICATIONS == 'true' }} + + aggregate-cache: + name: "Aggregate provider cache" + runs-on: ubuntu-20.04 + needs: update-provider + # set the permissions granted to the github token to read the pull cache from ghcr.io + permissions: + packages: write + contents: read + steps: + + - uses: actions/setup-go@v3 + with: + go-version: ${{ env.GO_VERSION }} + + - uses: actions/checkout@v3 + + - name: Restore go cache + id: go-cache + uses: actions/cache@v3 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }}-${{ env.GO_CACHE_KEY }} + restore-keys: | + ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ env.GO_CACHE_KEY }}- + + - name: Restore tool cache + id: tool-cache + uses: actions/cache@v3 + with: + path: ${{ github.workspace }}/.tmp + key: ${{ runner.os }}-tool-${{ hashFiles('Makefile') }}-${{ env.GO_CACHE_KEY }} + + - name: (cache-miss) Bootstrap all project dependencies + if: steps.tool-cache.outputs.cache-hit != 'true' || steps.go-cache.outputs.cache-hit != 'true' + run: make bootstrap + + - name: Login to ghcr.io + run: | + echo ${{ secrets.GITHUB_TOKEN }} | oras login ghcr.io --username ${{ github.actor }} --password-stdin + + - name: Aggregate vulnerability data + # TODO: hook up to matrix override + run: make aggregate-all-provider-cache + + - name: Upload vulnerability data cache image + run: make upload-all-provider-cache + +# TODO: slack on failure \ No newline at end of file diff --git a/.github/workflows/daily-db-publisher.yaml b/.github/workflows/daily-db-publisher.yaml new file mode 100644 index 00000000..dd885620 --- /dev/null +++ b/.github/workflows/daily-db-publisher.yaml @@ -0,0 +1,259 @@ +name: 'Daily DB Publisher' +on: + # allow for kicking off DB builds manually + workflow_dispatch: + inputs: + publish-databases: + description: "build new databases and upload to S3" + type: boolean + required: true + default: true + publish-listing: + description: "use S3 state to update and publish listing file" + type: boolean + required: true + default: true + + # run 10 AM (UTC) daily +# TODO: enable for release +# schedule: +# - cron: '0 10 * * *' +env: + GO_VERSION: "1.20.x" + CGO_ENABLED: "0" + PYTHON_VERSION: "3.10" + POETRY_VERSION: "1.2.1" + AWS_BUCKET: toolbox-data.anchore.io + AWS_BUCKET_PATH: grype/databases + AWS_DEFAULT_REGION: us-west-2 + SLACK_NOTIFICATIONS: true + # note: modify the value as needed to bust any python-related CI caches + PYTHON_CACHE_KEY: "510302e7c9b9da" + # note: modify the value as needed to bust the vuln pull cache + VULN_PULL_CACHE_KEY: "5jwqa1w3so2r9" + +jobs: + discover-schema-versions: + # note about workflow dispatch inputs and booleans: + # a) booleans come across as string types :( + # b) if not using workflow_dispatch the default values are empty, which means we want these to effectively evaluate to true (so only check the negative case) + if: ${{ github.event.inputs.publish-databases != 'false' }} + name: "Pull vulnerability data" + runs-on: ubuntu-20.04 + outputs: + schema-versions: ${{ steps.read-schema-versions.outputs.schema-versions }} + pull-date: ${{ steps.timestamp.outputs.date }} + # set the permissions granted to the github token to read the pull cache from ghcr.io + permissions: + contents: read + packages: read + steps: + - uses: actions/setup-go@v3 + with: + go-version: ${{ env.GO_VERSION }} + + - uses: actions/checkout@v3 + + - name: Read supported schema versions + id: read-schema-versions + run: | + content=`cat grype-schema-version-mapping.json | jq -c 'keys'` + echo "::set-output name=schema-versions::$content" + + + generate-and-publish-dbs: + # note about workflow dispatch inputs and booleans: + # a) booleans come across as string types :( + # b) if not using workflow_dispatch the default values are empty, which means we want these to effectively evaluate to true (so only check the negative case) + if: ${{ github.event.inputs.publish-databases != 'false' }} + name: "Generate and publish DBs" + needs: discover-schema-versions + runs-on: ubuntu-20.04 + strategy: + matrix: + schema-version: ${{fromJson(needs.discover-schema-versions.outputs.schema-versions)}} + # set the permissions granted to the github token to read the pull cache from ghcr.io + permissions: + contents: read + packages: read + steps: + - uses: actions/setup-go@v3 + with: + go-version: ${{ env.GO_VERSION }} + + - uses: actions/checkout@v3 + with: + # this downloads and initializes LFS, but does not pull the objects + lfs: true + + - name: Checkout LFS objects + # lfs pull does a lfs fetch and lfs checkout, this is NOT the same as "git pull" + run: git lfs pull + + - name: Setup python + uses: actions/setup-python@v3 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Install poetry + uses: abatilo/actions-poetry@v3.0.0 + with: + poetry-version: ${{ env.POETRY_VERSION }} + + - name: Cache Poetry virtualenv + uses: actions/cache@v3 + id: poetry-cache + with: + path: ~/.virtualenvs + key: poetry-${{ hashFiles('publish/poetry.lock') }}-${{ env.PYTHON_CACHE_KEY }} + + - name: Setup Poetry config + run: | + cd test/acceptance && \ + poetry config virtualenvs.in-project false && \ + poetry config virtualenvs.path ~/.virtualenvs + + - name: Restore tool cache + id: tool-cache + uses: actions/cache@v3 + with: + path: ${{ github.workspace }}/.tmp + key: ${{ runner.os }}-tool-${{ hashFiles('Makefile') }} + + - name: Restore go cache + id: go-cache + uses: actions/cache@v3 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go-${{ env.GO_VERSION }}- + + - name: (cache-miss) Bootstrap all project dependencies + if: steps.tool-cache.outputs.cache-hit != 'true' || steps.go-cache.outputs.cache-hit != 'true' + run: make bootstrap + + - name: Install dependencies and package + run: | + cd publish && poetry install + + - name: Login to ghcr.io + run: | + echo ${{ secrets.GITHUB_TOKEN }} | oras login ghcr.io --username ${{ github.actor }} --password-stdin + + - name: Pull vulnerability data + run: make download-all-provider-cache + + - name: Generate DB (schema ${{ matrix.schema-version }}) + run: | + cd publish && \ + poetry run publisher generate --schema-version ${{ matrix.schema-version }} + + - name: Upload DB (schema ${{ matrix.schema-version }}) + run: publish/upload-dbs.sh ${{ env.AWS_BUCKET }} ${{ env.AWS_BUCKET_PATH }} + env: + AWS_ACCESS_KEY_ID: ${{ secrets.TOOLBOX_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.TOOLBOX_AWS_SECRET_ACCESS_KEY }} + + - uses: 8398a7/action-slack@v3 + with: + status: ${{ job.status }} + fields: workflow,eventName + text: Publishing the DB has failed (schema ${{ matrix.schema-version }}) + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TOOLBOX_WEBHOOK_URL }} + if: ${{ failure() && env.SLACK_NOTIFICATIONS == 'true' }} + + publish-listing-file: + # fun! https://github.com/actions/runner/issues/491#issuecomment-850884422 + # essentially even if the workflow dispatch job is skipping steps, we still want to run this step. + # however, if not running from a workflow dispatch then we want the job ordering to be honored. + # also... + # note about workflow dispatch inputs and booleans: + # a) booleans come across as string types :( + # b) if not using workflow_dispatch the default values are empty, which means we want these to effectively evaluate to true (so only check the negative case) + if: | + always() && + (needs.generate-and-publish-dbs.result == 'success' || needs.generate-and-publish-dbs.result == 'skipped') && + github.event.inputs.publish-listing != 'false' + + name: "Publish listing file" + needs: generate-and-publish-dbs + runs-on: ubuntu-20.04 + steps: + - uses: actions/setup-go@v3 + with: + go-version: ${{ env.GO_VERSION }} + + - uses: actions/checkout@v3 + with: + # this downloads and initializes LFS, but does not pull the objects + lfs: true + + - name: Checkout LFS objects + # lfs pull does a lfs fetch and lfs checkout, this is NOT the same as "git pull" + run: git lfs pull + + - name: Setup python + uses: actions/setup-python@v3 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Install poetry + uses: abatilo/actions-poetry@v3.0.0 + with: + poetry-version: ${{ env.POETRY_VERSION }} + + - name: Cache Poetry virtualenv + uses: actions/cache@v3 + id: poetry-cache + with: + path: ~/.virtualenvs + key: poetry-${{ hashFiles('publish/poetry.lock') }}-${{ env.PYTHON_CACHE_KEY }} + + - name: Setup Poetry config + run: | + cd test/acceptance && \ + poetry config virtualenvs.in-project false && \ + poetry config virtualenvs.path ~/.virtualenvs + + - name: Restore tool cache + id: tool-cache + uses: actions/cache@v3 + with: + path: ${{ github.workspace }}/.tmp + key: ${{ runner.os }}-tool-${{ hashFiles('Makefile') }} + + - name: Restore go cache + id: go-cache + uses: actions/cache@v3 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go-${{ env.GO_VERSION }}- + + - name: (cache-miss) Bootstrap all project dependencies + if: steps.tool-cache.outputs.cache-hit != 'true' || steps.go-cache.outputs.cache-hit != 'true' + run: make bootstrap + + - name: Install dependencies and package + run: | + cd publish && poetry install + + - name: Publish listing file + run: | + cd publish && \ + poetry run publisher upload-listing --s3-bucket ${{ env.AWS_BUCKET }} --s3-path ${{ env.AWS_BUCKET_PATH }} + env: + AWS_ACCESS_KEY_ID: ${{ secrets.TOOLBOX_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.TOOLBOX_AWS_SECRET_ACCESS_KEY }} + + - uses: 8398a7/action-slack@v3 + with: + status: ${{ job.status }} + fields: workflow,eventName + text: Publishing the listing file has failed + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TOOLBOX_WEBHOOK_URL }} + if: ${{ failure() && env.SLACK_NOTIFICATIONS == 'true' }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 00000000..7b15c623 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,135 @@ +name: "Release" +on: + workflow_dispatch: + inputs: + version: + description: tag the latest commit on main with the given version (prefixed with v) + required: true + +env: + GO_VERSION: "1.20.x" + +jobs: + + quality-gate: + environment: release + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Check if tag already exists + # note: this will fail if the tag already exists + run: | + [[ "${{ github.event.inputs.version }}" == v* ]] || (echo "version '${{ github.event.inputs.version }}' does not have a 'v' prefix" && exit 1) + git tag ${{ github.event.inputs.version }} + + - run: echo "pass!" + +# - name: Check static analysis results +# uses: fountainhead/action-wait-for-check@v1.0.0 +# id: static-analysis +# with: +# token: ${{ secrets.GITHUB_TOKEN }} +# # This check name is defined as the github action job name (in .github/workflows/validation.yaml) +# checkName: "Static analysis" +# ref: ${{ github.event.pull_request.head.sha || github.sha }} +# +# - name: Check unit test results +# uses: fountainhead/action-wait-for-check@v1.0.0 +# id: unit +# with: +# token: ${{ secrets.GITHUB_TOKEN }} +# # This check name is defined as the github action job name (in .github/workflows/validation.yaml) +# checkName: "Unit tests" +# ref: ${{ github.event.pull_request.head.sha || github.sha }} +# +# - name: Quality gate +# if: steps.static-analysis.outputs.conclusion != 'success' || steps.unit.outputs.conclusion != 'success' +# run: | +# echo "Static Analysis Status: ${{ steps.static-analysis.conclusion }}" +# echo "Unit Test Status: ${{ steps.unit.outputs.conclusion }}" +# false + + read-schema-versions: + runs-on: ubuntu-20.04 + outputs: + schema-versions: ${{ steps.read-schema-versions.outputs.schema-versions }} + steps: + + - uses: actions/checkout@v2 + + - name: Read supported schema versions + id: read-schema-versions + run: | + content=`cat grype-schema-version-mapping.json | jq -c 'keys'` + echo "::set-output name=schema-versions::$content" + + quality-gate-acceptance-test: + needs: read-schema-versions + runs-on: ubuntu-20.04 + strategy: + matrix: + schema-version: ${{fromJson(needs.read-schema-versions.outputs.schema-versions)}} + steps: + - run: echo "pass!" + +# - name: Check acceptance test results +# uses: fountainhead/action-wait-for-check@v1.0.0 +# id: acceptance +# with: +# token: ${{ secrets.GITHUB_TOKEN }} +# # This check name is defined as the github action job name (in .github/workflows/validation.yaml) +# checkName: "Acceptance tests (${{ matrix.schema-version }})" +# ref: ${{ github.event.pull_request.head.sha || github.sha }} +# +# - name: Quality gate +# if: steps.acceptance.outputs.conclusion != 'success' +# run: | +# echo "Acceptance Test Status: ${{ steps.acceptance.outputs.conclusion }}" +# false + + release: + needs: + - quality-gate + - quality-gate-acceptance-test + permissions: + contents: write + packages: write + runs-on: ubuntu-latest + steps: + + - uses: actions/setup-go@v2 + with: + go-version: ${{ env.GO_VERSION }} + + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Restore bootstrap cache + id: cache + uses: actions/cache@v2 + with: + path: | + ~/go/pkg/mod + ${{ github.workspace }}/.tmp + key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('Makefile') }}-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('Makefile') }}- + ${{ runner.os }}-go-${{ env.GO_VERSION }}- + + - name: Bootstrap dependencies + if: steps.cache.outputs.cache-hit != 'true' + run: make ci-bootstrap + + - name: Tag release + run: | + git tag ${{ github.event.inputs.version }} + git push origin --tags + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Build release artifacts + run: make ci-release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/staging-db-publisher.yaml b/.github/workflows/staging-db-publisher.yaml new file mode 100644 index 00000000..71c8c909 --- /dev/null +++ b/.github/workflows/staging-db-publisher.yaml @@ -0,0 +1,176 @@ +# Only manual kickoff of builds are allowed, with some required inputs. The +# staging DB builder allows publishing a database to an AWS bucket. This is +# useful when no official DB with a newer schema has been published. Once the +# database is published, you can point grype to it: +# +# $ GRYPE_DB_UPDATE_URL=https://toolbox-data.anchore.io/grype/staging-databases/listing.json go run main.go centos:8 +# +name: 'Staging DB Publisher' +on: + workflow_dispatch: + inputs: + schema-version: + description: 'the schema version to build (e.g. "3", NOT "v3").' + required: true + default: "5" + grype-branch: + description: 'the release version or branch of grype to use for verification of the built DB.' + required: true + default: "main" + publish-databases: + description: "build new databases and upload to S3" + type: boolean + required: true + default: true + publish-listing: + description: "use S3 state to update and publish listing file" + type: boolean + required: true + run-tmate: + description: "start a tmate session (for debugging)" + required: false + type: boolean + default: false + tmate-duration: + description: "tmate session duration" + required: false + default: 20 + +env: + GO_VERSION: "1.20.x" + CGO_ENABLED: "0" + PYTHON_VERSION: "3.10" + POETRY_VERSION: "1.2.0" + AWS_BUCKET: toolbox-data.anchore.io + # do NOT change this value + AWS_BUCKET_PATH: grype/staging-databases + AWS_DEFAULT_REGION: us-west-2 + GRYPE_TEST_SCHEMA: ${{ github.event.inputs.schema-version }} + GRYPE_TEST_BRANCH: ${{ github.event.inputs.grype-branch }} + +jobs: + publish-staging-db: + name: "Generate and publish staging DB" + runs-on: ubuntu-20.04 + # set the permissions granted to the github token to read the pull cache from ghcr.io + permissions: + packages: read + contents: read + steps: + + - uses: actions/setup-go@v2 + with: + go-version: ${{ env.GO_VERSION }} + + - uses: actions/checkout@v2 + + - name: Setup python + uses: actions/setup-python@v2 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Install poetry + uses: abatilo/actions-poetry@v2.0.0 + with: + poetry-version: ${{ env.POETRY_VERSION }} + + - name: Cache Poetry virtualenv + uses: actions/cache@v3 + id: poetry-cache + with: + path: ~/.virtualenvs + key: poetry-${{ hashFiles('publish/poetry.lock') }} + + - name: Setup Poetry config + run: | + cd test/acceptance && \ + poetry config virtualenvs.in-project false && \ + poetry config virtualenvs.path ~/.virtualenvs + + - name: Restore python cache + id: python-cache + uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-publish-${{ hashFiles('publish/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip-publish- + + - name: Restore tool cache + id: tool-cache + uses: actions/cache@v3 + with: + path: ${{ github.workspace }}/.tmp + key: ${{ runner.os }}-tool-${{ hashFiles('Makefile') }} + + - name: Restore go cache + id: go-cache + uses: actions/cache@v3 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go-${{ env.GO_VERSION }}- + + - name: (cache-miss) Bootstrap all project dependencies + if: steps.tool-cache.outputs.cache-hit != 'true' || steps.go-cache.outputs.cache-hit != 'true' + run: make bootstrap + + - name: Install dependencies and package + run: | + # note: pyyaml is needed for the one-off python script for pulling the provider cache + cd publish && poetry install && pip install pyyaml + + - name: create timestamp for caching + id: timestamp + run: | + echo "::set-output name=date::$(/bin/date -u "+%Y%m%d")" + shell: bash + + - name: Login to ghcr.io + run: | + echo ${{ secrets.GITHUB_TOKEN }} | oras login ghcr.io --username ${{ github.actor }} --password-stdin + + - name: Setup tmate session + # note about workflow dispatch inputs and booleans: + # a) booleans come across as string types :( + # b) if not using workflow_dispatch the default values are empty, which means we want these to effectively evaluate to true (so only check the negative case) + if: github.event.inputs.run-tmate != 'false' + uses: mxschmitt/action-tmate@v3 + timeout-minutes: ${{ fromJSON(github.event.inputs.tmate-duration) }} + with: + limit-access-to-actor: true + + - name: Pull vulnerability data + # note about workflow dispatch inputs and booleans: + # a) booleans come across as string types :( + # b) if not using workflow_dispatch the default values are empty, which means we want these to effectively evaluate to true (so only check the negative case) + if: github.event.inputs.publish-databases != 'false' + run: make download-all-provider-cache + + - name: Generate DB (schema ${{ github.event.inputs.schema-version }}) + # note about workflow dispatch inputs and booleans: + # a) booleans come across as string types :( + # b) if not using workflow_dispatch the default values are empty, which means we want these to effectively evaluate to true (so only check the negative case) + if: github.event.inputs.publish-databases != 'false' + run: | + cd publish && + poetry run publisher generate --schema-version ${{ github.event.inputs.schema-version }} + + - name: Upload DB (schema ${{ github.event.inputs.schema-version }}) + run: publish/upload-dbs.sh ${{ env.AWS_BUCKET }} ${{ env.AWS_BUCKET_PATH }} + env: + AWS_ACCESS_KEY_ID: ${{ secrets.TOOLBOX_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.TOOLBOX_AWS_SECRET_ACCESS_KEY }} + + - name: Publish listing file + # note about workflow dispatch inputs and booleans: + # a) booleans come across as string types :( + # b) if not using workflow_dispatch the default values are empty, which means we want these to effectively evaluate to true (so only check the negative case) + if: github.event.inputs.publish-listing != 'false' + run: | + cd publish && + poetry run publisher upload-listing --s3-bucket ${{ env.AWS_BUCKET }} --s3-path ${{ env.AWS_BUCKET_PATH }} + env: + AWS_ACCESS_KEY_ID: ${{ secrets.TOOLBOX_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.TOOLBOX_AWS_SECRET_ACCESS_KEY }} diff --git a/.github/workflows/update-bootstrap-tools.yml b/.github/workflows/update-bootstrap-tools.yml new file mode 100644 index 00000000..80a49060 --- /dev/null +++ b/.github/workflows/update-bootstrap-tools.yml @@ -0,0 +1,63 @@ +name: PR for latest versions of bootstrap tools +on: + schedule: + - cron: "0 8 * * *" # 3 AM EST + + workflow_dispatch: + +env: + GO_VERSION: "1.20.x" + GO_STABLE_VERSION: true + +jobs: + update-bootstrap-tools: + runs-on: ubuntu-latest + if: github.repository == 'anchore/grype-db' # only run for main repo + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-go@v2 + with: + go-version: ${{ env.GO_VERSION }} + stable: ${{ env.GO_STABLE_VERSION }} + + - run: | + GOLANGCILINT_LATEST_VERSION=$(go list -m -json github.com/golangci/golangci-lint@latest 2>/dev/null | jq -r '.Version') + BOUNCER_LATEST_VERSION=$(go list -m -json github.com/wagoodman/go-bouncer@latest 2>/dev/null | jq -r '.Version') + CHRONICLE_LATEST_VERSION=$(go list -m -json github.com/anchore/chronicle@latest 2>/dev/null | jq -r '.Version') + GORELEASER_LATEST_VERSION=$(go list -m -json github.com/goreleaser/goreleaser@latest 2>/dev/null | jq -r '.Version') + + # update version variables in the Makefile + sed -r -i -e 's/^(GOLANGCILINT_VERSION = ).*/\1'${GOLANGCILINT_LATEST_VERSION}'/' Makefile + sed -r -i -e 's/^(BOUNCER_VERSION = ).*/\1'${BOUNCER_LATEST_VERSION}'/' Makefile + sed -r -i -e 's/^(CHRONICLE_VERSION = ).*/\1'${CHRONICLE_LATEST_VERSION}'/' Makefile + sed -r -i -e 's/^(GORELEASER_VERSION = ).*/\1'${GORELEASER_LATEST_VERSION}'/' Makefile + + # export the versions for use with create-pull-request + echo "::set-output name=GOLANGCILINT::$GOLANGCILINT_LATEST_VERSION" + echo "::set-output name=BOUNCER::$BOUNCER_LATEST_VERSION" + echo "::set-output name=CHRONICLE::$CHRONICLE_LATEST_VERSION" + echo "::set-output name=GORELEASER::$GORELEASER_LATEST_VERSION" + id: latest-versions + + - uses: tibdex/github-app-token@v1 + id: generate-token + with: + app_id: ${{ secrets.TOKEN_APP_ID }} + private_key: ${{ secrets.TOKEN_APP_PRIVATE_KEY }} + + - uses: peter-evans/create-pull-request@v4 + with: + signoff: true + delete-branch: true + branch: auto/latest-bootstrap-tools + labels: dependencies + commit-message: 'Update grype-db bootstrap tools to latest versions.' + title: 'Update grype-db bootstrap tools to latest versions.' + body: | + - [golangci-lint ${{ steps.latest-versions.outputs.GOLANGCILINT }}](https://github.com/golangci/golangci-lint/releases/tag/${{ steps.latest-versions.outputs.GOLANGCILINT }}) + - [bouncer ${{ steps.latest-versions.outputs.BOUNCER }}](https://github.com/wagoodman/go-bouncer/releases/tag/${{ steps.latest-versions.outputs.BOUNCER }}) + - [chronicle ${{ steps.latest-versions.outputs.CHRONICLE }}](https://github.com/anchore/chronicle/releases/tag/${{ steps.latest-versions.outputs.CHRONICLE }}) + - [goreleaser ${{ steps.latest-versions.outputs.GORELEASER }}](https://github.com/goreleaser/goreleaser/releases/tag/${{ steps.latest-versions.outputs.GORELEASER }}) + This is an auto-generated pull request to update all of the bootstrap tools to the latest versions. + token: ${{ steps.generate-token.outputs.token }} \ No newline at end of file diff --git a/.github/workflows/update-grype-release.yml b/.github/workflows/update-grype-release.yml new file mode 100644 index 00000000..393300e5 --- /dev/null +++ b/.github/workflows/update-grype-release.yml @@ -0,0 +1,51 @@ +name: PR for latest Grype release +on: + schedule: + - cron: "0 8 * * *" # 3 AM EST + + workflow_dispatch: + +env: + GO_VERSION: "1.20.x" + GO_STABLE_VERSION: true + +jobs: + upgrade-grype: + runs-on: ubuntu-latest + if: github.repository == 'anchore/grype-db' # only run for main repo + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-go@v2 + with: + go-version: ${{ env.GO_VERSION }} + stable: ${{ env.GO_STABLE_VERSION }} + + - run: | + LATEST_VERSION=$(curl "https://api.github.com/repos/anchore/grype/releases/latest" 2>/dev/null | jq -r '.tag_name') + + # update go.mod + go get github.com/anchore/grype@$LATEST_VERSION + go mod tidy + + # export the version for use with create-pull-request + echo "::set-output name=LATEST_VERSION::$LATEST_VERSION" + id: latest-version + + - uses: tibdex/github-app-token@v1 + id: generate-token + with: + app_id: ${{ secrets.TOKEN_APP_ID }} + private_key: ${{ secrets.TOKEN_APP_PRIVATE_KEY }} + + - uses: peter-evans/create-pull-request@v4 + with: + signoff: true + delete-branch: true + branch: auto/latest + labels: dependencies + commit-message: "Update Grype to ${{ steps.latest-version.outputs.LATEST_VERSION }}" + title: "Update Grype to ${{ steps.latest-version.outputs.LATEST_VERSION }}" + body: | + Update Grype to ${{ steps.latest-version.outputs.LATEST_VERSION }} + token: ${{ steps.generate-token.outputs.token }} diff --git a/.github/workflows/validations.yaml b/.github/workflows/validations.yaml new file mode 100644 index 00000000..feeef14d --- /dev/null +++ b/.github/workflows/validations.yaml @@ -0,0 +1,234 @@ +name: "Validations" +on: + workflow_dispatch: + push: + +env: + GO_VERSION: "1.20.x" + CGO_ENABLED: "0" + PYTHON_VERSION: "3.10" + POETRY_VERSION: "1.2.0" + # note: modify the value as needed to bust the feed pull cache + FEED_PULL_CACHE_KEY: "5bd9c073a029f" + # note: modify the value as needed to bust any python-related CI caches + PYTHON_CACHE_KEY: "510302e7c9b9da" + GRYPE_DB_VALIDATE_AGE: "false" + +jobs: + + Static-Analysis: + name: "Static analysis" + runs-on: ubuntu-20.04 + steps: + - uses: actions/setup-go@v2 + with: + go-version: ${{ env.GO_VERSION }} + + - uses: actions/checkout@v2 + + - name: Restore tool cache + id: tool-cache + uses: actions/cache@v2 + with: + path: ${{ github.workspace }}/.tmp + key: ${{ runner.os }}-tool-${{ hashFiles('Makefile') }} + + - name: Restore go cache + id: go-cache + uses: actions/cache@v2 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go-${{ env.GO_VERSION }}- + + - name: (cache-miss) Bootstrap all project dependencies + if: steps.tool-cache.outputs.cache-hit != 'true' || steps.go-cache.outputs.cache-hit != 'true' + run: make bootstrap + + - name: Bootstrap CI environment dependencies + run: make ci-bootstrap + + - name: Run static analysis + run: make static-analysis + + + Unit-Test: + name: "Unit tests" + runs-on: ubuntu-20.04 + steps: + - uses: actions/setup-go@v2 + with: + go-version: ${{ env.GO_VERSION }} + + - uses: actions/checkout@v2 + + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Install poetry + uses: abatilo/actions-poetry@v2.0.0 + with: + poetry-version: ${{ env.POETRY_VERSION }} + + - name: Cache Poetry virtualenv + uses: actions/cache@v1 + id: poetry-cache + with: + path: ~/.virtualenvs + key: poetry-${{ hashFiles('publish/poetry.lock') }}-${{ env.PYTHON_CACHE_KEY }} + + - name: Setup Poetry config + run: | + cd test/acceptance && \ + poetry config virtualenvs.in-project false && \ + poetry config virtualenvs.path ~/.virtualenvs + + - name: Restore tool cache + id: tool-cache + uses: actions/cache@v2 + with: + path: ${{ github.workspace }}/.tmp + key: ${{ runner.os }}-tool-${{ hashFiles('Makefile') }} + + - name: Restore go cache + id: go-cache + uses: actions/cache@v2 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go-${{ env.GO_VERSION }}- + + - name: (cache-miss) Bootstrap all project dependencies + if: steps.tool-cache.outputs.cache-hit != 'true' || steps.go-cache.outputs.cache-hit != 'true' + run: make bootstrap + + - name: Bootstrap CI environment dependencies + run: make ci-bootstrap + + - name: Install dependencies and package + run: | + cd publish && poetry install + + - name: Cache Tox + uses: actions/cache@v1 + id: cache + with: + path: publish/.tox + key: tox-${{ hashFiles('publish/poetry.lock') }}-${{ env.PYTHON_CACHE_KEY }} + + - name: Run unit tests + run: make unit + + Discover-Schema-Versions: + name: "Discover supported schema versions" + runs-on: ubuntu-20.04 + outputs: + schema-versions: ${{ steps.read-schema-versions.outputs.schema-versions }} + steps: + - uses: actions/setup-go@v2 + with: + go-version: ${{ env.GO_VERSION }} + + - uses: actions/checkout@v2 + + - name: Read supported schema versions + id: read-schema-versions + run: | + content=`cat grype-schema-version-mapping.json | jq -c 'keys'` + echo "::set-output name=schema-versions::$content" + + Acceptance-Test: + name: "Acceptance tests" + needs: Discover-Schema-Versions + runs-on: ubuntu-20.04 + strategy: + matrix: + schema-version: ${{fromJson(needs.Discover-Schema-Versions.outputs.schema-versions)}} + # set the permissions granted to the github token to read the pull cache from ghcr.io + permissions: + contents: read + packages: read + steps: + - uses: actions/setup-go@v2 + with: + go-version: ${{ env.GO_VERSION }} + + - uses: actions/checkout@v2 + with: + # this downloads and initializes LFS, but does not pull the objects + lfs: true + + - name: Checkout LFS objects + # lfs pull does a lfs fetch and lfs checkout, this is NOT the same as "git pull" + run: git lfs pull + + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Install poetry + uses: abatilo/actions-poetry@v2.0.0 + with: + poetry-version: ${{ env.POETRY_VERSION }} + + - name: Cache Poetry virtualenv + uses: actions/cache@v1 + id: poetry-cache + with: + path: ~/.virtualenvs + key: poetry-${{ hashFiles('test/acceptance/poetry.lock') }}-${{ env.PYTHON_CACHE_KEY }} + + - name: Setup Poetry config + run: | + cd test/acceptance && \ + poetry config virtualenvs.in-project false && \ + poetry config virtualenvs.path ~/.virtualenvs + + - name: Restore tool cache + id: tool-cache + uses: actions/cache@v2 + with: + path: ${{ github.workspace }}/.tmp + key: ${{ runner.os }}-tool-${{ hashFiles('Makefile') }} + + - name: Restore go cache + id: go-cache + uses: actions/cache@v2 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go-${{ env.GO_VERSION }}- + + - name: (cache-miss) Bootstrap all project dependencies + if: steps.tool-cache.outputs.cache-hit != 'true' || steps.go-cache.outputs.cache-hit != 'true' + run: make bootstrap + + - name: Bootstrap CI environment dependencies + run: make ci-bootstrap + + - name: Install dependencies and package + run: | + cd test/acceptance && poetry install + + - name: Login to ghcr.io + run: | + echo ${{ secrets.GITHUB_TOKEN }} | oras login ghcr.io --username ${{ github.actor }} --password-stdin + + - name: Pull vulnerability data + run: make download-all-provider-cache + + - name: Build DB + run: | + cd test/acceptance && \ + poetry run python grype-ingest.py generate --schema-version ${{ matrix.schema-version }} + + - name: Test DB + run: | + cd test/acceptance && \ + poetry run python grype-ingest.py test --schema-version ${{ matrix.schema-version }} diff --git a/.gitignore b/.gitignore index 31a2405f..4ac26857 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,10 @@ +/data +/vunnel + +/grype-db +/build +/dist +/snapshot /metadata.json /listing.json .vscode/ @@ -11,6 +18,8 @@ .images .tmp/ coverage.txt +CHANGELOG.md +VERSION # Binaries for programs and plugins *.exe @@ -24,3 +33,6 @@ coverage.txt # Output of the go coverage tool, specifically when used with LiteIDE *.out + +# macOS Finder metadata +.DS_STORE diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 00000000..86ed0e67 --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,72 @@ +linters-settings: + funlen: + lines: 90 + statements: 65 + gocognit: + min-complexity: 32 + +output: + uniq-by-line: false +run: + timeout: 10m + tests: false + +linters: + # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint + disable-all: true + enable: + - asciicheck + - bodyclose + - depguard + - dogsled + - dupl + - errcheck + - exportloopref + - funlen + - gocognit + - goconst + - gocritic + - gocyclo + - gofmt + - goimports + - goprintffuncname + - gosec + - gosimple + - govet + - ineffassign + - misspell + - nakedret + - revive + - staticcheck + - stylecheck + - typecheck + - unconvert + - unparam + - unused + - whitespace + +# do not enable... +# - deadcode # The owner seems to have abandoned the linter. Replaced by "unused". +# - goprintffuncname # does not catch all cases and there are exceptions +# - nakedret # does not catch all cases and should not fail a build +# - gochecknoglobals +# - gochecknoinits # this is too aggressive +# - rowserrcheck disabled per generics https://github.com/golangci/golangci-lint/issues/2649 +# - godot +# - godox +# - goerr113 +# - goimports # we're using gosimports now instead to account for extra whitespaces (see https://github.com/golang/go/issues/20818) +# - golint # deprecated +# - gomnd # this is too aggressive +# - interfacer # this is a good idea, but is no longer supported and is prone to false positives +# - lll # without a way to specify per-line exception cases, this is not usable +# - maligned # this is an excellent linter, but tricky to optimize and we are not sensitive to memory layout optimizations +# - nestif +# - nolintlint # as of go1.19 this conflicts with the behavior of gofmt, which is a deal-breaker (lint-fix will still fail when running lint) +# - prealloc # following this rule isn't consistently a good idea, as it sometimes forces unnecessary allocations that result in less idiomatic code +# - rowserrcheck # not in a repo with sql, so this is not useful +# - scopelint # deprecated +# - structcheck # The owner seems to have abandoned the linter. Replaced by "unused". +# - testpackage +# - varcheck # The owner seems to have abandoned the linter. Replaced by "unused". +# - wsl # this doens't have an auto-fixer yet and is pretty noisy (https://github.com/bombsimon/wsl/issues/90) # this doens't have an auto-fixer yet and is pretty noisy (https://github.com/bombsimon/wsl/issues/90) \ No newline at end of file diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 00000000..5a10ca9b --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,29 @@ +release: + # If set to auto, will mark the release as not ready for production + # in case there is an indicator for this in the tag e.g. v1.0.0-rc1 + # If set to true, will mark the release as not ready for production. + prerelease: auto + +project_name: grype-db + +builds: + - binary: grype-db + dir: ./cmd/grype-db + env: + - CGO_ENABLED=0 + goos: + - linux + - darwin + goarch: + - amd64 + - arm64 + # Set the modified timestamp on the output binary to the git timestamp (to ensure a reproducible build) + mod_timestamp: '{{ .CommitTimestamp }}' + ldflags: | + -w + -X github.com/anchore/grype-db/internal/version.version={{.Version}} + -X github.com/anchore/grype-db/internal/version.gitCommit={{.Commit}} + -X github.com/anchore/grype-db/internal/version.buildDate={{.Date}} + -X github.com/anchore/grype-db/internal/version.gitDescription={{.Summary}} +archives: + - format: tar.gz diff --git a/.grype-db.yaml b/.grype-db.yaml new file mode 100644 index 00000000..1a44eb14 --- /dev/null +++ b/.grype-db.yaml @@ -0,0 +1,22 @@ +# note: this file is not intended to be used for the daily-db-sync workflow to populate a vulnerability data cache +pull: + parallelism: 4 +provider: + root: ./data + vunnel: + executor: docker + dockerTag: v0.3.4 + env: + GITHUB_TOKEN: $GITHUB_TOKEN + NVD_API_KEY: $NVD_API_KEY + configs: + - name: alpine + - name: amazon + - name: debian + - name: github + - name: nvd + - name: oracle + - name: rhel + - name: sles + - name: ubuntu + - name: wolfi diff --git a/.vunnel.yaml b/.vunnel.yaml new file mode 100644 index 00000000..a9dbdc31 --- /dev/null +++ b/.vunnel.yaml @@ -0,0 +1,10 @@ +# note: this file is not intended to be used for the daily-db-sync workflow to populate a vulnerability data cache + +log: + slim: true + level: debug + +providers: + ubuntu: + # there is a lot of IO when running git log commands in this provider, so some concurrency helps here + max_workers: 10 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 448389eb..d7e80bce 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,3 +1,117 @@ # Contributing to grype-db -All code has been ported to the [grype](https://github.com/anchore/grype) repo under the `grype/db` package. We are no longer accepting contributions in this repo. +If you are looking to contribute to this project and want to open a GitHub pull request ("PR"), there are a few guidelines of what we are looking for in patches. Make sure you go through this document and ensure that your code proposal is aligned. + +For development instructions see the [DEVELOPING.md](DEVELOPING.md) + +## Adding a feature or fix + +If you look at the [list of grype-db issues](https://github.com/anchore/grype-db/issues) there are plenty of bugs and feature requests. + +## Commit guidelines + +In the grype-db project we like commits and pull requests (PR) to be easy to understand and review. Open source thrives best when everything happening is over documented and small enough to be understood. + +### Granular commits + +Please try to make every commit as simple as possible, but no simpler. The idea is that each commit should be a logical unit of code. Try not to commit too many tiny changes, for example every line changed in a file as a separate commit. And also try not to make a commit enormous, for example committing all your work at the end of the day. + +Rather than try to follow a strict guide on what is or is not best, we try to be flexible and simple in this space. Do what makes the most sense for the changes you are trying to include. + +### Commit title and description + +Remember that the message you leave for a commit is for the reviewer in the present, and for someone (maybe you) changing something in the future. Please make sure the title and description used is easy to understand and explains what was done. Jokes and clever comments generally don't age well in commit messages, try to stick to the facts please. + +## Sign off your work + +The `sign-off` is an added line at the end of the explanation for the commit, certifying that you wrote it or otherwise have the right to submit it as an open-source patch. By submitting a contribution, you agree to be bound by the terms of the DCO Version 1.1 and Apache License Version 2.0. + +Signing off a commit certifies the below Developer's Certificate of Origin (DCO): + +```text +Developer's Certificate of Origin 1.1 + +By making a contribution to this project, I certify that: + + (a) The contribution was created in whole or in part by me and I + have the right to submit it under the open source license + indicated in the file; or + + (b) The contribution is based upon previous work that, to the best + of my knowledge, is covered under an appropriate open source + license and I have the right under that license to submit that + work with modifications, whether created in whole or in part + by me, under the same open source license (unless I am + permitted to submit under a different license), as indicated + in the file; or + + (c) The contribution was provided directly to me by some other + person who certified (a), (b) or (c) and I have not modified + it. + + (d) I understand and agree that this project and the contribution + are public and that a record of the contribution (including all + personal information I submit with it, including my sign-off) is + maintained indefinitely and may be redistributed consistent with + this project or the open source license(s) involved. +``` + +All contributions to this project are licensed under the [Apache License Version 2.0, January 2004](http://www.apache.org/licenses/). + +When committing your change, you can add the required line manually so that it looks like this: + +```text +Signed-off-by: John Doe +``` + +Creating a signed-off commit is then possible with `-s` or `--signoff`: + +```text +$ git commit -s -m "this is a commit message" +``` + +To double-check that the commit was signed-off, look at the log output: + +```text +$ git log -1 +commit 37ceh170e4hb283bb73d958f2036ee5k07e7fde7 (HEAD -> issue-35, origin/main, main) +Author: John Doe +Date: Mon Aug 1 11:27:13 2020 -0400 + + this is a commit message + + Signed-off-by: John Doe +``` + +## Test your changes + +This project has a `Makefile` which includes many helpers running both unit and integration tests. You can run `make help` to see all the options. Although PRs will have automatic checks for these, it is useful to run them locally, ensuring they pass before submitting changes. + +You can run the static analysis and tests: + +```bash +make + +# same as +make static-analysis test +``` + +## Pull Request + +If you made it this far and all the tests are passing, it's time to submit a Pull Request (PR) for grype-db. Submitting a PR is always a scary moment as what happens next can be an unknown. The grype-db project strives to be easy to work with, we appreciate all contributions. Nobody is going to yell at you or try to make you feel bad. We love contributions and know how scary that first PR can be. + +### PR Title and Description + +Just like the commit title and description mentioned above, the PR title and description is very important for letting others know what's happening. Please include any details you think a reviewer will need to more properly review your PR. + +A PR that is very large or poorly described has a higher likelihood of being pushed to the end of the list. Reviewers like PRs they can understand and quickly review. + +### What to expect next + +Please be patient with the project. We try to review PRs in a timely manner, but this is highly dependent on all the other tasks we have going on. It's OK to ask for a status update every week or two, it's not OK to ask for a status update every day. + +It's very likely the reviewer will have questions and suggestions for changes to your PR. If your changes don't match the current style and flow of the other code, expect a request to change what you've done. + +## Document your changes + +And lastly, when proposed changes are modifying user-facing functionality or output, it is expected the PR will include updates to the documentation as well. If nobody knows new features exist, they can't use them! \ No newline at end of file diff --git a/DEVELOPING.md b/DEVELOPING.md new file mode 100644 index 00000000..9bd361d8 --- /dev/null +++ b/DEVELOPING.md @@ -0,0 +1,325 @@ +# Developing + +## Getting started + +This codebase is primarily Go, however, there are also Python scripts critical to the daily DB publishing process as +well as acceptance testing. You will require the following: + +- Python 3.8+ installed on your system. Consider using [pyenv](https://github.com/pyenv/pyenv) if you do not have a + preference for managing python interpreter installations. + + +- [Poetry](https://python-poetry.org/) installed for dependency and virtualenv management for python dependencies, to install: + + ```bash + curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/install-poetry.py | python - + ``` + +- [Git LFS](https://git-lfs.github.com/) installed for managing test fixtures in the repo, once installed initialze with: + + ```bash + git lfs install + ``` + +To download go tooling used for static analysis and dependent go modules run the following: + +```bash +make bootstrap +``` + +## Getting an initial vulnerability data cache + +In order to build a grype DB you will need a local cache of vulnerability data: + +```bash +make download-all-provider-cache +``` + +This will populate the `./data` directory locally with everything needed to run `grype-db build` (without needing to run `grype-db pull`). + +## Running tests + +To unit test the Go code and unit test the publisher python scripts: + +```bash +make unit +``` + +To verify that all supported schema versions interop with grype run: + +```bash +make acceptance +# Note: this may take a while... go make some coffee. +``` + + +## Create a new DB schema + +1. Create a new `v#` schema package in the `grype` repo (within `pkg/db`) +2. Create a new `v#` schema package in the `grype-db` repo (use the `bump-schema.py` helper script) that uses the new changes from `grype-db` +3. Modify the `grype-schema-version-mapping.json` to pin the last-latest version to a specific version of grype and add the new schema version pinned to the "main" branch of grype (or a development branch) +4. Update all references in `grype` to use the new schema +5. Use the [Staging DB Publisher](https://github.com/anchore/grype-db/actions/workflows/staging-db-publisher.yaml) workflow to test your DB changes with grype in a flow similar to the daily DB publisher workflow + + +## Making a staging DB + +While developing a new schema version it may be useful to get a DB built for you by the [Staging DB Publisher](https://github.com/anchore/grype-db/actions/workflows/staging-db-publisher.yaml) GitHub Actions workflow. +This code exercises the same code as the Daily DB Publisher, with the exception that only a single schema is built and is validated against a given development branch of grype. +When these DBs are published you can point grype at the proper listing file like so: + +```bash +$ GRYPE_DB_UPDATE_URL=https://toolbox-data.anchore.io/grype/staging-databases/listing.json grype centos:8 ... +``` + +## Architecture + +`grype-db` is essentially an application that extracts information from upstream vulnerability data providers, +transforms it into smaller records targeted for grype consumption, and loads the individual records into a new SQLite DB. + +``` +~~~~~ "Pull" ~~~~~ ~~~~~~~~~~~~~~~~~~ "Build" ~~~~~~~~~~~~~~~~ ~~ "Package" ~~ + +┌─────────────────┐ ┌───────────────────┐ ┌───────────────┐ ┌─────────────┐ +│ Pull vuln data │ │ Transform entries │ │ Load entries │ │ Package DB │ +│ from upstream ├────►│ ├────►│ into new DB ├────►│ │ +└─────────────────┘ └───────────────────┘ └───────────────┘ └─────────────┘ +``` + +What makes `grype-db` a little more unique than a typical ETL job is the extra responsibility of needing to +transform the most recent vulnerability data shape (defined in the [vunnel repo](https://github.com/anchore/vunnel/tree/main/schema/vulnerability)) to all supported DB schema versions. +From the perspective of the Daily DB Publisher workflow, (abridged) execution looks something like this: + +``` + ┌─────────────────┐ ┌──────────────┐ ┌────────────────┐ + │ Pull vuln data ├────┬────►│ Build V1 DB │────►│ Package V1 DB │ ... + └─────────────────┘ │ └──────────────┘ └────────────────┘ + │ ┌──────────────┐ ┌────────────────┐ + ├────►│ Build V2 DB │────►│ Package V2 DB │ ... + │ └──────────────┘ └────────────────┘ + │ ┌──────────────┐ ┌────────────────┐ + ├────►│ Build V3 DB │────►│ Package V3 DB │ ... + │ └──────────────┘ └────────────────┘ + ... +``` + +In order to support multiple DB schemas easily from a code-organization perspective the following abstractions exist: + + +- **Provider**: responsible for providing raw vulnerability data files that are cached locally for later processing. + + +- **Processor**: responsible for unmarshalling any entries given by the `Provider`, passing them into `Transformers`, and + returning any resulting entries. Note: the object definition is schema-agnostic but instances are schema-specific + since Transformers are dependency-injected into this object. + + +- **Transformer**: Takes raw data entries of a specific [vunnel-defined schema](https://github.com/anchore/vunnel/tree/main/schema/vulnerability) + and transforms the data into schema-specific entries to later be written to the database. Note: the object definition + is schema-specific, encapsulating `grypeDB/v#` specific objects within schema-agnostic `Entry` objects. + + +- **Entry**: Encapsulates schema-specific database records produced by `Processors`/`Transformers` (from the provider data) + and accepted by `Writers`. + + +- **Writer**: Takes `Entry` objects and writes them to a backing store (today a SQLite database). Note: the object + definition is schema-specific and typically references `grypeDB/v#` schema-specific writers. + + +All the above abstractions are defined in the `pkg/data` Go package and are used together commonly in the following flow: + +``` + ┌────────────────────────────────────────────┐ + cache │data.Processor │ + ┌─────────────┐ file │ ┌────────────┐ ┌───────────────────┐ │ []data.Entry ┌───────────┐ ┌───────────────────────┐ + │data.Provider├──────►│ │unmarshaller├──────►│v# data.Transformer│ ├──────────────►│data.Writer├────►│grypeDB/v#/writer.Write│ + └─────────────┘ │ └────────────┘ └───────────────────┘ │ └───────────┘ └───────────────────────┘ + └───────────────────────────────────────────-┘ +``` + +Where there is a `data.Provider` for each upstream data source (e.g. canonical, redhat, github, NIST, etc.), +a `data.Processor` for every vunnel-defined data shape (github, os, msrc, nvd, etc... defined in the [vunnel repo](https://github.com/anchore/vunnel/tree/main/schema/vulnerability)), +a `data.Transformer` for every processor and DB schema version pairing, and a `data.Writer` for every DB schema version. + +From a Go package organization perspective, the above abstractions are organized as follows: + +``` +grype-db/ +└── pkg + ├── data # common data structures and objects that define the ETL flow + ├── process + │ ├── processors # common data.Processors to call common unmarshallers and pass entries into data.Transformers + │ ├── v1 + │ │ ├── processors.go # wires up all common data.Processors to v1-specific data.Transformers + │ │ ├── writer.go # v1-specific store writer + │ │ └── transformers # v1-specific transformers + │ ├── v2 + │ │ ├── processors.go # wires up all common data.Processors to v2-specific data.Transformers + │ │ ├── writer.go # v2-specific store writer + │ │ └── transformers # v2-specific transformers + │ └── ...more schema versions here... + └── provider # common code to pull, unmarshal, and cache updstream vuln data into local files + └── ... + +``` + + +### DB structure and definitions + +The definitions of what goes into the database and how to access it (both reads and writes) live in the public `grype` +repo under the `db` package. Responsibilities of `grype` (not `grype-db`) include (but are not limited to): + +- What tables are in the database +- What columns are in each table +- How each record should be serialized for writing into the database +- How records should be read/written from/to the database +- Providing rich objects for dealing with schema-specific data structures +- The name of the SQLite DB file within an archive +- The definition of a listing file and listing file entries + +The purpose of `grype-db` is to use the definitions from `grype.db` and the upstream vulnerability data to +create DB archives and make them publicly available for consumption via grype. + + +### DB listing file + +The listing file contains URLs to grype DB archives that are available for download, organized by schema version, and +ordered by latest-date-first. +The definition of the listing file resides in `grype`, however, it is the responsibility of the grype-db repo +to generate DBs and re-create the listing file daily. +As long as grype has been configured to point to the correct listing file, the DBs can be stored separately from the +listing file, be replaced with a running service returning the listing file contents, or can be mirrored for systems +behind an air gap. + + +### Getting a grype DB out to OSS users (daily) + +There are two workflows that drive getting a new grype DB out to OSS users: +1. The daily data sync workflow, which uses [vunnel](https://github.com/anchore/vunnel) to pull upstream vulnerability data. +2. The daily DB publisher workflow, which uses builds and publishes a grype DB from the data obtained in the daily data sync workflow. + + +#### Daily data sync workflow + +**This workflow takes the upstream vulnerability data (from canonical, redhat, debian, NVD, etc), processes it, and +writes the results to the OCI repos.** + +``` +┌──────────────┐ ┌──────────────────────────────────────────────────────────┐ +│ Pull alpine ├────────►│ Publish to ghcr.io/anchore/grype-db/data/alpine: │ +└──────────────┘ └──────────────────────────────────────────────────────────┘ +┌──────────────┐ ┌──────────────────────────────────────────────────────────┐ +│ Pull amazon ├────────►│ Publish to ghcr.io/anchore/grype-db/data/amazon: │ +└──────────────┘ └──────────────────────────────────────────────────────────┘ +┌──────────────┐ ┌──────────────────────────────────────────────────────────┐ +│ Pull debian ├────────►│ Publish to ghcr.io/anchore/grype-db/data/debian: │ +└──────────────┘ └──────────────────────────────────────────────────────────┘ +┌──────────────┐ ┌──────────────────────────────────────────────────────────┐ +│ Pull github ├────────►│ Publish to ghcr.io/anchore/grype-db/data/github: │ +└──────────────┘ └──────────────────────────────────────────────────────────┘ +┌──────────────┐ ┌──────────────────────────────────────────────────────────┐ +│ Pull nvd ├────────►│ Publish to ghcr.io/anchore/grype-db/data/nvd: │ +└──────────────┘ └──────────────────────────────────────────────────────────┘ +... repeat for all upstream providers ... +``` + +Once all providers have been updated a single vulnerability cache OCI repo is updated with all of the latest vulnerability data at `ghcr.io/anchore/grype-db/data:`. This repo is what is used downstream by the DB publisher workflow to create grype DBs. + +The in-repo `.grype-db.yaml` and `.vunnel.yaml` configurations are used to define the upstream data sources, how to obtain them, and where to put the results locally. + + +#### Daily DB publishing workflow + +**This workflow takes the latest vulnerability data cache, builds a grype DB, and publishes it for general consumption.** + +The `publish/` directory contains all code responsible for driving the Daily DB Publisher workflow, generating DBs +for all supported schema versions and making them available to the public. The publishing process is made of four steps +(depicted and described below): + +``` +~~~~~ 1. Pull ~~~~~ ~~~~~~~~~~~~~ 2. Generate ~~~~~~~~~~~~ ~~ 3. Upload DBs ~~ ~~ 4. Upload Listing ~~ + +┌─────────────────┐ ┌──────────────┐ ┌───────────────┐ ┌────────────────┐ ┌─────────────────────┐ +│ Pull vuln data ├──┬──►│ Build V1 DB ├────►│ Package V1 DB ├────►│ Upload Archive ├──┬──►│ Update listing file │ +└─────────────────┘ │ └──────────────┘ └───────────────┘ └────────────────┘ │ └─────────────────────┘ + (from the daily │ ┌──────────────┐ ┌───────────────┐ ┌────────────────┐ │ + sync workflow ├──►│ Build V2 DB ├────►│ Package V2 DB ├────►│ Upload Archive ├──┤ + output) │ └──────────────┘ └───────────────┘ └────────────────┘ │ + │ │ + └──► ...repeat for as many DB schemas are supported... ──┘ +``` + +**Note: Running these steps locally may result in publishing a locally generated DB to production, which should never be done.** + +1. **pull**: Download the latest vulnerability data from various upstream data sources into a local cache directory. + In CI, this is invoked from the root of the repo: + + ```bash + # from the repo root + GRYPE_DB_BUILDER_CACHE_DIR=publish/cache go run main.go pull -vv + ``` + + Note that all pulled vuln data is cached into the `publish/cache` directory. + + +2. **generate**: Build and package a DB from the cached vuln data for a specific DB schema version: + + ```bash + # from publish/ + poetry run publisher generate --schema-version + ``` + + This call needs to be repeated for all schema versions that are supported (see the `grype-schema-version-mapping.json` + file in the root of the repo). During this step all DBs and DB archives generated are stored in the `publish/build` directory. + Once built, DBs are subjected to testing against grype. Each DB is imported by grype, grype is invoked with a test + image, and the result is compared against a golden test-fixture saved at `publish/test-fixtures`. If the test-fixture + and the actual report obtained from grype differ significantly, then the script fails. + Once the comparison test passes, the DB archive is moved from the `publish/build` directory to the `publish/stage` + directory. + + +3. **upload-dbs**: All DB archives found in the `publish/stage` directory are uploaded to S3. + + ```bash + # from publish/ + ./upload-dbs.sh + ``` + + Note: only the DB archives are uploaded to S3, the listing file has not been touched. This means that though the + uploaded DBs are now publicly accessible, installations of grype will not attempt to use the DBs. + + +4. **upload-listing**: Generate and upload a new listing file to S3 based on the existing listing file and newly + discovered DB archives already uploaded to S3. + + ```bash + # from publish/ + poetry run publisher upload-listing --s3-bucket --s3-path + ``` + + During this step the locally crafted listing file is tested against installations of grype. The correctness of the + reports are NOT verified (since this was done in a previous step), however, in order to pass the scan must have + a non-zero count of matches found. + + Once the listing file has been uploaded user-facing grype installations should pick up that there are new DBs available to download. + + +#### FAQ for daily DB generation + +**1. The "generate DB" step fails with "RuntimeError: failed quality gate: vulnerabilities not within tolerance"...** + +The "generate DB" step validates the DB against a test fixture in `publish/test-fixtures`. This fixture needs to be +periodically updated with the latest expected set of vulnerabilities. This process should probably be one day updated +to ignore CVEs after a certain year to aid in not needing to update this test fixture that frequently. + +To easily update the test fixtures (and similar ones used for acceptance tests): +``` +make update-test-fixtures +``` + +If using an M1 use +``` +arch -x86_64 make update-test-fixtures +``` +This is necessary because old versions of Grype didn't release arm64 binaries. diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..7e5c7b65 --- /dev/null +++ b/Makefile @@ -0,0 +1,313 @@ +BIN = grype-db + +SOURCE_REPO_URL = https://github.com/anchore/grype-db +TEMP_DIR = ./.tmp +RESULTS_DIR = $(TEMP_DIR)/results +COVER_REPORT = $(RESULTS_DIR)/cover.report +COVER_TOTAL = $(RESULTS_DIR)/cover.total +LICENSES_REPORT = $(RESULTS_DIR)/licenses.json + +DB_ARCHIVE = ./grype-db-cache.tar.gz +GRYPE_DB = go run ./cmd/$(BIN)/main.go +GRYPE_DB_DATA_IMAGE_NAME = ghcr.io/anchore/$(BIN)/data +date = $(shell date +"%y-%m-%d") + +# Command templates ################################# +LINT_CMD = $(TEMP_DIR)/golangci-lint run --config .golangci.yaml +RELEASE_CMD := $(TEMP_DIR)/goreleaser release --rm-dist +SNAPSHOT_CMD := $(RELEASE_CMD) --skip-publish --skip-sign --snapshot +CHRONICLE_CMD = $(TEMP_DIR)/chronicle +GLOW_CMD = $(TEMP_DIR)/glow + +# Tool versions ################################# +GOLANGCILINT_VERSION = v1.51.1 +BOUNCER_VERSION = v0.4.0 +CHRONICLE_VERSION = v0.6.0 +GORELEASER_VERSION = v1.13.0 +CRANE_VERSION=v0.12.1 +GLOW_VERSION := v1.5.0 + +# Formatting variables ################################# +BOLD := $(shell tput -T linux bold) +PURPLE := $(shell tput -T linux setaf 5) +GREEN := $(shell tput -T linux setaf 2) +CYAN := $(shell tput -T linux setaf 6) +RED := $(shell tput -T linux setaf 1) +RESET := $(shell tput -T linux sgr0) +TITLE := $(BOLD)$(PURPLE) +SUCCESS := $(BOLD)$(GREEN) + +# Test variables ################################# +# the quality gate lower threshold for unit test total % coverage (by function statements) +COVERAGE_THRESHOLD := 55 +RELEASE_CMD=$(TEMP_DIR)/goreleaser release --rm-dist +SNAPSHOT_CMD=$(RELEASE_CMD) --skip-publish --snapshot +DIST_DIR=./dist +SNAPSHOT_DIR=./snapshot +CHANGELOG := CHANGELOG.md + + +define safe_rm_rf + bash -c 'test -z "$(1)" && false || rm -rf $(1)' +endef + +define safe_rm_rf_children + bash -c 'test -z "$(1)" && false || rm -rf $(1)/*' +endef + +ifeq "$(strip $(VERSION))" "" + override VERSION = $(shell git describe --always --tags --dirty) +endif + +## Variable assertions + +ifndef TEMP_DIR + $(error TEMP_DIR is not set) +endif + +ifndef RESULTS_DIR + $(error RESULTS_DIR is not set) +endif + +ifndef DIST_DIR + $(error DIST_DIR is not set) +endif + +ifndef SNAPSHOT_DIR + $(error SNAPSHOT_DIR is not set) +endif + +define title + @printf '$(TITLE)$(1)$(RESET)\n' +endef + +.DEFAULT_GOAL := all + +.PHONY: all +all: static-analysis test ## Run all checks (linting, license checks, unit, and acceptance tests) + @printf '$(SUCCESS)All checks pass!$(RESET)\n' + +.PHONY: test +test: unit ## Run all tests (unit) + + +## Bootstrapping targets ################################# + +.PHONY: ci-bootstrap +ci-bootstrap: bootstrap ci-build-libs + sudo apt install -y bc + +.PHONY: ci-build-libs +ci-build-libs: + sudo DEBIAN_FRONTEND=noninteractive apt-get update + sudo DEBIAN_FRONTEND=noninteractive apt-get -yq install -y sqlite3 libsqlite3-dev + +.PHONY: bootstrap +bootstrap: ## Download and install all project dependencies (+ prep tooling in the ./tmp dir) + $(call title,Downloading dependencies) + # prep temp dirs + mkdir -p $(TEMP_DIR) + mkdir -p $(RESULTS_DIR) + # install go dependencies + go mod download + # install utilities + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(TEMP_DIR)/ $(GOLANGCILINT_VERSION) + curl -sSfL https://raw.githubusercontent.com/wagoodman/go-bouncer/master/bouncer.sh | sh -s -- -b $(TEMP_DIR)/ $(BOUNCER_VERSION) + curl -sSfL https://raw.githubusercontent.com/anchore/chronicle/main/install.sh | sh -s -- -b $(TEMP_DIR)/ $(CHRONICLE_VERSION) + .github/scripts/goreleaser-install.sh -b $(TEMP_DIR)/ $(GORELEASER_VERSION) + GOBIN="$(abspath $(TEMP_DIR))" go install github.com/google/go-containerregistry/cmd/crane@$(CRANE_VERSION) + GOBIN="$(realpath $(TEMP_DIR))" go install github.com/charmbracelet/glow@$(GLOW_VERSION) + + +## Static analysis targets ################################# + +.PHONY: static-analysis ## Run all static analysis checks (linting and license checks) +static-analysis: lint check-licenses + +.PHONY: lint +lint: ## Run gofmt + golangci lint checks + $(call title,Running linters) + # ensure there are no go fmt differences + @printf "files with gofmt issues: [$(shell gofmt -l -s .)]\n" + @test -z "$(shell gofmt -l -s .)" + + # run all golangci-lint rules + $(LINT_CMD) + +.PHONY: lint-fix +lint-fix: ## Auto-format all source code + run golangci lint fixers + $(call title,Running lint fixers) + gofmt -w -s . + $(LINT_CMD) --fix + go mod tidy + +.PHONY: check-licenses +check-licenses: + $(TEMP_DIR)/bouncer check ./cmd/$(BIN) + + +## Testing targets ################################# + +.PHONY: unit +unit: unit-go unit-python ## Run go and python unit tests + +.PHONY: unit-python +unit-python: ## Run python unit tests + $(call title,Running Python unit tests) + cd publish && poetry install && poetry run pytest -v tests + +.PHONY: unit-go +unit-go: ## Run GO unit tests (with coverage) + $(call title,Running Go unit tests) + go test -coverprofile $(COVER_REPORT) ./... + @go tool cover -func $(COVER_REPORT) | grep total | awk '{print substr($$3, 1, length($$3)-1)}' > $(COVER_TOTAL) + @echo "Coverage: $$(cat $(COVER_TOTAL))" + @if [ $$(echo "$$(cat $(COVER_TOTAL)) >= $(COVERAGE_THRESHOLD)" | bc -l) -ne 1 ]; then echo "$(RED)$(BOLD)Failed coverage quality gate (> $(COVERAGE_THRESHOLD)%)$(RESET)" && false; fi + +.PHONY: acceptance +acceptance: ## Run acceptance tests (for local use, not CI) + $(call title,"Running local acceptance tests (this takes a while... 45 minutes or so)") + cd test/acceptance && poetry run python ./grype-ingest.py test-all + + +## Test-fixture-related targets ################################# + +.PHONY: update-test-fixtures +update-test-fixtures: + docker run \ + --pull always \ + --rm \ + -it \ + anchore/grype:latest \ + -q \ + -o json \ + centos:8.2.2004 > publish/test-fixtures/centos-8.2.2004.json + dos2unix publish/test-fixtures/centos-8.2.2004.json + cd test/acceptance && poetry install && poetry run python grype-ingest.py capture-test-fixtures + + +## Data management targets ################################# + +.PHONY: show-providers +show-providers: + @# this is used in CI to generate a job matrix, pulling data for each provider concurrently + @cat .grype-db.yaml | python -c 'import yaml; import json; import sys; print(json.dumps([x["name"] for x in yaml.safe_load(sys.stdin).get("provider",{}).get("configs",[])]));' + +.PHONY: download-provider-cache +download-provider-cache: + $(call title,Downloading and restoring todays "$(provider)" provider data cache) + @bash -c "oras pull $(GRYPE_DB_DATA_IMAGE_NAME)/$(provider):$(date) && $(GRYPE_DB) cache restore --path $(DB_ARCHIVE) || (echo 'no data cache found for today' && exit 1)" + +.PHONY: refresh-provider-cache +refresh-provider-cache: + $(call title,Refreshing "$(provider)" provider data cache) + $(GRYPE_DB) pull -v -p $(provider) + +.PHONY: upload-provider-cache +upload-provider-cache: ci-check + $(call title,Uploading "$(provider)" existing provider data cache) + + @rm -f $(DB_ARCHIVE) + $(GRYPE_DB) cache backup -v --path $(DB_ARCHIVE) -p $(provider) + oras push -v $(GRYPE_DB_DATA_IMAGE_NAME)/$(provider):$(date) $(DB_ARCHIVE) --annotation org.opencontainers.image.source=$(SOURCE_REPO_URL) + $(TEMP_DIR)/crane tag $(GRYPE_DB_DATA_IMAGE_NAME)/$(provider):$(date) latest + +.PHONY: aggregate-all-provider-cache +aggregate-all-provider-cache: + $(call title,Aggregating all of todays provider data cache) + .github/scripts/aggregate-all-provider-cache.py + +.PHONY: upload-all-provider-cache +upload-all-provider-cache: ci-check + $(call title,Uploading existing provider data cache) + + @rm -f $(DB_ARCHIVE) + $(GRYPE_DB) cache backup -v --path $(DB_ARCHIVE) + oras push -v $(GRYPE_DB_DATA_IMAGE_NAME):$(date) $(DB_ARCHIVE) --annotation org.opencontainers.image.source=$(SOURCE_REPO_URL) + $(TEMP_DIR)/crane tag $(GRYPE_DB_DATA_IMAGE_NAME):$(date) latest + + +.PHONY: download-all-provider-cache +download-all-provider-cache: + $(call title,Downloading and restoring all of todays provider data cache) + @rm -f $(DB_ARCHIVE) + @bash -c "oras pull $(GRYPE_DB_DATA_IMAGE_NAME):$(date) && $(GRYPE_DB) cache restore --path $(DB_ARCHIVE) || (echo 'no data cache found for today' && exit 1)" + + +## Build-related targets ################################# + +.PHONY: build +build: $(SNAPSHOT_DIR) ## Build release snapshot binaries and packages + +$(SNAPSHOT_DIR): ## Build snapshot release binaries and packages + $(call title,Building snapshot artifacts) + + # create a config with the dist dir overridden + echo "dist: $(SNAPSHOT_DIR)" > $(TEMP_DIR)/goreleaser.yaml + cat .goreleaser.yaml >> $(TEMP_DIR)/goreleaser.yaml + + # build release snapshots + $(SNAPSHOT_CMD) --config $(TEMP_DIR)/goreleaser.yaml + +.PHONY: changelog +changelog: clean-changelog ## Generate and show the changelog for the current unreleased version + $(CHRONICLE_CMD) -vvv -n --version-file VERSION > $(CHANGELOG) + @$(GLOW_CMD) $(CHANGELOG) + +$(CHANGELOG): + $(CHRONICLE_CMD) -vvv > $(CHANGELOG) + +.PHONY: release +release: + @.github/scripts/trigger-release.sh + +.PHONY: release +ci-release: ci-check clean-dist $(CHANGELOG) ## Build and publish final binaries and packages. Intended to be run only on macOS. + $(call title,Publishing release artifacts) + + # create a config with the dist dir overridden + echo "dist: $(DIST_DIR)" > $(TEMP_DIR)/goreleaser.yaml + cat .goreleaser.yaml >> $(TEMP_DIR)/goreleaser.yaml + + bash -c "$(RELEASE_CMD) --config $(TEMP_DIR)/goreleaser.yaml --release-notes <(cat $(CHANGELOG))" + +.PHONY: ci-check +ci-check: + @.github/scripts/ci-check.sh + + +## Cleanup targets ################################# + +.PHONY: clean +clean: clean-dist clean-snapshot clean-changelog ## Remove previous builds and result reports + $(call safe_rm_rf_children,$(RESULTS_DIR)) + +.PHONY: clean-changelog +clean-changelog: + rm -f $(CHANGELOG) VERSION + +.PHONY: clear-test-cache +clear-test-cache: + find . -type f -wholename "**/test-fixtures/tar-cache/*.tar" -delete + +.PHONY: clean-db +clean-db: + rm -rf build/ + rm -f metadata.json listing.json vulnerability-db*.tar.gz vulnerability.db + +.PHONY: clean-dist +clean-dist: clean-changelog + $(call safe_rm_rf,$(DIST_DIR)) + rm -f $(TEMP_DIR)/goreleaser.yaml + +.PHONY: clean-snapshot +clean-snapshot: + $(call safe_rm_rf,$(SNAPSHOT_DIR)) + rm -f $(TEMP_DIR)/goreleaser.yaml + + +## Halp! ################################# + +.PHONY: help +help: + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "$(BOLD)$(CYAN)%-25s$(RESET)%s\n", $$1, $$2}' diff --git a/README.md b/README.md index 0ddd649f..4d4edd6d 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,148 @@ # grype-db +Application to create a vulnerability database from upstream vulnerability data sources. -All code has been ported to the [grype](https://github.com/anchore/grype) repo under the `grype/db` package. We are no longer accepting contributions in this repo. +## Usage + +To pull the vulnerability source data, build the `vulnerability.db` file, and package the database into a `tar.gz` run the following: +```bash +# Pull all upstream vulnerability data sources to local cache +grype-db pull + +# Build a SQLite DB from the vulnerability data for a particular schema version +grype-db build [--dir=DIR] [--schema=SCHEMA] [--skip-validation] + +# Package the already built DB file into an archive ready for upload and serving +grype-db package [--dir=DIR] [--publish-base-url=URL] +``` + +The `pull` command downloads and caches vulnerability data from upstream sources (e.g. NIST, redhat, github, canonical, etc.) into +a cache directory. The cache location is a platform dependent XDG directory, however, the location can be overridden with the `cache.dir` +configuration option. The default configuration is to use [vunnel](https://github.com/anchore/vunnel) to fetch and +process the vulnerability data. + +**note: you can skip the `pull` step if you already have a local cache of vulnerability data (with `make download-all-provider-cache`).** + +The `build` command processes the cached vuln data generate a `vulnerability.db` sqlite3 file. Additionally, a `metadata.json` +is created that is used in packaging and curation of the database file by this application and downstream consuming applications. + +The `package` command archives the `vulnerability.db` and `metadata.json` files into a `tar.gz` file. Additionally, a `listing.json` +is generated to aid in serving one or more database archives for downstream consumption, where the consuming application should +use the listing file to discover available archives available for download. The base URL used to create the download URL for each +database archive is controlled by the `package.base-url` configuration option. + +You can additionally manage vulnerability data cache with the following commands: +```bash +# backup all cached vulnerability data or a specific PROVIDER to a tar.gz file (PATH) +grype-db cache backup [--path=PATH] [--provider-name=PROVIDER] + +# delete all cached vulnerability data or a specific PROVIDER +grype-db cache delete [--provider-name=PROVIDER] + +# restore vulnerability cache from a tar.gz file (PATH) +grype-db cache restore [--path=PATH] [--delete-existing] + +# show the current state of the all vulnerability data cache or a specific PROVIDER +grype-db cache status [--provider-name=PROVIDER ...] +``` + +## DB Schemas + +This repo supports building databases for all supported versions of grype, even when the data shape has changed. +For every change in the data shape over time, a new schema is created (see the DEVELOPING.md for details on how to bump the schema). + +**For every schema grype-db supports, we build a DB for that schema nightly. To reduce nightly DB maintenance, try to keep the schema bumps to a minimum during development.** + +Once a schema has been created, the previous schema should be considered locked unless making bug fixes or updates related to [vunnel](https://github.com/anchore/vunnel), or otherwise upstream data shape changes. + +If the development being done requires any of the following, then a **new schema is required to be created** (over further developing the current schema): +- If a previous version of grype using the same schema would not function with the new changes +- If the current version of grype using a previously published database (but still the same schema) would not function with the new changes + +Where "would not function" means either grype will error out during processing, or the results are otherwise compromised (e.g. missing data that otherwise could/should have been found and reported). + +The following kinds of changes **do not necessarily require a new schema**: +- Adding a new data source +- Removing an existing data source (as long as the grype matchers are not requiring its presence) + +There are plenty of grey areas between these cases (e.g. changing the expected set of values for a field, or changing the semantics for a column) --use your best judgement. + +This repo is responsible for publishing DBs with the latest vulnerability data for every supported schema daily. +This is achieved with the [Daily Data Sync](https://github.com/anchore/grype-db/actions/workflows/daily-data-sync.yaml) and [Daily DB Publisher](https://github.com/anchore/grype-db/actions/workflows/daily-db-publisher.yaml) GitHub Actions workflows. +Which schemas are built and which grype versions are used to verify functionality is controlled with the `grype-schema-version-mapping.json` file in the root of this repo +(see the DEVELOPING.md for more details). + +## Configuration + +```yaml +# suppress all output +# same as -q ; GRYPE_DB_QUIET env var +quiet: false + +log: + # the log level; note: detailed logging suppress the ETUI + # same as GRYPE_DB_LOG_LEVEL env var + level: "error" + + # location to write the log file (default is not to have a log file) + # same as GRYPE_DB_LOG_FILE env var + file: "" + +cache: + # where the root cache directory is + # same as GRYPE_DB_CACHE_DIR env var + dir: "$XDG_CACHE/grype-db" + +pull: + # the number of concurrent workers to use when pulling and processing data + parallelism: 1 + + # the location where all provider state is stored. The state must be oriented as described + # in https://github.com/anchore/vunnel/tree/main/schema/provider-workspace-state . + # Note: all location references under `providers` should be relative to this directory + root: ./data + + # a list of provider configurations, for example: + # + # providers: + # - name: nvd + # - name: alpine + # - name: amazon + # + # this will populate the `.root` directory with the results. + # You can also manually craft a similar configuration with the "external" provider: + # + # providers: + # - name: nvd + # type: external + # config: + # cmd: vunnel -vv run nvd + # path: nvd/metadata.json + # + # - name: alpine + # type: external + # config: + # cmd: vunnel -vv run alpine + # path: alpine/metadata.json + # + # - name: amazon + # type: external + # config: + # cmd: vunnel -vv run amazon + # path: amazon/metadata.json + # + providers: [] + +build: + # where to place the built SQLite DB that is built from the "build" command + # same as GRYPE_DB_BUILD_DIR env var + dir: "./build" + + # the DB schema version to build + # same as GRYPE_DB_BUILD_SCHEMA_VERSION env var + schema-version: 5 + +package: + # this is the base URL that is referenced in the listing file created during the "package" command + # same as GRYPE_DB_PACKAGE_PUBLISH_BASE_URL env var + publish-base-url: "https://toolbox-data.anchore.io/grype/databases" +``` \ No newline at end of file diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 00000000..a29fdbe5 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,110 @@ +# Release + +## Creating a release + +This release process itself should be as automated as possible, and has only a few steps: + +1. **Trigger a new release with `make release`**. At this point you'll see a preview + changelog in the terminal. If you're happy with the changelog, press `y` to continue, otherwise + you can abort and adjust the labels on the PRs and issues to be included in the release and + re-run the release trigger command. + +1. A release admin must approve the release on the GitHub Actions release pipeline run page. + Once approved, the release pipeline will generate all assets and publish a GitHub Release. + +1. If there is a release Milestone, close it. + +Ideally releasing should be done often with small increments when possible. Unless a +breaking change is blocking the release, or no fixes/features have been merged, a good +target release cadence is between every 1 or 2 weeks. + + +## Retracting a release + +If a release is found to be problematic, it can be retracted with the following steps: + +- Deleting the GitHub Release +- Add a new `retract` entry in the go.mod for the versioned release + +**Note**: do not delete release tags from the git repository since there may already be references to the release +in the go proxy, which will cause confusion when trying to reuse the tag later (the H1 hash will not match and there +will be a warning when users try to pull the new release). + + +## Background + +A good release process has the following qualities: + +1. There is a way to plan what should be in a release +1. There is a way to see what is actually in a release +1. Allow for different kinds of releases (major breaking vs backwards compatible enhancements vs patch updates) +1. Specify a repeatable way to build and publish software artifacts + + +### Planning a release + +To indicate a set of features to be released together add each issue to an in-repository +Milestone named with major-minor version to be released (e.g. `v0.1`). It is OK for other +features to be in the release that were not originally planned, and these issues and PRs +do not need to be added to the Milestone in question. Only the set of features that, when +completed, would allow the release to be considered complete. A Milestone is only used to: + +- Plan what is desired to be in a release +- Track progress to indicate when we may be ready to cut a new release + +Not all releases need to be planned. For instance, patch releases for fixes should be +released when they are ready and when releasing would not interfere with another current +release (where some partial or breaking features have already been merged). + +Unless necessary, feature releases should be small and frequent, which may obviate the +need for regular release planning under a Milestone. + + +### What is in a release + +Milestones are specifically for planning a release, not necessarily tracking all changes +that a release may bring (and more importantly, not all releases are necessarily planned +either). + +This is one of the (many) reasons for a Changelog. A good Changelog lists changes grouped +by the type of change (new, enhancement, deprecation, breaking, bug fix, security fix), in +chronological order (within groups), linking the PR where the change was made in the +Changelog line. Furthermore, there should be a place to see all released versions, the +release date for each release, the semantic version of the release, and the set of changes +for each release. + +**This project auto-generates the Changelog contents for each current release and posts the +generated contents to the GitHub Release page**. Leveraging the GitHub Releases feature +allows GitHub to manage the Changelog on each release outside of the git source tree while +still being hosted with the released assets. + +The Changelog is generated from the metadata from in-repository issues and PRs, using +labels to guide what kind of change each item is (e.g. breaking, new feature, bug fix, +etx). Only issues/PRs with select labels are included in the Changelog, and only if the +issue/PR was created after the last release. Additional labels are used to exclude items +from the Changelog. + +The above suggestions imply that we should: + +- Ensure there is a sufficient title for each PR and issue title to be included in the + Changelog +- The appropriate label is applied to PRs and/or issues to drive specific change type + sections (deprecated, breaking, security, bug, etc) + +**With this approach as we cultivate good organization of PRs and issues we automatically +get an equally good Changelog.** + + +### Major, minor, and patch releases + +The latest version of the tool is the only supported version, which implies that multiple +parallel release branches will not be a regular process (if ever). Multiple releases can +be planned in parallel, however, only one can be actively developed at a time. That is, if +PRs attached to a release Milestone have been merged into the main branch, that release is +now the "next" release. **This implies that the source of truth for release lies with the +git log and Changelog, not with the release Milestones** (which are purely for planning and +tracking). + +Semantic versioning should be used to indicate breaking changes, new features, and fixes. +The exception to this is `< 1.0`, where the major version is not bumped for breaking changes, +instead the minor version indicates both new features and breaking changes. diff --git a/bump-schema.py b/bump-schema.py new file mode 100644 index 00000000..7294a102 --- /dev/null +++ b/bump-schema.py @@ -0,0 +1,86 @@ +import glob +import os +import re +import shutil +from typing import List + +PROCESS_PATH = "./pkg/process" +DEFAULT_SCHEMA_PATH = os.path.join(PROCESS_PATH, "default_schema_version.go") +IMPORT_TEMPLATES = [ + "github.com/anchore/grype-db/pkg/process/{}", + "github.com/anchore/grype/pkg/db/{}", +] + + +def get_schema_versions(path: str = PROCESS_PATH) -> List[str]: + return [os.path.basename(p) for p in glob.glob(path + "/v*") if re.match(r'.*/v\d+$', p)] + + +def latest_schema_version(versions: List[str]) -> str: + return "v" + str(max([int(v.replace("v", "")) for v in versions])) + + +def next_schema_version(version: str): + return "v" + str(int(version.replace("v", ""))+1) + + +def replace_in_file(path: str, old: str, new: str): + with open(path, 'r') as file : + contents = file.read() + with open(path, 'w') as file: + file.write(contents.replace(old, new)) + + +def bump_import_versions(old_version: str, new_version: str): + path = os.path.join(PROCESS_PATH, new_version) + + for root, dirs, files in os.walk(path): + for file in files: + if not file.endswith(".go"): + continue + full_path = os.path.join(root, file) + + for import_template in IMPORT_TEMPLATES: + old_import = import_template.format(old_version) + new_import = import_template.format(new_version) + replace_in_file(full_path, old_import, new_import) + + +def create_new_schema_dir(old_version: str, new_version: str): + shutil.copytree( + os.path.join(PROCESS_PATH, old_version), + os.path.join(PROCESS_PATH, new_version) + ) + + bump_import_versions(old_version, new_version) + + +def update_default_schema_version(old_version: str, new_version: str): + for import_template in IMPORT_TEMPLATES: + old_import = import_template.format(old_version) + new_import = import_template.format(new_version) + replace_in_file(DEFAULT_SCHEMA_PATH, old_import, new_import) + + +def run(): + existing_versions = get_schema_versions() + + if not existing_versions: + print("could not find any existing schema versions") + exit(1) + + old_version = latest_schema_version(existing_versions) + new_version = next_schema_version(old_version) + + print("detected schema versions: ", existing_versions) + print("latest schema version: ", old_version) + print("new schema version: ", new_version) + print("generating new schema sources...") + create_new_schema_dir(old_version, new_version) + print("updating default schema version...") + update_default_schema_version(old_version, new_version) + print("done!") + + +if __name__ == "__main__": + run() diff --git a/cmd/grype-db/application/application.go b/cmd/grype-db/application/application.go new file mode 100644 index 00000000..1686af12 --- /dev/null +++ b/cmd/grype-db/application/application.go @@ -0,0 +1,208 @@ +package application + +import ( + "context" + "fmt" + "io" + "os" + "strings" + + "github.com/anchore/go-logger/adapter/logrus" + "github.com/anchore/grype-db/cmd/grype-db/cli/options" + "github.com/anchore/grype-db/internal" + "github.com/anchore/grype-db/internal/bus" + "github.com/anchore/grype-db/internal/eventloop" + "github.com/anchore/grype-db/internal/log" + "github.com/anchore/grype-db/internal/ui" + "github.com/anchore/grype-db/internal/utils" + "github.com/gookit/color" + "github.com/pkg/profile" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "github.com/wagoodman/go-partybus" + "gopkg.in/yaml.v3" +) + +const Name = internal.ApplicationName + +type Application struct { + Config *Config + subscription *partybus.Subscription +} + +func New() *Application { + return &Application{ + Config: &Config{}, + } +} + +func (a *Application) Setup(opts options.Interface) func(cmd *cobra.Command, args []string) error { + v := newViper() + return func(cmd *cobra.Command, args []string) error { + // bind options to viper + if opts != nil { + if err := opts.BindFlags(cmd.Flags(), v); err != nil { + return err + } + } + + if err := a.Config.BindFlags(cmd.Root().PersistentFlags(), v); err != nil { + return fmt.Errorf("unable to bind persistent flags: %w", err) + } + + if err := a.Config.Load(v); err != nil { + return fmt.Errorf("invalid application config: %w", err) + } + + // load initial command configuration from file... + if a.Config.ConfigPath != "" { + f, err := os.Open(a.Config.ConfigPath) + if err != nil { + return fmt.Errorf("unable to open config file: %w", err) + } + defer f.Close() + contents, err := io.ReadAll(f) + if err != nil { + return fmt.Errorf("unable to read config file: %w", err) + } + if err := yaml.Unmarshal(contents, opts); err != nil { + return fmt.Errorf("unable to unmarshal command elements from application config: %w", err) + } + } + + // setup command config... + if opts != nil { + err := v.Unmarshal(opts) + if err != nil { + return fmt.Errorf("unable to unmarshal command configuration for cmd=%q: %w", strings.TrimSpace(cmd.CommandPath()), err) + } + } + + // setup logger... + if err := setupLogger(a.Config); err != nil { + return err + } + + // show the app version and configuration... + logVersion() + logConfiguration(a.Config, opts) + + // setup the event bus (before any publishers in the workers run)... + b := partybus.NewBus() + bus.SetPublisher(b) + a.subscription = b.Subscribe() + + return nil + } +} + +func (a Application) Run(ctx context.Context, errs <-chan error) error { + if a.Config.Dev.ProfileCPU { + defer profile.Start(profile.CPUProfile).Stop() + } else if a.Config.Dev.ProfileMem { + defer profile.Start(profile.MemProfile).Stop() + } + err := eventloop.Run( + ctx, + errs, + a.subscription, + nil, + ui.Select(ui.Config{ + Verbose: isVerbose(a.Config.Log.Verbosity), + Quiet: a.Config.Log.Quiet, + Debug: false, + })..., + ) + + if err != nil { + log.Error(err.Error()) + } + return err +} + +func logConfiguration(app *Config, opts interface{}) { + var optsStr string + + if opts != nil { + if stringer, ok := opts.(fmt.Stringer); ok { + optsStr = stringer.String() + } else { + // yaml is pretty human friendly (at least when compared to json) + cfgBytes, err := yaml.Marshal(&opts) + if err != nil { + optsStr = fmt.Sprintf("%+v", opts) + } else { + optsStr = string(cfgBytes) + } + } + } + + log.Debugf("config:\n%+v", formatConfig(app.String())+"\n"+formatConfig(optsStr)) +} + +func logVersion() { + versionInfo := ReadBuildInfo() + log.Infof("%s version: %+v", Name, versionInfo.Version) +} + +func setupLogger(app *Config) error { + cfg := logrus.Config{ + //EnableConsole: (app.Log.FileLocation == "" || app.Log.Verbosity > 0) && !app.Log.Quiet, + EnableConsole: app.Log.Verbosity > 0 && !app.Log.Quiet, + FileLocation: app.Log.FileLocation, + Level: app.Log.Level, + } + + l, err := logrus.New(cfg) + if err != nil { + return err + } + + log.Set(l) + + return nil +} + +func formatConfig(config string) string { + return color.Magenta.Sprint(utils.Indent(strings.TrimSpace(config), " ")) +} + +func isVerbose(verbosity int) (result bool) { + pipedInput, err := isPipedInput() + if err != nil { + // since we can't tell if there was piped input we assume that there could be to disable the ETUI + log.Warnf("unable to determine if there is piped input: %w", err) + return true + } + // verbosity should consider if there is piped input (in which case we should not show the ETUI) + return verbosity > 0 || pipedInput +} + +// isPipedInput returns true if there is no input device, which means the user **may** be providing input via a pipe. +func isPipedInput() (bool, error) { + fi, err := os.Stdin.Stat() + if err != nil { + return false, fmt.Errorf("unable to determine if there is piped input: %w", err) + } + + // note: we should NOT use the absence of a character device here as the hint that there may be input expected + // on stdin, as running this application as a subprocess you would expect no character device to be present but input can + // be from either stdin or indicated by the CLI. Checking if stdin is a pipe is the most direct way to determine + // if there *may* be bytes that will show up on stdin that should be used for the analysis source. + return fi.Mode()&os.ModeNamedPipe != 0, nil +} + +func newViper() *viper.Viper { + v := viper.NewWithOptions( + viper.EnvKeyReplacer( + strings.NewReplacer(".", "_", "-", "_"), + ), + ) + + // load environment variables + v.SetEnvPrefix(Name) + v.AllowEmptyEnv(true) + v.AutomaticEnv() + + return v +} diff --git a/cmd/grype-db/application/build_info.go b/cmd/grype-db/application/build_info.go new file mode 100644 index 00000000..af500493 --- /dev/null +++ b/cmd/grype-db/application/build_info.go @@ -0,0 +1,75 @@ +package application + +import ( + "fmt" + "runtime" + "runtime/debug" + + grypeDB "github.com/anchore/grype/grype/db/v3" +) + +const valueNotProvided = "[not provided]" + +var version = valueNotProvided +var gitCommit = valueNotProvided +var gitDescription = valueNotProvided +var buildDate = valueNotProvided +var platform = fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH) + +type BuildInfo struct { + Version string `json:"version"` // application semantic version + GitCommit string `json:"gitCommit"` // git SHA at build-time + GitDescription string `json:"gitDescription"` // indication of git tree (either "clean" or "dirty") at build-time + BuildDate string `json:"buildDate"` // date of the build + GoVersion string `json:"goVersion"` // go runtime version at build-time + Compiler string `json:"compiler"` // compiler used at build-time + Platform string `json:"platform"` // GOOS and GOARCH at build-time + DBSchema int `json:"dbSchema"` +} + +func ReadBuildInfo() BuildInfo { + var buildRevision string + var vcsModified bool + var foundVcsModified bool + if info, ok := debug.ReadBuildInfo(); ok { + for _, s := range info.Settings { + if s.Key == "vcs.revision" { + buildRevision = s.Value + } else if s.Key == "vcs.modified" { + vcsModified = s.Value == "true" + foundVcsModified = true + } + } + } + + if version == valueNotProvided { + if buildRevision != "" { + version = fmt.Sprintf("%s-adhoc-build", buildRevision) + } else { + version = fmt.Sprintf("%s (adhoc-build)", valueNotProvided) + } + } + + if gitCommit == valueNotProvided && buildRevision != "" { + gitCommit = buildRevision + } + + if gitDescription == valueNotProvided && foundVcsModified { + if vcsModified { + gitDescription = "dirty" + } else { + gitDescription = "clean" + } + } + + return BuildInfo{ + Version: version, + GitCommit: gitCommit, + GitDescription: gitDescription, + BuildDate: buildDate, + GoVersion: runtime.Version(), + Compiler: runtime.Compiler, + Platform: platform, + DBSchema: grypeDB.SchemaVersion, + } +} diff --git a/cmd/grype-db/application/cache.go b/cmd/grype-db/application/cache.go new file mode 100644 index 00000000..e0d3977b --- /dev/null +++ b/cmd/grype-db/application/cache.go @@ -0,0 +1,5 @@ +package application + +type Cache struct { + Directory string `yaml:"dir" json:"dir" mapstructure:"dir"` +} diff --git a/cmd/grype-db/application/config.go b/cmd/grype-db/application/config.go new file mode 100644 index 00000000..4cbbd5f1 --- /dev/null +++ b/cmd/grype-db/application/config.go @@ -0,0 +1,208 @@ +package application + +import ( + "errors" + "fmt" + "path" + "reflect" + + "github.com/adrg/xdg" + "github.com/anchore/grype-db/cmd/grype-db/cli/options" + "github.com/mitchellh/go-homedir" + "github.com/spf13/pflag" + "github.com/spf13/viper" + "gopkg.in/yaml.v3" +) + +var ConfigSearchLocations = []string{ + fmt.Sprintf(".%s.yaml", Name), + fmt.Sprintf("%s.yaml", Name), + fmt.Sprintf(".%s/config.yaml", Name), + fmt.Sprintf("~/.%s.yaml", Name), + fmt.Sprintf("~/%s.yaml", Name), + fmt.Sprintf("$XDG_CONFIG_HOME/%s/config.yaml", Name), +} + +type defaultValueLoader interface { + loadDefaultValues(*viper.Viper) +} + +type parser interface { + parseConfigValues() error +} + +type Config struct { + ConfigPath string `yaml:"config,omitempty" json:"config"` // the location where the application config was read from (either from -c or discovered while loading) + + Dev Development `yaml:"dev" json:"dev" mapstructure:"dev"` + Log Logging `yaml:"log" json:"log" mapstructure:"log"` // all logging-related options + + DisableLoadFromDisk bool `yaml:"-" json:"-" mapstructure:"-"` +} + +func (cfg *Config) BindFlags(flags *pflag.FlagSet, v *viper.Viper) error { + options.BindOrExit(v, "config", flags.Lookup("config")) + + return nil +} + +// Load populates the given viper object with application configuration discovered on disk +func (cfg *Config) Load(v *viper.Viper) error { + // read the config from a specified path from the environment (only if not already preconfigured) + if cfg.ConfigPath == "" { + // unmarshal only control from viper in order to get config file path + var control struct { + ConfigPath string `yaml:"config" json:"config" mapstructure:"config"` + } + err := v.Unmarshal(&control) + if err != nil { + return fmt.Errorf("unable to unmarshal control section of application config: %w", err) + } + if control.ConfigPath != "" { + cfg.ConfigPath = control.ConfigPath + } + } + + // check if user specified config; otherwise read all possible paths + if !cfg.DisableLoadFromDisk && cfg.ConfigPath != "-" { + if err := readFromDisk(v, cfg.ConfigPath); err != nil { + return err + } + cfg.ConfigPath = v.ConfigFileUsed() + } + + // load default config values into viper + cfg.loadDefaultValues(v) + + // unmarshal fully populated viper object onto config + if err := v.Unmarshal(cfg); err != nil { + return fmt.Errorf("unable to unmarshal application config: %w", err) + } + + // Convert all populated config options to their internal application values ex: scope string => scopeOpt source.Scope + return cfg.parseConfigValues() +} + +// init loads the default configuration values into the viper instance (before the config values are read and parsed). +func (cfg Config) loadDefaultValues(v *viper.Viper) { + // for each field in the configuration struct, see if the field implements the defaultValueLoader interface and invoke it if it does + value := reflect.ValueOf(cfg) + for i := 0; i < value.NumField(); i++ { + // note: the defaultValueLoader method receiver is NOT a pointer receiver. + if loadable, ok := value.Field(i).Interface().(defaultValueLoader); ok { + // the field implements defaultValueLoader, call it + loadable.loadDefaultValues(v) + } + } +} + +func (cfg *Config) parseConfigValues() error { + // parse nested config options + // for each field in the configuration struct, see if the field implements the parser interface + // note: the app config is a pointer, so we need to grab the elements explicitly (to traverse the address) + value := reflect.ValueOf(cfg).Elem() + for i := 0; i < value.NumField(); i++ { + // note: since the interface method of parser is a pointer receiver we need to get the value of the field as a pointer. + if parsable, ok := value.Field(i).Addr().Interface().(parser); ok { + // the field implements parser, call it + if err := parsable.parseConfigValues(); err != nil { + return err + } + } + } + return nil +} + +func (cfg Config) String() string { + // yaml is pretty human friendly (at least when compared to json) + appCfgStr, err := yaml.Marshal(&cfg) + + if err != nil { + return err.Error() + } + + return string(appCfgStr) +} + +// readConfig attempts to read the given config path from disk or discover an alternate store location +// + +func readFromDisk(v *viper.Viper, configPath string) error { + var err error + // use explicitly the given user config + if configPath != "" { + v.SetConfigFile(configPath) + if err := v.ReadInConfig(); err != nil { + return fmt.Errorf("unable to read application config=%q: %w", configPath, err) + } + v.Set("config", v.ConfigFileUsed()) + // don't fall through to other options if the config path was explicitly provided + return nil + } + + // start searching for valid configs in order... + // 1. look for ..yaml (in the current directory) + v.AddConfigPath(".") + v.SetConfigName("." + Name) + if err = v.ReadInConfig(); err == nil { + v.Set("config", v.ConfigFileUsed()) + return nil + } else if !errors.As(err, &viper.ConfigFileNotFoundError{}) { + return fmt.Errorf("unable to parse config=%q: %w", v.ConfigFileUsed(), err) + } + + // 2. look for .yaml (in the current directory) + v.AddConfigPath(".") + v.SetConfigName(Name) + if err = v.ReadInConfig(); err == nil { + v.Set("config", v.ConfigFileUsed()) + return nil + } else if !errors.As(err, &viper.ConfigFileNotFoundError{}) { + return fmt.Errorf("unable to parse config=%q: %w", v.ConfigFileUsed(), err) + } + + // 3. look for ./config.yaml (in the current directory) + v.AddConfigPath("." + Name) + v.SetConfigName("config") + if err = v.ReadInConfig(); err == nil { + v.Set("config", v.ConfigFileUsed()) + return nil + } else if !errors.As(err, &viper.ConfigFileNotFoundError{}) { + return fmt.Errorf("unable to parse config=%q: %w", v.ConfigFileUsed(), err) + } + + // 4. look for ~/..yaml && ~/.yaml + home, err := homedir.Dir() + if err == nil { + v.AddConfigPath(home) + v.SetConfigName("." + Name) + if err = v.ReadInConfig(); err == nil { + v.Set("config", v.ConfigFileUsed()) + return nil + } else if !errors.As(err, &viper.ConfigFileNotFoundError{}) { + return fmt.Errorf("unable to parse config=%q: %w", v.ConfigFileUsed(), err) + } + + v.SetConfigName(Name) + if err = v.ReadInConfig(); err == nil { + v.Set("config", v.ConfigFileUsed()) + return nil + } else if !errors.As(err, &viper.ConfigFileNotFoundError{}) { + return fmt.Errorf("unable to parse config=%q: %w", v.ConfigFileUsed(), err) + } + } + + // 5. look for /config.yaml in xdg locations (starting with xdg home config dir, then moving upwards) + v.AddConfigPath(path.Join(xdg.ConfigHome, Name)) + for _, dir := range xdg.ConfigDirs { + v.AddConfigPath(path.Join(dir, Name)) + } + v.SetConfigName("config") + if err = v.ReadInConfig(); err == nil { + v.Set("config", v.ConfigFileUsed()) + return nil + } else if !errors.As(err, &viper.ConfigFileNotFoundError{}) { + return fmt.Errorf("unable to parse config=%q: %w", v.ConfigFileUsed(), err) + } + return nil +} diff --git a/cmd/grype-db/application/development.go b/cmd/grype-db/application/development.go new file mode 100644 index 00000000..39da935a --- /dev/null +++ b/cmd/grype-db/application/development.go @@ -0,0 +1,13 @@ +package application + +import "github.com/spf13/viper" + +type Development struct { + ProfileCPU bool `yaml:"profile-cpu" json:"profile-cpu" mapstructure:"profile-cpu"` + ProfileMem bool `yaml:"profile-mem" json:"profile-mem" mapstructure:"profile-mem"` +} + +func (c Development) loadDefaultValues(v *viper.Viper) { + v.SetDefault("dev.profile-cpu", c.ProfileCPU) // zero-value (false) or the current instance value + v.SetDefault("dev.profile-mem", c.ProfileMem) // zero-value (false) or the current instance value +} diff --git a/cmd/grype-db/application/logging.go b/cmd/grype-db/application/logging.go new file mode 100644 index 00000000..00f70767 --- /dev/null +++ b/cmd/grype-db/application/logging.go @@ -0,0 +1,51 @@ +package application + +import ( + "github.com/anchore/go-logger" + "github.com/spf13/viper" +) + +// Logging contains all logging-related configuration options available to the user via the application config. +type Logging struct { + Quiet bool `yaml:"quiet" json:"quiet" mapstructure:"quiet" description:"suppress logging output"` // -q, indicates to not show any status output to stderr + Verbosity int `yaml:"-" json:"-" mapstructure:"verbosity"` // -v or -vv , controlling which UI (ETUI vs logging) and what the log level should be + Level logger.Level `yaml:"level" json:"level" mapstructure:"level" description:"error, warn, info, debug, trace"` // the log level string hint + FileLocation string `yaml:"file" json:"file" mapstructure:"file" description:"file to write all loge entries to"` // the file path to write logs to + + // not implemented upstream + // Structured bool `yaml:"structured" json:"structured" mapstructure:"structured"` // show all log entries as JSON formatted strings +} + +func (cfg Logging) loadDefaultValues(v *viper.Viper) { + v.SetDefault("log.level", string(logger.InfoLevel)) + v.SetDefault("log.file", cfg.FileLocation) +} + +func (cfg *Logging) parseConfigValues() error { + switch { + case cfg.Quiet: + // TODO: this is bad: quiet option trumps all other logging options (such as to a file on disk) + // we should be able to quiet the console logging and leave file logging alone... + // ... this will be an enhancement for later + cfg.Level = logger.DisabledLevel + + case cfg.Verbosity > 0: + // TODO: there is a panic in this function when specifying more verbosity than whats available + cfg.Level = logger.LevelFromVerbosity(cfg.Verbosity, logger.InfoLevel, logger.DebugLevel, logger.TraceLevel) + + case cfg.Level != "": + var err error + cfg.Level, err = logger.LevelFromString(string(cfg.Level)) + if err != nil { + return err + } + + if logger.IsVerbose(cfg.Level) { + cfg.Verbosity = 1 + } + default: + cfg.Level = logger.InfoLevel + } + + return nil +} diff --git a/cmd/grype-db/cli/cli.go b/cmd/grype-db/cli/cli.go new file mode 100644 index 00000000..ab1fa8bb --- /dev/null +++ b/cmd/grype-db/cli/cli.go @@ -0,0 +1,46 @@ +package cli + +import ( + "github.com/anchore/grype-db/cmd/grype-db/application" + "github.com/anchore/grype-db/cmd/grype-db/cli/commands" + "github.com/spf13/cobra" +) + +type config struct { + app *application.Application +} + +type Option func(*config) + +func WithApplication(app *application.Application) Option { + return func(config *config) { + config.app = app + } +} + +func New(opts ...Option) *cobra.Command { + cfg := &config{ + app: application.New(), + } + for _, fn := range opts { + fn(cfg) + } + + app := cfg.app + + cache := commands.Cache(app) + cache.AddCommand(commands.CacheListFiles(app)) + cache.AddCommand(commands.CacheStatus(app)) + cache.AddCommand(commands.CacheDelete(app)) + cache.AddCommand(commands.CacheBackup(app)) + cache.AddCommand(commands.CacheRestore(app)) + + root := commands.Root(app) + root.AddCommand(commands.Version(app)) + root.AddCommand(commands.Pull(app)) + root.AddCommand(commands.Build(app)) + root.AddCommand(commands.Package(app)) + root.AddCommand(cache) + + return root +} diff --git a/cmd/grype-db/cli/commands/build.go b/cmd/grype-db/cli/commands/build.go new file mode 100644 index 00000000..30b1b882 --- /dev/null +++ b/cmd/grype-db/cli/commands/build.go @@ -0,0 +1,111 @@ +package commands + +import ( + "fmt" + "os" + "time" + + "github.com/anchore/grype-db/pkg/provider/providers/vunnel" + + "github.com/anchore/grype-db/cmd/grype-db/application" + "github.com/anchore/grype-db/cmd/grype-db/cli/options" + "github.com/anchore/grype-db/internal/log" + "github.com/anchore/grype-db/pkg/process" + "github.com/anchore/grype-db/pkg/provider" + "github.com/anchore/grype-db/pkg/provider/providers" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/spf13/viper" +) + +var _ options.Interface = &buildConfig{} + +type buildConfig struct { + options.Build `yaml:"build" json:"build" mapstructure:"build"` + options.Provider `yaml:"provider" json:"provider" mapstructure:"provider"` +} + +func (o *buildConfig) AddFlags(flags *pflag.FlagSet) { + options.AddAllFlags(flags, &o.Build, &o.Provider) +} + +func (o *buildConfig) BindFlags(flags *pflag.FlagSet, v *viper.Viper) error { + return options.BindAllFlags(flags, v, &o.Build, &o.Provider) +} + +func Build(app *application.Application) *cobra.Command { + cfg := buildConfig{ + Build: options.DefaultBuild(), + Provider: options.DefaultProvider(), + } + + cmd := &cobra.Command{ + Use: "build", + Short: "build a SQLite DB from the vulnerability feeds data for a particular schema version", + Args: cobra.NoArgs, + PreRunE: app.Setup(&cfg), + RunE: func(cmd *cobra.Command, args []string) error { + return app.Run(cmd.Context(), async(func() error { + return build(cfg) + })) + }, + } + + commonConfiguration(app, cmd, &cfg) + + return cmd +} + +func build(cfg buildConfig) error { + // make the db dir if it does not already exist + if _, err := os.Stat(cfg.Build.Directory); os.IsNotExist(err) { + if err := os.MkdirAll(cfg.Build.Directory, 0755); err != nil { + return fmt.Errorf("unable to make db build dir: %w", err) + } + } + + pvdrs, err := providers.New(cfg.Provider.Root, vunnel.Config{ + Executor: cfg.Vunnel.Executor, + DockerTag: cfg.Vunnel.DockerTag, + DockerImage: cfg.Vunnel.DockerImage, + Env: cfg.Vunnel.Env, + }, cfg.Provider.Configs...) + if err != nil { + return fmt.Errorf("unable to create providers: %w", err) + } + + var states []provider.State + stateTimestamp := time.Now() + log.Info("reading all provider state") + for _, p := range pvdrs { + log.WithFields("provider", p.ID().Name).Debug("reading state") + + sd, err := p.State() + if err != nil { + return fmt.Errorf("unable to read provider state: %w", err) + } + + if !cfg.SkipValidation { + log.WithFields("provider", p.ID().Name).Trace("validating state") + if err := sd.Verify(); err != nil { + return fmt.Errorf("invalid provider state: %w", err) + } + } + + if sd.Timestamp.Before(stateTimestamp) { + stateTimestamp = sd.Timestamp + } + states = append(states, *sd) + } + + if !cfg.SkipValidation { + log.Debugf("state validated for all providers") + } + + return process.Build(process.BuildConfig{ + SchemaVersion: cfg.SchemaVersion, + Directory: cfg.Directory, + States: states, + Timestamp: stateTimestamp, + }) +} diff --git a/cmd/grype-db/cli/commands/cache.go b/cmd/grype-db/cli/commands/cache.go new file mode 100644 index 00000000..02ffc9d4 --- /dev/null +++ b/cmd/grype-db/cli/commands/cache.go @@ -0,0 +1,17 @@ +package commands + +import ( + "github.com/anchore/grype-db/cmd/grype-db/application" + "github.com/spf13/cobra" +) + +func Cache(_ *application.Application) *cobra.Command { + cmd := &cobra.Command{ + Use: "cache", + Short: "manage the local pull cache", + Args: cobra.NoArgs, + } + + commonConfiguration(nil, cmd, nil) + return cmd +} diff --git a/cmd/grype-db/cli/commands/cache_backup.go b/cmd/grype-db/cli/commands/cache_backup.go new file mode 100644 index 00000000..bca3afb3 --- /dev/null +++ b/cmd/grype-db/cli/commands/cache_backup.go @@ -0,0 +1,164 @@ +package commands + +import ( + "archive/tar" + "compress/gzip" + "io" + "os" + "path/filepath" + + "github.com/scylladb/go-set/strset" + + "github.com/anchore/grype-db/internal/log" + "github.com/spf13/pflag" + "github.com/spf13/viper" + + "github.com/anchore/grype-db/cmd/grype-db/application" + "github.com/anchore/grype-db/cmd/grype-db/cli/options" + "github.com/anchore/grype-db/pkg/provider" + "github.com/spf13/cobra" +) + +var _ options.Interface = &cacheBackupConfig{} + +type cacheBackupConfig struct { + options.FilterProviders `yaml:",inline" json:",inline" mapstructure:",squash"` + options.CacheArchive `yaml:"cache" json:"cache" mapstructure:"cache"` + options.Provider `yaml:"provider" json:"provider" mapstructure:"provider"` +} + +func (o *cacheBackupConfig) AddFlags(flags *pflag.FlagSet) { + options.AddAllFlags(flags, &o.CacheArchive, &o.Provider, &o.FilterProviders) +} + +func (o *cacheBackupConfig) BindFlags(flags *pflag.FlagSet, v *viper.Viper) error { + return options.BindAllFlags(flags, v, &o.CacheArchive, &o.Provider, &o.FilterProviders) +} + +func CacheBackup(app *application.Application) *cobra.Command { + cfg := cacheBackupConfig{ + CacheArchive: options.DefaultCacheArchive(), + Provider: options.DefaultProvider(), + } + + cmd := &cobra.Command{ + Use: "backup", + Short: "backup provider cache to an archive", + Args: cobra.NoArgs, + PreRunE: app.Setup(&cfg), + RunE: func(cmd *cobra.Command, args []string) error { + return app.Run(cmd.Context(), async(func() error { + return cacheBackup(cfg) + })) + }, + } + + commonConfiguration(app, cmd, &cfg) + + return cmd +} + +func cacheBackup(cfg cacheBackupConfig) error { + log.WithFields("archive", cfg.CacheArchive.Path).Info("backing up provider cache") + + archive, err := os.Create(cfg.CacheArchive.Path) + if err != nil { + return err + } + + gw := gzip.NewWriter(archive) + defer func(gw *gzip.Writer) { + if err := gw.Close(); err != nil { + log.Errorf("unable to close gzip writer: %w", err) + } + }(gw) + tw := tar.NewWriter(gw) + defer func(tw *tar.Writer) { + if err := tw.Close(); err != nil { + log.Errorf("unable to close tar writer: %w", err) + } + }(tw) + + allowableProviders := strset.New(cfg.FilterProviders.ProviderNames...) + + for _, p := range cfg.Provider.Configs { + if allowableProviders.Size() > 0 && !allowableProviders.Has(p.Name) { + log.WithFields("provider", p.Name).Trace("skipping...") + continue + } + log.WithFields("provider", p.Name).Debug("backing up cache") + if err := archiveProvider(cfg.Provider.Root, p, tw); err != nil { + return err + } + } + + log.WithFields("path", cfg.CacheArchive.Path).Info("provider cache archived") + + return nil +} + +func archiveProvider(root string, p provider.Config, writer *tar.Writer) error { + wd, err := os.Getwd() + if err != nil { + return err + } + err = os.Chdir(root) + if err != nil { + return err + } + defer func(dir string) { + if err := os.Chdir(dir); err != nil { + log.Errorf("unable to restore directory: %w", err) + } + }(wd) + + return filepath.Walk(p.Name, + func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if info.IsDir() { + return nil + } + + return addToArchive(writer, path) + }, + ) +} + +func addToArchive(writer *tar.Writer, filename string) error { + file, err := os.Open(filename) + if err != nil { + return err + } + defer file.Close() + + info, err := file.Stat() + if err != nil { + return err + } + + header, err := tar.FileInfoHeader(info, info.Name()) + if err != nil { + return err + } + + // use full path as name (FileInfoHeader only takes the basename) + // If we don't do this the directory structure would + // not be preserved + // https://golang.org/src/archive/tar/common.go?#L626 + header.Name = filename + + err = writer.WriteHeader(header) + if err != nil { + return err + } + + _, err = io.Copy(writer, file) + if err != nil { + return err + } + + return nil +} diff --git a/cmd/grype-db/cli/commands/cache_delete.go b/cmd/grype-db/cli/commands/cache_delete.go new file mode 100644 index 00000000..ded2f190 --- /dev/null +++ b/cmd/grype-db/cli/commands/cache_delete.go @@ -0,0 +1,88 @@ +package commands + +import ( + "errors" + "os" + + "github.com/scylladb/go-set/strset" + "github.com/spf13/pflag" + "github.com/spf13/viper" + + "github.com/anchore/grype-db/internal/log" + + "github.com/anchore/grype-db/cmd/grype-db/application" + "github.com/anchore/grype-db/cmd/grype-db/cli/options" + "github.com/anchore/grype-db/pkg/provider" + "github.com/spf13/cobra" +) + +var _ options.Interface = &cacheDeleteConfig{} + +type cacheDeleteConfig struct { + options.FilterProviders `yaml:",inline" json:",inline" mapstructure:",squash"` + options.Provider `yaml:"provider" json:"provider" mapstructure:"provider"` +} + +func (o *cacheDeleteConfig) AddFlags(flags *pflag.FlagSet) { + options.AddAllFlags(flags, &o.Provider, &o.FilterProviders) +} + +func (o *cacheDeleteConfig) BindFlags(flags *pflag.FlagSet, v *viper.Viper) error { + return options.BindAllFlags(flags, v, &o.Provider, &o.FilterProviders) +} + +func CacheDelete(app *application.Application) *cobra.Command { + cfg := cacheDeleteConfig{ + Provider: options.DefaultProvider(), + } + + cmd := &cobra.Command{ + Use: "delete", + Short: "delete all provider cache", + Args: cobra.NoArgs, + PreRunE: app.Setup(&cfg), + RunE: func(cmd *cobra.Command, args []string) error { + return app.Run(cmd.Context(), async(func() error { + return cacheDelete(cfg) + })) + }, + } + + commonConfiguration(app, cmd, &cfg) + + return cmd +} + +func cacheDelete(cfg cacheDeleteConfig) error { + allowableProviders := strset.New(cfg.FilterProviders.ProviderNames...) + + for _, p := range cfg.Provider.Configs { + if allowableProviders.Size() > 0 && !allowableProviders.Has(p.Name) { + log.WithFields("provider", p.Name).Trace("skipping...") + continue + } + + if err := deleteProviderCache(cfg.Provider.Root, p); err != nil { + return err + } + } + + if allowableProviders.Size() == 0 { + log.Info("all provider cache deleted") + } + + return nil +} + +func deleteProviderCache(root string, p provider.Config) error { + workspace := provider.NewWorkspace(root, p.Name) + dir := workspace.Path() + + if _, err := os.Stat(dir); errors.Is(err, os.ErrNotExist) { + log.WithFields("dir", dir).Debug("provider cache does not exist, skipping...") + return nil + } + + log.WithFields("dir", dir).Info("deleting provider cache") + return os.RemoveAll(dir) +} diff --git a/cmd/grype-db/cli/commands/cache_list_files.go b/cmd/grype-db/cli/commands/cache_list_files.go new file mode 100644 index 00000000..d6b82d32 --- /dev/null +++ b/cmd/grype-db/cli/commands/cache_list_files.go @@ -0,0 +1,57 @@ +package commands + +import ( + "fmt" + + "github.com/anchore/grype-db/cmd/grype-db/application" + "github.com/anchore/grype-db/cmd/grype-db/cli/options" + "github.com/anchore/grype-db/pkg/provider" + "github.com/hashicorp/go-multierror" + "github.com/spf13/cobra" +) + +var _ options.Interface = &cacheListFilesConfig{} + +type cacheListFilesConfig struct { + options.Provider `yaml:"provider" json:"provider" mapstructure:"provider"` +} + +func CacheListFiles(app *application.Application) *cobra.Command { + cfg := cacheListFilesConfig{ + Provider: options.DefaultProvider(), + } + + cmd := &cobra.Command{ + Use: "list-files", + Short: "list the result files for all providers", + Args: cobra.NoArgs, + PreRunE: app.Setup(&cfg), + RunE: func(cmd *cobra.Command, args []string) error { + return app.Run(cmd.Context(), async(func() error { + return cacheListFiles(cfg) + })) + }, + } + + commonConfiguration(app, cmd, &cfg) + + return cmd +} + +func cacheListFiles(cfg cacheListFilesConfig) error { + var errs error + for _, p := range cfg.Provider.Configs { + workspace := provider.NewWorkspace(cfg.Provider.Root, p.Name) + + sd, err := workspace.ReadState() + if err != nil { + errs = multierror.Append(errs, err) + continue + } + + for _, f := range sd.ResultPaths() { + fmt.Println(f) + } + } + return errs +} diff --git a/cmd/grype-db/cli/commands/cache_restore.go b/cmd/grype-db/cli/commands/cache_restore.go new file mode 100644 index 00000000..4238d3cf --- /dev/null +++ b/cmd/grype-db/cli/commands/cache_restore.go @@ -0,0 +1,188 @@ +package commands + +import ( + "archive/tar" + "compress/gzip" + "errors" + "fmt" + "io" + "os" + "path/filepath" + + "github.com/anchore/grype-db/cmd/grype-db/application" + "github.com/anchore/grype-db/cmd/grype-db/cli/options" + "github.com/anchore/grype-db/internal/log" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/spf13/viper" +) + +var _ options.Interface = &cacheRestoreConfig{} + +type cacheRestoreConfig struct { + Cache cacheRestoreCache `yaml:"cache" json:"cache" mapstructure:"cache"` + options.Provider `yaml:"provider" json:"provider" mapstructure:"provider"` +} + +type cacheRestoreCache struct { + options.CacheArchive `yaml:",inline" json:"inline" mapstructure:",squash"` + options.CacheRestore `yaml:"restore" json:"restore" mapstructure:"restore"` +} + +func (o *cacheRestoreConfig) AddFlags(flags *pflag.FlagSet) { + options.AddAllFlags(flags, &o.Cache.CacheRestore, &o.Cache.CacheArchive, &o.Provider) +} + +func (o *cacheRestoreConfig) BindFlags(flags *pflag.FlagSet, v *viper.Viper) error { + if err := options.Bind(v, "cache.delete-existing", flags.Lookup("delete-existing")); err != nil { + return err + } + return options.BindAllFlags(flags, v, &o.Cache.CacheRestore, &o.Cache.CacheArchive, &o.Provider) +} + +func CacheRestore(app *application.Application) *cobra.Command { + cfg := cacheRestoreConfig{ + Cache: cacheRestoreCache{ + CacheArchive: options.DefaultCacheArchive(), + CacheRestore: options.DefaultCacheRestore(), + }, + Provider: options.DefaultProvider(), + } + + cmd := &cobra.Command{ + Use: "restore", + Short: "restore provider cache from a backup archive", + Args: cobra.NoArgs, + PreRunE: app.Setup(&cfg), + RunE: func(cmd *cobra.Command, args []string) error { + return app.Run(cmd.Context(), async(func() error { + return cacheRestore(cfg) + })) + }, + } + + commonConfiguration(app, cmd, &cfg) + + return cmd +} + +func cacheRestore(cfg cacheRestoreConfig) error { + if err := os.MkdirAll(cfg.Provider.Root, 0755); err != nil { + return fmt.Errorf("failed to create provider root directory: %w", err) + } + + if cfg.Cache.DeleteExisting { + log.Info("deleting existing provider cache") + for _, p := range cfg.Provider.Configs { + if err := deleteProviderCache(cfg.Provider.Root, p); err != nil { + return fmt.Errorf("failed to delete provider cache: %w", err) + } + } + } else { + for _, p := range cfg.Provider.Configs { + dir := filepath.Join(cfg.Provider.Root, p.Name) + if _, err := os.Stat(dir); !errors.Is(err, os.ErrNotExist) { + log.WithFields("dir", dir).Debug("note: there is pre-existing provider cache, but it will not be deleted") + } + } + } + + log.WithFields("archive", cfg.Cache.CacheArchive.Path).Info("restoring provider cache from backup") + + f, err := os.Open(cfg.Cache.CacheArchive.Path) + if err != nil { + return fmt.Errorf("failed to open cache archive: %w", err) + } + + wd, err := os.Getwd() + if err != nil { + return err + } + err = os.Chdir(cfg.Provider.Root) + if err != nil { + return err + } + defer func(dir string) { + if err := os.Chdir(dir); err != nil { + log.Errorf("unable to restore directory: %w", err) + } + }(wd) + + if err := extractTarGz(f); err != nil { + return fmt.Errorf("failed to extract cache archive: %w", err) + } + + log.WithFields("path", cfg.Cache.CacheArchive.Path).Info("provider cache restored") + + return nil +} + +func extractTarGz(reader io.Reader) error { + gr, err := gzip.NewReader(reader) + if err != nil { + return fmt.Errorf("failed to create gzip reader: %w", err) + } + + tr := tar.NewReader(gr) + + for { + header, err := tr.Next() + + if errors.Is(err, io.EOF) { + break + } + + if err != nil { + return fmt.Errorf("failed to read tar header: %w", err) + } + + switch header.Typeflag { + case tar.TypeDir: + if err := os.Mkdir(header.Name, 0755); err != nil { + return fmt.Errorf("failed to create directory: %w", err) + } + case tar.TypeReg: + parentPath := filepath.Dir(header.Name) + if parentPath != "" { + if err := os.MkdirAll(parentPath, 0755); err != nil { + return fmt.Errorf("failed to create parent directory %q for file %q: %w", parentPath, header.Name, err) + } + } + + outFile, err := os.Create(header.Name) + if err != nil { + return fmt.Errorf("failed to create file: %w", err) + } + if err := safeCopy(outFile, tr); err != nil { + return fmt.Errorf("failed to copy file: %w", err) + } + if err := outFile.Close(); err != nil { + return fmt.Errorf("failed to close file: %w", err) + } + + default: + log.WithFields("name", header.Name, "type", header.Typeflag).Warn("unknown file type in backup archive") + } + } + return nil +} + +const ( + // represents the order of bytes + _ = iota + kb = 1 << (10 * iota) //nolint:deadcode + mb //nolint:deadcode + gb +) + +const perFileReadLimit = 10 * gb + +// safeCopy limits the copy from the reader. This is useful when extracting files from archives to +// protect against decompression bomb attacks. +func safeCopy(writer io.Writer, reader io.Reader) error { + numBytes, err := io.Copy(writer, io.LimitReader(reader, perFileReadLimit)) + if numBytes >= perFileReadLimit || errors.Is(err, io.EOF) { + return fmt.Errorf("zip read limit hit (potential decompression bomb attack)") + } + return nil +} diff --git a/cmd/grype-db/cli/commands/cache_status.go b/cmd/grype-db/cli/commands/cache_status.go new file mode 100644 index 00000000..e079d61c --- /dev/null +++ b/cmd/grype-db/cli/commands/cache_status.go @@ -0,0 +1,121 @@ +package commands + +import ( + "fmt" + + "github.com/anchore/grype-db/internal/log" + "github.com/anchore/grype-db/pkg/provider/entry" + "github.com/scylladb/go-set/strset" + "github.com/spf13/pflag" + "github.com/spf13/viper" + + "github.com/anchore/grype-db/cmd/grype-db/application" + "github.com/anchore/grype-db/cmd/grype-db/cli/options" + "github.com/anchore/grype-db/pkg/provider" + "github.com/spf13/cobra" +) + +var _ options.Interface = &cacheStatusConfig{} + +type cacheStatusConfig struct { + options.FilterProviders `yaml:",inline" json:",inline" mapstructure:",squash"` + options.Provider `yaml:"provider" json:"provider" mapstructure:"provider"` +} + +func (o *cacheStatusConfig) AddFlags(flags *pflag.FlagSet) { + options.AddAllFlags(flags, &o.Provider, &o.FilterProviders) +} + +func (o *cacheStatusConfig) BindFlags(flags *pflag.FlagSet, v *viper.Viper) error { + return options.BindAllFlags(flags, v, &o.Provider, &o.FilterProviders) +} + +func CacheStatus(app *application.Application) *cobra.Command { + cfg := cacheStatusConfig{ + FilterProviders: options.DefaultFilterProviders(), + Provider: options.DefaultProvider(), + } + + cmd := &cobra.Command{ + Use: "status", + Short: "verify the status of the existing provider cache", + Args: cobra.NoArgs, + PreRunE: app.Setup(&cfg), + RunE: func(cmd *cobra.Command, args []string) error { + return app.Run(cmd.Context(), async(func() error { + return cacheStatus(cfg) + })) + }, + } + + commonConfiguration(app, cmd, &cfg) + + return cmd +} + +func cacheStatus(cfg cacheStatusConfig) error { + if len(cfg.Provider.Configs) == 0 { + fmt.Println("no provider state cache found") + return nil + } + + var sds []*provider.State + var errs []error + + allowableProviders := strset.New(cfg.FilterProviders.ProviderNames...) + + for _, p := range cfg.Provider.Configs { + if allowableProviders.Size() > 0 && !allowableProviders.Has(p.Name) { + log.WithFields("provider", p.Name).Trace("skipping...") + continue + } + + workspace := provider.NewWorkspace(cfg.Provider.Root, p.Name) + sd, err := workspace.ReadState() + if err != nil { + sds = append(sds, nil) + errs = append(errs, err) + continue + } + + if err := sd.Verify(workspace.Path()); err != nil { + sds = append(sds, nil) + errs = append(errs, err) + continue + } + + errs = append(errs, nil) + sds = append(sds, sd) + } + + if allowableProviders.Size() == 0 { + fmt.Printf("providers: %d\n", len(sds)) + } + + for idx, sd := range sds { + validMsg := "valid" + if errs[idx] != nil { + validMsg = fmt.Sprintf("INVALID: %s", errs[idx].Error()) + } else if sd == nil { + validMsg = "INVALID: no state description found" + } + + providerIndex := idx + 1 + + if sd == nil { + fmt.Printf(" • provider (%d): %s\n", providerIndex, validMsg) + continue + } + + count, err := entry.Count(sd.Store, sd.ResultPaths()) + if err != nil { + log.WithFields("provider", sd.Provider, "error", err).Error("unable to count entries") + } + + fmt.Printf(" • %s\n", sd.Provider) + fmt.Printf(" ├── is valid? %s\n", validMsg) + fmt.Printf(" ├── timestamp: %s\n", sd.Timestamp) + fmt.Printf(" └── result files: %d\n", count) + } + return nil +} diff --git a/cmd/grype-db/cli/commands/package.go b/cmd/grype-db/cli/commands/package.go new file mode 100644 index 00000000..34bd3e6e --- /dev/null +++ b/cmd/grype-db/cli/commands/package.go @@ -0,0 +1,52 @@ +package commands + +import ( + "github.com/anchore/grype-db/cmd/grype-db/application" + "github.com/anchore/grype-db/cmd/grype-db/cli/options" + "github.com/anchore/grype-db/pkg/process" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/spf13/viper" +) + +var _ options.Interface = &buildConfig{} + +type packageConfig struct { + options.DBLocation `yaml:"build" json:"build" mapstructure:"build"` + options.Package `yaml:"package" json:"package" mapstructure:"package"` +} + +func (o *packageConfig) AddFlags(flags *pflag.FlagSet) { + options.AddAllFlags(flags, &o.DBLocation, &o.Package) +} + +func (o *packageConfig) BindFlags(flags *pflag.FlagSet, v *viper.Viper) error { + return options.BindAllFlags(flags, v, &o.DBLocation, &o.Package) +} + +func Package(app *application.Application) *cobra.Command { + cfg := packageConfig{ + DBLocation: options.DefaultDBLocation(), + Package: options.DefaultPackage(), + } + + cmd := &cobra.Command{ + Use: "package", + Short: "package the already built database file into an archive ready for upload and serving", + Args: cobra.NoArgs, + PreRunE: app.Setup(&cfg), + RunE: func(cmd *cobra.Command, args []string) error { + return app.Run(cmd.Context(), async(func() error { + return doPackage(cfg) + })) + }, + } + + commonConfiguration(app, cmd, &cfg) + + return cmd +} + +func doPackage(cfg packageConfig) error { + return process.Package(cfg.DBLocation.Directory, cfg.PublishBaseURL) +} diff --git a/cmd/grype-db/cli/commands/pull.go b/cmd/grype-db/cli/commands/pull.go new file mode 100644 index 00000000..2047c54f --- /dev/null +++ b/cmd/grype-db/cli/commands/pull.go @@ -0,0 +1,79 @@ +package commands + +import ( + "github.com/anchore/grype-db/cmd/grype-db/application" + "github.com/anchore/grype-db/cmd/grype-db/cli/options" + "github.com/anchore/grype-db/internal/log" + "github.com/anchore/grype-db/pkg/process" + "github.com/anchore/grype-db/pkg/provider" + "github.com/anchore/grype-db/pkg/provider/providers" + "github.com/anchore/grype-db/pkg/provider/providers/vunnel" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/spf13/viper" +) + +var _ options.Interface = &pullConfig{} + +type pullConfig struct { + options.Pull `yaml:"pull" json:"pull" mapstructure:"pull"` + options.Provider `yaml:"provider" json:"provider" mapstructure:"provider"` +} + +func (o *pullConfig) AddFlags(flags *pflag.FlagSet) { + options.AddAllFlags(flags, &o.Pull, &o.Provider) +} + +func (o *pullConfig) BindFlags(flags *pflag.FlagSet, v *viper.Viper) error { + return options.BindAllFlags(flags, v, &o.Pull, &o.Provider) +} + +func Pull(app *application.Application) *cobra.Command { + cfg := pullConfig{ + Pull: options.DefaultPull(), + Provider: options.DefaultProvider(), + } + + cmd := &cobra.Command{ + Use: "pull", + Short: "pull and process all upstream vulnerability data", + Args: cobra.NoArgs, + PreRunE: app.Setup(&cfg), + RunE: func(cmd *cobra.Command, args []string) error { + return app.Run(cmd.Context(), async(func() error { + return pull(cfg) + })) + }, + } + + commonConfiguration(app, cmd, &cfg) + + return cmd +} + +func pull(cfg pullConfig) error { + ps, err := providers.New(cfg.Root, vunnel.Config{ + Executor: cfg.Vunnel.Executor, + DockerTag: cfg.Vunnel.DockerTag, + DockerImage: cfg.Vunnel.DockerImage, + Env: cfg.Vunnel.Env, + }, cfg.Provider.Configs...) + if err != nil { + return err + } + + if len(cfg.FilterProviders.ProviderNames) > 0 { + log.WithFields("keep-only", cfg.FilterProviders.ProviderNames).Debug("filtering providers by name") + ps = ps.Filter(cfg.FilterProviders.ProviderNames...) + } + + c := process.PullConfig{ + Parallelism: cfg.Parallelism, + Collection: provider.Collection{ + Root: cfg.Root, + Providers: ps, + }, + } + + return process.Pull(c) +} diff --git a/cmd/grype-db/cli/commands/root.go b/cmd/grype-db/cli/commands/root.go new file mode 100644 index 00000000..32844961 --- /dev/null +++ b/cmd/grype-db/cli/commands/root.go @@ -0,0 +1,49 @@ +package commands + +import ( + "fmt" + "strings" + + "github.com/anchore/grype-db/cmd/grype-db/application" + "github.com/anchore/grype-db/cmd/grype-db/cli/options" + "github.com/anchore/grype-db/internal/utils" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +func Root(app *application.Application) *cobra.Command { + opts := app.Config + + cmd := &cobra.Command{ + Use: "", + Version: application.ReadBuildInfo().Version, + PreRunE: app.Setup(nil), + Example: formatRootExamples(), + } + + commonConfiguration(nil, cmd, nil) + + cmd.SetVersionTemplate(fmt.Sprintf("%s {{.Version}}\n", application.Name)) + + flags := cmd.PersistentFlags() + + flags.StringVarP(&opts.ConfigPath, "config", "c", "", "path to the application config") + flags.CountVarP(&opts.Log.Verbosity, "verbose", "v", "increase verbosity (-v = debug, -vv = trace)") + flags.BoolVarP(&opts.Log.Quiet, "quiet", "q", false, "suppress all logging output") + + return cmd +} + +func formatRootExamples() string { + cfg := application.Config{ + DisableLoadFromDisk: true, + } + // best effort to load current or default values + // intentionally don't read from the environment + _ = cfg.Load(viper.New()) + + cfgString := utils.Indent(options.Summarize(cfg, nil), " ") + return fmt.Sprintf(`Application Config: + (search locations: %+v) +%s`, strings.Join(application.ConfigSearchLocations, ", "), strings.TrimSuffix(cfgString, "\n")) +} diff --git a/cmd/grype-db/cli/commands/utils.go b/cmd/grype-db/cli/commands/utils.go new file mode 100644 index 00000000..42f731df --- /dev/null +++ b/cmd/grype-db/cli/commands/utils.go @@ -0,0 +1,63 @@ +package commands + +import ( + "github.com/anchore/grype-db/cmd/grype-db/application" + "github.com/anchore/grype-db/cmd/grype-db/cli/options" + "github.com/anchore/grype-db/internal/bus" + "github.com/spf13/cobra" +) + +func async(f func() error) <-chan error { + errs := make(chan error) + go func() { + defer close(errs) + if err := f(); err != nil { + errs <- err + } + bus.Exit() + }() + + return errs +} + +func commonConfiguration(app *application.Application, cmd *cobra.Command, opts options.Interface) { + if opts != nil { + opts.AddFlags(cmd.Flags()) + + if app != nil { + // we want to be able to attach config binding information to the help output + cmd.SetHelpFunc(func(cmd *cobra.Command, args []string) { + _ = app.Setup(opts)(cmd, args) + cmd.Parent().HelpFunc()(cmd, args) + }) + } + } + + cmd.SilenceUsage = true + cmd.SilenceErrors = true + cmd.SetHelpTemplate(`{{if (or .Long .Short)}}{{.Long}}{{if not .Long}}{{.Short}}{{end}} + +{{end}}Usage:{{if .Runnable}} + {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}} + {{.CommandPath}} [command]{{end}}{{if .HasExample}} + +{{.Example}}{{end}}{{if gt (len .Aliases) 0}} + +Aliases: + {{.NameAndAliases}}{{end}}{{if .HasAvailableSubCommands}} + +Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name "help"))}} + {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}} + +{{if not .CommandPath}}Global {{end}}Flags: +{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if (and .HasAvailableInheritedFlags (not .CommandPath))}} + +Global Flags: +{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}} + +Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}} + {{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}} + +Use "{{if .CommandPath}}{{.CommandPath}} {{end}}[command] --help" for more information about a command.{{end}} +`) +} diff --git a/cmd/grype-db/cli/commands/version.go b/cmd/grype-db/cli/commands/version.go new file mode 100644 index 00000000..a8f50d65 --- /dev/null +++ b/cmd/grype-db/cli/commands/version.go @@ -0,0 +1,70 @@ +package commands + +import ( + "encoding/json" + "fmt" + "os" + + "github.com/anchore/grype-db/cmd/grype-db/application" + "github.com/spf13/cobra" +) + +func Version(_ *application.Application) *cobra.Command { + var format string + + cmd := &cobra.Command{ + Use: "version", + Short: fmt.Sprintf("show %s version information", application.Name), + Args: func(cmd *cobra.Command, args []string) error { + if err := cobra.NoArgs(cmd, args); err != nil { + return err + } + // note: we intentionally do not execute through the application infrastructure (no app config is required for this command) + + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + // note: we intentionally do not execute through the application infrastructure (no app config is required for this command) + + buildInfo := application.ReadBuildInfo() + + switch format { + case "text": + fmt.Println("Application: ", application.Name) + fmt.Println("Version: ", buildInfo.Version) + fmt.Println("BuildDate: ", buildInfo.BuildDate) + fmt.Println("GitCommit: ", buildInfo.GitCommit) + fmt.Println("GitDescription: ", buildInfo.GitDescription) + fmt.Println("Platform: ", buildInfo.Platform) + fmt.Println("GoVersion: ", buildInfo.GoVersion) + fmt.Println("Compiler: ", buildInfo.Compiler) + + case "json": + enc := json.NewEncoder(os.Stdout) + enc.SetEscapeHTML(false) + enc.SetIndent("", " ") + err := enc.Encode(&struct { + application.BuildInfo + Application string `json:"application"` + }{ + BuildInfo: buildInfo, + Application: application.Name, + }) + if err != nil { + return fmt.Errorf("failed to show version information: %w", err) + } + default: + return fmt.Errorf("unsupported output format: %s", format) + } + + return nil + }, + } + + flags := cmd.Flags() + flags.StringVarP(&format, "output", "o", "text", "the format to show the results (allowable: [text json])") + + commonConfiguration(nil, cmd, nil) + + return cmd +} diff --git a/cmd/grype-db/cli/options/build.go b/cmd/grype-db/cli/options/build.go new file mode 100644 index 00000000..a6245b47 --- /dev/null +++ b/cmd/grype-db/cli/options/build.go @@ -0,0 +1,59 @@ +package options + +import ( + "github.com/anchore/grype-db/pkg/process" + "github.com/spf13/pflag" + "github.com/spf13/viper" +) + +var _ Interface = &Build{} + +type Build struct { + DBLocation `yaml:",inline" mapstructure:",squash"` // note: json will anonymously embed the struct if there is no tag (like yaml inline) + + // bound options + SkipValidation bool `yaml:"skip-validation" json:"skip-validation" mapstructure:"skip-validation"` + SchemaVersion int `yaml:"schema-version" json:"schema-version" mapstructure:"schema-version"` + + // unbound options + // (none) +} + +func DefaultBuild() Build { + return Build{ + DBLocation: DefaultDBLocation(), + SkipValidation: false, + SchemaVersion: process.DefaultSchemaVersion, + } +} + +func (o *Build) AddFlags(flags *pflag.FlagSet) { + flags.BoolVarP( + &o.SkipValidation, + "skip-validation", "", o.SkipValidation, + "skip validation of the provider state", + ) + + flags.IntVarP( + &o.SchemaVersion, + "schema", "s", o.SchemaVersion, + "DB Schema version to build for", + ) + + o.DBLocation.AddFlags(flags) +} + +func (o *Build) BindFlags(flags *pflag.FlagSet, v *viper.Viper) error { + // set default values for bound struct items + if err := Bind(v, "build.skip-validation", flags.Lookup("skip-validation")); err != nil { + return err + } + if err := Bind(v, "build.schema-version", flags.Lookup("schema")); err != nil { + return err + } + + // set default values for non-bound struct items + // (none) + + return o.DBLocation.BindFlags(flags, v) +} diff --git a/cmd/grype-db/cli/options/cache_archive.go b/cmd/grype-db/cli/options/cache_archive.go new file mode 100644 index 00000000..0de4746c --- /dev/null +++ b/cmd/grype-db/cli/options/cache_archive.go @@ -0,0 +1,42 @@ +package options + +import ( + "github.com/spf13/pflag" + "github.com/spf13/viper" +) + +var _ Interface = &CacheArchive{} + +type CacheArchive struct { + // bound options + Path string `yaml:"archive" json:"archive" mapstructure:"archive"` + + // unbound options + // (none) +} + +func DefaultCacheArchive() CacheArchive { + return CacheArchive{ + Path: "./grype-db-cache.tar.gz", + } +} + +func (o *CacheArchive) AddFlags(flags *pflag.FlagSet) { + flags.StringVarP( + &o.Path, + "path", "", o.Path, + "path to the grype-db cache archive", + ) +} + +func (o *CacheArchive) BindFlags(flags *pflag.FlagSet, v *viper.Viper) error { + // set default values for bound struct items + if err := Bind(v, "cache.archive", flags.Lookup("path")); err != nil { + return err + } + + // set default values for non-bound struct items + // (none) + + return nil +} diff --git a/cmd/grype-db/cli/options/cache_restore.go b/cmd/grype-db/cli/options/cache_restore.go new file mode 100644 index 00000000..f04ed349 --- /dev/null +++ b/cmd/grype-db/cli/options/cache_restore.go @@ -0,0 +1,42 @@ +package options + +import ( + "github.com/spf13/pflag" + "github.com/spf13/viper" +) + +var _ Interface = &CacheRestore{} + +type CacheRestore struct { + // bound options + DeleteExisting bool `yaml:"delete-existing" json:"delete-existing" mapstructure:"delete-existing"` + + // unbound options + // (none) +} + +func DefaultCacheRestore() CacheRestore { + return CacheRestore{ + DeleteExisting: false, + } +} + +func (o *CacheRestore) AddFlags(flags *pflag.FlagSet) { + flags.BoolVarP( + &o.DeleteExisting, + "delete-existing", "d", o.DeleteExisting, + "delete existing cache before restoring from backup", + ) +} + +func (o *CacheRestore) BindFlags(flags *pflag.FlagSet, v *viper.Viper) error { + // set default values for bound struct items + if err := Bind(v, "restore.delete-existing", flags.Lookup("delete-existing")); err != nil { + return err + } + + // set default values for non-bound struct items + // (none) + + return nil +} diff --git a/cmd/grype-db/cli/options/db_location.go b/cmd/grype-db/cli/options/db_location.go new file mode 100644 index 00000000..caa40d79 --- /dev/null +++ b/cmd/grype-db/cli/options/db_location.go @@ -0,0 +1,42 @@ +package options + +import ( + "github.com/spf13/pflag" + "github.com/spf13/viper" +) + +var _ Interface = &DBLocation{} + +type DBLocation struct { + // bound options + Directory string `yaml:"dir" json:"dir" mapstructure:"dir"` + + // unbound options + // (none) +} + +func DefaultDBLocation() DBLocation { + return DBLocation{ + Directory: "./build", + } +} + +func (o *DBLocation) AddFlags(flags *pflag.FlagSet) { + flags.StringVarP( + &o.Directory, + "dir", "d", o.Directory, + "directory where the database is written", + ) +} + +func (o *DBLocation) BindFlags(flags *pflag.FlagSet, v *viper.Viper) error { + // set default values for bound struct items + if err := Bind(v, "build.dir", flags.Lookup("dir")); err != nil { + return err + } + + // set default values for non-bound struct items + // (none) + + return nil +} diff --git a/cmd/grype-db/cli/options/filter_providers.go b/cmd/grype-db/cli/options/filter_providers.go new file mode 100644 index 00000000..73f2c0ed --- /dev/null +++ b/cmd/grype-db/cli/options/filter_providers.go @@ -0,0 +1,40 @@ +package options + +import ( + "github.com/spf13/pflag" + "github.com/spf13/viper" +) + +var _ Interface = &FilterProviders{} + +type FilterProviders struct { + // bound options + ProviderNames []string `yaml:"provider-names" json:"provider-names" mapstructure:"provider-names"` + + // unbound options + // (none) +} + +func DefaultFilterProviders() FilterProviders { + return FilterProviders{} +} + +func (o *FilterProviders) AddFlags(flags *pflag.FlagSet) { + flags.StringArrayVarP( + &o.ProviderNames, + "provider-name", "p", o.ProviderNames, + "one or more provider names to manipulate data for (default: empty = all)", + ) +} + +func (o *FilterProviders) BindFlags(flags *pflag.FlagSet, v *viper.Viper) error { + // set default values for bound struct items + if err := Bind(v, "filter.provider-names", flags.Lookup("provider-name")); err != nil { + return err + } + + // set default values for non-bound struct items + // (none) + + return nil +} diff --git a/cmd/grype-db/cli/options/options.go b/cmd/grype-db/cli/options/options.go new file mode 100644 index 00000000..c2e72715 --- /dev/null +++ b/cmd/grype-db/cli/options/options.go @@ -0,0 +1,28 @@ +package options + +import ( + "github.com/hashicorp/go-multierror" + "github.com/spf13/pflag" + "github.com/spf13/viper" +) + +type Interface interface { + AddFlags(*pflag.FlagSet) + BindFlags(*pflag.FlagSet, *viper.Viper) error +} + +func AddAllFlags(flags *pflag.FlagSet, i ...Interface) { + for _, o := range i { + o.AddFlags(flags) + } +} + +func BindAllFlags(flags *pflag.FlagSet, v *viper.Viper, i ...Interface) error { + var errs error + for _, o := range i { + if err := o.BindFlags(flags, v); err != nil { + errs = multierror.Append(errs, err) + } + } + return errs +} diff --git a/cmd/grype-db/cli/options/package.go b/cmd/grype-db/cli/options/package.go new file mode 100644 index 00000000..4e4fe528 --- /dev/null +++ b/cmd/grype-db/cli/options/package.go @@ -0,0 +1,42 @@ +package options + +import ( + "github.com/spf13/pflag" + "github.com/spf13/viper" +) + +var _ Interface = &Package{} + +type Package struct { + // bound options + PublishBaseURL string `yaml:"publish-base-url" json:"publish-base-url" mapstructure:"publish-base-url"` + + // unbound options + // (none) +} + +func DefaultPackage() Package { + return Package{ + PublishBaseURL: "https://toolbox-data.anchore.io/grype/databases", + } +} + +func (o *Package) AddFlags(flags *pflag.FlagSet) { + flags.StringVarP( + &o.PublishBaseURL, + "publish-base-url", "u", o.PublishBaseURL, + "the base URL used for reference in the listing.json index file", + ) +} + +func (o *Package) BindFlags(flags *pflag.FlagSet, v *viper.Viper) error { + // set default values for bound struct items + if err := Bind(v, "package.publish-base-url", flags.Lookup("publish-base-url")); err != nil { + return err + } + + // set default values for non-bound struct items + // (none) + + return nil +} diff --git a/cmd/grype-db/cli/options/provider.go b/cmd/grype-db/cli/options/provider.go new file mode 100644 index 00000000..038f6933 --- /dev/null +++ b/cmd/grype-db/cli/options/provider.go @@ -0,0 +1,96 @@ +package options + +import ( + "github.com/anchore/grype-db/pkg/provider" + "github.com/spf13/pflag" + "github.com/spf13/viper" +) + +var _ Interface = &Provider{} + +type Provider struct { + // bound options + // (none) + + // unbound options + Root string `yaml:"root" json:"root" mapstructure:"root"` + Vunnel Vunnel `yaml:"vunnel" json:"vunnel" mapstructure:"vunnel"` + Configs []provider.Config `yaml:"configs" json:"configs" mapstructure:"configs"` +} + +func DefaultProvider() Provider { + return Provider{ + Root: "./data", + Vunnel: DefaultVunnel(), + Configs: []provider.Config{ + { + Identifier: provider.Identifier{ + Name: "alpine", + Kind: provider.VunnelKind, + }, + }, + { + Identifier: provider.Identifier{ + Name: "amazon", + Kind: provider.VunnelKind, + }, + }, + { + Identifier: provider.Identifier{ + Name: "debian", + Kind: provider.VunnelKind, + }, + }, + { + Identifier: provider.Identifier{ + Name: "github", + Kind: provider.VunnelKind, + }, + }, + { + Identifier: provider.Identifier{ + Name: "nvd", + Kind: provider.VunnelKind, + }, + }, + { + Identifier: provider.Identifier{ + Name: "rhel", + Kind: provider.VunnelKind, + }, + }, + { + Identifier: provider.Identifier{ + Name: "sles", + Kind: provider.VunnelKind, + }, + }, + { + Identifier: provider.Identifier{ + Name: "ubuntu", + Kind: provider.VunnelKind, + }, + }, + { + Identifier: provider.Identifier{ + Name: "wolfi", + Kind: provider.VunnelKind, + }, + }, + }, + } +} + +func (o *Provider) AddFlags(flags *pflag.FlagSet) { +} + +func (o *Provider) BindFlags(flags *pflag.FlagSet, v *viper.Viper) error { + // set default values for bound struct items + // (none) + + // set default values for non-bound struct items + v.SetDefault("provider.root", o.Root) + v.SetDefault("provider.configs", o.Configs) + + return nil +} diff --git a/cmd/grype-db/cli/options/pull.go b/cmd/grype-db/cli/options/pull.go new file mode 100644 index 00000000..d70294cd --- /dev/null +++ b/cmd/grype-db/cli/options/pull.go @@ -0,0 +1,45 @@ +package options + +import ( + "github.com/spf13/pflag" + "github.com/spf13/viper" +) + +var _ Interface = &Pull{} + +type Pull struct { + // bound options + Parallelism int `yaml:"parallelism" json:"parallelism" mapstructure:"parallelism"` + FilterProviders `yaml:",inline" json:",inline" mapstructure:",squash"` + + // unbound options + // (none) +} + +func DefaultPull() Pull { + return Pull{ + Parallelism: 4, + } +} + +func (o *Pull) AddFlags(flags *pflag.FlagSet) { + flags.IntVarP( + &o.Parallelism, + "parallelism", "", o.Parallelism, + "number of vulnerability providers to update concurrently", + ) + + o.FilterProviders.AddFlags(flags) +} + +func (o *Pull) BindFlags(flags *pflag.FlagSet, v *viper.Viper) error { + // set default values for bound struct items + if err := Bind(v, "pull.parallelism", flags.Lookup("parallelism")); err != nil { + return err + } + + // set default values for non-bound struct items + // (none) + + return o.FilterProviders.BindFlags(flags, v) +} diff --git a/cmd/grype-db/cli/options/utils.go b/cmd/grype-db/cli/options/utils.go new file mode 100644 index 00000000..61adca01 --- /dev/null +++ b/cmd/grype-db/cli/options/utils.go @@ -0,0 +1,118 @@ +package options + +import ( + "fmt" + "os" + "reflect" + "sort" + "strings" + + "github.com/anchore/grype-db/internal" + "github.com/anchore/grype-db/internal/utils" + "github.com/gookit/color" + "github.com/iancoleman/strcase" + "github.com/spf13/pflag" + "github.com/spf13/viper" +) + +func Bind(v *viper.Viper, configKey string, flag *pflag.Flag) error { + if flag == nil { + return fmt.Errorf("unable to bind config to CLI flag: no flag given for config-key=%q", configKey) + } + + if err := v.BindPFlag(configKey, flag); err != nil { + return fmt.Errorf("unable to bind config-key=%q to CLI flag=%q: %w", configKey, flag.Name, err) + } + + envVar := strings.ToUpper(strings.NewReplacer(".", "_", "-", "_").Replace(internal.ApplicationName + "_" + configKey)) + + flag.Usage += fmt.Sprintf(" (env var: %q)", envVar) + + return nil +} + +func BindOrExit(v *viper.Viper, configKey string, flag *pflag.Flag) { + if err := Bind(v, configKey, flag); err != nil { + color.Red.Printf("%+v\n", err) + os.Exit(1) + } +} + +func FormatPositionalArgsHelp(args map[string]string) string { + var keys []string + for k := range args { + keys = append(keys, k) + } + sort.Strings(keys) + + var ret string + for _, name := range keys { + val := args[name] + if val == "" { + continue + } + ret += fmt.Sprintf(" %s: %s\n", name, val) + } + if ret == "" { + return ret + } + return "Arguments:\n" + strings.TrimSuffix(ret, "\n") +} + +func Summarize(itf interface{}, currentPath []string) string { + var desc []string + + t := reflect.TypeOf(itf) + v := reflect.ValueOf(itf) + + if t.Kind() == reflect.Struct { + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + description := field.Tag.Get("description") + yamlName := field.Tag.Get("yaml") + + tag := field.Tag.Get("mapstructure") + switch tag { + case "-", "": + continue + } + + fieldVal := v.Field(i) + + var newPath []string + newPath = append(newPath, currentPath...) + newPath = append(newPath, tag) + + envVar := strcase.ToScreamingSnake(strings.Join(append([]string{internal.ApplicationName}, newPath...), "_")) + + if description != "" { + var section string + section += fmt.Sprintf("# %s (env var: %q)\n", description, envVar) + + var val string + switch field.Type.Kind() { + case reflect.String: + val = fmt.Sprintf("%q", fieldVal) + default: + val = fmt.Sprintf("%+v", fieldVal) + } + + section += fmt.Sprintf("%s: %s", yamlName, val) + + desc = append(desc, section) + } else { + d := Summarize(fieldVal.Interface(), newPath) + if d != "" { + section := yamlName + ":\n" + utils.Indent(d, strings.Repeat(" ", len(newPath))) + desc = append(desc, strings.TrimSpace(section)) + } + } + } + } + + if len(desc) == 0 { + return "" + } + + return strings.Join(desc, "\n\n") +} diff --git a/cmd/grype-db/cli/options/vunnel.go b/cmd/grype-db/cli/options/vunnel.go new file mode 100644 index 00000000..bfea3453 --- /dev/null +++ b/cmd/grype-db/cli/options/vunnel.go @@ -0,0 +1,43 @@ +package options + +import ( + "github.com/spf13/pflag" + "github.com/spf13/viper" +) + +var _ Interface = &Vunnel{} + +type Vunnel struct { + // bound options + // (none) + + // unbound options + Executor string `yaml:"executor" json:"executor" mapstructure:"executor"` + DockerTag string `yaml:"dockerTag" json:"dockerTag" mapstructure:"dockerTag"` + DockerImage string `yaml:"dockerImage" json:"dockerImage" mapstructure:"dockerImage"` + Env map[string]string `yaml:"env" json:"env" mapstructure:"-"` // note: we don't want users to specify run env vars by app config env vars +} + +func DefaultVunnel() Vunnel { + return Vunnel{ + Executor: "docker", + DockerTag: "latest", + DockerImage: "ghcr.io/anchore/vunnel", + } +} + +func (o *Vunnel) AddFlags(flags *pflag.FlagSet) { +} + +func (o *Vunnel) BindFlags(flags *pflag.FlagSet, v *viper.Viper) error { + // set default values for bound struct items + // (none) + + // set default values for non-bound struct items + v.SetDefault("vunnel.executor", o.Executor) + v.SetDefault("vunnel.dockerTag", o.DockerTag) + v.SetDefault("vunnel.dockerImage", o.DockerImage) + v.SetDefault("vunnel.env", o.Env) + + return nil +} diff --git a/cmd/grype-db/main.go b/cmd/grype-db/main.go new file mode 100644 index 00000000..7b569b2c --- /dev/null +++ b/cmd/grype-db/main.go @@ -0,0 +1,47 @@ +package main + +import ( + "context" + "os" + "os/signal" + + "github.com/anchore/grype-db/cmd/grype-db/cli" + "github.com/anchore/grype-db/internal/log" + "github.com/gookit/color" +) + +func main() { + cmd := cli.New() + + // drive application control from a single context which can be cancelled (notifying the event loop to stop) + ctx, cancel := context.WithCancel(context.Background()) + cmd.SetContext(ctx) + + // note: it is important to always do signal handling from the main package. In this way if grype-db is used + // as a lib a refactor would not need to be done (since anything from the main package cannot be imported this + // nicely enforces this constraint) + signals := make(chan os.Signal, 10) // Note: A buffered channel is recommended for this; see https://golang.org/pkg/os/signal/#Notify + signal.Notify(signals, os.Interrupt) + + defer func() { + signal.Stop(signals) + cancel() + }() + + go func() { + select { + case <-signals: // first signal, cancel context + log.Trace("signal interrupt, stop requested") + cancel() + case <-ctx.Done(): + } + <-signals // second signal, hard exit + log.Trace("signal interrupt, killing") + os.Exit(1) + }() + + if err := cmd.Execute(); err != nil { + color.Red.Printf("error: %v", err) + defer os.Exit(1) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 00000000..f56ead42 --- /dev/null +++ b/go.mod @@ -0,0 +1,174 @@ +module github.com/anchore/grype-db + +go 1.20 + +require ( + github.com/Masterminds/semver/v3 v3.2.0 + github.com/OneOfOne/xxhash v1.2.8 + github.com/adrg/xdg v0.4.0 + github.com/anchore/go-logger v0.0.0-20230120230012-47be9bb822a2 + github.com/anchore/grype v0.56.1-0.20230210162440-47ab7f55d3d1 + github.com/anchore/sqlite v1.4.6-0.20220607210448-bcc6ee5c4963 + github.com/anchore/syft v0.71.0 + github.com/dustin/go-humanize v1.0.1 + github.com/go-test/deep v1.1.0 + github.com/google/go-cmp v0.5.9 + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 + github.com/gookit/color v1.5.2 + github.com/hashicorp/go-cleanhttp v0.5.2 + github.com/hashicorp/go-getter v1.6.2 + github.com/hashicorp/go-multierror v1.1.1 + github.com/iancoleman/strcase v0.2.0 + github.com/jinzhu/copier v0.3.5 + github.com/mitchellh/go-homedir v1.1.0 + github.com/mitchellh/mapstructure v1.5.0 + github.com/pkg/profile v1.7.0 + github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e + github.com/sergi/go-diff v1.3.1 + github.com/spf13/afero v1.9.3 + github.com/spf13/cobra v1.6.1 + github.com/spf13/pflag v1.0.5 + github.com/spf13/viper v1.15.0 + github.com/stretchr/testify v1.8.1 + github.com/umisama/go-cpe v0.0.0-20190323060751-cdd6c3c28a23 + github.com/wagoodman/go-partybus v0.0.0-20210627031916-db1f5573bbc5 + github.com/wagoodman/go-progress v0.0.0-20200807221327-51d465df1451 + golang.org/x/sync v0.1.0 + golang.org/x/text v0.7.0 + gopkg.in/yaml.v3 v3.0.1 + gorm.io/gorm v1.23.5 +) + +require ( + cloud.google.com/go v0.105.0 // indirect + cloud.google.com/go/compute v1.14.0 // indirect + cloud.google.com/go/compute/metadata v0.2.3 // indirect + cloud.google.com/go/iam v0.8.0 // indirect + cloud.google.com/go/storage v1.27.0 // indirect + github.com/CycloneDX/cyclonedx-go v0.7.1-0.20221222100750-41a1ac565cce // indirect + github.com/DataDog/zstd v1.4.5 // indirect + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/sprig/v3 v3.2.3 // indirect + github.com/Microsoft/go-winio v0.6.0 // indirect + github.com/acobaugh/osrelease v0.1.0 // indirect + github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb // indirect + github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 // indirect + github.com/anchore/go-version v1.2.2-0.20210903204242-51efa5b487c4 // indirect + github.com/anchore/packageurl-go v0.1.1-0.20230104203445-02e0a6721501 // indirect + github.com/anchore/stereoscope v0.0.0-20230208154630-5a306f07f2e7 // indirect + github.com/andybalholm/brotli v1.0.4 // indirect + github.com/aws/aws-sdk-go v1.44.180 // indirect + github.com/becheran/wildmatch-go v1.0.0 // indirect + github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect + github.com/bmatcuk/doublestar/v2 v2.0.4 // indirect + github.com/bmatcuk/doublestar/v4 v4.6.0 // indirect + github.com/containerd/containerd v1.6.12 // indirect + github.com/containerd/stargz-snapshotter/estargz v0.12.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/docker/cli v20.10.20+incompatible // indirect + github.com/docker/distribution v2.8.1+incompatible // indirect + github.com/docker/docker v23.0.1+incompatible // indirect + github.com/docker/docker-credential-helpers v0.7.0 // indirect + github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect + github.com/facebookincubator/nvdtools v0.1.5 // indirect + github.com/felixge/fgprof v0.9.3 // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.1 // indirect + github.com/go-restruct/restruct v1.2.0-alpha // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/google/go-containerregistry v0.13.0 // indirect + github.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.2.1 // indirect + github.com/googleapis/gax-go/v2 v2.7.0 // indirect + github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-safetemp v1.0.0 // indirect + github.com/hashicorp/go-version v1.6.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/huandu/xstrings v1.3.3 // indirect + github.com/imdario/mergo v0.3.12 // indirect + github.com/inconshreveable/mousetrap v1.0.1 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.4 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/klauspost/compress v1.15.11 // indirect + github.com/klauspost/pgzip v1.2.5 // indirect + github.com/knqyf263/go-apk-version v0.0.0-20200609155635-041fdbb8563f // indirect + github.com/knqyf263/go-deb-version v0.0.0-20190517075300-09fca494f03d // indirect + github.com/knqyf263/go-rpmdb v0.0.0-20221030135625-4082a22221ce // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.16 // indirect + github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect + github.com/mholt/archiver/v3 v3.5.1 // indirect + github.com/microsoft/go-rustaudit v0.0.0-20220730194248-4b17361d90a5 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/go-testing-interface v1.14.1 // indirect + github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/nwaples/rardecode v1.1.0 // indirect + github.com/olekukonko/tablewriter v0.0.5 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0-rc2 // indirect + github.com/pelletier/go-toml v1.9.5 // indirect + github.com/pelletier/go-toml/v2 v2.0.6 // indirect + github.com/pierrec/lz4/v4 v4.1.15 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/rogpeppe/go-internal v1.9.0 // indirect + github.com/sassoftware/go-rpmutils v0.2.0 // indirect + github.com/shopspring/decimal v1.2.0 // indirect + github.com/sirupsen/logrus v1.9.0 // indirect + github.com/spdx/tools-golang v0.5.0-rc1 // indirect + github.com/spf13/cast v1.5.0 // indirect + github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/stretchr/objx v0.5.0 // indirect + github.com/subosito/gotenv v1.4.2 // indirect + github.com/sylabs/sif/v2 v2.8.1 // indirect + github.com/sylabs/squashfs v0.6.1 // indirect + github.com/therootcompany/xz v1.0.1 // indirect + github.com/ulikunitz/xz v0.5.10 // indirect + github.com/vbatts/go-mtree v0.5.2 // indirect + github.com/vbatts/tar-split v0.11.2 // indirect + github.com/vifraa/gopom v0.2.1 // indirect + github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect + github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect + go.opencensus.io v0.24.0 // indirect + golang.org/x/crypto v0.5.0 // indirect + golang.org/x/exp v0.0.0-20230202163644-54bba9f4231b // indirect + golang.org/x/mod v0.8.0 // indirect + golang.org/x/net v0.6.0 // indirect + golang.org/x/oauth2 v0.4.0 // indirect + golang.org/x/sys v0.5.0 // indirect + golang.org/x/term v0.5.0 // indirect + golang.org/x/tools v0.2.0 // indirect + golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect + google.golang.org/api v0.107.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef // indirect + google.golang.org/grpc v1.52.0 // indirect + google.golang.org/protobuf v1.28.1 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gotest.tools/v3 v3.1.0 // indirect + lukechampine.com/uint128 v1.1.1 // indirect + modernc.org/cc/v3 v3.36.0 // indirect + modernc.org/ccgo/v3 v3.16.6 // indirect + modernc.org/libc v1.16.8 // indirect + modernc.org/mathutil v1.4.1 // indirect + modernc.org/memory v1.1.1 // indirect + modernc.org/opt v0.1.1 // indirect + modernc.org/sqlite v1.17.3 // indirect + modernc.org/strutil v1.1.1 // indirect + modernc.org/token v1.0.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..f62008e8 --- /dev/null +++ b/go.sum @@ -0,0 +1,1202 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= +cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= +cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= +cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= +cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= +cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= +cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= +cloud.google.com/go v0.98.0/go.mod h1:ua6Ush4NALrHk5QXDWnjvZHN93OuF0HfuEPq9I1X0cM= +cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= +cloud.google.com/go v0.105.0 h1:DNtEKRBAAzeS4KyIory52wWHuClNaXJ5x1F7xa4q+5Y= +cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/compute v1.14.0 h1:hfm2+FfxVmnRlh6LpB7cg1ZNU+5edAHmW679JePztk0= +cloud.google.com/go/compute v1.14.0/go.mod h1:YfLtxrj9sU4Yxv+sXzZkyPjEyPBZfXHUvjxega5vAdo= +cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= +cloud.google.com/go/iam v0.8.0 h1:E2osAkZzxI/+8pZcxVLcDtAQx/u+hZXVryUaYQ5O0Kk= +cloud.google.com/go/iam v0.8.0/go.mod h1:lga0/y3iH6CX7sYqypWJ33hf7kkfXJag67naqGESjkE= +cloud.google.com/go/longrunning v0.3.0 h1:NjljC+FYPV3uh5/OwWT6pVU+doBqMg2x/rZlE+CamDs= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +cloud.google.com/go/storage v1.27.0 h1:YOO045NZI9RKfCj1c5A/ZtuuENUc8OAW+gHdGnDgyMQ= +cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/CycloneDX/cyclonedx-go v0.7.1-0.20221222100750-41a1ac565cce h1:o5r3msApzvtE5LhcMkxWaKernD/PK0HpMccu7ywBj5Q= +github.com/CycloneDX/cyclonedx-go v0.7.1-0.20221222100750-41a1ac565cce/go.mod h1:XURd0m8zvnLE5aIRqg6JOVRl7qZ/pWBtuFa9EHjQwFc= +github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= +github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g= +github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= +github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= +github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg= +github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8= +github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= +github.com/acobaugh/osrelease v0.1.0 h1:Yb59HQDGGNhCj4suHaFQQfBps5wyoKLSSX/J/+UifRE= +github.com/acobaugh/osrelease v0.1.0/go.mod h1:4bFEs0MtgHNHBrmHCt67gNisnabCRAlzdVasCEGHTWY= +github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls= +github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/anchore/go-logger v0.0.0-20230120230012-47be9bb822a2 h1:gV9Mr4Tp/zvp40m1541dS9OhjlLOsWYdzP7tvqUfK/I= +github.com/anchore/go-logger v0.0.0-20230120230012-47be9bb822a2/go.mod h1:ubLFmlsv8/DFUQrZwY5syT5/8Er3ugSr4rDFwHsE3hg= +github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb h1:iDMnx6LIjtjZ46C0akqveX83WFzhpTD3eqOthawb5vU= +github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb/go.mod h1:DmTY2Mfcv38hsHbG78xMiTDdxFtkHpgYNVDPsF2TgHk= +github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 h1:aM1rlcoLz8y5B2r4tTLMiVTrMtpfY0O8EScKJxaSaEc= +github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092/go.mod h1:rYqSE9HbjzpHTI74vwPvae4ZVYZd1lue2ta6xHPdblA= +github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04 h1:VzprUTpc0vW0nnNKJfJieyH/TZ9UYAnTZs5/gHTdAe8= +github.com/anchore/go-version v1.2.2-0.20210903204242-51efa5b487c4 h1:rmZG77uXgE+o2gozGEBoUMpX27lsku+xrMwlmBZJtbg= +github.com/anchore/go-version v1.2.2-0.20210903204242-51efa5b487c4/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E= +github.com/anchore/grype v0.56.1-0.20230210162440-47ab7f55d3d1 h1:XefUEeU+AQfLZI+pCZz/+ZBmBLpiAOxskN+yReRZDYM= +github.com/anchore/grype v0.56.1-0.20230210162440-47ab7f55d3d1/go.mod h1:TKXvs6NWkDnTgoMyrO4oZeNo+K++mt+moUGaGsF20u4= +github.com/anchore/packageurl-go v0.1.1-0.20230104203445-02e0a6721501 h1:AV7qjwMcM4r8wFhJq3jLRztew3ywIyPTRapl2T1s9o8= +github.com/anchore/packageurl-go v0.1.1-0.20230104203445-02e0a6721501/go.mod h1:Blo6OgJNiYF41ufcgHKkbCKF2MDOMlrqhXv/ij6ocR4= +github.com/anchore/sqlite v1.4.6-0.20220607210448-bcc6ee5c4963 h1:vrf2PYH77vqVJoNR15ZuFJ63qwBMqrmGIt/7VsBhLF8= +github.com/anchore/sqlite v1.4.6-0.20220607210448-bcc6ee5c4963/go.mod h1:AVRyXOUP0hTz9Cb8OlD1XnwA8t4lBPfTuwPHmEUuiLc= +github.com/anchore/stereoscope v0.0.0-20230208154630-5a306f07f2e7 h1:PrdFBPMyika+AM1/AwDmYqrVeUATDU90wbrd81ugicU= +github.com/anchore/stereoscope v0.0.0-20230208154630-5a306f07f2e7/go.mod h1:TUCfo52tEz7ahTUFtKN//wcB7kJzQs0Oifmnd4NkIXw= +github.com/anchore/syft v0.71.0 h1:dagtH0oeq2K6dG0gVOj35+KDl438uCTMVU6swQu8Jk0= +github.com/anchore/syft v0.71.0/go.mod h1:LupCSiYF24mblW65/alRYa3jBWWwUM1l5jc37ErtkkU= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= +github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= +github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/aws/aws-sdk-go v1.15.78/go.mod h1:E3/ieXAlvM0XWO57iftYVDLLvQ824smPP3ATZkfNZeM= +github.com/aws/aws-sdk-go v1.44.180 h1:VLZuAHI9fa/3WME5JjpVjcPCNfpGHVMiHx8sLHWhMgI= +github.com/aws/aws-sdk-go v1.44.180/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= +github.com/becheran/wildmatch-go v1.0.0 h1:mE3dGGkTmpKtT4Z+88t8RStG40yN9T+kFEGj2PZFSzA= +github.com/becheran/wildmatch-go v1.0.0/go.mod h1:gbMvj0NtVdJ15Mg/mH9uxk2R1QCistMyU7d9KFzroX4= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas= +github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bmatcuk/doublestar/v2 v2.0.4 h1:6I6oUiT/sU27eE2OFcWqBhL1SwjyvQuOssxT4a1yidI= +github.com/bmatcuk/doublestar/v2 v2.0.4/go.mod h1:QMmcs3H2AUQICWhfzLXz+IYln8lRQmTZRptLie8RgRw= +github.com/bmatcuk/doublestar/v4 v4.6.0 h1:HTuxyug8GyFbRkrffIpzNCSK4luc0TY3wzXvzIZhEXc= +github.com/bmatcuk/doublestar/v4 v4.6.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cheggaaa/pb v1.0.27/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= +github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/containerd/containerd v1.6.12 h1:kJ9b3mOFKf8yqo05Ob+tMoxvt1pbVWhnB0re9Y+k+8c= +github.com/containerd/containerd v1.6.12/go.mod h1:K4Bw7gjgh4TnkmQY+py/PYQGp4e7xgnHAeg87VeWb3A= +github.com/containerd/stargz-snapshotter/estargz v0.12.1 h1:+7nYmHJb0tEkcRaAW+MHqoKaJYZmkikupxCqVtmPuY0= +github.com/containerd/stargz-snapshotter/estargz v0.12.1/go.mod h1:12VUuCq3qPq4y8yUW+l5w3+oXV3cx2Po3KSe/SmPGqw= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1/go.mod h1:+hnT3ywWDTAFrW5aE+u2Sa/wT555ZqwoCS+pk3p6ry4= +github.com/docker/cli v20.10.20+incompatible h1:lWQbHSHUFs7KraSN2jOJK7zbMS2jNCHI4mt4xUFUVQ4= +github.com/docker/cli v20.10.20+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68= +github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v23.0.1+incompatible h1:vjgvJZxprTTE1A37nm+CLNAdwu6xZekyoiVlUZEINcY= +github.com/docker/docker v23.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A= +github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 h1:iFaUwBSo5Svw6L7HYpRu/0lE3e0BaElwnNO1qkNQxBY= +github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s= +github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws= +github.com/facebookincubator/flog v0.0.0-20190930132826-d2511d0ce33c/go.mod h1:QGzNH9ujQ2ZUr/CjDGZGWeDAVStrWNjHeEcjJL96Nuk= +github.com/facebookincubator/nvdtools v0.1.5 h1:jbmDT1nd6+k+rlvKhnkgMokrCAzHoASWE5LtHbX2qFQ= +github.com/facebookincubator/nvdtools v0.1.5/go.mod h1:Kh55SAWnjckS96TBSrXI99KrEKH4iB0OJby3N8GRJO4= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/set v0.2.1 h1:nn2CaJyknWE/6txyUDGwysr3G5QC6xWB/PtVjPBbeaA= +github.com/fatih/set v0.2.1/go.mod h1:+RKtMCH+favT2+3YecHGxcc0b4KyVWA1QWWJUs4E0CI= +github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g= +github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= +github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/gabriel-vasile/mimetype v1.4.1 h1:TRWk7se+TOjCYgRth7+1/OYLNiRNIotknkFtf/dnN7Q= +github.com/gabriel-vasile/mimetype v1.4.1/go.mod h1:05Vi0w3Y9c/lNvJOdmIwvrrAhX3rYhfQQCaf9VJcv7M= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-restruct/restruct v1.2.0-alpha h1:2Lp474S/9660+SJjpVxoKuWX09JsXHSrdV7Nv3/gkvc= +github.com/go-restruct/restruct v1.2.0-alpha/go.mod h1:KqrpKpn4M8OLznErihXTGLlsXFGeLxHUrLRRI/1YjGk= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= +github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-containerregistry v0.13.0 h1:y1C7Z3e149OJbOPDBxLYR8ITPz8dTKqQwjErKVHJC8k= +github.com/google/go-containerregistry v0.13.0/go.mod h1:J9FQ+eSS4a1aC2GNZxvNpbWhgp0487v+cgiilB4FqDo= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y= +github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.2.1 h1:RY7tHKZcRlk788d5WSo/e83gOyyy742E8GSs771ySpg= +github.com/googleapis/enterprise-certificate-proxy v0.2.1/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= +github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= +github.com/googleapis/gax-go/v2 v2.7.0 h1:IcsPKeInNvYi7eqSaDjiZqDDKu5rsmunY0Y1YupQSSQ= +github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/gookit/color v1.2.5/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg= +github.com/gookit/color v1.5.2 h1:uLnfXcaFjlrDnQDT+NCBcfhrXqYTx/rcCa6xn01Y8yI= +github.com/gookit/color v1.5.2/go.mod h1:w8h4bGiHeeBpvQVePTutdbERIUf3oJE5lZ8HM0UgXyg= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b h1:wDUNC2eKiL35DbLvsDhiblTUXHxcOPwQSCzi7xpQUN4= +github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b/go.mod h1:VzxiSdG6j1pi7rwGm/xYI5RbtpBgM8sARDXlvEvxlu0= +github.com/hashicorp/consul/api v1.11.0/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= +github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-getter v1.6.2 h1:7jX7xcB+uVCliddZgeKyNxv0xoT7qL5KDtH7rU4IqIk= +github.com/hashicorp/go-getter v1.6.2/go.mod h1:IZCrswsZPeWv9IkVnLElzRU/gz/QPi6pZHn4tv6vbwA= +github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-hclog v1.0.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo= +github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= +github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= +github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= +github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= +github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= +github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= +github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= +github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4= +github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0= +github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= +github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= +github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= +github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg= +github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.4 h1:tHnRBy1i5F2Dh8BAFxqFzxKqqvezXrL2OW1TnX+Mlas= +github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.11.2/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.11.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.15.11 h1:Lcadnb3RKGin4FYM/orgq0qde+nc15E5Cbqg4B9Sx9c= +github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= +github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE= +github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= +github.com/knqyf263/go-apk-version v0.0.0-20200609155635-041fdbb8563f h1:GvCU5GXhHq+7LeOzx/haG7HSIZokl3/0GkoUFzsRJjg= +github.com/knqyf263/go-apk-version v0.0.0-20200609155635-041fdbb8563f/go.mod h1:q59u9px8b7UTj0nIjEjvmTWekazka6xIt6Uogz5Dm+8= +github.com/knqyf263/go-deb-version v0.0.0-20190517075300-09fca494f03d h1:X4cedH4Kn3JPupAwwWuo4AzYp16P0OyLO9d7OnMZc/c= +github.com/knqyf263/go-deb-version v0.0.0-20190517075300-09fca494f03d/go.mod h1:o8sgWoz3JADecfc/cTYD92/Et1yMqMy0utV1z+VaZao= +github.com/knqyf263/go-rpmdb v0.0.0-20221030135625-4082a22221ce h1:/w0hAcauo/FBVaBvNMQdPZgKjTu5Ip3jvGIM1+VUE7o= +github.com/knqyf263/go-rpmdb v0.0.0-20221030135625-4082a22221ce/go.mod h1:zp6SMcRd0GB+uwNJjr+DkrNZdQZ4er2HMO6KyD0vIGU= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 h1:bqDmpDG49ZRnB5PcgP0RXtQvnMSgIF14M7CBd2shtXs= +github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w= +github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-sqlite3 v1.14.12 h1:TJ1bhYJPV44phC+IMu1u2K/i5RriLTPe+yc68XDJ1Z0= +github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= +github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo= +github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4= +github.com/microsoft/go-rustaudit v0.0.0-20220730194248-4b17361d90a5 h1:tQRHcLQwnwrPq2j2Qra/NnyjyESBGwdeBeVdAE9kXYg= +github.com/microsoft/go-rustaudit v0.0.0-20220730194248-4b17361d90a5/go.mod h1:vYT9HE7WCvL64iVeZylKmCsWKfE+JZ8105iuh2Trk8g= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= +github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= +github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= +github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= +github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= +github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nwaples/rardecode v1.1.0 h1:vSxaY8vQhOcVr4mm5e8XllHWTiM4JF507A0Katqw7MQ= +github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0-rc2 h1:2zx/Stx4Wc5pIPDvIxHXvXtQFW/7XWJGmnM7r3wg034= +github.com/opencontainers/image-spec v1.1.0-rc2/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU= +github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= +github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0= +github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA= +github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo= +github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig= +github.com/sassoftware/go-rpmutils v0.2.0 h1:pKW0HDYMFWQ5b4JQPiI3WI12hGsVoW0V8+GMoZiI/JE= +github.com/sassoftware/go-rpmutils v0.2.0/go.mod h1:TJJQYtLe/BeEmEjelI3b7xNZjzAukEkeWKmoakvaOoI= +github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e h1:7q6NSFZDeGfvvtIRwBrU/aegEYJYmvev0cHAwo17zZQ= +github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e/go.mod h1:DkpGd78rljTxKAnTDPFqXSGxvETQnJyuSOQwsHycqfs= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sebdah/goldie/v2 v2.5.3 h1:9ES/mNN+HNUbNWpVAlrzuZ7jE+Nrczbj8uFRjM7624Y= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spdx/gordf v0.0.0-20201111095634-7098f93598fb/go.mod h1:uKWaldnbMnjsSAXRurWqqrdyZen1R7kxl8TkmWk2OyM= +github.com/spdx/tools-golang v0.5.0-rc1 h1:ooCSe48QatlidqEFd+nSI308tyeNTR6NJvauUj3ApX8= +github.com/spdx/tools-golang v0.5.0-rc1/go.mod h1:LI6onw172PdO57Ob/hgnLDD4Y2PMnroeNT3wO/2WJJI= +github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= +github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= +github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk= +github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= +github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= +github.com/spf13/cobra v1.3.0/go.mod h1:BrRVncBjOJa/eUcVVm9CE+oC6as8k+VYr4NY7WCi9V4= +github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= +github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.10.0/go.mod h1:SoyBPwAtKDzypXNDFKN5kzH7ppppbGZtls1UpIy5AsM= +github.com/spf13/viper v1.15.0 h1:js3yy885G8xwJa6iOISGFwd+qlUo5AvyXb7CiihdtiU= +github.com/spf13/viper v1.15.0/go.mod h1:fFcTBJxvhhzSJiZy8n+PeW6t8l+KeT/uTARa0jHOQLA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= +github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/sylabs/sif/v2 v2.8.1 h1:whr4Vz12RXfLnYyVGHoD/rD/hbF2g9OW7BJHa+WIqW8= +github.com/sylabs/sif/v2 v2.8.1/go.mod h1:LQOdYXC9a8i7BleTKRw9lohi0rTbXkJOeS9u0ebvgyM= +github.com/sylabs/squashfs v0.6.1 h1:4hgvHnD9JGlYWwT0bPYNt9zaz23mAV3Js+VEgQoRGYQ= +github.com/sylabs/squashfs v0.6.1/go.mod h1:ZwpbPCj0ocIvMy2br6KZmix6Gzh6fsGQcCnydMF+Kx8= +github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw= +github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8= +github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/umisama/go-cpe v0.0.0-20190323060751-cdd6c3c28a23 h1:+168JmE638t0OxroPRx7BUbkB91hF3GWS1OkvITgdT0= +github.com/umisama/go-cpe v0.0.0-20190323060751-cdd6c3c28a23/go.mod h1:Jv/KoYWD3+46wW8r3pEwISwtgv5Q8NTfFto2wFRKvoA= +github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/vbatts/go-mtree v0.5.2 h1:d8SAbLJiR1cR3pe1J+FBaalRkCQw95gP12/P+a9PUcA= +github.com/vbatts/go-mtree v0.5.2/go.mod h1:e0NDJ+bT3jG7ZINeB9HR5AxTvjskCsOR54+9KoaXyDc= +github.com/vbatts/tar-split v0.11.2 h1:Via6XqJr0hceW4wff3QRzD5gAk/tatMw/4ZA7cTlIME= +github.com/vbatts/tar-split v0.11.2/go.mod h1:vV3ZuO2yWSVsz+pfFzDG/upWH1JhjOiEaWq6kXyQ3VI= +github.com/vifraa/gopom v0.2.1 h1:MYVMAMyiGzXPPy10EwojzKIL670kl5Zbae+o3fFvQEM= +github.com/vifraa/gopom v0.2.1/go.mod h1:oPa1dcrGrtlO37WPDBm5SqHAT+wTgF8An1Q71Z6Vv4o= +github.com/wagoodman/go-partybus v0.0.0-20210627031916-db1f5573bbc5 h1:phTLPgMRDYTizrBSKsNSOa2zthoC2KsJsaY/8sg3rD8= +github.com/wagoodman/go-partybus v0.0.0-20210627031916-db1f5573bbc5/go.mod h1:JPirS5jde/CF5qIjcK4WX+eQmKXdPc6vcZkJ/P0hfPw= +github.com/wagoodman/go-progress v0.0.0-20200807221327-51d465df1451 h1:ULknorKcCigmaFEBfB99pzEQmYY2E0F5Yp/bIyaBdEI= +github.com/wagoodman/go-progress v0.0.0-20200807221327-51d465df1451/go.mod h1:jLXFoL31zFaHKAAyZUh+sxiTDFe1L1ZHrcK2T1itVKA= +github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= +github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= +go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= +go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= +golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20230202163644-54bba9f4231b h1:EqBVA+nNsObCwQoBEHy4wLU0pi7i8a4AL3pbItPdPkE= +golang.org/x/exp v0.0.0-20230202163644-54bba9f4231b/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= +golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.4.0 h1:NF0gk8LVPg1Ml7SSbGyySuoxdsXitj7TvgvuRxIMc/M= +golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220517195934-5e4e11fc645e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.2.0 h1:52I/1L54xyEQAYdtcSuxtiT84KGYTBGXwayxmIpNJhE= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE= +golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= +google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= +google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= +google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= +google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= +google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= +google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU= +google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= +google.golang.org/api v0.62.0/go.mod h1:dKmwPCydfsad4qCH08MSdgWjfHOyfpd4VtDGgRFdavw= +google.golang.org/api v0.107.0 h1:I2SlFjD8ZWabaIFOfeEDg3pf0BHJDh6iYQ1ic3Yu/UU= +google.golang.org/api v0.107.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= +google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= +google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef h1:uQ2vjV/sHTsWSqdKeLqmwitzgvjMl7o4IdtHwUDXSJY= +google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.52.0 h1:kd48UiU7EHsV4rnLyOJRuP/Il/UHE7gdDAQ+SZI7nZk= +google.golang.org/grpc v1.52.0/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/cheggaaa/pb.v1 v1.0.27/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/gorm v1.23.5 h1:TnlF26wScKSvknUC/Rn8t0NLLM22fypYBlvj1+aH6dM= +gorm.io/gorm v1.23.5/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= +gotest.tools/v3 v3.1.0 h1:rVV8Tcg/8jHUkPUorwjaMTtemIMVXfIPKiOqnhEhakk= +gotest.tools/v3 v3.1.0/go.mod h1:fHy7eyTmJFO5bQbUsEGQ1v4m2J3Jz9eWL54TP2/ZuYQ= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +lukechampine.com/uint128 v1.1.1 h1:pnxCASz787iMf+02ssImqk6OLt+Z5QHMoZyUXR4z6JU= +lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +modernc.org/cc/v3 v3.36.0 h1:0kmRkTmqNidmu3c7BNDSdVHCxXCkWLmWmCIVX4LUboo= +modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= +modernc.org/ccgo/v3 v3.0.0-20220428102840-41399a37e894/go.mod h1:eI31LL8EwEBKPpNpA4bU1/i+sKOwOrQy8D87zWUcRZc= +modernc.org/ccgo/v3 v3.0.0-20220430103911-bc99d88307be/go.mod h1:bwdAnOoaIt8Ax9YdWGjxWsdkPcZyRPHqrOvJxaKAKGw= +modernc.org/ccgo/v3 v3.16.4/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= +modernc.org/ccgo/v3 v3.16.6 h1:3l18poV+iUemQ98O3X5OMr97LOqlzis+ytivU4NqGhA= +modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= +modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk= +modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= +modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM= +modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= +modernc.org/libc v0.0.0-20220428101251-2d5f3daf273b/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= +modernc.org/libc v1.16.0/go.mod h1:N4LD6DBE9cf+Dzf9buBlzVJndKr/iJHG97vGLHYnb5A= +modernc.org/libc v1.16.1/go.mod h1:JjJE0eu4yeK7tab2n4S1w8tlWd9MxXLRzheaRnAKymU= +modernc.org/libc v1.16.7/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU= +modernc.org/libc v1.16.8 h1:Ux98PaOMvolgoFX/YwusFOHBnanXdGRmWgI8ciI2z4o= +modernc.org/libc v1.16.8/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU= +modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.4.1 h1:ij3fYGe8zBF4Vu+g0oT7mB06r8sqGWKuJu1yXeR4by8= +modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.1.1 h1:bDOL0DIDLQv7bWhP3gMvIrnoFw+Eo6F7a2QK9HPDiFU= +modernc.org/memory v1.1.1/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= +modernc.org/opt v0.1.1 h1:/0RX92k9vwVeDXj+Xn23DKp2VJubL7k8qNffND6qn3A= +modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sqlite v1.17.3 h1:iE+coC5g17LtByDYDWKpR6m2Z9022YrSh3bumwOnIrI= +modernc.org/sqlite v1.17.3/go.mod h1:10hPVYar9C0kfXuTWGz8s0XtB8uAGymUy51ZzStYe3k= +modernc.org/strutil v1.1.1 h1:xv+J1BXY3Opl2ALrBwyfEikFAj8pmqcpnfmuwUwcozs= +modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= +modernc.org/tcl v1.13.1 h1:npxzTwFTZYM8ghWicVIX1cRWzj7Nd8i6AqqX2p+IYao= +modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw= +modernc.org/token v1.0.0 h1:a0jaWiNMDhDUtqOj09wvjWWAqd3q7WpBulmL9H2egsk= +modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +modernc.org/z v1.5.1 h1:RTNHdsrOpeoSeOF4FbzTo8gBYByaJ5xT7NgZ9ZqRiJM= +modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/grype-schema-version-mapping.json b/grype-schema-version-mapping.json new file mode 100644 index 00000000..7520bc0d --- /dev/null +++ b/grype-schema-version-mapping.json @@ -0,0 +1,7 @@ +{ + "1": "v0.7.0", + "2": "v0.12.1", + "3": "v0.40.1", + "4": "v0.50.2", + "5": "main" +} diff --git a/install.sh b/install.sh new file mode 100755 index 00000000..a1da9f56 --- /dev/null +++ b/install.sh @@ -0,0 +1,780 @@ +#!/bin/sh +# note: we require errors to propagate (don't set -e) +set -eu + +PROJECT_NAME="grype-db" +OWNER=anchore +REPO="${PROJECT_NAME}" +GITHUB_DOWNLOAD_PREFIX=https://api.github.com/repos/${OWNER}/${REPO}/releases +INSTALL_SH_BASE_URL=https://raw.githubusercontent.com/${OWNER}/${PROJECT_NAME} +PROGRAM_ARGS=$@ + +# do not change the name of this parameter (this must always be backwards compatible) +# This is defaulted to false since older tags do not have an install script to download +DOWNLOAD_TAG_INSTALL_SCRIPT=${DOWNLOAD_TAG_INSTALL_SCRIPT:-false} + +# +# usage [script-name] +# +usage() ( + this=$1 + cat </dev/null +) + +echo_stderr() ( + echo "$@" 1>&2 +) + +_logp=2 +log_set_priority() { + _logp="$1" +} + +log_priority() ( + if test -z "$1"; then + echo "$_logp" + return + fi + [ "$1" -le "$_logp" ] +) + +init_colors() { + RED='' + BLUE='' + PURPLE='' + BOLD='' + RESET='' + # check if stdout is a terminal + if test -t 1 && is_command tput; then + # see if it supports colors + ncolors=$(tput colors) + if test -n "$ncolors" && test $ncolors -ge 8; then + RED='\033[0;31m' + BLUE='\033[0;34m' + PURPLE='\033[0;35m' + BOLD='\033[1m' + RESET='\033[0m' + fi + fi +} + +init_colors + +log_tag() ( + case $1 in + 0) echo "${RED}${BOLD}[error]${RESET}" ;; + 1) echo "${RED}[warn]${RESET}" ;; + 2) echo "[info]${RESET}" ;; + 3) echo "${BLUE}[debug]${RESET}" ;; + 4) echo "${PURPLE}[trace]${RESET}" ;; + *) echo "[$1]" ;; + esac +) + + +log_trace_priority=4 +log_trace() ( + priority=$log_trace_priority + log_priority "$priority" || return 0 + echo_stderr "$(log_tag $priority)" "${@}" "${RESET}" +) + +log_debug_priority=3 +log_debug() ( + priority=$log_debug_priority + log_priority "$priority" || return 0 + echo_stderr "$(log_tag $priority)" "${@}" "${RESET}" +) + +log_info_priority=2 +log_info() ( + priority=$log_info_priority + log_priority "$priority" || return 0 + echo_stderr "$(log_tag $priority)" "${@}" "${RESET}" +) + +log_warn_priority=1 +log_warn() ( + priority=$log_warn_priority + log_priority "$priority" || return 0 + echo_stderr "$(log_tag $priority)" "${@}" "${RESET}" +) + +log_err_priority=0 +log_err() ( + priority=$log_err_priority + log_priority "$priority" || return 0 + echo_stderr "$(log_tag $priority)" "${@}" "${RESET}" +) + +uname_os_check() ( + os=$1 + case "$os" in + darwin) return 0 ;; + dragonfly) return 0 ;; + freebsd) return 0 ;; + linux) return 0 ;; + android) return 0 ;; + nacl) return 0 ;; + netbsd) return 0 ;; + openbsd) return 0 ;; + plan9) return 0 ;; + solaris) return 0 ;; + windows) return 0 ;; + esac + log_err "uname_os_check '$(uname -s)' got converted to '$os' which is not a GOOS value. Please file bug at https://github.com/client9/shlib" + return 1 +) + +uname_arch_check() ( + arch=$1 + case "$arch" in + 386) return 0 ;; + amd64) return 0 ;; + arm64) return 0 ;; + armv5) return 0 ;; + armv6) return 0 ;; + armv7) return 0 ;; + ppc64) return 0 ;; + ppc64le) return 0 ;; + mips) return 0 ;; + mipsle) return 0 ;; + mips64) return 0 ;; + mips64le) return 0 ;; + s390x) return 0 ;; + amd64p32) return 0 ;; + esac + log_err "uname_arch_check '$(uname -m)' got converted to '$arch' which is not a GOARCH value. Please file bug report at https://github.com/client9/shlib" + return 1 +) + +unpack() ( + archive=$1 + + log_trace "unpack(archive=${archive})" + + case "${archive}" in + *.tar.gz | *.tgz) tar --no-same-owner -xzf "${archive}" ;; + *.tar) tar --no-same-owner -xf "${archive}" ;; + *.zip) unzip -q "${archive}" ;; + *.dmg) extract_from_dmg "${archive}" ;; + *) + log_err "unpack unknown archive format for ${archive}" + return 1 + ;; + esac +) + +extract_from_dmg() ( + dmg_file=$1 + + mount_point="/Volumes/tmp-dmg" + hdiutil attach -quiet -nobrowse -mountpoint "${mount_point}" "${dmg_file}" + cp -fR "${mount_point}/." ./ + hdiutil detach -quiet -force "${mount_point}" +) + +http_download_curl() ( + local_file=$1 + source_url=$2 + header=$3 + auth_header=$4 + + log_trace "http_download_curl(local_file=$local_file, source_url=$source_url, header=$header)" + + args="-w '%{http_code}' -sL -o \"$local_file\"" + header_args="" + auth_args="" + + if [ ! -z "$header" ]; then + header_args="-H \"$header\"" + fi + if [ ! -z "$auth_header" ]; then + auth_args="-H \"Authorization: token $auth_header\"" + fi + + code=$(eval "curl $args $header_args $auth_args $source_url") + + if [ "$code" != "200" ]; then + log_err "received HTTP status=$code for url='$source_url'" + return 1 + fi + return 0 +) + +http_download_wget() ( + local_file=$1 + source_url=$2 + header=$3 + auth_header=$4 + + log_trace "http_download_wget(local_file=$local_file, source_url=$source_url, header=$header)" + + args="-q -O \"$local_file\"" + header_args="" + auth_args="" + + if [ ! -z "$header" ]; then + header_args="--header \"$header\"" + fi + if [ ! -z "$auth_header" ]; then + auth_args="--header \"Authorization: token $auth_header\"" + fi + + wget $args $header_args $auth_args $source_url +) + +http_download() ( + log_debug "http_download(url=$2)" + if is_command curl; then + http_download_curl "$@" + return + elif is_command wget; then + http_download_wget "$@" + return + fi + log_err "http_download unable to find wget or curl" + return 1 +) + +http_copy() ( + tmp=$(mktemp) + http_download "${tmp}" "$@" || return 1 + body=$(cat "$tmp") + rm -f "${tmp}" + printf "%s" "$body" +) + +hash_sha256() ( + TARGET=${1:-/dev/stdin} + if is_command gsha256sum; then + hash=$(gsha256sum "$TARGET") || return 1 + echo "$hash" | cut -d ' ' -f 1 + elif is_command sha256sum; then + hash=$(sha256sum "$TARGET") || return 1 + echo "$hash" | cut -d ' ' -f 1 + elif is_command shasum; then + hash=$(shasum -a 256 "$TARGET" 2>/dev/null) || return 1 + echo "$hash" | cut -d ' ' -f 1 + elif is_command openssl; then + hash=$(openssl -dst openssl dgst -sha256 "$TARGET") || return 1 + echo "$hash" | cut -d ' ' -f a + else + log_err "hash_sha256 unable to find command to compute sha-256 hash" + return 1 + fi +) + +hash_sha256_verify() ( + TARGET=$1 + checksums=$2 + if [ -z "$checksums" ]; then + log_err "hash_sha256_verify checksum file not specified in arg2" + return 1 + fi + BASENAME=${TARGET##*/} + want=$(grep "${BASENAME}" "${checksums}" 2>/dev/null | tr '\t' ' ' | cut -d ' ' -f 1) + if [ -z "$want" ]; then + log_err "hash_sha256_verify unable to find checksum for '${TARGET}' in '${checksums}'" + return 1 + fi + got=$(hash_sha256 "$TARGET") + if [ "$want" != "$got" ]; then + log_err "hash_sha256_verify checksum for '$TARGET' did not verify ${want} vs $got" + return 1 + fi +) + +# ------------------------------------------------------------------------ +# End of functions from https://github.com/client9/shlib +# ------------------------------------------------------------------------ + +# asset_file_exists [path] +# +# returns 1 if the given file does not exist +# +asset_file_exists() ( + path="$1" + if [ ! -f "${path}" ]; then + return 1 + fi +) + + +# github_release_json [owner] [repo] [version] +# +# outputs release json string +# +github_release_json() ( + owner=$1 + repo=$2 + version=$3 + # note: private repos require the API route, not the browser-friendly route + if [ -z "$version" ]; then + version="latest" + giturl="https://api.github.com/repos/${owner}/${repo}/releases/${version}" + else + giturl="https://api.github.com/repos/${owner}/${repo}/releases/tags/${version}" + fi + json=$(http_copy "$giturl" "Accept:application/json" "$(github_auth_token)") + + log_trace "github_release_json(owner=${owner}, repo=${repo}, version=${version}) returned '${json}'" + + test -z "$json" && return 1 + + # cannot use echo since we need to preserve escaped characters + printf "%s" "${json}" +) + +# extract_value [key-value-pair] +# +# outputs value from a colon delimited key-value pair +# +extract_value() ( + key_value="$1" + IFS=':' read -r _ value << EOF +${key_value} +EOF + echo "$value" +) + +# extract_json_value [json] [key] +# +# outputs value of the key from the given json string +# +extract_json_value() ( + json="$1" + key="$2" + key_value=$(printf "%s" "${json}" | grep -o "\"$key\":[^,]*[,}]" | tr -d '",}') + + extract_value "$key_value" +) + +get_checksum_file_url() ( + release_json="$1" + + # cannot use echo since we need to preserve escaped characters + url=$(printf "%s" "$release_json" | jq -r '.assets[] | select(.name | contains("checksums.txt")) | .url') + test -z "$url" && return 1 + echo "$url" +) + +get_release_tag() ( + release_json="$1" + + # cannot use echo since we need to preserve escaped characters + tag=$(printf "%s" "$release_json" | jq -r '.tag_name') + test -z "$tag" && return 1 + echo "$tag" +) + +get_asset_file_url() ( + release_json="$1" + asset_filename="$2" + + # cannot use echo since we need to preserve escaped characters + url=$(printf "%s" "$release_json" | jq -r --arg asset_filename "$asset_filename" '.assets[] | select(.name == $asset_filename) | .url') + test -z "$url" && return 1 + echo "$url" +) + +# github_release_tag [release-json] +# +# outputs release tag string +# +github_release_tag() ( + json="$1" + tag=$(extract_json_value "${json}" "tag_name") + test -z "$tag" && return 1 + echo "$tag" +) + +# download_github_release_checksums [release-url-prefix] [name] [version] [output-dir] +# +# outputs path to the downloaded checksums file +# +download_github_release_checksums() ( + checksum_url="$1" + name="$2" + version="$3" + output_dir="$4" + + log_trace "download_github_release_checksums(url=${checksum_url}, name=${name}, version=${version}, output_dir=${output_dir})" + + checksum_filename=${name}_${version}_checksums.txt + output_path="${output_dir}/${checksum_filename}" + + http_download "${output_path}" "${checksum_url}" "Accept:application/octet-stream" "$(github_auth_token)" + asset_file_exists "${output_path}" + + log_trace "download_github_release_checksums() returned '${output_path}'" + + echo "${output_path}" +) + +# search_for_asset [checksums-file-path] [name] [os] [arch] [format] +# +# outputs name of the asset to download +# +search_for_asset() ( + checksum_path="$1" + name="$2" + os="$3" + arch="$4" + format="$5" + + log_trace "search_for_asset(checksum-path=${checksum_path}, name=${name}, os=${os}, arch=${arch}, format=${format})" + + asset_glob="${name}_.*_${os}_${arch}.${format}" + output_path=$(grep -o "${asset_glob}" "${checksum_path}" || true) + + log_trace "search_for_asset() returned '${output_path}'" + + echo "${output_path}" +) + +# uname_os +# +# outputs an adjusted os value +# +uname_os() ( + os="$1" + + if [ -z "$os" ]; then + os=$(uname -s | tr '[:upper:]' '[:lower:]') + fi + case "$os" in + cygwin_nt*) os="windows" ;; + mingw*) os="windows" ;; + msys_nt*) os="windows" ;; + esac + + uname_os_check "$os" + + log_trace "uname_os() returned '${os}'" + + echo "$os" +) + +# uname_arch +# +# outputs an adjusted architecture value +# +uname_arch() ( + arch="$1" + + if [ -z "$arch" ]; then + arch=$(uname -m) + fi + case $arch in + x86_64) arch="amd64" ;; + x86) arch="386" ;; + i686) arch="386" ;; + i386) arch="386" ;; + aarch64) arch="arm64" ;; + armv5*) arch="armv5" ;; + armv6*) arch="armv6" ;; + armv7*) arch="armv7" ;; + esac + + uname_arch_check "${arch}" + + log_trace "uname_arch() returned '${arch}'" + + echo "${arch}" +) + +# get_release_tag [owner] [repo] [tag] +# +# outputs tag string +# +#get_release_tag() ( +# owner="$1" +# repo="$2" +# tag="$3" +# +# log_trace "get_release_tag(owner=${owner}, repo=${repo}, tag=${tag})" +# +# json=$(github_release_json "${owner}" "${repo}" "${tag}") +# real_tag=$(github_release_tag "${json}") +# if test -z "${real_tag}"; then +# return 1 +# fi +# +# log_trace "get_release_tag() returned '${real_tag}'" +# +# echo "${real_tag}" +#) + +# tag_to_version [tag] +# +# outputs version string +# +tag_to_version() ( + tag="$1" + value="${tag#v}" + + log_trace "tag_to_version(tag=${tag}) returned '${value}'" + + echo "$value" +) + +# get_binary_name [os] [arch] [default-name] +# +# outputs a the binary string name +# +get_binary_name() ( + os="$1" + arch="$2" + binary="$3" + original_binary="${binary}" + + case "${os}" in + windows) binary="${binary}.exe" ;; + esac + + log_trace "get_binary_name(os=${os}, arch=${arch}, binary=${original_binary}) returned '${binary}'" + + echo "${binary}" +) + + +# get_format_name [os] [arch] [default-format] +# +# outputs an adjusted file format +# +get_format_name() ( + os="$1" + arch="$2" + format="$3" + original_format="${format}" + + case ${os} in + windows) format=zip ;; + esac + + log_trace "get_format_name(os=${os}, arch=${arch}, format=${original_format}) returned '${format}'" + + echo "${format}" +) + +# download_and_install_asset [release-url-prefix] [download-path] [install-path] [name] [os] [arch] [version] [format] [binary] +# +# attempts to download the archive and install it to the given path. +# +download_and_install_asset() ( + release_json="$1" + download_path="$2" + install_path=$3 + name="$4" + os="$5" + arch="$6" + version="$7" + format="$8" + binary="$9" + + asset_filepath=$(download_asset "${release_json}" "${download_path}" "${name}" "${os}" "${arch}" "${version}" "${format}") + + # don't continue if we couldn't download an asset + if [ -z "${asset_filepath}" ]; then + log_err "could not find release asset for os='${os}' arch='${arch}' format='${format}' " + return 1 + fi + + install_asset "${asset_filepath}" "${install_path}" "${binary}" +) + +# download_asset [release-url-prefix] [download-path] [name] [os] [arch] [version] [format] [binary] +# +# outputs the path to the downloaded asset asset_filepath +# +download_asset() ( + release_json="$1" + destination="$2" + name="$3" + os="$4" + arch="$5" + version="$6" + format="$7" + + log_trace "download_asset(destination=${destination}, name=${name}, os=${os}, arch=${arch}, version=${version}, format=${format})" + + #get checksum url + checksum_file_url=$(get_checksum_file_url "${release_json}") + + #get checksum path asset + checksums_filepath=$(download_github_release_checksums "${checksum_file_url}" "${name}" "${version}" "${destination}") + + log_trace "checksums content:\n$(cat ${checksums_filepath})" + + asset_filename=$(search_for_asset "${checksums_filepath}" "${name}" "${os}" "${arch}" "${format}") + + # don't continue if we couldn't find a matching asset from the checksums file + if [ -z "${asset_filename}" ]; then + return 1 + fi + + asset_url=$(get_asset_file_url "${release_json}" "${asset_filename}") + asset_filepath="${destination}/${asset_filename}" + http_download "${asset_filepath}" "${asset_url}" "Accept:application/octet-stream" "$(github_auth_token)" + + hash_sha256_verify "${asset_filepath}" "${checksums_filepath}" + + log_trace "download_asset_by_checksums_file() returned '${asset_filepath}'" + + echo "${asset_filepath}" +) + +# install_asset [asset-path] [destination-path] [binary] +# +install_asset() ( + asset_filepath="$1" + destination="$2" + binary="$3" + + log_trace "install_asset(asset=${asset_filepath}, destination=${destination}, binary=${binary})" + + # don't continue if we don't have anything to install + if [ -z "${asset_filepath}" ]; then + return + fi + + archive_dir=$(dirname "${asset_filepath}") + + # unarchive the downloaded archive to the temp dir + (cd "${archive_dir}" && unpack "${asset_filepath}") + + # create the destination dir + test ! -d "${destination}" && install -d "${destination}" + + # install the binary to the destination dir + install "${archive_dir}/${binary}" "${destination}/" +) + +# github_auth_token returns the value of GITHUB_TOKEN or an empty string +# +github_auth_token() ( + if [ "${GITHUB_TOKEN:-undefined}" != "undefined" ]; then + echo "${GITHUB_TOKEN}" + fi +) + +main() ( + # parse arguments + + # note: never change default install directory (this must always be backwards compatible) + install_dir=${install_dir:-./bin} + os="" + arch="" + + # note: never change the program flags or arguments (this must always be backwards compatible) + while getopts "a:b:dh?o:x" arg; do + case "$arg" in + a) arch="$OPTARG" ;; + b) install_dir="$OPTARG" ;; + d) + if [ "$_logp" = "$log_info_priority" ]; then + # -d == debug + log_set_priority $log_debug_priority + else + # -dd (or -ddd...) == trace + log_set_priority $log_trace_priority + fi + ;; + h | \?) usage "$0" ;; + o) os="$OPTARG" ;; + x) set -x ;; + esac + done + shift $((OPTIND - 1)) + set +u + tag=$1 + + if [ -z "${tag}" ]; then + log_info "checking github for the current release tag" + tag="" + else + log_info "checking github for release tag='${tag}'" + fi + set -u + + # get the release json + release_json=$(github_release_json "${OWNER}" "${REPO}" "${tag}") + + tag=$(get_release_tag "${release_json}") + + if [ "$?" != "0" ]; then + log_err "unable to find tag='${tag}'" + log_err "do not specify a version or select a valid version from https://github.com/${OWNER}/${REPO}/releases" + return 1 + fi + + # run the application + + version=$(tag_to_version "${tag}") + os=$(uname_os "$os") + arch=$(uname_arch "$arch") + format=$(get_format_name "${os}" "${arch}" "tar.gz") + binary=$(get_binary_name "${os}" "${arch}" "${PROJECT_NAME}") + download_url="${GITHUB_DOWNLOAD_PREFIX}/${tag}" + + # we always use the install.sh script that is associated with the tagged release. Why? the latest install.sh is not + # guaranteed to be able to install every version of the application. We use the DOWNLOAD_TAG_INSTALL_SCRIPT env var + # to indicate if we should continue processing with the existing script or to download the script from the given tag. + if [ "${DOWNLOAD_TAG_INSTALL_SCRIPT}" = "true" ]; then + export DOWNLOAD_TAG_INSTALL_SCRIPT=false + log_info "fetching release script for tag='${tag}'" + http_copy "${INSTALL_SH_BASE_URL}/${tag}/install.sh" "" "$(github_auth_token)" | sh -s -- ${PROGRAM_ARGS} + exit $? + fi + + log_info "using release tag='${tag}' version='${version}' os='${os}' arch='${arch}'" + + download_dir=$(mktemp -d) + trap 'rm -rf -- "$download_dir"' EXIT + + log_debug "downloading files into ${download_dir}" + + # pass down json instead of download url? + download_and_install_asset "${release_json}" "${download_dir}" "${install_dir}" "${PROJECT_NAME}" "${os}" "${arch}" "${version}" "${format}" "${binary}" + + # don't continue if we couldn't install the asset + if [ "$?" != "0" ]; then + log_err "failed to install ${PROJECT_NAME}" + return 1 + fi + + log_info "installed ${install_dir}/${binary}" +) + +# entrypoint + +set +u +if [ -z "${TEST_INSTALL_SH}" ]; then + set -u + main "$@" +fi +set -u \ No newline at end of file diff --git a/internal/bus/bus.go b/internal/bus/bus.go new file mode 100644 index 00000000..90bd13d9 --- /dev/null +++ b/internal/bus/bus.go @@ -0,0 +1,45 @@ +/* +Package bus provides access to a singleton instance of an event bus (provided by the calling application). The event bus +is intended to allow for the library to publish events which library consumers can subscribe to. These events +can provide static information, but also have an object as a payload for which the consumer can poll for updates. +This is akin to a logger, except instead of only allowing strings to be logged, rich objects that can be interacted with. + +Note that the singleton instance is only allowed to publish events and not subscribe to them --this is intentional. +Internal library interactions should continue to use traditional in-execution-path approaches for data sharing +(e.g. function returns and channels) and not depend on bus subscriptions for critical interactions (e.g. one part of the +lib publishes an event and another part of the lib subscribes and reacts to that event). The bus is provided only as a +means for consumers to observe events emitted from the library (such as to provide a rich UI) and not to allow +consumers to augment or otherwise change execution. +*/ +package bus + +import ( + "github.com/wagoodman/go-partybus" + + "github.com/anchore/grype-db/pkg/event" +) + +var publisher partybus.Publisher +var active bool + +// SetPublisher sets the singleton event bus publisher. This is optional; if no bus is provided, the library will +// behave no differently than if a bus had been provided. +func SetPublisher(p partybus.Publisher) { + publisher = p + if p != nil { + active = true + } +} + +// Publish an event onto the bus. If there is no bus set by the calling application, this does nothing. +func Publish(event partybus.Event) { + if active { + publisher.Publish(event) + } +} + +func Exit() { + Publish(partybus.Event{ + Type: event.Exit, + }) +} diff --git a/internal/constants.go b/internal/constants.go new file mode 100644 index 00000000..bf0ea670 --- /dev/null +++ b/internal/constants.go @@ -0,0 +1,3 @@ +package internal + +const ApplicationName = "grype-db" diff --git a/internal/eventloop/eventloop.go b/internal/eventloop/eventloop.go new file mode 100644 index 00000000..12380abd --- /dev/null +++ b/internal/eventloop/eventloop.go @@ -0,0 +1,118 @@ +package eventloop + +import ( + "context" + "errors" + "fmt" + + "github.com/anchore/grype-db/internal/log" + "github.com/anchore/grype-db/internal/ui" + + "github.com/hashicorp/go-multierror" + "github.com/wagoodman/go-partybus" +) + +func Run(ctx context.Context, workerErrs <-chan error, subscription *partybus.Subscription, cleanupFn func(), uxs ...ui.UI) error { + return run( + ctx, + workerErrs, + subscription, + cleanupFn, + uxs..., + ) +} + +// Run listens to worker errors (from execution path), worker events (from a partybus subscription), and +// signal interrupts. Is responsible for handling each event relative to a given UI an to coordinate eventing until +// an eventual graceful exit. +// + +func run(ctx context.Context, workerErrs <-chan error, subscription *partybus.Subscription, cleanupFn func(), uxs ...ui.UI) error { + if cleanupFn != nil { + defer cleanupFn() + } + events := subscription.Events() + var err error + var ux ui.UI + + if ux, err = setupUI(subscription.Unsubscribe, uxs...); err != nil { + return err + } + + logger := log.Nested("component", "eventloop") + + var retErr error + var forceTeardown bool + + for { + if workerErrs == nil && events == nil { + break + } + select { + case err, isOpen := <-workerErrs: + if !isOpen { + logger.Trace("worker stopped") + workerErrs = nil + continue + } + if err != nil { + // capture the error from the worker and unsubscribe to complete a graceful shutdown + retErr = multierror.Append(retErr, err) + _ = subscription.Unsubscribe() + // the worker has exited, we may have been mid-handling events for the UI which should now be + // ignored, in which case forcing a teardown of the UI regardless of the state is required. + forceTeardown = true + } + case e, isOpen := <-events: + if !isOpen { + logger.Trace("bus stopped") + events = nil + continue + } + + if err := ux.Handle(e); err != nil { + if errors.Is(err, partybus.ErrUnsubscribe) { + events = nil + } else { + retErr = multierror.Append(retErr, err) + // TODO: should we unsubscribe? should we try to halt execution? or continue? + } + } + case <-ctx.Done(): + logger.Trace("signal interrupt") + + // ignore further results from any event source and exit ASAP, but ensure that all cache is cleaned up. + // we ignore further errors since cleaning up the tmp directories will affect running catalogers that are + // reading/writing from/to their nested temp dirs. This is acceptable since we are bailing without result. + + // TODO: potential future improvement would be to pass context into workers with a cancel function that is + // to the event loop. In this way we can have a more controlled shutdown even at the most nested levels + // of processing. + events = nil + workerErrs = nil + forceTeardown = true + } + } + + if err := ux.Teardown(forceTeardown); err != nil { + retErr = multierror.Append(retErr, err) + } + + return retErr +} + +// setupUI takes one or more UIs that responds to events and takes a event bus unsubscribe function for use +// during teardown. With the given UIs, the first UI which the ui.Setup() function does not return an error +// will be utilized in execution. Providing a set of UIs allows for the caller to provide graceful fallbacks +// when there are environmental problem (e.g. unable to setup a TUI with the current TTY). +func setupUI(unsubscribe func() error, uis ...ui.UI) (ui.UI, error) { + for _, ux := range uis { + if err := ux.Setup(unsubscribe); err != nil { + log.Warnf("unable to setup given UI, falling back to alternative UI: %w", err) + continue + } + + return ux, nil + } + return nil, fmt.Errorf("unable to setup any UI") +} diff --git a/internal/eventloop/eventloop_test.go b/internal/eventloop/eventloop_test.go new file mode 100644 index 00000000..3bdc4836 --- /dev/null +++ b/internal/eventloop/eventloop_test.go @@ -0,0 +1,430 @@ +package eventloop + +import ( + "context" + "fmt" + "github.com/anchore/grype-db/internal/ui" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/wagoodman/go-partybus" + + "github.com/anchore/grype-db/pkg/event" +) + +var _ ui.UI = (*uiMock)(nil) + +type uiMock struct { + t *testing.T + finalEvent partybus.Event + unsubscribe func() error + mock.Mock +} + +func (u *uiMock) Setup(unsubscribe func() error) error { + u.t.Logf("UI Setup called") + u.unsubscribe = unsubscribe + return u.Called(unsubscribe).Error(0) +} + +func (u *uiMock) Handle(event partybus.Event) error { + u.t.Logf("UI Handle called: %+v", event.Type) + if event == u.finalEvent { + assert.NoError(u.t, u.unsubscribe()) + } + return u.Called(event).Error(0) +} + +func (u *uiMock) Teardown(_ bool) error { + u.t.Logf("UI Teardown called") + return u.Called().Error(0) +} + +func Test_EventLoop_gracefulExit(t *testing.T) { + test := func(t *testing.T) { + + testBus := partybus.NewBus() + subscription := testBus.Subscribe() + t.Cleanup(testBus.Close) + + finalEvent := partybus.Event{ + Type: event.Exit, + } + + worker := func() <-chan error { + ret := make(chan error) + go func() { + t.Log("worker running") + // send an empty item (which is ignored) ensuring we've entered the select statement, + // then close (a partial shutdown). + ret <- nil + t.Log("worker sent nothing") + close(ret) + t.Log("worker closed") + // do the other half of the shutdown + testBus.Publish(finalEvent) + t.Log("worker published final event") + }() + return ret + } + + ux := &uiMock{ + t: t, + finalEvent: finalEvent, + } + + // ensure the mock sees at least the final event + ux.On("Handle", finalEvent).Return(nil) + // ensure the mock sees basic setup/teardown events + ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil) + ux.On("Teardown").Return(nil) + + var cleanupCalled bool + cleanupFn := func() { + t.Log("cleanup called") + cleanupCalled = true + } + + assert.NoError(t, + Run( + context.Background(), + worker(), + subscription, + cleanupFn, + ux, + ), + ) + + assert.True(t, cleanupCalled, "cleanup function not called") + ux.AssertExpectations(t) + } + + // if there is a bug, then there is a risk of the event loop never returning + testWithTimeout(t, 5*time.Second, test) +} + +func Test_EventLoop_workerError(t *testing.T) { + test := func(t *testing.T) { + + testBus := partybus.NewBus() + subscription := testBus.Subscribe() + t.Cleanup(testBus.Close) + + workerErr := fmt.Errorf("worker error") + + worker := func() <-chan error { + ret := make(chan error) + go func() { + t.Log("worker running") + // send an empty item (which is ignored) ensuring we've entered the select statement, + // then close (a partial shutdown). + ret <- nil + t.Log("worker sent nothing") + ret <- workerErr + t.Log("worker sent error") + close(ret) + t.Log("worker closed") + // note: NO final event is fired + }() + return ret + } + + ux := &uiMock{ + t: t, + } + + // ensure the mock sees basic setup/teardown events + ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil) + ux.On("Teardown").Return(nil) + + var cleanupCalled bool + cleanupFn := func() { + t.Log("cleanup called") + cleanupCalled = true + } + + // ensure we see an error returned + assert.ErrorIs(t, + Run( + context.Background(), + worker(), + subscription, + cleanupFn, + ux, + ), + workerErr, + "should have seen a worker error, but did not", + ) + + assert.True(t, cleanupCalled, "cleanup function not called") + ux.AssertExpectations(t) + } + + // if there is a bug, then there is a risk of the event loop never returning + testWithTimeout(t, 5*time.Second, test) +} + +func Test_EventLoop_unsubscribeError(t *testing.T) { + test := func(t *testing.T) { + + testBus := partybus.NewBus() + subscription := testBus.Subscribe() + t.Cleanup(testBus.Close) + + finalEvent := partybus.Event{ + Type: event.Exit, + } + + worker := func() <-chan error { + ret := make(chan error) + go func() { + t.Log("worker running") + // send an empty item (which is ignored) ensuring we've entered the select statement, + // then close (a partial shutdown). + ret <- nil + t.Log("worker sent nothing") + close(ret) + t.Log("worker closed") + // do the other half of the shutdown + testBus.Publish(finalEvent) + t.Log("worker published final event") + }() + return ret + } + + ux := &uiMock{ + t: t, + finalEvent: finalEvent, + } + + // ensure the mock sees at least the final event... note the unsubscribe error here + ux.On("Handle", finalEvent).Return(partybus.ErrUnsubscribe) + // ensure the mock sees basic setup/teardown events + ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil) + ux.On("Teardown").Return(nil) + + var cleanupCalled bool + cleanupFn := func() { + t.Log("cleanup called") + cleanupCalled = true + } + + // unsubscribe errors should be handled and ignored, not propagated. We are additionally asserting that + // this case is handled as a controlled shutdown (this test should not timeout) + assert.NoError(t, + Run( + context.Background(), + worker(), + subscription, + cleanupFn, + ux, + ), + ) + + assert.True(t, cleanupCalled, "cleanup function not called") + ux.AssertExpectations(t) + } + + // if there is a bug, then there is a risk of the event loop never returning + testWithTimeout(t, 5*time.Second, test) +} + +func Test_EventLoop_handlerError(t *testing.T) { + test := func(t *testing.T) { + + testBus := partybus.NewBus() + subscription := testBus.Subscribe() + t.Cleanup(testBus.Close) + + finalEvent := partybus.Event{ + Type: event.Exit, + Error: fmt.Errorf("an exit error occured"), + } + + worker := func() <-chan error { + ret := make(chan error) + go func() { + t.Log("worker running") + // send an empty item (which is ignored) ensuring we've entered the select statement, + // then close (a partial shutdown). + ret <- nil + t.Log("worker sent nothing") + close(ret) + t.Log("worker closed") + // do the other half of the shutdown + testBus.Publish(finalEvent) + t.Log("worker published final event") + }() + return ret + } + + ux := &uiMock{ + t: t, + finalEvent: finalEvent, + } + + // ensure the mock sees at least the final event... note the event error is propagated + ux.On("Handle", finalEvent).Return(finalEvent.Error) + // ensure the mock sees basic setup/teardown events + ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil) + ux.On("Teardown").Return(nil) + + var cleanupCalled bool + cleanupFn := func() { + t.Log("cleanup called") + cleanupCalled = true + } + + // handle errors SHOULD propagate the event loop. We are additionally asserting that this case is + // handled as a controlled shutdown (this test should not timeout) + assert.ErrorIs(t, + Run( + context.Background(), + worker(), + subscription, + cleanupFn, + ux, + ), + finalEvent.Error, + "should have seen a event error, but did not", + ) + + assert.True(t, cleanupCalled, "cleanup function not called") + ux.AssertExpectations(t) + } + + // if there is a bug, then there is a risk of the event loop never returning + testWithTimeout(t, 5*time.Second, test) +} + +func Test_EventLoop_contextCancelStopExecution(t *testing.T) { + test := func(t *testing.T) { + + testBus := partybus.NewBus() + subscription := testBus.Subscribe() + t.Cleanup(testBus.Close) + + worker := func() <-chan error { + // the worker will never return work and the event loop will always be waiting... + return make(chan error) + } + + ctx, cancel := context.WithCancel(context.Background()) + + ux := &uiMock{ + t: t, + } + + // ensure the mock sees basic setup/teardown events + ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil) + ux.On("Teardown").Return(nil) + + var cleanupCalled bool + cleanupFn := func() { + t.Log("cleanup called") + cleanupCalled = true + } + + go cancel() + + assert.NoError(t, + run( + ctx, + worker(), + subscription, + cleanupFn, + ux, + ), + ) + + assert.True(t, cleanupCalled, "cleanup function not called") + ux.AssertExpectations(t) + } + + // if there is a bug, then there is a risk of the event loop never returning + testWithTimeout(t, 5*time.Second, test) +} + +func Test_EventLoop_uiTeardownError(t *testing.T) { + test := func(t *testing.T) { + + testBus := partybus.NewBus() + subscription := testBus.Subscribe() + t.Cleanup(testBus.Close) + + finalEvent := partybus.Event{ + Type: event.Exit, + } + + worker := func() <-chan error { + ret := make(chan error) + go func() { + t.Log("worker running") + // send an empty item (which is ignored) ensuring we've entered the select statement, + // then close (a partial shutdown). + ret <- nil + t.Log("worker sent nothing") + close(ret) + t.Log("worker closed") + // do the other half of the shutdown + testBus.Publish(finalEvent) + t.Log("worker published final event") + }() + return ret + } + + ux := &uiMock{ + t: t, + finalEvent: finalEvent, + } + + teardownError := fmt.Errorf("sorry, dave, the UI doesn't want to be torn down") + + // ensure the mock sees at least the final event... note the event error is propagated + ux.On("Handle", finalEvent).Return(nil) + // ensure the mock sees basic setup/teardown events + ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil) + ux.On("Teardown").Return(teardownError) + + var cleanupCalled bool + cleanupFn := func() { + t.Log("cleanup called") + cleanupCalled = true + } + + // ensure we see an error returned + assert.ErrorIs(t, + Run( + context.Background(), + worker(), + subscription, + cleanupFn, + ux, + ), + teardownError, + "should have seen a UI teardown error, but did not", + ) + + assert.True(t, cleanupCalled, "cleanup function not called") + ux.AssertExpectations(t) + } + + // if there is a bug, then there is a risk of the event loop never returning + testWithTimeout(t, 5*time.Second, test) +} + +func testWithTimeout(t *testing.T, timeout time.Duration, test func(*testing.T)) { + done := make(chan bool) + go func() { + test(t) + done <- true + }() + + select { + case <-time.After(timeout): + t.Fatal("test timed out") + case <-done: + } +} diff --git a/internal/file/exists.go b/internal/file/exists.go new file mode 100644 index 00000000..702a6828 --- /dev/null +++ b/internal/file/exists.go @@ -0,0 +1,15 @@ +package file + +import ( + "os" + + "github.com/spf13/afero" +) + +func Exists(fs afero.Fs, path string) bool { + info, err := fs.Stat(path) + if os.IsNotExist(err) { + return false + } + return !info.IsDir() +} diff --git a/internal/file/getter.go b/internal/file/getter.go new file mode 100644 index 00000000..7c614fab --- /dev/null +++ b/internal/file/getter.go @@ -0,0 +1,254 @@ +package file + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "io" + "io/fs" + "math" + "net/http" + "strings" + "time" + + "github.com/anchore/go-logger" + "github.com/anchore/grype-db/internal/log" + "github.com/hashicorp/go-cleanhttp" + + "github.com/hashicorp/go-getter" + "github.com/hashicorp/go-getter/helper/url" + "github.com/wagoodman/go-progress" +) + +var ( + archiveExtensions = getterDecompressorNames() + ErrNonArchiveSource = fmt.Errorf("non-archive sources are not supported for directory destinations") +) + +type Getter interface { + // GetFile downloads the give URL into the given path. The URL must reference a single file. + GetFile(dst, src string, monitor ...*progress.Manual) error + + // GetToDir downloads the resource found at the `src` URL into the given `dst` directory. + // The directory must already exist, and the remote resource MUST BE AN ARCHIVE (e.g. `.tar.gz`). + GetToDir(dst, src string, monitor ...*progress.Manual) error +} + +type hashiGoGetter struct { + httpGetter getter.HttpGetter +} + +// NewGetter creates and returns a new Getter. Providing an http.Client is optional. If one is provided, +// it will be used for all HTTP(S) getting; otherwise, go-getter's default getters will be used. +func NewGetter(httpClient *http.Client) Getter { + return &hashiGoGetter{ + httpGetter: getter.HttpGetter{ + Client: httpClient, + }, + } +} + +func NewDefaultGetter() Getter { + return NewGetter(cleanhttp.DefaultClient()) +} + +func HTTPClientWithCerts(fileSystem fs.FS, caCertPath string) (*http.Client, error) { + httpClient := cleanhttp.DefaultClient() + if caCertPath != "" { + rootCAs := x509.NewCertPool() + + pemBytes, err := fs.ReadFile(fileSystem, caCertPath) + if err != nil { + return nil, fmt.Errorf("unable to configure root CAs for curator: %w", err) + } + rootCAs.AppendCertsFromPEM(pemBytes) + + httpClient.Transport.(*http.Transport).TLSClientConfig = &tls.Config{ + MinVersion: tls.VersionTLS12, + RootCAs: rootCAs, + } + } + return httpClient, nil +} + +func (g hashiGoGetter) GetFile(dst, src string, monitors ...*progress.Manual) error { + if len(monitors) > 1 { + return fmt.Errorf("multiple monitors provided, which is not allowed") + } + + return getWithRetry(getterClient(dst, src, false, g.httpGetter, monitors)) +} + +func (g hashiGoGetter) GetToDir(dst, src string, monitors ...*progress.Manual) error { + // though there are multiple getters, only the http/https getter requires extra validation + if err := validateHTTPSource(src); err != nil { + return err + } + if len(monitors) > 1 { + return fmt.Errorf("multiple monitors provided, which is not allowed") + } + + return getWithRetry(getterClient(dst, src, true, g.httpGetter, monitors)) +} + +func getWithRetry(client *getter.Client) error { + var err error + attempt := 1 + for interval := range retryIntervals() { + fields := logger.Fields{ + "url": client.Src, + "to": client.Dst, + } + + if attempt > 1 { + fields["attempt"] = attempt + } + + log.WithFields(fields).Info("downloading file") + + err = client.Get() + if err == nil { + break + } + + time.Sleep(interval) + attempt++ + } + return err +} + +func retryIntervals() <-chan time.Duration { + return exponentialBackoffDurations(250*time.Millisecond, 5*time.Second, 2) +} + +func exponentialBackoffDurations(minDuration, maxDuration time.Duration, step float64) <-chan time.Duration { + sleepDurations := make(chan time.Duration) + go func() { + defer close(sleepDurations) + for attempt := 0; ; attempt++ { + duration := exponentialBackoffDuration(minDuration, maxDuration, step, attempt) + + sleepDurations <- duration + + if duration == maxDuration { + break + } + } + }() + return sleepDurations +} + +func exponentialBackoffDuration(minDuration, maxDuration time.Duration, step float64, attempt int) time.Duration { + duration := time.Duration(float64(minDuration) * math.Pow(step, float64(attempt))) + if duration < minDuration { + return minDuration + } else if duration > maxDuration { + return maxDuration + } + return duration +} + +func validateHTTPSource(src string) error { + // we are ignoring any sources that are not destined to use the http getter object + if !hasAnyOfPrefixes(src, "http://", "https://") { + return nil + } + + u, err := url.Parse(src) + if err != nil { + return fmt.Errorf("bad URL provided %q: %w", src, err) + } + // only allow for sources with archive extensions + if !hasAnyOfSuffixes(u.Path, archiveExtensions...) { + return ErrNonArchiveSource + } + return nil +} + +func getterClient(dst, src string, dir bool, httpGetter getter.HttpGetter, monitors []*progress.Manual) *getter.Client { + client := &getter.Client{ + Src: src, + Dst: dst, + Dir: dir, + Getters: map[string]getter.Getter{ + "http": &httpGetter, + "https": &httpGetter, + // note: these are the default getters from https://github.com/hashicorp/go-getter/blob/v1.5.9/get.go#L68-L74 + // it is possible that other implementations need to account for custom httpclient injection, however, + // that has not been accounted for at this time. + "file": new(getter.FileGetter), + "git": new(getter.GitGetter), + "gcs": new(getter.GCSGetter), + "hg": new(getter.HgGetter), + "s3": new(getter.S3Getter), + }, + Options: mapToGetterClientOptions(monitors), + } + + return client +} + +func withProgress(monitor *progress.Manual) func(client *getter.Client) error { + return getter.WithProgress( + &progressAdapter{monitor: monitor}, + ) +} + +func mapToGetterClientOptions(monitors []*progress.Manual) []getter.ClientOption { + // TODO: This function is no longer needed once a generic `map` method is available. + + var result []getter.ClientOption + + for _, monitor := range monitors { + result = append(result, withProgress(monitor)) + } + + return result +} + +type readCloser struct { + progress.Reader +} + +func (c *readCloser) Close() error { return nil } + +type progressAdapter struct { + monitor *progress.Manual +} + +func (a *progressAdapter) TrackProgress(_ string, currentSize, totalSize int64, stream io.ReadCloser) io.ReadCloser { + a.monitor.N = currentSize + a.monitor.Total = totalSize + return &readCloser{ + Reader: *progress.NewProxyReader(stream, a.monitor), + } +} + +func getterDecompressorNames() (names []string) { + for name := range getter.Decompressors { + names = append(names, name) + } + return names +} + +// hasAnyOfSuffixes returns an indication if the given string has any of the given suffixes. +func hasAnyOfSuffixes(input string, suffixes ...string) bool { + for _, suffix := range suffixes { + if strings.HasSuffix(input, suffix) { + return true + } + } + + return false +} + +// hasAnyOfPrefixes returns an indication if the given string has any of the given prefixes. +func hasAnyOfPrefixes(input string, prefixes ...string) bool { + for _, prefix := range prefixes { + if strings.HasPrefix(input, prefix) { + return true + } + } + + return false +} diff --git a/internal/file/hasher.go b/internal/file/hasher.go new file mode 100644 index 00000000..240f0449 --- /dev/null +++ b/internal/file/hasher.go @@ -0,0 +1,36 @@ +package file + +import ( + "encoding/hex" + "fmt" + "hash" + "io" + "strings" + + "github.com/spf13/afero" +) + +func ContentDigest(fs afero.Fs, path string, hasher hash.Hash) (string, error) { + f, err := fs.Open(path) + if err != nil { + return "", fmt.Errorf("failed to open file '%s': %w", path, err) + } + defer f.Close() + + if _, err := io.Copy(hasher, f); err != nil { + return "", fmt.Errorf("failed to hash file '%s': %w", path, err) + } + + return hex.EncodeToString(hasher.Sum(nil)), nil +} + +func ValidateDigest(path, expectedDigest string, hasher hash.Hash) error { + actual, err := ContentDigest(afero.NewOsFs(), path, hasher) + if err != nil { + return fmt.Errorf("failed to hash file %q: %w", path, err) + } + if !strings.HasSuffix(expectedDigest, actual) { + return fmt.Errorf("hash mismatch for file %q: got %q expected %q", path, actual, expectedDigest) + } + return nil +} diff --git a/internal/format/color.go b/internal/format/color.go new file mode 100644 index 00000000..fa1757c3 --- /dev/null +++ b/internal/format/color.go @@ -0,0 +1,21 @@ +package format + +import "fmt" + +const ( + DefaultColor Color = iota + 30 + Red + Green + Yellow + Blue + Magenta + Cyan + White +) + +type Color uint8 + +// TODO: not cross platform (windows...) +func (c Color) Format(s string) string { + return fmt.Sprintf("\x1b[%dm%s\x1b[0m", c, s) +} diff --git a/internal/format/tprint.go b/internal/format/tprint.go new file mode 100644 index 00000000..fc75400b --- /dev/null +++ b/internal/format/tprint.go @@ -0,0 +1,16 @@ +package format + +import ( + "bytes" + "text/template" +) + +// Tprintf renders a string from a given template string and field values +func Tprintf(tmpl string, data map[string]interface{}) string { + t := template.Must(template.New("").Parse(tmpl)) + buf := &bytes.Buffer{} + if err := t.Execute(buf, data); err != nil { + return "" + } + return buf.String() +} diff --git a/internal/log/log.go b/internal/log/log.go new file mode 100644 index 00000000..f066720d --- /dev/null +++ b/internal/log/log.go @@ -0,0 +1,87 @@ +package log + +import ( + "github.com/anchore/go-logger" + "github.com/anchore/go-logger/adapter/discard" + "github.com/anchore/go-logger/adapter/redact" +) + +var ( + log = discard.New() + store = redact.NewStore() +) + +func init() { + // why redact the default discard logger? there may be sensitive information added to the redact store + // before the logger is set. This ensures that any future loggers will still redact the sensitive information. + // from previous Redact() calls. + log = redact.New(log, store) +} + +func Set(l logger.Logger) { + log = redact.New(l, store) +} + +func Redact(value ...string) { + store.Add(value...) +} + +// Errorf takes a formatted template string and template arguments for the error logging level. +func Errorf(format string, args ...interface{}) { + log.Errorf(format, args...) +} + +// Error logs the given arguments at the error logging level. +func Error(args ...interface{}) { + log.Error(args...) +} + +// Warnf takes a formatted template string and template arguments for the warning logging level. +func Warnf(format string, args ...interface{}) { + log.Warnf(format, args...) +} + +// Warn logs the given arguments at the warning logging level. +func Warn(args ...interface{}) { + log.Warn(args...) +} + +// Infof takes a formatted template string and template arguments for the info logging level. +func Infof(format string, args ...interface{}) { + log.Infof(format, args...) +} + +// Info logs the given arguments at the info logging level. +func Info(args ...interface{}) { + log.Info(args...) +} + +// Debugf takes a formatted template string and template arguments for the debug logging level. +func Debugf(format string, args ...interface{}) { + log.Debugf(format, args...) +} + +// Debug logs the given arguments at the debug logging level. +func Debug(args ...interface{}) { + log.Debug(args...) +} + +// Tracef takes a formatted template string and template arguments for the trace logging level. +func Tracef(format string, args ...interface{}) { + log.Tracef(format, args...) +} + +// Trace logs the given arguments at the trace logging level. +func Trace(args ...interface{}) { + log.Trace(args...) +} + +// WithFields returns a message logger with multiple key-value fields. +func WithFields(fields ...interface{}) logger.MessageLogger { + return log.WithFields(fields...) +} + +// Nested returns a new logger with hard coded key-value pairs +func Nested(fields ...interface{}) logger.Logger { + return log.Nested(fields...) +} diff --git a/internal/stringset.go b/internal/stringset.go new file mode 100644 index 00000000..ce8f5d77 --- /dev/null +++ b/internal/stringset.go @@ -0,0 +1,38 @@ +package internal + +type Set map[string]struct{} + +func NewStringSet() Set { + return make(Set) +} + +func NewStringSetFromSlice(start []string) Set { + ret := make(Set) + for _, s := range start { + ret.Add(s) + } + return ret +} + +func (s Set) Add(i string) { + s[i] = struct{}{} +} + +func (s Set) Remove(i string) { + delete(s, i) +} + +func (s Set) Contains(i string) bool { + _, ok := s[i] + return ok +} + +func (s Set) ToSlice() []string { + ret := make([]string, len(s)) + idx := 0 + for v := range s { + ret[idx] = v + idx++ + } + return ret +} diff --git a/internal/tar/tar.go b/internal/tar/tar.go new file mode 100644 index 00000000..f72d73f1 --- /dev/null +++ b/internal/tar/tar.go @@ -0,0 +1,66 @@ +package tar + +import ( + "archive/tar" + "compress/gzip" + "fmt" + "io" + "os" +) + +// Populate creates a gzipped tar from the given paths. +func Populate(tarPath string, filePaths ...string) error { + f, err := os.Create(tarPath) + if err != nil { + return fmt.Errorf("unable to create tar (%s): %w", tarPath, err) + } + defer f.Close() + + gzipWriter := gzip.NewWriter(f) + defer gzipWriter.Close() + + tarWriter := tar.NewWriter(gzipWriter) + defer tarWriter.Close() + + for _, filePath := range filePaths { + err := addFileToTarWriter(filePath, tarWriter) + if err != nil { + return fmt.Errorf("unable to add file to tar (file='%s'): %w", filePath, err) + } + } + + return nil +} + +// addFileToTarWriter takes a given filepath and saves the content to the given tar.Writer. +func addFileToTarWriter(filePath string, tarWriter *tar.Writer) error { + f, err := os.Open(filePath) + if err != nil { + return fmt.Errorf("unable to open file (%s): %w", filePath, err) + } + defer f.Close() + + stat, err := f.Stat() + if err != nil { + return fmt.Errorf("unable to get stat for file (%s): %w", filePath, err) + } + + header := &tar.Header{ + Name: filePath, + Size: stat.Size(), + Mode: int64(stat.Mode()), + ModTime: stat.ModTime(), + } + + err = tarWriter.WriteHeader(header) + if err != nil { + return fmt.Errorf("unable to write header for file (%s): %w", filePath, err) + } + + _, err = io.Copy(tarWriter, f) + if err != nil { + return fmt.Errorf("unable to copy data to the tar (file='%s'): %w", filePath, err) + } + + return nil +} diff --git a/internal/ui/config.go b/internal/ui/config.go new file mode 100644 index 00000000..2781ebf5 --- /dev/null +++ b/internal/ui/config.go @@ -0,0 +1,7 @@ +package ui + +type Config struct { + Verbose bool + Quiet bool + Debug bool +} diff --git a/internal/ui/loggerui/ui.go b/internal/ui/loggerui/ui.go new file mode 100644 index 00000000..3eaa76d7 --- /dev/null +++ b/internal/ui/loggerui/ui.go @@ -0,0 +1,137 @@ +package loggerui + +import ( + "context" + "errors" + "fmt" + "sync" + "time" + + "github.com/wagoodman/go-partybus" + "github.com/wagoodman/go-progress" + + "github.com/anchore/go-logger" + "github.com/anchore/grype-db/internal/log" + "github.com/anchore/grype-db/pkg/event" +) + +type UI struct { + unsubscribe func() error + logger logger.Logger + debug bool + quiet bool + background *sync.WaitGroup + finalizers []partybus.Event +} + +// New writes all events to the common application logger and writes the final report to the given writer. + +func New(debug, quiet bool) *UI { + return &UI{ + debug: debug, + quiet: quiet, + logger: log.Nested("from", "UI"), + background: &sync.WaitGroup{}, + } +} + +func (u *UI) Setup(unsubscribe func() error) error { + u.unsubscribe = unsubscribe + return nil +} + +func (u *UI) Handle(e partybus.Event) error { + if u.debug { + u.handleEvent(e) + } + + if e.Type == event.Exit { + u.finalizers = append(u.finalizers, e) + return u.unsubscribe() + } + + return nil +} + +func (u UI) Teardown(force bool) error { + if !force { + u.background.Wait() + } + + return nil +} + +func (u *UI) logEventPoll(localLogger logger.Logger, p progress.Progress, stage string) { + fields := make(logger.Fields) + if p.Size() > 0 { + fields["size"] = p.Size() + fields["ratio"] = fmt.Sprintf("%0.2f", p.Ratio()) + } + if stage != "" { + fields["stage"] = stage + } + if p.Current() > 0 { + fields["n"] = p.Current() + } + err := p.Error() + if err != nil && !errors.Is(err, progress.ErrCompleted) { + fields["error"] = err + } + + if p.Complete() { + fields["finished"] = p.Complete() + } + + localLogger. + WithFields(fields). + Debugf("polling event progress") +} + +func (u *UI) handleEvent(e partybus.Event) { + eventFields := make(logger.Fields) + eventFields["event"] = e.Type + if e.Source != nil { + eventFields["source"] = e.Source + } + + localLogger := u.logger.Nested(eventFields) + + localLogger.Debug("new event") + + prog, ok := e.Value.(progress.Progressable) + if !ok { + return + } + + u.background.Add(1) + go func() { + defer u.background.Done() + + var stager progress.Stager = progress.Stage{} + if s, ok := e.Value.(progress.Stager); ok { + stager = s + } + + var last progress.Progress + var lastStage string + var lastShow = time.Now() + for current := range progress.Stream(context.Background(), prog, time.Second*1) { + stage := stager.Stage() + + // try to only log progress updates when there is either new information, or it's been a while since the last log + hasUpdatedInfo := last != current || lastStage != stage + isStale := lastShow.Add(5 * time.Second).Before(time.Now()) + if hasUpdatedInfo || isStale { + u.logEventPoll(localLogger, current, stage) + + lastShow = time.Now() + } + lastStage = stage + last = current + } + + if !last.Complete() { + localLogger.Debugf("event progress finished in an incomplete state") + } + }() +} diff --git a/internal/ui/select.go b/internal/ui/select.go new file mode 100644 index 00000000..2476371c --- /dev/null +++ b/internal/ui/select.go @@ -0,0 +1,11 @@ +package ui + +import ( + "github.com/anchore/grype-db/internal/ui/loggerui" +) + +func Select(cfg Config) (uis []UI) { + // TODO: in the future we may support a TUI, this is the spot to select it + + return []UI{loggerui.New(cfg.Debug, cfg.Quiet)} +} diff --git a/internal/ui/ui.go b/internal/ui/ui.go new file mode 100644 index 00000000..e720a8bb --- /dev/null +++ b/internal/ui/ui.go @@ -0,0 +1,9 @@ +package ui + +import "github.com/wagoodman/go-partybus" + +type UI interface { + Setup(unsubscribe func() error) error + partybus.Handler + Teardown(force bool) error +} diff --git a/internal/utils/indent.go b/internal/utils/indent.go new file mode 100644 index 00000000..10f0ff59 --- /dev/null +++ b/internal/utils/indent.go @@ -0,0 +1,21 @@ +package utils + +import "strings" + +func Indent(text, indent string) string { + if len(strings.TrimSpace(text)) == 0 { + return indent + } + if text[len(text)-1:] == "\n" { + result := "" + for _, j := range strings.Split(text[:len(text)-1], "\n") { + result += indent + j + "\n" + } + return result + } + result := "" + for _, j := range strings.Split(strings.TrimRight(text, "\n"), "\n") { + result += indent + j + "\n" + } + return result[:len(result)-1] +} diff --git a/pkg/data/entry.go b/pkg/data/entry.go new file mode 100644 index 00000000..d5a264ed --- /dev/null +++ b/pkg/data/entry.go @@ -0,0 +1,8 @@ +package data + +// Entry is a data structure responsible for capturing an individual writable entry from a data.Processor (written by a data.Writer). +type Entry struct { + DBSchemaVersion int + // Data is the specific payload that should be written (usually a grype-db v*.Entry struct) + Data interface{} +} diff --git a/pkg/data/processor.go b/pkg/data/processor.go new file mode 100644 index 00000000..0258f614 --- /dev/null +++ b/pkg/data/processor.go @@ -0,0 +1,10 @@ +package data + +import "io" + +// Processor takes individual feed group cache files (for select feed groups) and is responsible to producing +// data.Entry objects to be written to the DB. +type Processor interface { + IsSupported(schemaURL string) bool + Process(reader io.Reader) ([]Entry, error) +} diff --git a/pkg/data/severity.go b/pkg/data/severity.go new file mode 100644 index 00000000..122de6a1 --- /dev/null +++ b/pkg/data/severity.go @@ -0,0 +1,34 @@ +package data + +import "strings" + +type Severity string + +const ( + SeverityUnknown Severity = "Unknown" + SeverityNegligible Severity = "Negligible" + SeverityLow Severity = "Low" + SeverityMedium Severity = "Medium" + SeverityHigh Severity = "High" + SeverityCritical Severity = "Critical" +) + +func ParseSeverity(s string) Severity { + clean := strings.TrimSpace(strings.ToLower(s)) + switch clean { + case "unknown", "": + return SeverityUnknown + case "negligible": + return SeverityNegligible + case "low": + return SeverityLow + case "medium": + return SeverityMedium + case "high": + return SeverityHigh + case "critical": + return SeverityCritical + default: + return SeverityUnknown + } +} diff --git a/pkg/data/severity_test.go b/pkg/data/severity_test.go new file mode 100644 index 00000000..f30cc438 --- /dev/null +++ b/pkg/data/severity_test.go @@ -0,0 +1,51 @@ +package data + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestParseSeverity(t *testing.T) { + tests := []struct { + input string + want Severity + }{ + { + input: "negLIGible", + want: SeverityNegligible, + }, + { + input: "loW", + want: SeverityLow, + }, + { + input: "meDIum", + want: SeverityMedium, + }, + { + input: " hiGH", + want: SeverityHigh, + }, + { + input: "cRiTical ", + want: SeverityCritical, + }, + { + input: "unKNOWN", + want: SeverityUnknown, + }, + { + input: "", + want: SeverityUnknown, + }, + { + input: " ", + want: SeverityUnknown, + }, + } + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + assert.Equal(t, tt.want, ParseSeverity(tt.input)) + }) + } +} diff --git a/pkg/data/transformers.go b/pkg/data/transformers.go new file mode 100644 index 00000000..531651f0 --- /dev/null +++ b/pkg/data/transformers.go @@ -0,0 +1,13 @@ +package data + +import "github.com/anchore/grype-db/pkg/provider/unmarshal" + +// Transformers are functions that know how ta take individual data shapes defined in the unmarshal package and +// reshape the data into data.Entry objects that are writable by a data.Writer. Transformers are dependency-injected +// into commonly-shared data.Processors in the individual process.v* packages. + +type GitHubTransformer func(entry unmarshal.GitHubAdvisory) ([]Entry, error) +type MSRCTransformer func(entry unmarshal.MSRCVulnerability) ([]Entry, error) +type NVDTransformer func(entry unmarshal.NVDVulnerability) ([]Entry, error) +type OSTransformer func(entry unmarshal.OSVulnerability) ([]Entry, error) +type MatchExclusionTransformer func(entry unmarshal.MatchExclusion) ([]Entry, error) diff --git a/pkg/data/writer.go b/pkg/data/writer.go new file mode 100644 index 00000000..075578e0 --- /dev/null +++ b/pkg/data/writer.go @@ -0,0 +1,9 @@ +package data + +// Writer knows how to persist one or more data.Entry objects to a database. Note that the backing implementations +// may take advantage of bulk writes when possible (positively improving performance), which is why multiple +// entries can be written at once. +type Writer interface { + Write(...Entry) error + Close() error +} diff --git a/pkg/event/event.go b/pkg/event/event.go new file mode 100644 index 00000000..155d8f00 --- /dev/null +++ b/pkg/event/event.go @@ -0,0 +1,17 @@ +/* +Package event provides event types for all events that the library published onto the event bus. By convention, for each event +defined here there should be a corresponding event parser defined in the parsers/ child package. +*/ +package event + +import ( + "github.com/anchore/grype-db/internal" + "github.com/wagoodman/go-partybus" +) + +const ( + prefix = internal.ApplicationName + + // Exit is a partybus event indicating the main process is to exit + Exit partybus.EventType = prefix + "-exit-event" +) diff --git a/pkg/lib.go b/pkg/lib.go new file mode 100644 index 00000000..e2a0719a --- /dev/null +++ b/pkg/lib.go @@ -0,0 +1,16 @@ +package pkg + +import ( + "github.com/anchore/go-logger" + "github.com/anchore/grype-db/internal/bus" + "github.com/anchore/grype-db/internal/log" + "github.com/wagoodman/go-partybus" +) + +func SetLogger(l logger.Logger) { + log.Set(l) +} + +func SetBus(b *partybus.Bus) { + bus.SetPublisher(b) +} diff --git a/pkg/process/build.go b/pkg/process/build.go new file mode 100644 index 00000000..ff1b0fe5 --- /dev/null +++ b/pkg/process/build.go @@ -0,0 +1,163 @@ +package process + +import ( + "bytes" + "fmt" + "time" + + "github.com/dustin/go-humanize" + + "github.com/anchore/grype-db/pkg/provider/entry" + + v1 "github.com/anchore/grype-db/pkg/process/v1" + v2 "github.com/anchore/grype-db/pkg/process/v2" + v3 "github.com/anchore/grype-db/pkg/process/v3" + v4 "github.com/anchore/grype-db/pkg/process/v4" + v5 "github.com/anchore/grype-db/pkg/process/v5" + "github.com/anchore/grype-db/pkg/provider" + "github.com/anchore/grype-db/pkg/provider/unmarshal" + grypeDBv1 "github.com/anchore/grype/grype/db/v1" + grypeDBv2 "github.com/anchore/grype/grype/db/v2" + grypeDBv3 "github.com/anchore/grype/grype/db/v3" + grypeDBv4 "github.com/anchore/grype/grype/db/v4" + + "github.com/anchore/grype-db/internal/log" + "github.com/anchore/grype-db/pkg/data" + grypeDBv5 "github.com/anchore/grype/grype/db/v5" +) + +type BuildConfig struct { + SchemaVersion int + Directory string + States provider.States + Timestamp time.Time +} + +func Build(cfg BuildConfig) error { + log.WithFields("schema", cfg.SchemaVersion, "build-directory", cfg.Directory, "providers", cfg.States.Names()).Info("building DB") + + processors, err := getProcessors(cfg.SchemaVersion) + if err != nil { + return err + } + + writer, err := getWriter(cfg.SchemaVersion, cfg.Timestamp, cfg.Directory) + if err != nil { + return err + } + + var openers []openerEntry + for _, sd := range cfg.States { + sdOpeners, count, err := entry.Openers(sd.Store, sd.ResultPaths()) + if err != nil { + return fmt.Errorf("failed to open provider result files: %w", err) + } + openers = append(openers, openerEntry{ + openers: sdOpeners, + name: sd.Provider, + count: count, + }) + } + + if err := build(mergeOpeners(openers), writer, processors...); err != nil { + return err + } + + return writer.Close() +} + +type openerEntry struct { + openers <-chan entry.Opener + name string + count int64 +} + +func mergeOpeners(entries []openerEntry) <-chan entry.Opener { + out := make(chan entry.Opener) + go func() { + defer close(out) + for _, e := range entries { + log.WithFields("provider", e.name, "records", humanize.Comma(e.count)).Debug("writing to DB") + + for opener := range e.openers { + out <- opener + } + } + }() + return out +} + +func getProcessors(schemaVersion int) ([]data.Processor, error) { + switch schemaVersion { + case grypeDBv1.SchemaVersion: + return v1.Processors(), nil + case grypeDBv2.SchemaVersion: + return v2.Processors(), nil + case grypeDBv3.SchemaVersion: + return v3.Processors(), nil + case grypeDBv4.SchemaVersion: + return v4.Processors(), nil + case grypeDBv5.SchemaVersion: + return v5.Processors(), nil + default: + return nil, fmt.Errorf("unable to create processor: unsupported schema version: %+v", schemaVersion) + } +} + +func getWriter(schemaVersion int, dataAge time.Time, directory string) (data.Writer, error) { + switch schemaVersion { + case grypeDBv1.SchemaVersion: + return v1.NewWriter(directory, dataAge) + case grypeDBv2.SchemaVersion: + return v2.NewWriter(directory, dataAge) + case grypeDBv3.SchemaVersion: + return v3.NewWriter(directory, dataAge) + case grypeDBv4.SchemaVersion: + return v4.NewWriter(directory, dataAge) + case grypeDBv5.SchemaVersion: + return v5.NewWriter(directory, dataAge) + default: + return nil, fmt.Errorf("unable to create writer: unsupported schema version: %+v", schemaVersion) + } +} + +func build(openers <-chan entry.Opener, writer data.Writer, processors ...data.Processor) error { + for opener := range openers { + log.WithFields("entry", opener.String()).Tracef("processing") + var processor data.Processor + + f, err := opener.Open() + if err != nil { + return fmt.Errorf("failed to open cache entry %q: %w", opener.String(), err) + } + envelope, err := unmarshal.Envelope(f) + if err != nil { + return fmt.Errorf("failed to unmarshal cache entry %q: %w", opener.String(), err) + } + + for _, candidate := range processors { + if candidate.IsSupported(envelope.Schema) { + processor = candidate + log.WithFields("schema", envelope.Schema).Trace("matched with processor") + break + } + } + if processor == nil { + log.WithFields("schema", envelope.Schema).Warnf("schema is not implemented for any processor. Dropping item") + continue + } + + entries, err := processor.Process(bytes.NewReader(envelope.Item)) + if err != nil { + return fmt.Errorf("failed to process cache entry %q: %w", opener.String(), err) + } + + if err := writer.Write(entries...); err != nil { + return fmt.Errorf("failed to write records to the DB for cache entry %q: %w", opener.String(), err) + } + } + + log.Debugf("wrote all provider state") + + return nil +} diff --git a/pkg/process/common/clean_fixed_in_version.go b/pkg/process/common/clean_fixed_in_version.go new file mode 100644 index 00000000..7507471a --- /dev/null +++ b/pkg/process/common/clean_fixed_in_version.go @@ -0,0 +1,12 @@ +package common + +import "strings" + +func CleanFixedInVersion(version string) string { + switch strings.TrimSpace(strings.ToLower(version)) { + case "none", "": + return "" + default: + return version + } +} diff --git a/pkg/process/common/constraint.go b/pkg/process/common/constraint.go new file mode 100644 index 00000000..821c8286 --- /dev/null +++ b/pkg/process/common/constraint.go @@ -0,0 +1,35 @@ +package common + +import ( + "regexp" + "strings" +) + +// match examples: +// >= 5.0.0 +// <= 6.1.2.beta +// >= 5.0.0 +// < 6.1 +// > 5.0.0 +// >=5 +// <6 +var forceSemVerPattern = regexp.MustCompile(`[><=]+\s*[^<>=]+`) + +func EnforceSemVerConstraint(constraint string) string { + constraint = CleanConstraint(constraint) + if len(constraint) == 0 { + return "" + } + return strings.ReplaceAll(strings.Join(forceSemVerPattern.FindAllString(constraint, -1), ", "), " ", "") +} + +func OrConstraints(c ...string) string { + return strings.Join(c, " || ") +} + +func CleanConstraint(constraint string) string { + if strings.ToLower(constraint) == "none" { + return "" + } + return constraint +} diff --git a/pkg/process/common/constraint_test.go b/pkg/process/common/constraint_test.go new file mode 100644 index 00000000..0bf8568c --- /dev/null +++ b/pkg/process/common/constraint_test.go @@ -0,0 +1,27 @@ +package common + +import "testing" + +func TestEnforceSemVerConstraint(t *testing.T) { + tests := []struct { + value string + expected string + }{ + { + value: " >= 5.0.0<7.1 ", + expected: ">=5.0.0,<7.1", + }, + { + value: "None", + expected: "", + }, + } + for _, test := range tests { + t.Run(test.value, func(t *testing.T) { + actual := EnforceSemVerConstraint(test.value) + if actual != test.expected { + t.Errorf("mismatch: '%s'!='%s'", actual, test.expected) + } + }) + } +} diff --git a/pkg/process/default_schema_version.go b/pkg/process/default_schema_version.go new file mode 100644 index 00000000..369d8ecf --- /dev/null +++ b/pkg/process/default_schema_version.go @@ -0,0 +1,5 @@ +package process + +import grypeDB "github.com/anchore/grype/grype/db/v5" + +const DefaultSchemaVersion = grypeDB.SchemaVersion diff --git a/pkg/process/package.go b/pkg/process/package.go new file mode 100644 index 00000000..8fee6940 --- /dev/null +++ b/pkg/process/package.go @@ -0,0 +1,102 @@ +package process + +import ( + "crypto/rand" + "encoding/hex" + "fmt" + "net/url" + "os" + "path" + "strings" + "time" + + "github.com/anchore/grype-db/internal/log" + "github.com/anchore/grype-db/internal/tar" + "github.com/anchore/grype/grype/db" + "github.com/spf13/afero" +) + +func randomString() (string, error) { + b := make([]byte, 10) + _, err := rand.Read(b) + return hex.EncodeToString(b), err +} + +func Package(dbDir, publishBaseURL string) error { + log.Infof("packaging DB from=%q for=%q", dbDir, publishBaseURL) + + fs := afero.NewOsFs() + metadata, err := db.NewMetadataFromDir(fs, dbDir) + if err != nil { + return err + } + + if metadata == nil { + return fmt.Errorf("no metadata found in %q", dbDir) + } + + u, err := url.Parse(publishBaseURL) + if err != nil { + return err + } + + trailer, err := randomString() + if err != nil { + return fmt.Errorf("unable to create random archive trailer: %w", err) + } + + // we attach a random value at the end of the file name to prevent from overwriting DBs in S3 that are already + // cached in the CDN. Ideally this would be based off of the archive checksum but a random string is simpler. + tarName := fmt.Sprintf("vulnerability-db_v%d_%s_%s.tar.gz", metadata.Version, metadata.Built.Format(time.RFC3339), trailer) + tarPath := path.Join(dbDir, tarName) + + if err := populate(tarName, dbDir); err != nil { + return err + } + + log.WithFields("path", tarPath).Info("created DB archive") + + entry, err := db.NewListingEntryFromArchive(fs, *metadata, tarPath, u) + if err != nil { + return fmt.Errorf("unable to create listing entry from archive: %w", err) + } + + listing := db.NewListing(entry) + listingPath := path.Join(dbDir, db.ListingFileName) + return listing.Write(listingPath) +} + +func populate(tarName, dbDir string) error { + originalDir, err := os.Getwd() + if err != nil { + return fmt.Errorf("unable to get CWD: %w", err) + } + + if err = os.Chdir(dbDir); err != nil { + return fmt.Errorf("unable to cd to build dir: %w", err) + } + + defer func() { + if err = os.Chdir(originalDir); err != nil { + log.Errorf("unable to cd to original dir: %w", err) + } + }() + + fileInfos, err := os.ReadDir("./") + if err != nil { + return fmt.Errorf("unable to list db directory: %w", err) + } + + var files []string + for _, fi := range fileInfos { + if fi.Name() != "listing.json" && !strings.Contains(fi.Name(), ".tar.gz") { + files = append(files, fi.Name()) + } + } + + if err = tar.Populate(tarName, files...); err != nil { + return fmt.Errorf("unable to create db archive: %w", err) + } + + return nil +} diff --git a/pkg/process/processors/github_processor.go b/pkg/process/processors/github_processor.go new file mode 100644 index 00000000..8af93aca --- /dev/null +++ b/pkg/process/processors/github_processor.go @@ -0,0 +1,60 @@ +//nolint:dupl +package processors + +import ( + "io" + "strings" + + "github.com/anchore/grype-db/internal/log" + "github.com/anchore/grype-db/pkg/data" + "github.com/anchore/grype-db/pkg/provider/unmarshal" +) + +type githubProcessor struct { + transformer data.GitHubTransformer +} + +func NewGitHubProcessor(transformer data.GitHubTransformer) data.Processor { + return &githubProcessor{ + transformer: transformer, + } +} + +func (p githubProcessor) Process(reader io.Reader) ([]data.Entry, error) { + var results []data.Entry + + entries, err := unmarshal.GitHubAdvisoryEntries(reader) + if err != nil { + return nil, err + } + + for _, entry := range entries { + if entry.IsEmpty() { + log.Warn("dropping empty GHSA entry") + continue + } + + transformedEntries, err := p.transformer(entry) + if err != nil { + return nil, err + } + + results = append(results, transformedEntries...) + } + + return results, nil +} + +func (p githubProcessor) IsSupported(schemaURL string) bool { + matchesSchemaType := strings.Contains(schemaURL, "https://raw.githubusercontent.com/anchore/vunnel/main/schema/vulnerability/github-security-advisory/schema-") + if !matchesSchemaType { + return false + } + + if !strings.HasSuffix(schemaURL, "schema-1.0.0.json") { + log.WithFields("schema", schemaURL).Trace("unsupported GHSA schema version") + return false + } + + return true +} diff --git a/pkg/process/processors/github_processor_test.go b/pkg/process/processors/github_processor_test.go new file mode 100644 index 00000000..ac830a5e --- /dev/null +++ b/pkg/process/processors/github_processor_test.go @@ -0,0 +1,32 @@ +package processors + +import ( + "github.com/anchore/grype-db/pkg/data" + testUtils "github.com/anchore/grype-db/pkg/process/tests" + "github.com/anchore/grype-db/pkg/provider/unmarshal" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "os" + "testing" +) + +func mockGithubProcessorTransform(vulnerability unmarshal.GitHubAdvisory) ([]data.Entry, error) { + return []data.Entry{ + { + DBSchemaVersion: 0, + Data: vulnerability, + }, + }, nil +} + +func TestGitHubProcessor_Process(t *testing.T) { + f, err := os.Open("test-fixtures/github.json") + require.NoError(t, err) + defer testUtils.CloseFile(f) + + processor := NewGitHubProcessor(mockGithubProcessorTransform) + entries, err := processor.Process(f) + + assert.NoError(t, err) + assert.Len(t, entries, 3) +} diff --git a/pkg/process/processors/match_exclusion_processor.go b/pkg/process/processors/match_exclusion_processor.go new file mode 100644 index 00000000..3d89001d --- /dev/null +++ b/pkg/process/processors/match_exclusion_processor.go @@ -0,0 +1,61 @@ +//nolint:dupl +package processors + +import ( + "io" + "strings" + + "github.com/anchore/grype-db/internal/log" + + "github.com/anchore/grype-db/pkg/data" + "github.com/anchore/grype-db/pkg/provider/unmarshal" +) + +type matchExclusionProcessor struct { + transformer data.MatchExclusionTransformer +} + +func NewMatchExclusionProcessor(transformer data.MatchExclusionTransformer) data.Processor { + return &matchExclusionProcessor{ + transformer: transformer, + } +} + +func (p matchExclusionProcessor) Process(reader io.Reader) ([]data.Entry, error) { + var results []data.Entry + + entries, err := unmarshal.MatchExclusions(reader) + if err != nil { + return nil, err + } + + for _, entry := range entries { + if entry.IsEmpty() { + log.Warn("dropping empty match-exclusion entry") + continue + } + + transformedEntries, err := p.transformer(entry) + if err != nil { + return nil, err + } + + results = append(results, transformedEntries...) + } + + return results, nil +} + +func (p matchExclusionProcessor) IsSupported(schemaURL string) bool { + matchesSchemaType := strings.Contains(schemaURL, "https://raw.githubusercontent.com/anchore/vunnel/main/schema/match-exclusion/schema-") + if !matchesSchemaType { + return false + } + + if !strings.HasSuffix(schemaURL, "schema-1.0.0.json") { + log.WithFields("schema", schemaURL).Trace("unsupported match-exclusion schema version") + return false + } + + return true +} diff --git a/pkg/process/processors/match_exclusion_processor_test.go b/pkg/process/processors/match_exclusion_processor_test.go new file mode 100644 index 00000000..f17e1788 --- /dev/null +++ b/pkg/process/processors/match_exclusion_processor_test.go @@ -0,0 +1,32 @@ +package processors + +import ( + "github.com/anchore/grype-db/pkg/data" + testUtils "github.com/anchore/grype-db/pkg/process/tests" + "github.com/anchore/grype-db/pkg/provider/unmarshal" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "os" + "testing" +) + +func mockMatchExclusionProcessorTransform(vulnerability unmarshal.MatchExclusion) ([]data.Entry, error) { + return []data.Entry{ + { + DBSchemaVersion: 0, + Data: vulnerability, + }, + }, nil +} + +func TestMatchExclusionProcessor_Process(t *testing.T) { + f, err := os.Open("test-fixtures/exclusions.json") + require.NoError(t, err) + defer testUtils.CloseFile(f) + + processor := NewMatchExclusionProcessor(mockMatchExclusionProcessorTransform) + entries, err := processor.Process(f) + + require.NoError(t, err) + assert.Len(t, entries, 3) +} diff --git a/pkg/process/processors/msrc_processor.go b/pkg/process/processors/msrc_processor.go new file mode 100644 index 00000000..502d192d --- /dev/null +++ b/pkg/process/processors/msrc_processor.go @@ -0,0 +1,63 @@ +package processors + +import ( + "io" + "strings" + + "github.com/anchore/grype-db/internal/log" + + "github.com/anchore/grype-db/pkg/data" + "github.com/anchore/grype-db/pkg/provider/unmarshal" +) + +// msrcProcessor defines the regular expression needed to signal what is supported +type msrcProcessor struct { + transformer data.MSRCTransformer +} + +// NewMSRCProcessor creates a new instance of msrcProcessor particular to MSRC +func NewMSRCProcessor(transformer data.MSRCTransformer) data.Processor { + return &msrcProcessor{ + transformer: transformer, + } +} + +// Parse reads all entries in all metadata matching the supported schema and produces vulnerabilities and their corresponding metadata +func (p msrcProcessor) Process(reader io.Reader) ([]data.Entry, error) { + var results []data.Entry + + entries, err := unmarshal.MSRCVulnerabilityEntries(reader) + if err != nil { + return nil, err + } + + for _, entry := range entries { + if entry.ID == "" { + log.Warn("dropping empty MSRC entry") + continue + } + + transformedEntries, err := p.transformer(entry) + if err != nil { + return nil, err + } + + results = append(results, transformedEntries...) + } + + return results, nil +} + +func (p msrcProcessor) IsSupported(schemaURL string) bool { + matchesSchemaType := strings.Contains(schemaURL, "https://raw.githubusercontent.com/anchore/vunnel/main/schema/vulnerability/msrc/schema-") + if !matchesSchemaType { + return false + } + + if !strings.HasSuffix(schemaURL, "schema-1.0.0.json") { + log.WithFields("schema", schemaURL).Trace("unsupported MSRC schema version") + return false + } + + return true +} diff --git a/pkg/process/processors/msrc_processor_test.go b/pkg/process/processors/msrc_processor_test.go new file mode 100644 index 00000000..eebdbd03 --- /dev/null +++ b/pkg/process/processors/msrc_processor_test.go @@ -0,0 +1,32 @@ +package processors + +import ( + "github.com/anchore/grype-db/pkg/data" + testUtils "github.com/anchore/grype-db/pkg/process/tests" + "github.com/anchore/grype-db/pkg/provider/unmarshal" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "os" + "testing" +) + +func mockMSRCProcessorTransform(vulnerability unmarshal.MSRCVulnerability) ([]data.Entry, error) { + return []data.Entry{ + { + DBSchemaVersion: 0, + Data: vulnerability, + }, + }, nil +} + +func TestMSRCProcessor_Process(t *testing.T) { + f, err := os.Open("test-fixtures/msrc.json") + require.NoError(t, err) + defer testUtils.CloseFile(f) + + processor := NewMSRCProcessor(mockMSRCProcessorTransform) + entries, err := processor.Process(f) + + require.NoError(t, err) + assert.Len(t, entries, 2) +} diff --git a/pkg/process/processors/nvd_processor.go b/pkg/process/processors/nvd_processor.go new file mode 100644 index 00000000..dd732abf --- /dev/null +++ b/pkg/process/processors/nvd_processor.go @@ -0,0 +1,60 @@ +package processors + +import ( + "io" + "strings" + + "github.com/anchore/grype-db/internal/log" + + "github.com/anchore/grype-db/pkg/data" + "github.com/anchore/grype-db/pkg/provider/unmarshal" +) + +type nvdProcessor struct { + transformer data.NVDTransformer +} + +func NewNVDProcessor(transformer data.NVDTransformer) data.Processor { + return &nvdProcessor{ + transformer: transformer, + } +} + +func (p nvdProcessor) Process(reader io.Reader) ([]data.Entry, error) { + var results []data.Entry + + entries, err := unmarshal.NvdVulnerabilityEntries(reader) + if err != nil { + return nil, err + } + + for _, entry := range entries { + if entry.IsEmpty() { + log.Warn("dropping empty NVD entry") + continue + } + + transformedEntries, err := p.transformer(entry.Cve) + if err != nil { + return nil, err + } + + results = append(results, transformedEntries...) + } + + return results, nil +} + +func (p nvdProcessor) IsSupported(schemaURL string) bool { + matchesSchemaType := strings.Contains(schemaURL, "https://raw.githubusercontent.com/anchore/vunnel/main/schema/vulnerability/nvd/schema-") + if !matchesSchemaType { + return false + } + + if !strings.HasSuffix(schemaURL, "schema-1.0.0.json") { + log.WithFields("schema", schemaURL).Trace("unsupported NVD schema version") + return false + } + + return true +} diff --git a/pkg/process/processors/nvd_processor_test.go b/pkg/process/processors/nvd_processor_test.go new file mode 100644 index 00000000..01095216 --- /dev/null +++ b/pkg/process/processors/nvd_processor_test.go @@ -0,0 +1,32 @@ +package processors + +import ( + "github.com/anchore/grype-db/pkg/data" + testUtils "github.com/anchore/grype-db/pkg/process/tests" + "github.com/anchore/grype-db/pkg/provider/unmarshal" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "os" + "testing" +) + +func mockNVDProcessorTransform(vulnerability unmarshal.NVDVulnerability) ([]data.Entry, error) { + return []data.Entry{ + { + DBSchemaVersion: 0, + Data: vulnerability, + }, + }, nil +} + +func TestNVDProcessor_Process(t *testing.T) { + f, err := os.Open("test-fixtures/nvd.json") + require.NoError(t, err) + defer testUtils.CloseFile(f) + + processor := NewNVDProcessor(mockNVDProcessorTransform) + entries, err := processor.Process(f) + + require.NoError(t, err) + assert.Len(t, entries, 3) +} diff --git a/pkg/process/processors/os_processor.go b/pkg/process/processors/os_processor.go new file mode 100644 index 00000000..674889c7 --- /dev/null +++ b/pkg/process/processors/os_processor.go @@ -0,0 +1,61 @@ +//nolint:dupl +package processors + +import ( + "io" + "strings" + + "github.com/anchore/grype-db/internal/log" + + "github.com/anchore/grype-db/pkg/data" + "github.com/anchore/grype-db/pkg/provider/unmarshal" +) + +type osProcessor struct { + transformer data.OSTransformer +} + +func NewOSProcessor(transformer data.OSTransformer) data.Processor { + return &osProcessor{ + transformer: transformer, + } +} + +func (p osProcessor) Process(reader io.Reader) ([]data.Entry, error) { + var results []data.Entry + + entries, err := unmarshal.OSVulnerabilityEntries(reader) + if err != nil { + return nil, err + } + + for _, entry := range entries { + if entry.IsEmpty() { + log.Warn("dropping empty OS entry") + continue + } + + transformedEntries, err := p.transformer(entry) + if err != nil { + return nil, err + } + + results = append(results, transformedEntries...) + } + + return results, nil +} + +func (p osProcessor) IsSupported(schemaURL string) bool { + matchesSchemaType := strings.Contains(schemaURL, "https://raw.githubusercontent.com/anchore/vunnel/main/schema/vulnerability/os/schema-") + if !matchesSchemaType { + return false + } + + if !strings.HasSuffix(schemaURL, "schema-1.0.0.json") { + log.WithFields("schema", schemaURL).Trace("unsupported OS schema version") + return false + } + + return true +} diff --git a/pkg/process/processors/os_processor_test.go b/pkg/process/processors/os_processor_test.go new file mode 100644 index 00000000..3730ad2e --- /dev/null +++ b/pkg/process/processors/os_processor_test.go @@ -0,0 +1,32 @@ +package processors + +import ( + "github.com/anchore/grype-db/pkg/data" + testUtils "github.com/anchore/grype-db/pkg/process/tests" + "github.com/anchore/grype-db/pkg/provider/unmarshal" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "os" + "testing" +) + +func mockOSProcessorTransform(vulnerability unmarshal.OSVulnerability) ([]data.Entry, error) { + return []data.Entry{ + { + DBSchemaVersion: 0, + Data: vulnerability, + }, + }, nil +} + +func TestOSProcessor_Process(t *testing.T) { + f, err := os.Open("test-fixtures/os.json") + require.NoError(t, err) + defer testUtils.CloseFile(f) + + processor := NewOSProcessor(mockOSProcessorTransform) + entries, err := processor.Process(f) + + require.NoError(t, err) + assert.Len(t, entries, 4) +} diff --git a/pkg/process/processors/test-fixtures/exclusions.json b/pkg/process/processors/test-fixtures/exclusions.json new file mode 100644 index 00000000..67c12175 --- /dev/null +++ b/pkg/process/processors/test-fixtures/exclusions.json @@ -0,0 +1,38 @@ +[ + { + }, + { + "id": "CVE-1234-5678", + "justification": "CVE-1234-5678 is imaginary" + }, + { + "id": "CVE-2012-abcxyz", + "constraints": [ + { + "namespaces": [ + "nvd:cpe", + "abc.xyz:python" + ] + } + ], + "justification": "some reason" + }, + { + "id": "CVE-2015-abc123", + "constraints": [ + { + "ecosystem_constraints": [ + { + "language": "python", + "package_constraints": [ + { + "package_name": "clock" + } + ] + } + ] + } + ], + "justification": "" + } +] \ No newline at end of file diff --git a/pkg/process/processors/test-fixtures/github.json b/pkg/process/processors/test-fixtures/github.json new file mode 100644 index 00000000..cf35d52d --- /dev/null +++ b/pkg/process/processors/test-fixtures/github.json @@ -0,0 +1,184 @@ +[ + { + "Advisory": { + "CVE": [ + "CVE-2020-14000" + ], + "FixedIn": [ + { + "ecosystem": "npm", + "identifier": "0.2.0-prerelease.20200714185213", + "name": "scratch-vm", + "namespace": "github:npm", + "range": "<= 0.2.0-prerelease.20200709173451" + } + ], + "Metadata": { + "CVE": [ + "CVE-2020-14000" + ] + }, + "Severity": "High", + "Summary": "Remote Code Execution in scratch-vm", + "ghsaId": "GHSA-vc9j-fhvv-8vrf", + "namespace": "github:npm", + "url": "https://github.com/advisories/GHSA-vc9j-fhvv-8vrf", + "withdrawn": null + }, + "Vulnerability": {} + }, + { + "Advisory": { + "CVE": [ + "CVE-2018-8768" + ], + "FixedIn": [ + { + "ecosystem": "python", + "identifier": "5.4.1", + "name": "notebook", + "namespace": "github:python", + "range": "< 5.4.1" + } + ], + "Metadata": { + "CVE": [ + "CVE-2018-8768" + ] + }, + "Severity": "Low", + "Summary": "Low severity vulnerability that affects notebook", + "ghsaId": "GHSA-6cwv-x26c-w2q4", + "namespace": "github:python", + "url": "https://github.com/advisories/GHSA-6cwv-x26c-w2q4", + "withdrawn": "2022-01-31T14:32:09Z" + }, + "Vulnerability": {} + }, + { + "Advisory": { + "CVE": [ + "CVE-2017-5524" + ], + "FixedIn": [ + { + "ecosystem": "python", + "identifier": "4.3.12", + "name": "Plone", + "namespace": "github:python", + "range": ">= 4.0 < 4.3.12" + }, + { + "ecosystem": "python", + "identifier": "5.1b1", + "name": "Plone", + "namespace": "github:python", + "range": ">= 5.1a1 < 5.1b1" + }, + { + "ecosystem": "python", + "identifier": "5.0.7", + "name": "Plone-debug", + "namespace": "github:python", + "range": ">= 5.0rc1 < 5.0.7" + } + ], + "Metadata": { + "CVE": [ + "CVE-2017-5524" + ] + }, + "Severity": "Medium", + "Summary": "Moderate severity vulnerability that affects Plone", + "ghsaId": "GHSA-p5wr-vp8g-q5p4", + "namespace": "github:python", + "url": "https://github.com/advisories/GHSA-p5wr-vp8g-q5p4", + "withdrawn": null + }, + "Vulnerability": {} + }, + { + "Advisory": { + "CVE": [ + "CVE-2017-5524" + ], + "FixedIn": [ + { + "ecosystem": "python", + "identifier": "4.3.12", + "name": "Plone", + "namespace": "github:python", + "range": ">= 4.0 < 4.3.12" + }, + { + "ecosystem": "python", + "identifier": "5.1b1", + "name": "Plone", + "namespace": "github:python", + "range": ">= 5.1a1 < 5.1b1" + }, + { + "ecosystem": "python", + "identifier": "5.0.7", + "name": "Plone-debug", + "namespace": "github:python", + "range": ">= 5.0rc1 < 5.0.7" + } + ], + "Metadata": { + "CVE": [ + "CVE-2017-5524" + ] + }, + "Severity": "Medium", + "Summary": "Moderate severity vulnerability that affects Plone", + "ghsaId": "", + "namespace": "github:python", + "url": "https://github.com/advisories/GHSA-p5wr-vp8g-q5p4", + "withdrawn": null + }, + "Vulnerability": {} + }, + { + "Advisory": { + "CVE": [ + "CVE-2017-5524" + ], + "FixedIn": [ + { + "ecosystem": "python", + "identifier": "4.3.12", + "name": "Plone", + "namespace": "github:python", + "range": ">= 4.0 < 4.3.12" + }, + { + "ecosystem": "python", + "identifier": "5.1b1", + "name": "Plone", + "namespace": "github:python", + "range": ">= 5.1a1 < 5.1b1" + }, + { + "ecosystem": "python", + "identifier": "5.0.7", + "name": "Plone-debug", + "namespace": "github:python", + "range": ">= 5.0rc1 < 5.0.7" + } + ], + "Metadata": { + "CVE": [ + "CVE-2017-5524" + ] + }, + "Severity": "Medium", + "Summary": "Moderate severity vulnerability that affects Plone", + "namespace": "github:python", + "url": "https://github.com/advisories/GHSA-p5wr-vp8g-q5p4", + "withdrawn": null + }, + "Vulnerability": {} + } +] + diff --git a/pkg/process/processors/test-fixtures/msrc.json b/pkg/process/processors/test-fixtures/msrc.json new file mode 100644 index 00000000..9e87ac3c --- /dev/null +++ b/pkg/process/processors/test-fixtures/msrc.json @@ -0,0 +1,421 @@ +[ + { + "cvss": { + "base_score": 7.8, + "temporal_score": 7, + "vector": "CVSS:3.0/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H/E:P/RL:O/RC:C" + }, + "fixed_in": [ + { + "id": "4493470", + "is_first": true, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4493470", + "https://support.microsoft.com/help/4493470" + ] + }, + { + "id": "4494440", + "is_first": false, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4494440", + "https://support.microsoft.com/help/4494440" + ] + }, + { + "id": "4503267", + "is_first": false, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4503267", + "https://support.microsoft.com/en-us/help/4503267" + ] + }, + { + "id": "4507460", + "is_first": false, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4507460", + "https://support.microsoft.com/help/4507460" + ] + }, + { + "id": "4512517", + "is_first": false, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4512517", + "https://support.microsoft.com/help/4512517" + ] + }, + { + "id": "4516044", + "is_first": false, + "is_latest": true, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4516044", + "https://support.microsoft.com/help/4516044" + ] + } + ], + "id": "CVE-2019-0671", + "link": "https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2019-0671", + "product": { + "family": "Windows", + "id": "10852", + "name": "Windows 10 Version 1607 for 32-bit Systems" + }, + "severity": "High", + "summary": "Microsoft Office Access Connectivity Engine Remote Code Execution Vulnerability", + "vulnerable": [ + "4480961", + "4483229", + "4487026", + "4489882" + ] + }, + { + "cvss": { + "base_score": 4.4, + "temporal_score": 4, + "vector": "CVSS:3.0/AV:L/AC:H/PR:L/UI:R/S:U/C:N/I:N/A:H/E:P/RL:O/RC:C" + }, + "fixed_in": [ + { + "id": "4093119", + "is_first": true, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4093119" + ] + }, + { + "id": "4103723", + "is_first": false, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4103723" + ] + }, + { + "id": "4284880", + "is_first": false, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4284880" + ] + }, + { + "id": "4338814", + "is_first": false, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4338814" + ] + }, + { + "id": "4343887", + "is_first": false, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4343887" + ] + }, + { + "id": "4345418", + "is_first": false, + "is_latest": true, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4345418" + ] + }, + { + "id": "4457131", + "is_first": false, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4457131" + ] + }, + { + "id": "4462917", + "is_first": false, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4462917" + ] + }, + { + "id": "4467691", + "is_first": false, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4467691" + ] + }, + { + "id": "4471321", + "is_first": false, + "is_latest": true, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4471321" + ] + } + ], + "id": "CVE-2018-8116", + "link": "https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2018-8116", + "product": { + "family": "Windows", + "id": "10852", + "name": "Windows 10 Version 1607 for 32-bit Systems" + }, + "severity": "Medium", + "summary": "Microsoft Graphics Component Denial of Service Vulnerability", + "vulnerable": [ + "3213986", + "4013429", + "4015217", + "4019472", + "4022715", + "4025339", + "4034658", + "4038782", + "4041691", + "4048953", + "4053579", + "4056890", + "4074590", + "4088787" + ] + }, + { + "cvss": { + "base_score": 4.4, + "temporal_score": 4, + "vector": "CVSS:3.0/AV:L/AC:H/PR:L/UI:R/S:U/C:N/I:N/A:H/E:P/RL:O/RC:C" + }, + "fixed_in": [ + { + "id": "4093119", + "is_first": true, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4093119" + ] + }, + { + "id": "4103723", + "is_first": false, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4103723" + ] + }, + { + "id": "4284880", + "is_first": false, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4284880" + ] + }, + { + "id": "4338814", + "is_first": false, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4338814" + ] + }, + { + "id": "4343887", + "is_first": false, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4343887" + ] + }, + { + "id": "4345418", + "is_first": false, + "is_latest": true, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4345418" + ] + }, + { + "id": "4457131", + "is_first": false, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4457131" + ] + }, + { + "id": "4462917", + "is_first": false, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4462917" + ] + }, + { + "id": "4467691", + "is_first": false, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4467691" + ] + }, + { + "id": "4471321", + "is_first": false, + "is_latest": true, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4471321" + ] + } + ], + "id": "", + "link": "https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2018-8116", + "product": { + "family": "Windows", + "id": "10852", + "name": "Windows 10 Version 1607 for 32-bit Systems" + }, + "severity": "Medium", + "summary": "Microsoft Graphics Component Denial of Service Vulnerability", + "vulnerable": [ + "3213986", + "4013429", + "4015217", + "4019472", + "4022715", + "4025339", + "4034658", + "4038782", + "4041691", + "4048953", + "4053579", + "4056890", + "4074590", + "4088787" + ] + }, + { + "cvss": { + "base_score": 4.4, + "temporal_score": 4, + "vector": "CVSS:3.0/AV:L/AC:H/PR:L/UI:R/S:U/C:N/I:N/A:H/E:P/RL:O/RC:C" + }, + "fixed_in": [ + { + "id": "4093119", + "is_first": true, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4093119" + ] + }, + { + "id": "4103723", + "is_first": false, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4103723" + ] + }, + { + "id": "4284880", + "is_first": false, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4284880" + ] + }, + { + "id": "4338814", + "is_first": false, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4338814" + ] + }, + { + "id": "4343887", + "is_first": false, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4343887" + ] + }, + { + "id": "4345418", + "is_first": false, + "is_latest": true, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4345418" + ] + }, + { + "id": "4457131", + "is_first": false, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4457131" + ] + }, + { + "id": "4462917", + "is_first": false, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4462917" + ] + }, + { + "id": "4467691", + "is_first": false, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4467691" + ] + }, + { + "id": "4471321", + "is_first": false, + "is_latest": true, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4471321" + ] + } + ], + "link": "https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2018-8116", + "product": { + "family": "Windows", + "id": "10852", + "name": "Windows 10 Version 1607 for 32-bit Systems" + }, + "severity": "Medium", + "summary": "Microsoft Graphics Component Denial of Service Vulnerability", + "vulnerable": [ + "3213986", + "4013429", + "4015217", + "4019472", + "4022715", + "4025339", + "4034658", + "4038782", + "4041691", + "4048953", + "4053579", + "4056890", + "4074590", + "4088787" + ] + } +] diff --git a/pkg/process/processors/test-fixtures/nvd.json b/pkg/process/processors/test-fixtures/nvd.json new file mode 100644 index 00000000..b2db3269 --- /dev/null +++ b/pkg/process/processors/test-fixtures/nvd.json @@ -0,0 +1,138 @@ +[ + { + "cve": { + "id": "CVE-1987-1111", + "sourceIdentifier": "cve@mitre.org", + "published": "2016-11-22T17:59:00.180", + "lastModified": "2016-11-28T19:50:59.600", + "vulnStatus": "Modified", + "descriptions": [], + "metrics": { + "cvssMetricV30": [], + "cvssMetricV2": [] + }, + "weaknesses": [], + "configurations": [ + { + "nodes": [ + { + "operator": "OR", + "negate": false, + "cpeMatch": [ + { + "vulnerable": true, + "criteria": "cpe:2.3:a:soap\\:\\:lite_project:soap\\:\\:lite:*:*:*:*:*:perl:*:*", + "versionEndIncluding": "1.14", + "matchCriteriaId": "FB4DACB9-2E9E-4CBE-825F-FC0303D8CC86" + } + ] + } + ] + } + ], + "references": [] + } + }, + { + "cve": { + "id": "CVE-1987-2222", + "sourceIdentifier": "cve@mitre.org", + "published": "2016-11-22T17:59:00.180", + "lastModified": "2016-11-28T19:50:59.600", + "vulnStatus": "Modified", + "descriptions": [], + "metrics": { + "cvssMetricV30": [], + "cvssMetricV2": [] + }, + "weaknesses": [], + "configurations": [ + { + "nodes": [ + { + "operator": "OR", + "negate": false, + "cpeMatch": [ + { + "vulnerable": true, + "criteria": "cpe:2.3:a:soap\\:\\:lite_project:soap\\:\\:lite:*:*:*:*:*:perl:*:*", + "versionEndIncluding": "1.14", + "matchCriteriaId": "FB4DACB9-2E9E-4CBE-825F-FC0303D8CC86" + } + ] + } + ] + } + ], + "references": [] + } + }, + { + "cve": { + "id": "CVE-1987-3333", + "sourceIdentifier": "cve@mitre.org", + "published": "2016-11-22T17:59:00.180", + "lastModified": "2016-11-28T19:50:59.600", + "vulnStatus": "Modified", + "descriptions": [], + "metrics": { + "cvssMetricV30": [], + "cvssMetricV2": [] + }, + "weaknesses": [], + "configurations": [ + { + "nodes": [ + { + "operator": "OR", + "negate": false, + "cpeMatch": [ + { + "vulnerable": true, + "criteria": "cpe:2.3:a:soap\\:\\:lite_project:soap\\:\\:lite:*:*:*:*:*:perl:*:*", + "versionEndIncluding": "1.14", + "matchCriteriaId": "FB4DACB9-2E9E-4CBE-825F-FC0303D8CC86" + } + ] + } + ] + } + ], + "references": [] + } + }, + { + "cve": { + "id": "", + "sourceIdentifier": "^ note... there is no CVE ID in this test!!!", + "published": "2016-11-22T17:59:00.180", + "lastModified": "2016-11-28T19:50:59.600", + "vulnStatus": "Modified", + "descriptions": [], + "metrics": { + "cvssMetricV30": [], + "cvssMetricV2": [] + }, + "weaknesses": [], + "configurations": [ + { + "nodes": [ + { + "operator": "OR", + "negate": false, + "cpeMatch": [ + { + "vulnerable": true, + "criteria": "cpe:2.3:a:soap\\:\\:lite_project:soap\\:\\:lite:*:*:*:*:*:perl:*:*", + "versionEndIncluding": "1.14", + "matchCriteriaId": "FB4DACB9-2E9E-4CBE-825F-FC0303D8CC86" + } + ] + } + ] + } + ], + "references": [] + } + } +] \ No newline at end of file diff --git a/pkg/process/processors/test-fixtures/oracle.json b/pkg/process/processors/test-fixtures/oracle.json new file mode 100644 index 00000000..3373c37f --- /dev/null +++ b/pkg/process/processors/test-fixtures/oracle.json @@ -0,0 +1,121 @@ +[ + { + "Vulnerability": { + "CVSS": [], + "Description": "", + "FixedIn": [ + { + "Name": "libexif", + "NamespaceName": "ol:8", + "Version": "0:0.6.21-17.el8_2", + "VersionFormat": "rpm" + }, + { + "Name": "libexif-devel", + "NamespaceName": "ol:8", + "Version": "0:0.6.21-17.el8_2", + "VersionFormat": "rpm" + }, + { + "Name": "libexif-dummy", + "NamespaceName": "ol:8", + "Version": "None", + "VersionFormat": "rpm" + } + ], + "Link": "http://linux.oracle.com/errata/ELSA-2020-2550.html", + "Metadata": { + "CVE": [ + { + "Link": "http://linux.oracle.com/cve/CVE-2020-13112.html", + "Name": "CVE-2020-13112" + } + ], + "Issued": "2020-06-15", + "RefId": "ELSA-2020-2550" + }, + "Name": "ELSA-2020-2550", + "NamespaceName": "ol:8", + "Severity": "Medium" + } + }, + { + "Vulnerability": { + "CVSS": [], + "Description": "", + "FixedIn": [ + { + "Name": "libexif", + "NamespaceName": "ol:8", + "Version": "0:0.6.21-17.el8_2", + "VersionFormat": "rpm" + }, + { + "Name": "libexif-devel", + "NamespaceName": "ol:8", + "Version": "0:0.6.21-17.el8_2", + "VersionFormat": "rpm" + }, + { + "Name": "libexif-dummy", + "NamespaceName": "ol:8", + "Version": "None", + "VersionFormat": "rpm" + } + ], + "Link": "http://linux.oracle.com/errata/ELSA-2020-2550.html", + "Metadata": { + "CVE": [ + { + "Link": "http://linux.oracle.com/cve/CVE-2020-13112.html", + "Name": "CVE-2020-13112" + } + ], + "Issued": "2020-06-15", + "RefId": "ELSA-2020-2550" + }, + "Name": "", + "NamespaceName": "ol:8", + "Severity": "Medium" + } + }, + { + "Vulnerability": { + "CVSS": [], + "Description": "", + "FixedIn": [ + { + "Name": "libexif", + "NamespaceName": "ol:8", + "Version": "0:0.6.21-17.el8_2", + "VersionFormat": "rpm" + }, + { + "Name": "libexif-devel", + "NamespaceName": "ol:8", + "Version": "0:0.6.21-17.el8_2", + "VersionFormat": "rpm" + }, + { + "Name": "libexif-dummy", + "NamespaceName": "ol:8", + "Version": "None", + "VersionFormat": "rpm" + } + ], + "Link": "http://linux.oracle.com/errata/ELSA-2020-2550.html", + "Metadata": { + "CVE": [ + { + "Link": "http://linux.oracle.com/cve/CVE-2020-13112.html", + "Name": "CVE-2020-13112" + } + ], + "Issued": "2020-06-15", + "RefId": "ELSA-2020-2550" + }, + "NamespaceName": "ol:8", + "Severity": "Medium" + } + } +] \ No newline at end of file diff --git a/pkg/process/processors/test-fixtures/os.json b/pkg/process/processors/test-fixtures/os.json new file mode 100644 index 00000000..f32cbf3a --- /dev/null +++ b/pkg/process/processors/test-fixtures/os.json @@ -0,0 +1,203 @@ +[ + { + "Vulnerability": { + "CVSS": [], + "Description": "", + "FixedIn": [ + { + "Name": "asterisk", + "NamespaceName": "debian:8", + "VendorAdvisory": { + "AdvisorySummary": [], + "NoAdvisory": false + }, + "Version": "1:1.6.2.0~rc3-1", + "VersionFormat": "dpkg" + }, + { + "Name": "auth2db", + "NamespaceName": "debian:8", + "VendorAdvisory": { + "AdvisorySummary": [], + "NoAdvisory": false + }, + "Version": "0.2.5-2+dfsg-1", + "VersionFormat": "dpkg" + }, + { + "Name": "exaile", + "NamespaceName": "debian:8", + "VendorAdvisory": { + "AdvisorySummary": [], + "NoAdvisory": false + }, + "Version": "0.2.14+debian-2.2", + "VersionFormat": "dpkg" + }, + { + "Name": "wordpress", + "NamespaceName": "debian:8", + "VendorAdvisory": { + "AdvisorySummary": [], + "NoAdvisory": false + }, + "Version": "", + "VersionFormat": "dpkg" + } + ], + "Link": "https://security-tracker.debian.org/tracker/CVE-2008-7220", + "Metadata": { + "NVD": { + "CVSSv2": { + "Score": 7.5, + "Vectors": "AV:N/AC:L/Au:N/C:P/I:P/A:P" + } + } + }, + "Name": "CVE-2008-7220", + "NamespaceName": "debian:8", + "Severity": "High" + } + }, + { + "Vulnerability": { + "CVSS": [], + "Description": "", + "FixedIn": [ + { + "Name": "rsyslog", + "NamespaceName": "debian:8", + "VendorAdvisory": { + "AdvisorySummary": [], + "NoAdvisory": false + }, + "Version": "5.7.4-1", + "VersionFormat": "dpkg" + } + ], + "Link": "https://security-tracker.debian.org/tracker/CVE-2011-4623", + "Metadata": { + "NVD": { + "CVSSv2": { + "Score": 2.1, + "Vectors": "AV:L/AC:L/Au:N/C:N/I:N/A:P" + } + } + }, + "Name": "CVE-2011-4623", + "NamespaceName": "debian:8", + "Severity": "Low" + } + }, + { + "Vulnerability": { + "CVSS": [], + "Description": "", + "FixedIn": [ + { + "Name": "rsyslog", + "NamespaceName": "debian:8", + "VendorAdvisory": { + "AdvisorySummary": [], + "NoAdvisory": false + }, + "Version": "3.18.6-1", + "VersionFormat": "dpkg" + } + ], + "Link": "https://security-tracker.debian.org/tracker/CVE-2008-5618", + "Metadata": { + "NVD": { + "CVSSv2": { + "Score": 5, + "Vectors": "AV:N/AC:L/Au:N/C:N/I:N/A:P" + } + } + }, + "Name": "CVE-2008-5618", + "NamespaceName": "debian:8", + "Severity": "Low" + } + }, + { + "Vulnerability": { + "Description": "", + "FixedIn": [ + { + "Name": "389-ds-base", + "NamespaceName": "amzn:2", + "Version": "1.3.8.4-15.amzn2.0.1", + "VersionFormat": "rpm" + }, + { + "Name": "389-ds-base-debuginfo", + "NamespaceName": "amzn:2", + "Version": "1.3.8.4-15.amzn2.0.1", + "VersionFormat": "rpm" + }, + { + "Name": "389-ds-base-devel", + "NamespaceName": "amzn:2", + "Version": "1.3.8.4-15.amzn2.0.1", + "VersionFormat": "rpm" + }, + { + "Name": "389-ds-base-libs", + "NamespaceName": "amzn:2", + "Version": "1.3.8.4-15.amzn2.0.1", + "VersionFormat": "rpm" + }, + { + "Name": "389-ds-base-snmp", + "NamespaceName": "amzn:2", + "Version": "1.3.8.4-15.amzn2.0.1", + "VersionFormat": "rpm" + } + ], + "Link": "https://alas.aws.amazon.com/AL2/ALAS-2018-1106.html", + "Metadata": { + "CVE": [ + {"Name": "CVE-2018-14648"} + ] + }, + "Name": "ALAS-2018-1106", + "NamespaceName": "amzn:2", + "Severity": "Medium" + } + }, + { + "Vulnerability": { + "CVSS": [], + "Description": "", + "FixedIn": [], + "Link": null, + "Metadata": {}, + "Name": null, + "NamespaceName": null, + "Severity": null + } + }, + { + "Vulnerability": { + "CVSS": [], + "Description": "", + "FixedIn": [], + "Link": null, + "Metadata": {}, + "Name": "", + "NamespaceName": null, + "Severity": null + } + }, + { + "Vulnerability": { + "CVSS": [], + "Description": "", + "FixedIn": [], + "Link": null, + "Metadata": {}, + "NamespaceName": null, + "Severity": null + } + } +] \ No newline at end of file diff --git a/pkg/process/pull.go b/pkg/process/pull.go new file mode 100644 index 00000000..c123ecf1 --- /dev/null +++ b/pkg/process/pull.go @@ -0,0 +1,83 @@ +package process + +import ( + "context" + "sync" + + "github.com/hashicorp/go-multierror" + "golang.org/x/sync/semaphore" + + "github.com/anchore/grype-db/pkg/provider" + + "github.com/anchore/grype-db/internal/log" +) + +type PullConfig struct { + Parallelism int + Collection provider.Collection +} + +func Pull(cfg PullConfig) error { + logProviders(cfg) + + // TODO: validate config + + // execute config + var wg sync.WaitGroup + sem := semaphore.NewWeighted(int64(cfg.Parallelism)) + + ctx := context.Background() + + var errs error + var errsLock sync.Mutex + updateErrs := func(err error) { + if err != nil { + errsLock.Lock() + defer errsLock.Unlock() + errs = multierror.Append(errs, err) + } + } + + for _, p := range cfg.Collection.Providers { + if err := sem.Acquire(ctx, 1); err != nil { + updateErrs(err) + break + } + if errs != nil { + // note: we don't cancel the context to stop existing provider updates. Why? this may leave otherwise + // valid providers in a bad state. Instead, we just let the other providers that have already been started + // to finish and return the error from the failed provider. + log.WithFields("error", errs).Error("provider update failed, waiting for already started provider updates to finish before exiting...") + break + } + wg.Add(1) + go func(prov provider.Provider) { + defer sem.Release(1) + defer wg.Done() + log.WithFields("provider", prov.ID().Name).Info("running vulnerability provider") + updateErrs(prov.Update(ctx)) + }(p) + } + + log.Debug("all providers started, waiting for graceful completion...") + wg.Wait() + + return errs +} + +func logProviders(cfg PullConfig) { + log.WithFields("providers", len(cfg.Collection.Providers), "parallelism", cfg.Parallelism).Info("configured providers") + + var keys []string + for _, p := range cfg.Collection.Providers { + keys = append(keys, p.ID().Name) + } + + for idx, key := range keys { + branch := "├──" + if idx == len(keys)-1 { + branch = "└──" + } + log.Debugf(" %s %s", branch, key) + } +} diff --git a/pkg/process/tests/utils.go b/pkg/process/tests/utils.go new file mode 100644 index 00000000..ddfb6028 --- /dev/null +++ b/pkg/process/tests/utils.go @@ -0,0 +1,14 @@ +package tests + +import ( + "log" + "os" +) + +func CloseFile(f *os.File) { + err := f.Close() + + if err != nil { + log.Fatal("error closing file") + } +} diff --git a/pkg/process/v1/processors.go b/pkg/process/v1/processors.go new file mode 100644 index 00000000..3f05d004 --- /dev/null +++ b/pkg/process/v1/processors.go @@ -0,0 +1,17 @@ +package v1 + +import ( + "github.com/anchore/grype-db/pkg/data" + "github.com/anchore/grype-db/pkg/process/processors" + "github.com/anchore/grype-db/pkg/process/v1/transformers/github" + "github.com/anchore/grype-db/pkg/process/v1/transformers/nvd" + "github.com/anchore/grype-db/pkg/process/v1/transformers/os" +) + +func Processors() []data.Processor { + return []data.Processor{ + processors.NewGitHubProcessor(github.Transform), + processors.NewNVDProcessor(nvd.Transform), + processors.NewOSProcessor(os.Transform), + } +} diff --git a/pkg/process/v1/transformers/entry.go b/pkg/process/v1/transformers/entry.go new file mode 100644 index 00000000..172f7aa9 --- /dev/null +++ b/pkg/process/v1/transformers/entry.go @@ -0,0 +1,22 @@ +package transformers + +import ( + "github.com/anchore/grype-db/pkg/data" + grypeDB "github.com/anchore/grype/grype/db/v1" +) + +func NewEntries(vs []grypeDB.Vulnerability, metadata grypeDB.VulnerabilityMetadata) []data.Entry { + entries := []data.Entry{ + { + DBSchemaVersion: grypeDB.SchemaVersion, + Data: metadata, + }, + } + for _, vuln := range vs { + entries = append(entries, data.Entry{ + DBSchemaVersion: grypeDB.SchemaVersion, + Data: vuln, + }) + } + return entries +} diff --git a/pkg/process/v1/transformers/github/test-fixtures/github-github-npm-0.json b/pkg/process/v1/transformers/github/test-fixtures/github-github-npm-0.json new file mode 100644 index 00000000..b0a7d1e9 --- /dev/null +++ b/pkg/process/v1/transformers/github/test-fixtures/github-github-npm-0.json @@ -0,0 +1,31 @@ + +{ + "Advisory": { + "CVE": [ + "CVE-2020-14000" + ], + "FixedIn": [ + { + "ecosystem": "npm", + "identifier": "0.2.0-prerelease.20200714185213", + "name": "scratch-vm", + "namespace": "github:npm", + "range": "<= 0.2.0-prerelease.20200709173451" + } + ], + "Metadata": { + "CVE": [ + "CVE-2020-14000" + ] + }, + "Severity": "High", + "Summary": "Remote Code Execution in scratch-vm", + "ghsaId": "GHSA-vc9j-fhvv-8vrf", + "namespace": "github:npm", + "url": "https://github.com/advisories/GHSA-vc9j-fhvv-8vrf", + "withdrawn": null + }, + "Vulnerability": {} +} + + diff --git a/pkg/process/v1/transformers/github/test-fixtures/github-github-python-0.json b/pkg/process/v1/transformers/github/test-fixtures/github-github-python-0.json new file mode 100644 index 00000000..ad14aa60 --- /dev/null +++ b/pkg/process/v1/transformers/github/test-fixtures/github-github-python-0.json @@ -0,0 +1,58 @@ +[ + { + "Advisory": { + "CVE": [ + "CVE-2018-8768" + ], + "FixedIn": [ + { + "ecosystem": "python", + "identifier": "5.4.1", + "name": "notebook", + "namespace": "github:python", + "range": "< 5.4.1" + } + ], + "Metadata": { + "CVE": [ + "CVE-2018-8768" + ] + }, + "Severity": "Low", + "Summary": "Low severity vulnerability that affects notebook", + "ghsaId": "GHSA-6cwv-x26c-w2q4", + "namespace": "github:python", + "url": "https://github.com/advisories/GHSA-6cwv-x26c-w2q4", + "withdrawn": null + }, + "Vulnerability": {} + }, + { + "Advisory": { + "CVE": [ + "CVE-2017-5524" + ], + "FixedIn": [ + { + "ecosystem": "python", + "identifier": "4.3.12", + "name": "Plone", + "namespace": "github:python", + "range": ">= 4.0 < 4.3.12" + } + ], + "Metadata": { + "CVE": [ + "CVE-2017-5524" + ] + }, + "Severity": "Medium", + "Summary": "Moderate severity vulnerability that affects Plone", + "ghsaId": "GHSA-p5wr-vp8g-q5p4", + "namespace": "github:python", + "url": "https://github.com/advisories/GHSA-p5wr-vp8g-q5p4", + "withdrawn": null + }, + "Vulnerability": {} + } +] \ No newline at end of file diff --git a/pkg/process/v1/transformers/github/test-fixtures/github-github-python-1.json b/pkg/process/v1/transformers/github/test-fixtures/github-github-python-1.json new file mode 100644 index 00000000..bfa84922 --- /dev/null +++ b/pkg/process/v1/transformers/github/test-fixtures/github-github-python-1.json @@ -0,0 +1,43 @@ + +{ + "Advisory": { + "CVE": [ + "CVE-2017-5524" + ], + "FixedIn": [ + { + "ecosystem": "python", + "identifier": "4.3.12", + "name": "Plone", + "namespace": "github:python", + "range": ">= 4.0 < 4.3.12" + }, + { + "ecosystem": "python", + "identifier": "5.1b1", + "name": "Plone", + "namespace": "github:python", + "range": ">= 5.1a1 < 5.1b1" + }, + { + "ecosystem": "python", + "identifier": "5.0.7", + "name": "Plone", + "namespace": "github:python", + "range": ">= 5.0rc1 < 5.0.7" + } + ], + "Metadata": { + "CVE": [ + "CVE-2017-5524" + ] + }, + "Severity": "Medium", + "Summary": "Moderate severity vulnerability that affects Plone", + "ghsaId": "GHSA-p5wr-vp8g-q5p4", + "namespace": "github:python", + "url": "https://github.com/advisories/GHSA-p5wr-vp8g-q5p4", + "withdrawn": null + }, + "Vulnerability": {} +} diff --git a/pkg/process/v1/transformers/github/test-fixtures/github-withdrawn.json b/pkg/process/v1/transformers/github/test-fixtures/github-withdrawn.json new file mode 100644 index 00000000..04995e38 --- /dev/null +++ b/pkg/process/v1/transformers/github/test-fixtures/github-withdrawn.json @@ -0,0 +1,29 @@ + +{ + "Advisory": { + "CVE": [ + "CVE-2018-8768" + ], + "FixedIn": [ + { + "ecosystem": "python", + "identifier": "5.4.1", + "name": "notebook", + "namespace": "github:python", + "range": "< 5.4.1" + } + ], + "Metadata": { + "CVE": [ + "CVE-2018-8768" + ] + }, + "Severity": "Low", + "Summary": "Low severity vulnerability that affects notebook", + "ghsaId": "GHSA-6cwv-x26c-w2q4", + "namespace": "github:python", + "url": "https://github.com/advisories/GHSA-6cwv-x26c-w2q4", + "withdrawn": "2022-01-31T14:32:09Z" + }, + "Vulnerability": {} +} diff --git a/pkg/process/v1/transformers/github/test-fixtures/multiple-fixed-in-names.json b/pkg/process/v1/transformers/github/test-fixtures/multiple-fixed-in-names.json new file mode 100644 index 00000000..ac1ef982 --- /dev/null +++ b/pkg/process/v1/transformers/github/test-fixtures/multiple-fixed-in-names.json @@ -0,0 +1,43 @@ + +{ + "Advisory": { + "CVE": [ + "CVE-2017-5524" + ], + "FixedIn": [ + { + "ecosystem": "python", + "identifier": "4.3.12", + "name": "Plone", + "namespace": "github:python", + "range": ">= 4.0 < 4.3.12" + }, + { + "ecosystem": "python", + "identifier": "5.1b1", + "name": "Plone", + "namespace": "github:python", + "range": ">= 5.1a1 < 5.1b1" + }, + { + "ecosystem": "python", + "identifier": "5.0.7", + "name": "Plone-debug", + "namespace": "github:python", + "range": ">= 5.0rc1 < 5.0.7" + } + ], + "Metadata": { + "CVE": [ + "CVE-2017-5524" + ] + }, + "Severity": "Medium", + "Summary": "Moderate severity vulnerability that affects Plone", + "ghsaId": "GHSA-p5wr-vp8g-q5p4", + "namespace": "github:python", + "url": "https://github.com/advisories/GHSA-p5wr-vp8g-q5p4", + "withdrawn": null + }, + "Vulnerability": {} +} diff --git a/pkg/process/v1/transformers/github/transform.go b/pkg/process/v1/transformers/github/transform.go new file mode 100644 index 00000000..75fbaf22 --- /dev/null +++ b/pkg/process/v1/transformers/github/transform.go @@ -0,0 +1,65 @@ +package github + +import ( + "github.com/anchore/grype-db/pkg/data" + "github.com/anchore/grype-db/pkg/process/common" + "github.com/anchore/grype-db/pkg/process/v1/transformers" + "github.com/anchore/grype-db/pkg/provider/unmarshal" + grypeDB "github.com/anchore/grype/grype/db/v1" +) + +const ( + // TODO: tech debt from a previous design + feed = "github" +) + +func Transform(vulnerability unmarshal.GitHubAdvisory) ([]data.Entry, error) { + var allVulns []grypeDB.Vulnerability + + // Exclude entries marked as withdrawn + if vulnerability.Advisory.Withdrawn != nil { + return nil, nil + } + + recordSource := grypeDB.RecordSource(feed, vulnerability.Advisory.Namespace) + + // there may be multiple packages indicated within the FixedIn field, we should make + // separate vulnerability entries (one for each name|namespace combo) while merging + // constraint ranges as they are found. + for _, advisory := range vulnerability.Advisory.FixedIn { + constraint := common.EnforceSemVerConstraint(advisory.Range) + + var versionFormat string + switch vulnerability.Advisory.Namespace { + case "github:python": + versionFormat = "python" + default: + versionFormat = "unknown" + } + + // create vulnerability entry + vuln := grypeDB.Vulnerability{ + ID: vulnerability.Advisory.GhsaID, + RecordSource: recordSource, + VersionConstraint: constraint, + VersionFormat: versionFormat, // TODO: this should reference a format, yes? (not a string) + ProxyVulnerabilities: vulnerability.Advisory.CVE, + PackageName: advisory.Name, + Namespace: advisory.Namespace, + FixedInVersion: common.CleanFixedInVersion(advisory.Identifier), + } + + allVulns = append(allVulns, vuln) + } + + // create vulnerability metadata entry (a single entry keyed off of the vulnerability ID) + metadata := grypeDB.VulnerabilityMetadata{ + ID: vulnerability.Advisory.GhsaID, + RecordSource: recordSource, + Severity: vulnerability.Advisory.Severity, + Links: []string{vulnerability.Advisory.URL}, + Description: vulnerability.Advisory.Summary, + } + + return transformers.NewEntries(allVulns, metadata), nil +} diff --git a/pkg/process/v1/transformers/github/transform_test.go b/pkg/process/v1/transformers/github/transform_test.go new file mode 100644 index 00000000..d71506b5 --- /dev/null +++ b/pkg/process/v1/transformers/github/transform_test.go @@ -0,0 +1,167 @@ +package github + +import ( + "github.com/stretchr/testify/require" + "os" + "testing" + + testUtils "github.com/anchore/grype-db/pkg/process/tests" + "github.com/anchore/grype-db/pkg/provider/unmarshal" + grypeDB "github.com/anchore/grype/grype/db/v1" + "github.com/stretchr/testify/assert" +) + +func TestUnmarshalGitHubEntries(t *testing.T) { + f, err := os.Open("test-fixtures/github-github-python-0.json") + require.NoError(t, err) + defer testUtils.CloseFile(f) + + entries, err := unmarshal.GitHubAdvisoryEntries(f) + require.NoError(t, err) + + assert.Len(t, entries, 2) +} + +func TestParseGitHubEntry(t *testing.T) { + expectedVulns := []grypeDB.Vulnerability{ + { + ID: "GHSA-p5wr-vp8g-q5p4", + RecordSource: "github:python", + VersionConstraint: ">=4.0,<4.3.12", + VersionFormat: "python", // TODO: this should reference a format, yes? (not a string) + ProxyVulnerabilities: []string{"CVE-2017-5524"}, + PackageName: "Plone", + Namespace: "github:python", + FixedInVersion: "4.3.12", + }, + { + ID: "GHSA-p5wr-vp8g-q5p4", + RecordSource: "github:python", + VersionConstraint: ">=5.1a1,<5.1b1", + VersionFormat: "python", // TODO: this should reference a format, yes? (not a string) + ProxyVulnerabilities: []string{"CVE-2017-5524"}, + PackageName: "Plone", + Namespace: "github:python", + FixedInVersion: "5.1b1", + }, + { + ID: "GHSA-p5wr-vp8g-q5p4", + RecordSource: "github:python", + VersionConstraint: ">=5.0rc1,<5.0.7", + VersionFormat: "python", // TODO: this should reference a format, yes? (not a string) + ProxyVulnerabilities: []string{"CVE-2017-5524"}, + PackageName: "Plone", + Namespace: "github:python", + FixedInVersion: "5.0.7", + }, + } + + expectedMetadata := grypeDB.VulnerabilityMetadata{ + ID: "GHSA-p5wr-vp8g-q5p4", + RecordSource: "github:python", + Severity: "Medium", + Links: []string{"https://github.com/advisories/GHSA-p5wr-vp8g-q5p4"}, + Description: "Moderate severity vulnerability that affects Plone", + } + + f, err := os.Open("test-fixtures/github-github-python-1.json") + require.NoError(t, err) + defer testUtils.CloseFile(f) + + entries, err := unmarshal.GitHubAdvisoryEntries(f) + assert.NoError(t, err) + assert.Len(t, entries, 1) + + entry := entries[0] + + dataEntries, err := Transform(entry) + assert.NoError(t, err) + + var vulns []grypeDB.Vulnerability + for _, entry := range dataEntries { + switch vuln := entry.Data.(type) { + case grypeDB.Vulnerability: + vulns = append(vulns, vuln) + case grypeDB.VulnerabilityMetadata: + assert.Equal(t, expectedMetadata, vuln) + default: + t.Fatalf("unexpected condition: data entry does not have a vulnerability or a metadata") + } + } + + // check vulnerability + assert.Len(t, vulns, len(expectedVulns)) + + assert.ElementsMatch(t, expectedVulns, vulns) + +} + +func TestDefaultVersionFormatNpmGitHubEntry(t *testing.T) { + expectedVulns := []grypeDB.Vulnerability{ + { + ID: "GHSA-vc9j-fhvv-8vrf", + RecordSource: "github:npm", + VersionConstraint: "<=0.2.0-prerelease.20200709173451", + VersionFormat: "unknown", // TODO: this should reference a format, yes? (not a string) + ProxyVulnerabilities: []string{"CVE-2020-14000"}, + PackageName: "scratch-vm", + Namespace: "github:npm", + FixedInVersion: "0.2.0-prerelease.20200714185213", + }, + } + + expectedMetadata := grypeDB.VulnerabilityMetadata{ + ID: "GHSA-vc9j-fhvv-8vrf", + RecordSource: "github:npm", + Severity: "High", + Links: []string{"https://github.com/advisories/GHSA-vc9j-fhvv-8vrf"}, + Description: "Remote Code Execution in scratch-vm", + } + + f, err := os.Open("test-fixtures/github-github-npm-0.json") + require.NoError(t, err) + defer testUtils.CloseFile(f) + + entries, err := unmarshal.GitHubAdvisoryEntries(f) + assert.NoError(t, err) + assert.Len(t, entries, 1) + + entry := entries[0] + + dataEntries, err := Transform(entry) + assert.NoError(t, err) + + var vulns []grypeDB.Vulnerability + for _, entry := range dataEntries { + switch vuln := entry.Data.(type) { + case grypeDB.Vulnerability: + vulns = append(vulns, vuln) + case grypeDB.VulnerabilityMetadata: + assert.Equal(t, expectedMetadata, vuln) + default: + t.Fatalf("unexpected condition: data entry does not have a vulnerability or a metadata") + } + } + + // check vulnerability + assert.Len(t, vulns, len(expectedVulns)) + + assert.ElementsMatch(t, expectedVulns, vulns) +} + +func TestFilterWithdrawnEntries(t *testing.T) { + f, err := os.Open("test-fixtures/github-withdrawn.json") + require.NoError(t, err) + defer testUtils.CloseFile(f) + + entries, err := unmarshal.GitHubAdvisoryEntries(f) + require.NoError(t, err) + + require.Len(t, entries, 1) + + entry := entries[0] + + dataEntries, err := Transform(entry) + assert.NoError(t, err) + assert.Nil(t, dataEntries) +} diff --git a/pkg/process/v1/transformers/nvd/test-fixtures/compound-pkg.json b/pkg/process/v1/transformers/nvd/test-fixtures/compound-pkg.json new file mode 100644 index 00000000..8e658dcd --- /dev/null +++ b/pkg/process/v1/transformers/nvd/test-fixtures/compound-pkg.json @@ -0,0 +1,115 @@ +{ + "cve": { + "id": "CVE-2018-10189", + "sourceIdentifier": "cve@mitre.org", + "published": "2018-04-17T20:29:00.410", + "lastModified": "2018-05-23T14:41:49.073", + "vulnStatus": "Analyzed", + "descriptions": [ + { + "lang": "en", + "value": "An issue was discovered in Mautic 1.x and 2.x before 2.13.0. It is possible to systematically emulate tracking cookies per contact due to tracking the contact by their auto-incremented ID. Thus, a third party can manipulate the cookie value with +1 to systematically assume being tracked as each contact in Mautic. It is then possible to retrieve information about the contact through forms that have progressive profiling enabled." + }, + { + "lang": "es", + "value": "Se ha descubierto un problema en Mautic, en versiones 1.x y 2.x anteriores a la 2.13.0. Es posible emular de forma sistemática el rastreo de cookies por contacto debido al rastreo de contacto por su ID autoincrementada. Por lo tanto, un tercero puede manipular el valor de la cookie con un +1 para asumir sistemáticamente que se está rastreando como cada contacto en Mautic. Así, sería posible recuperar información sobre el contacto a través de formularios que tengan habilitada la generación de perfiles progresiva." + } + ], + "metrics": { + "cvssMetricV30": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "cvssData": { + "version": "3.0", + "vectorString": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N", + "attackVector": "NETWORK", + "attackComplexity": "LOW", + "privilegesRequired": "NONE", + "userInteraction": "NONE", + "scope": "UNCHANGED", + "confidentialityImpact": "HIGH", + "integrityImpact": "NONE", + "availabilityImpact": "NONE", + "baseScore": 7.5, + "baseSeverity": "HIGH" + }, + "exploitabilityScore": 3.9, + "impactScore": 3.6 + } + ], + "cvssMetricV2": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "cvssData": { + "version": "2.0", + "vectorString": "AV:N/AC:L/Au:N/C:P/I:N/A:N", + "accessVector": "NETWORK", + "accessComplexity": "LOW", + "authentication": "NONE", + "confidentialityImpact": "PARTIAL", + "integrityImpact": "NONE", + "availabilityImpact": "NONE", + "baseScore": 5.0 + }, + "baseSeverity": "MEDIUM", + "exploitabilityScore": 10.0, + "impactScore": 2.9, + "acInsufInfo": false, + "obtainAllPrivilege": false, + "obtainUserPrivilege": false, + "obtainOtherPrivilege": false, + "userInteractionRequired": false + } + ] + }, + "weaknesses": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "description": [ + { + "lang": "en", + "value": "CWE-200" + } + ] + } + ], + "configurations": [ + { + "nodes": [ + { + "operator": "OR", + "negate": false, + "cpeMatch": [ + { + "vulnerable": true, + "criteria": "cpe:2.3:a:mautic:mautic:*:*:*:*:*:*:*:*", + "versionStartIncluding": "1.0.0", + "versionEndIncluding": "1.4.1", + "matchCriteriaId": "5779710D-099E-40EE-8DF3-55BD3179A50C" + }, + { + "vulnerable": true, + "criteria": "cpe:2.3:a:mautic:mautic:*:*:*:*:*:*:*:*", + "versionStartIncluding": "2.0.0", + "versionEndExcluding": "2.13.0", + "matchCriteriaId": "4EFAEE48-4AEF-4F8C-95E0-6E8D848D900F" + } + ] + } + ] + } + ], + "references": [ + { + "url": "https://github.com/mautic/mautic/releases/tag/2.13.0", + "source": "cve@mitre.org", + "tags": [ + "Third Party Advisory" + ] + } + ] + } +} diff --git a/pkg/process/v1/transformers/nvd/test-fixtures/invalid_cpe.json b/pkg/process/v1/transformers/nvd/test-fixtures/invalid_cpe.json new file mode 100644 index 00000000..eac2ebd4 --- /dev/null +++ b/pkg/process/v1/transformers/nvd/test-fixtures/invalid_cpe.json @@ -0,0 +1,111 @@ +{ + "cve": { + "id": "CVE-2015-8978", + "sourceIdentifier": "cve@mitre.org", + "published": "2016-11-22T17:59:00.180", + "lastModified": "2016-11-28T19:50:59.600", + "vulnStatus": "Modified", + "descriptions": [ + { + "lang": "en", + "value": "In Soap Lite (aka the SOAP::Lite extension for Perl) 1.14 and earlier, an example attack consists of defining 10 or more XML entities, each defined as consisting of 10 of the previous entity, with the document consisting of a single instance of the largest entity, which expands to one billion copies of the first entity. The amount of computer memory used for handling an external SOAP call would likely exceed that available to the process parsing the XML." + }, + { + "lang": "es", + "value": "En Soap Lite (también conocido como la extensión SOAP::Lite para Perl) 1.14 y versiones anteriores, un ejemplo de ataque consiste en definir 10 o más entidades XML, cada una definida como consistente de 10 de la entidad anterior, con el documento consistente de una única instancia de la entidad más grande, que se expande a mil millones de copias de la primera entidad. La suma de la memoria del ordenador utilizada para manejar una llamada SOAP externa probablemente superaría el disponible para el proceso de análisis del XML." + } + ], + "metrics": { + "cvssMetricV30": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "cvssData": { + "version": "3.0", + "vectorString": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", + "attackVector": "NETWORK", + "attackComplexity": "LOW", + "privilegesRequired": "NONE", + "userInteraction": "NONE", + "scope": "UNCHANGED", + "confidentialityImpact": "NONE", + "integrityImpact": "NONE", + "availabilityImpact": "HIGH", + "baseScore": 7.5, + "baseSeverity": "HIGH" + }, + "exploitabilityScore": 3.9, + "impactScore": 3.6 + } + ], + "cvssMetricV2": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "cvssData": { + "version": "2.0", + "vectorString": "AV:N/AC:L/Au:N/C:N/I:N/A:P", + "accessVector": "NETWORK", + "accessComplexity": "LOW", + "authentication": "NONE", + "confidentialityImpact": "NONE", + "integrityImpact": "NONE", + "availabilityImpact": "PARTIAL", + "baseScore": 5.0 + }, + "baseSeverity": "MEDIUM", + "exploitabilityScore": 10.0, + "impactScore": 2.9, + "acInsufInfo": false, + "obtainAllPrivilege": false, + "obtainUserPrivilege": false, + "obtainOtherPrivilege": false, + "userInteractionRequired": false + } + ] + }, + "weaknesses": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "description": [ + { + "lang": "en", + "value": "CWE-399" + } + ] + } + ], + "configurations": [ + { + "nodes": [ + { + "operator": "OR", + "negate": false, + "cpeMatch": [ + { + "vulnerable": true, + "criteria": "cpe:2.3:a:soap::lite_project:soap::lite:*:*:*:*:*:perl:*:*", + "versionEndIncluding": "1.14", + "matchCriteriaId": "FB4DACB9-2E9E-4CBE-825F-FC0303D8CC86" + } + ] + } + ] + } + ], + "references": [ + { + "url": "http://cpansearch.perl.org/src/PHRED/SOAP-Lite-1.20/Changes", + "source": "cve@mitre.org", + "tags": [ + "Vendor Advisory" + ] + }, + { + "url": "http://www.securityfocus.com/bid/94487", + "source": "cve@mitre.org" + } + ] + } +} diff --git a/pkg/process/v1/transformers/nvd/test-fixtures/single-package-multi-distro.json b/pkg/process/v1/transformers/nvd/test-fixtures/single-package-multi-distro.json new file mode 100644 index 00000000..ed108475 --- /dev/null +++ b/pkg/process/v1/transformers/nvd/test-fixtures/single-package-multi-distro.json @@ -0,0 +1,174 @@ +{ + "cve": { + "id": "CVE-2018-1000222", + "sourceIdentifier": "cve@mitre.org", + "published": "2018-08-20T20:29:01.347", + "lastModified": "2020-03-31T02:15:12.667", + "vulnStatus": "Modified", + "descriptions": [ + { + "lang": "en", + "value": "Libgd version 2.2.5 contains a Double Free Vulnerability vulnerability in gdImageBmpPtr Function that can result in Remote Code Execution . This attack appear to be exploitable via Specially Crafted Jpeg Image can trigger double free. This vulnerability appears to have been fixed in after commit ac16bdf2d41724b5a65255d4c28fb0ec46bc42f5." + }, + { + "lang": "es", + "value": "Libgd 2.2.5 contiene una vulnerabilidad de doble liberación (double free) en la función gdImageBmpPtr que puede resultar en la ejecución remota de código. Este ataque parece ser explotable mediante una imagen JPEG especialmente manipulada que desencadene una doble liberación (double free). La vulnerabilidad parece haber sido solucionada tras el commit con ID ac16bdf2d41724b5a65255d4c28fb0ec46bc42f5." + } + ], + "metrics": { + "cvssMetricV30": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "cvssData": { + "version": "3.0", + "vectorString": "CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H", + "attackVector": "NETWORK", + "attackComplexity": "LOW", + "privilegesRequired": "NONE", + "userInteraction": "REQUIRED", + "scope": "UNCHANGED", + "confidentialityImpact": "HIGH", + "integrityImpact": "HIGH", + "availabilityImpact": "HIGH", + "baseScore": 8.8, + "baseSeverity": "HIGH" + }, + "exploitabilityScore": 2.8, + "impactScore": 5.9 + } + ], + "cvssMetricV2": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "cvssData": { + "version": "2.0", + "vectorString": "AV:N/AC:M/Au:N/C:P/I:P/A:P", + "accessVector": "NETWORK", + "accessComplexity": "MEDIUM", + "authentication": "NONE", + "confidentialityImpact": "PARTIAL", + "integrityImpact": "PARTIAL", + "availabilityImpact": "PARTIAL", + "baseScore": 6.8 + }, + "baseSeverity": "MEDIUM", + "exploitabilityScore": 8.6, + "impactScore": 6.4, + "acInsufInfo": false, + "obtainAllPrivilege": false, + "obtainUserPrivilege": false, + "obtainOtherPrivilege": false, + "userInteractionRequired": true + } + ] + }, + "weaknesses": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "description": [ + { + "lang": "en", + "value": "CWE-415" + } + ] + } + ], + "configurations": [ + { + "nodes": [ + { + "operator": "OR", + "negate": false, + "cpeMatch": [ + { + "vulnerable": true, + "criteria": "cpe:2.3:a:libgd:libgd:2.2.5:*:*:*:*:*:*:*", + "matchCriteriaId": "C257CC1C-BF6A-4125-AA61-9C2D09096084" + } + ] + } + ] + }, + { + "nodes": [ + { + "operator": "OR", + "negate": false, + "cpeMatch": [ + { + "vulnerable": true, + "criteria": "cpe:2.3:o:canonical:ubuntu_linux:14.04:*:*:*:lts:*:*:*", + "matchCriteriaId": "B5A6F2F3-4894-4392-8296-3B8DD2679084" + }, + { + "vulnerable": true, + "criteria": "cpe:2.3:o:canonical:ubuntu_linux:16.04:*:*:*:lts:*:*:*", + "matchCriteriaId": "F7016A2A-8365-4F1A-89A2-7A19F2BCAE5B" + }, + { + "vulnerable": true, + "criteria": "cpe:2.3:o:canonical:ubuntu_linux:18.04:*:*:*:lts:*:*:*", + "matchCriteriaId": "23A7C53F-B80F-4E6A-AFA9-58EEA84BE11D" + } + ] + } + ] + }, + { + "nodes": [ + { + "operator": "OR", + "negate": false, + "cpeMatch": [ + { + "vulnerable": true, + "criteria": "cpe:2.3:o:debian:debian_linux:8.0:*:*:*:*:*:*:*", + "matchCriteriaId": "C11E6FB0-C8C0-4527-9AA0-CB9B316F8F43" + } + ] + } + ] + } + ], + "references": [ + { + "url": "https://github.com/libgd/libgd/issues/447", + "source": "cve@mitre.org", + "tags": [ + "Issue Tracking", + "Third Party Advisory" + ] + }, + { + "url": "https://lists.debian.org/debian-lts-announce/2019/01/msg00028.html", + "source": "cve@mitre.org", + "tags": [ + "Mailing List", + "Third Party Advisory" + ] + }, + { + "url": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/3CZ2QADQTKRHTGB2AHD7J4QQNDLBEMM6/", + "source": "cve@mitre.org" + }, + { + "url": "https://security.gentoo.org/glsa/201903-18", + "source": "cve@mitre.org", + "tags": [ + "Third Party Advisory" + ] + }, + { + "url": "https://usn.ubuntu.com/3755-1/", + "source": "cve@mitre.org", + "tags": [ + "Mitigation", + "Third Party Advisory" + ] + } + ] + } +} diff --git a/pkg/process/v1/transformers/nvd/test-fixtures/unmarshal-test.json b/pkg/process/v1/transformers/nvd/test-fixtures/unmarshal-test.json new file mode 100644 index 00000000..2dc698fa --- /dev/null +++ b/pkg/process/v1/transformers/nvd/test-fixtures/unmarshal-test.json @@ -0,0 +1,109 @@ +{ + "cve": { + "id": "CVE-2003-0349", + "sourceIdentifier": "cve@mitre.org", + "published": "2003-07-24T04:00:00.000", + "lastModified": "2018-10-12T21:32:41.083", + "vulnStatus": "Modified", + "descriptions": [ + { + "lang": "en", + "value": "Buffer overflow in the streaming media component for logging multicast requests in the ISAPI for the logging capability of Microsoft Windows Media Services (nsiislog.dll), as installed in IIS 5.0, allows remote attackers to execute arbitrary code via a large POST request to nsiislog.dll." + }, + { + "lang": "es", + "value": "Desbordamiento de búfer en el componente de secuenciamiento (streaming) de medios para registrar peticiones de multidifusión en la librería ISAPI de la capacidad de registro (logging) de Microsoft Windows Media Services (nsiislog.dll), como el instalado en IIS 5.9, permite a atacantes remotos ejecutar código arbitrario mediante una petición POST larga a nsiislog.dll." + } + ], + "metrics": { + "cvssMetricV2": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "cvssData": { + "version": "2.0", + "vectorString": "AV:N/AC:L/Au:N/C:P/I:P/A:P", + "accessVector": "NETWORK", + "accessComplexity": "LOW", + "authentication": "NONE", + "confidentialityImpact": "PARTIAL", + "integrityImpact": "PARTIAL", + "availabilityImpact": "PARTIAL", + "baseScore": 7.5 + }, + "baseSeverity": "HIGH", + "exploitabilityScore": 10.0, + "impactScore": 6.4, + "acInsufInfo": false, + "obtainAllPrivilege": false, + "obtainUserPrivilege": true, + "obtainOtherPrivilege": false, + "userInteractionRequired": false + } + ] + }, + "weaknesses": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "description": [ + { + "lang": "en", + "value": "NVD-CWE-Other" + } + ] + } + ], + "configurations": [ + { + "nodes": [ + { + "operator": "OR", + "negate": false, + "cpeMatch": [ + { + "vulnerable": true, + "criteria": "cpe:2.3:o:microsoft:windows_2000:*:*:*:*:*:*:*:*", + "matchCriteriaId": "4E545C63-FE9C-4CA1-AF0F-D999D84D2AFD" + } + ] + } + ] + } + ], + "references": [ + { + "url": "http://marc.info/?l=bugtraq&m=105665030925504&w=2", + "source": "cve@mitre.org" + }, + { + "url": "http://securitytracker.com/id?1007059", + "source": "cve@mitre.org" + }, + { + "url": "http://www.kb.cert.org/vuls/id/113716", + "source": "cve@mitre.org", + "tags": [ + "US Government Resource" + ] + }, + { + "url": "http://www.ntbugtraq.com/default.asp?pid=36&sid=1&A2=ind0306&L=NTBUGTRAQ&P=R4563", + "source": "cve@mitre.org", + "tags": [ + "Exploit", + "Patch", + "Vendor Advisory" + ] + }, + { + "url": "https://docs.microsoft.com/en-us/security-updates/securitybulletins/2003/ms03-022", + "source": "cve@mitre.org" + }, + { + "url": "https://oval.cisecurity.org/repository/search/definition/oval%3Aorg.mitre.oval%3Adef%3A938", + "source": "cve@mitre.org" + } + ] + } +} diff --git a/pkg/process/v1/transformers/nvd/test-fixtures/version-range.json b/pkg/process/v1/transformers/nvd/test-fixtures/version-range.json new file mode 100644 index 00000000..3df5b86d --- /dev/null +++ b/pkg/process/v1/transformers/nvd/test-fixtures/version-range.json @@ -0,0 +1,121 @@ +{ + "cve": { + "id": "CVE-2018-5487", + "sourceIdentifier": "security-alert@netapp.com", + "published": "2018-05-24T14:29:00.390", + "lastModified": "2018-07-05T13:52:30.627", + "vulnStatus": "Analyzed", + "descriptions": [ + { + "lang": "en", + "value": "NetApp OnCommand Unified Manager for Linux versions 7.2 through 7.3 ship with the Java Management Extension Remote Method Invocation (JMX RMI) service bound to the network, and are susceptible to unauthenticated remote code execution." + }, + { + "lang": "es", + "value": "NetApp OnCommand Unified Manager for Linux, de la versión 7.2 hasta la 7.3, se distribuye con el servicio Java Management Extension Remote Method Invocation (JMX RMI) enlazado a la red y es susceptible a la ejecución remota de código sin autenticación." + } + ], + "metrics": { + "cvssMetricV30": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "cvssData": { + "version": "3.0", + "vectorString": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + "attackVector": "NETWORK", + "attackComplexity": "LOW", + "privilegesRequired": "NONE", + "userInteraction": "NONE", + "scope": "UNCHANGED", + "confidentialityImpact": "HIGH", + "integrityImpact": "HIGH", + "availabilityImpact": "HIGH", + "baseScore": 9.8, + "baseSeverity": "CRITICAL" + }, + "exploitabilityScore": 3.9, + "impactScore": 5.9 + } + ], + "cvssMetricV2": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "cvssData": { + "version": "2.0", + "vectorString": "AV:N/AC:L/Au:N/C:P/I:P/A:P", + "accessVector": "NETWORK", + "accessComplexity": "LOW", + "authentication": "NONE", + "confidentialityImpact": "PARTIAL", + "integrityImpact": "PARTIAL", + "availabilityImpact": "PARTIAL", + "baseScore": 7.5 + }, + "baseSeverity": "HIGH", + "exploitabilityScore": 10.0, + "impactScore": 6.4, + "acInsufInfo": true, + "obtainAllPrivilege": false, + "obtainUserPrivilege": false, + "obtainOtherPrivilege": false, + "userInteractionRequired": false + } + ] + }, + "weaknesses": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "description": [ + { + "lang": "en", + "value": "CWE-20" + } + ] + } + ], + "configurations": [ + { + "operator": "AND", + "nodes": [ + { + "operator": "OR", + "negate": false, + "cpeMatch": [ + { + "vulnerable": true, + "criteria": "cpe:2.3:a:netapp:oncommand_unified_manager:*:*:*:*:*:*:*:*", + "versionStartIncluding": "7.2", + "versionEndIncluding": "7.3", + "matchCriteriaId": "A5949307-3E9B-441F-B008-81A0E0228DC0" + } + ] + }, + { + "operator": "OR", + "negate": false, + "cpeMatch": [ + { + "vulnerable": false, + "criteria": "cpe:2.3:o:linux:linux_kernel:-:*:*:*:*:*:*:*", + "matchCriteriaId": "703AF700-7A70-47E2-BC3A-7FD03B3CA9C1" + } + ] + } + ] + } + ], + "references": [ + { + "url": "https://security.netapp.com/advisory/ntap-20180523-0001/", + "source": "security-alert@netapp.com", + "tags": [ + "Patch", + "Vendor Advisory" + ] + } + ] + } +} diff --git a/pkg/process/v1/transformers/nvd/transform.go b/pkg/process/v1/transformers/nvd/transform.go new file mode 100644 index 00000000..15a7780c --- /dev/null +++ b/pkg/process/v1/transformers/nvd/transform.go @@ -0,0 +1,111 @@ +package nvd + +import ( + "strings" + + "github.com/anchore/grype-db/pkg/provider/unmarshal/nvd" + + "github.com/anchore/grype-db/internal" + "github.com/anchore/grype-db/pkg/data" + "github.com/anchore/grype-db/pkg/process/v1/transformers" + "github.com/anchore/grype-db/pkg/provider/unmarshal" + grypeDB "github.com/anchore/grype/grype/db/v1" +) + +const ( + // TODO: tech debt from a previous design + feed = "nvdv2" + group = "nvdv2:cves" +) + +func Transform(vulnerability unmarshal.NVDVulnerability) ([]data.Entry, error) { + var allVulns []grypeDB.Vulnerability + + recordSource := grypeDB.RecordSource(feed, group) + + uniquePkgs := findUniquePkgs(vulnerability.Configurations...) + + // extract all links + var links []string + for _, externalRefs := range vulnerability.References { + // TODO: should we capture other information here? + if externalRefs.URL != "" { + links = append(links, externalRefs.URL) + } + } + // duplicate the vulnerabilities based on the set of unique packages the vulnerability is for + for _, p := range uniquePkgs.All() { + matches := uniquePkgs.Matches(p) + cpes := internal.NewStringSet() + for _, m := range matches { + cpes.Add(m.Criteria) + } + + // create vulnerability entry + vuln := grypeDB.Vulnerability{ + ID: vulnerability.ID, + RecordSource: recordSource, + VersionConstraint: buildConstraints(uniquePkgs.Matches(p)), + VersionFormat: "unknown", // TODO: derive this from the target software + PackageName: p.Product, + Namespace: "nvd", // should the vendor be here? or in other metadata? + ProxyVulnerabilities: []string{}, + CPEs: cpes.ToSlice(), + } + + allVulns = append(allVulns, vuln) + } + + // If all the CPEs are invalid and no vulnerabilities were generated then there is no point + // in creating metadata, so just return + if len(allVulns) == 0 { + return nil, nil + } + + // create vulnerability metadata entry (a single entry keyed off of the vulnerability ID) + allCVSS := vulnerability.CVSS() + + metadata := grypeDB.VulnerabilityMetadata{ + ID: vulnerability.ID, + RecordSource: recordSource, + Severity: nvd.CvssSummaries(allCVSS).Sorted().Severity(), + Links: links, + Description: vulnerability.Description(), + } + + for _, c := range allCVSS { + if strings.HasPrefix(c.Version, "2.") { + newCvss := &grypeDB.Cvss{ + BaseScore: c.BaseScore, + Vector: c.Vector, + } + if c.ExploitabilityScore != nil { + newCvss.ExploitabilityScore = *c.ExploitabilityScore + } + if c.ImpactScore != nil { + newCvss.ImpactScore = *c.ImpactScore + } + metadata.CvssV2 = newCvss + break + } + } + + for _, c := range allCVSS { + if strings.HasPrefix(c.Version, "3.") { + newCvss := &grypeDB.Cvss{ + BaseScore: c.BaseScore, + Vector: c.Vector, + } + if c.ExploitabilityScore != nil { + newCvss.ExploitabilityScore = *c.ExploitabilityScore + } + if c.ImpactScore != nil { + newCvss.ImpactScore = *c.ImpactScore + } + metadata.CvssV3 = newCvss + break + } + } + + return transformers.NewEntries(allVulns, metadata), nil +} diff --git a/pkg/process/v1/transformers/nvd/transform_test.go b/pkg/process/v1/transformers/nvd/transform_test.go new file mode 100644 index 00000000..79b043dd --- /dev/null +++ b/pkg/process/v1/transformers/nvd/transform_test.go @@ -0,0 +1,191 @@ +package nvd + +import ( + "github.com/stretchr/testify/require" + "os" + "testing" + + testUtils "github.com/anchore/grype-db/pkg/process/tests" + "github.com/anchore/grype-db/pkg/provider/unmarshal" + grypeDB "github.com/anchore/grype/grype/db/v1" + "github.com/go-test/deep" + "github.com/stretchr/testify/assert" +) + +const recordSource = "nvdv2:cves" + +func TestUnmarshalVulnerabilitiesEntries(t *testing.T) { + f, err := os.Open("test-fixtures/unmarshal-test.json") + require.NoError(t, err) + defer testUtils.CloseFile(f) + + entries, err := unmarshal.NvdVulnerabilityEntries(f) + require.NoError(t, err) + + assert.Len(t, entries, 1) +} + +func TestParseVulnerabilitiesAllEntries(t *testing.T) { + tests := []struct { + name string + numEntries int + fixture string + vulns []grypeDB.Vulnerability + metadata grypeDB.VulnerabilityMetadata + }{ + { + name: "AppVersionRange", + numEntries: 1, + fixture: "test-fixtures/version-range.json", + vulns: []grypeDB.Vulnerability{ + { + ID: "CVE-2018-5487", + RecordSource: recordSource, + PackageName: "oncommand_unified_manager", + VersionConstraint: ">= 7.2, <= 7.3", + VersionFormat: "unknown", // TODO: this should reference a format, yes? (not a string) + ProxyVulnerabilities: []string{}, + Namespace: "nvd", + CPEs: []string{"cpe:2.3:a:netapp:oncommand_unified_manager:*:*:*:*:*:*:*:*"}, + }, + }, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "CVE-2018-5487", + RecordSource: recordSource, + Severity: "Critical", + Links: []string{"https://security.netapp.com/advisory/ntap-20180523-0001/"}, + Description: "NetApp OnCommand Unified Manager for Linux versions 7.2 through 7.3 ship with the Java Management Extension Remote Method Invocation (JMX RMI) service bound to the network, and are susceptible to unauthenticated remote code execution.", + CvssV2: &grypeDB.Cvss{ + BaseScore: 7.5, + ExploitabilityScore: 10, + ImpactScore: 6.4, + Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P", + }, + CvssV3: &grypeDB.Cvss{ + BaseScore: 9.8, + ExploitabilityScore: 3.9, + ImpactScore: 5.9, + Vector: "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + }, + }, + }, + { + name: "App+OS", + numEntries: 1, + fixture: "test-fixtures/single-package-multi-distro.json", + vulns: []grypeDB.Vulnerability{ + { + ID: "CVE-2018-1000222", + RecordSource: recordSource, + PackageName: "libgd", + VersionConstraint: "= 2.2.5", + VersionFormat: "unknown", // TODO: this should reference a format, yes? (not a string) + ProxyVulnerabilities: []string{}, + Namespace: "nvd", + CPEs: []string{"cpe:2.3:a:libgd:libgd:2.2.5:*:*:*:*:*:*:*"}, + }, + // TODO: Question: should this match also the OS's? (as in the vulnerable_cpes list)... this seems wrong! + }, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "CVE-2018-1000222", + RecordSource: recordSource, + Severity: "High", + Links: []string{"https://github.com/libgd/libgd/issues/447", "https://lists.debian.org/debian-lts-announce/2019/01/msg00028.html", "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/3CZ2QADQTKRHTGB2AHD7J4QQNDLBEMM6/", "https://security.gentoo.org/glsa/201903-18", "https://usn.ubuntu.com/3755-1/"}, + Description: "Libgd version 2.2.5 contains a Double Free Vulnerability vulnerability in gdImageBmpPtr Function that can result in Remote Code Execution . This attack appear to be exploitable via Specially Crafted Jpeg Image can trigger double free. This vulnerability appears to have been fixed in after commit ac16bdf2d41724b5a65255d4c28fb0ec46bc42f5.", + CvssV2: &grypeDB.Cvss{ + BaseScore: 6.8, + ExploitabilityScore: 8.6, + ImpactScore: 6.4, + Vector: "AV:N/AC:M/Au:N/C:P/I:P/A:P", + }, + CvssV3: &grypeDB.Cvss{ + BaseScore: 8.8, + ExploitabilityScore: 2.8, + ImpactScore: 5.9, + Vector: "CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H", + }, + }, + }, + { + name: "AppCompoundVersionRange", + numEntries: 1, + fixture: "test-fixtures/compound-pkg.json", + vulns: []grypeDB.Vulnerability{ + { + ID: "CVE-2018-10189", + RecordSource: recordSource, + PackageName: "mautic", + VersionConstraint: ">= 1.0.0, <= 1.4.1 || >= 2.0.0, < 2.13.0", + VersionFormat: "unknown", // TODO: this should reference a format, yes? (not a string) + ProxyVulnerabilities: []string{}, + Namespace: "nvd", + CPEs: []string{"cpe:2.3:a:mautic:mautic:*:*:*:*:*:*:*:*"}, // note: entry was dedupicated + }, + }, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "CVE-2018-10189", + RecordSource: recordSource, + Severity: "High", + Links: []string{"https://github.com/mautic/mautic/releases/tag/2.13.0"}, + Description: "An issue was discovered in Mautic 1.x and 2.x before 2.13.0. It is possible to systematically emulate tracking cookies per contact due to tracking the contact by their auto-incremented ID. Thus, a third party can manipulate the cookie value with +1 to systematically assume being tracked as each contact in Mautic. It is then possible to retrieve information about the contact through forms that have progressive profiling enabled.", + CvssV2: &grypeDB.Cvss{ + BaseScore: 5, + ExploitabilityScore: 10, + ImpactScore: 2.9, + Vector: "AV:N/AC:L/Au:N/C:P/I:N/A:N", + }, + CvssV3: &grypeDB.Cvss{ + BaseScore: 7.5, + ExploitabilityScore: 3.9, + ImpactScore: 3.6, + Vector: "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N", + }, + }, + }, + { + name: "InvalidCPE", + numEntries: 1, + fixture: "test-fixtures/invalid_cpe.json", + vulns: []grypeDB.Vulnerability{}, + metadata: grypeDB.VulnerabilityMetadata{}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + f, err := os.Open(test.fixture) + require.NoError(t, err) + t.Cleanup(func() { + assert.NoError(t, f.Close()) + }) + + entries, err := unmarshal.NvdVulnerabilityEntries(f) + assert.NoError(t, err) + + var vulns []grypeDB.Vulnerability + for _, entry := range entries { + dataEntries, err := Transform(entry.Cve) + assert.NoError(t, err) + + for _, entry := range dataEntries { + switch vuln := entry.Data.(type) { + case grypeDB.Vulnerability: + vulns = append(vulns, vuln) + case grypeDB.VulnerabilityMetadata: + // check metadata + if diff := deep.Equal(test.metadata, vuln); diff != nil { + for _, d := range diff { + t.Errorf("metadata diff: %+v", d) + } + } + default: + t.Fatalf("unexpected condition: data entry does not have a vulnerability or a metadata") + } + } + + } + + assert.ElementsMatch(t, test.vulns, vulns) + }) + } +} diff --git a/pkg/process/v1/transformers/nvd/unique_pkg.go b/pkg/process/v1/transformers/nvd/unique_pkg.go new file mode 100644 index 00000000..48791517 --- /dev/null +++ b/pkg/process/v1/transformers/nvd/unique_pkg.go @@ -0,0 +1,115 @@ +package nvd + +import ( + "fmt" + "strings" + + "github.com/anchore/grype-db/pkg/provider/unmarshal/nvd" + + "github.com/anchore/grype-db/internal/log" + "github.com/anchore/grype-db/pkg/process/common" + "github.com/umisama/go-cpe" +) + +const ( + ANY = "*" + NA = "-" +) + +type pkgCandidate struct { + Product string + Vendor string + TargetSoftware string +} + +func (p pkgCandidate) String() string { + return fmt.Sprintf("%s|%s|%s", p.Vendor, p.Product, p.TargetSoftware) +} + +func newPkgCandidate(match nvd.CpeMatch) (*pkgCandidate, error) { + // we are only interested in packages that are vulnerable (not related to secondary match conditioning) + if !match.Vulnerable { + return nil, nil + } + + c, err := cpe.NewItemFromFormattedString(match.Criteria) + if err != nil { + return nil, fmt.Errorf("unable to create uniquePkgEntry from '%s': %w", match.Criteria, err) + } + + // we are only interested in applications, not hardware or operating systems + if c.Part() != cpe.Application { + return nil, nil + } + + return &pkgCandidate{ + Product: c.Product().String(), + Vendor: c.Vendor().String(), + TargetSoftware: c.TargetSw().String(), + }, nil +} + +func findUniquePkgs(cfgs ...nvd.Configuration) uniquePkgTracker { + set := newUniquePkgTracker() + for _, c := range cfgs { + _findUniquePkgs(set, c.Nodes...) + } + return set +} + +func _findUniquePkgs(set uniquePkgTracker, ns ...nvd.Node) { + if len(ns) == 0 { + return + } + for _, node := range ns { + for _, match := range node.CpeMatch { + candidate, err := newPkgCandidate(match) + if err != nil { + // Do not halt all execution because of being unable to create + // a PkgCandidate. This can happen when a CPE is invalid which + // could avoid creating a database + log.Debugf("unable processing uniquePkg: %v", err) + continue + } + if candidate != nil { + set.Add(*candidate, match) + } + } + } +} + +func buildConstraints(matches []nvd.CpeMatch) string { + constraints := make([]string, 0) + for _, match := range matches { + constraints = append(constraints, buildConstraint(match)) + } + return common.OrConstraints(constraints...) +} + +func buildConstraint(match nvd.CpeMatch) string { + constraints := make([]string, 0) + if match.VersionStartIncluding != nil && *match.VersionStartIncluding != "" { + constraints = append(constraints, fmt.Sprintf(">= %s", *match.VersionStartIncluding)) + } else if match.VersionStartExcluding != nil && *match.VersionStartExcluding != "" { + constraints = append(constraints, fmt.Sprintf("> %s", *match.VersionStartExcluding)) + } + + if match.VersionEndIncluding != nil && *match.VersionEndIncluding != "" { + constraints = append(constraints, fmt.Sprintf("<= %s", *match.VersionEndIncluding)) + } else if match.VersionEndExcluding != nil && *match.VersionEndExcluding != "" { + constraints = append(constraints, fmt.Sprintf("< %s", *match.VersionEndExcluding)) + } + + if len(constraints) == 0 { + c, err := cpe.NewItemFromFormattedString(match.Criteria) + if err != nil { + return "" + } + version := c.Version().String() + if version != ANY && version != NA { + constraints = append(constraints, fmt.Sprintf("= %s", version)) + } + } + + return strings.Join(constraints, ", ") +} diff --git a/pkg/process/v1/transformers/nvd/unique_pkg_test.go b/pkg/process/v1/transformers/nvd/unique_pkg_test.go new file mode 100644 index 00000000..9a98731f --- /dev/null +++ b/pkg/process/v1/transformers/nvd/unique_pkg_test.go @@ -0,0 +1,352 @@ +package nvd + +import ( + "github.com/anchore/grype-db/pkg/provider/unmarshal/nvd" + "testing" + + "github.com/sergi/go-diff/diffmatchpatch" +) + +func newUniquePkgTrackerFromSlice(candidates []pkgCandidate) uniquePkgTracker { + set := newUniquePkgTracker() + for _, c := range candidates { + set[c] = nil + } + return set +} + +func TestFindUniquePkgs(t *testing.T) { + tests := []struct { + name string + nodes []nvd.Node + expected uniquePkgTracker + }{ + { + name: "simple-match", + nodes: []nvd.Node{ + { + CpeMatch: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendor:product:2.2.0:*:*:*:*:target:*:*", + Vulnerable: true, + }, + }, + }, + }, + expected: newUniquePkgTrackerFromSlice( + []pkgCandidate{ + { + Product: "product", + Vendor: "vendor", + TargetSoftware: "target", + }, + }), + }, + { + name: "skip-hw", + nodes: []nvd.Node{ + { + CpeMatch: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:h:vendor:product:2.2.0:*:*:*:*:target:*:*", + Vulnerable: true, + }, + }, + }, + }, + expected: newUniquePkgTrackerFromSlice([]pkgCandidate{}), + }, + { + name: "skip-os", + nodes: []nvd.Node{ + { + CpeMatch: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:o:vendor:product:2.2.0:*:*:*:*:target:*:*", + Vulnerable: true, + }, + }, + }, + }, + expected: newUniquePkgTrackerFromSlice([]pkgCandidate{}), + }, + { + name: "duplicate-by-product", + nodes: []nvd.Node{ + { + CpeMatch: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendor:productA:3.3.3:*:*:*:*:target:*:*", + Vulnerable: true, + }, + { + Criteria: "cpe:2.3:a:vendor:productB:2.2.0:*:*:*:*:target:*:*", + Vulnerable: true, + }, + }, + Operator: "OR", + }, + }, + expected: newUniquePkgTrackerFromSlice( + []pkgCandidate{ + { + Product: "productA", + Vendor: "vendor", + TargetSoftware: "target", + }, + { + Product: "productB", + Vendor: "vendor", + TargetSoftware: "target", + }, + }), + }, + { + name: "duplicate-by-target", + nodes: []nvd.Node{ + { + CpeMatch: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendor:product:3.3.3:*:*:*:*:targetA:*:*", + Vulnerable: true, + }, + { + Criteria: "cpe:2.3:a:vendor:product:2.2.0:*:*:*:*:targetB:*:*", + Vulnerable: true, + }, + }, + Operator: "OR", + }, + }, + expected: newUniquePkgTrackerFromSlice( + []pkgCandidate{ + { + Product: "product", + Vendor: "vendor", + TargetSoftware: "targetA", + }, + { + Product: "product", + Vendor: "vendor", + TargetSoftware: "targetB", + }, + }), + }, + { + name: "duplicate-by-vendor", + nodes: []nvd.Node{ + { + CpeMatch: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendorA:product:3.3.3:*:*:*:*:target:*:*", + Vulnerable: true, + }, + { + Criteria: "cpe:2.3:a:vendorB:product:2.2.0:*:*:*:*:target:*:*", + Vulnerable: true, + }, + }, + Operator: "OR", + }, + }, + expected: newUniquePkgTrackerFromSlice( + []pkgCandidate{ + { + Product: "product", + Vendor: "vendorA", + TargetSoftware: "target", + }, + { + Product: "product", + Vendor: "vendorB", + TargetSoftware: "target", + }, + }), + }, + { + name: "de-duplicate-case", + nodes: []nvd.Node{ + { + CpeMatch: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendor:product:3.3.3:A:B:C:D:target:E:F", + Vulnerable: true, + }, + { + Criteria: "cpe:2.3:a:vendor:product:2.2.0:Q:R:S:T:target:U:V", + Vulnerable: true, + }, + }, + Operator: "OR", + }, + }, + expected: newUniquePkgTrackerFromSlice( + []pkgCandidate{ + { + Product: "product", + Vendor: "vendor", + TargetSoftware: "target", + }, + }), + }, + { + name: "duplicate-from-nested-nodes", + nodes: []nvd.Node{ + { + CpeMatch: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendorB:product:2.2.0:*:*:*:*:target:*:*", + Vulnerable: true, + }, + }, + Operator: "OR", + }, + { + CpeMatch: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendorA:product:2.2.0:*:*:*:*:target:*:*", + Vulnerable: true, + }, + }, + Operator: "OR", + }, + }, + expected: newUniquePkgTrackerFromSlice( + []pkgCandidate{ + { + Product: "product", + Vendor: "vendorA", + TargetSoftware: "target", + }, + { + Product: "product", + Vendor: "vendorB", + TargetSoftware: "target", + }, + }), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actual := findUniquePkgs(nvd.Configuration{Nodes: test.nodes}) + missing, extra := test.expected.Diff(actual) + if len(missing) != 0 { + for _, c := range missing { + t.Errorf("missing candidate: %+v", c) + } + } + + if len(extra) != 0 { + for _, c := range extra { + t.Errorf("extra candidate: %+v", c) + } + } + }) + } + +} + +func strRef(s string) *string { + return &s +} + +func TestBuildConstraints(t *testing.T) { + tests := []struct { + name string + matches []nvd.CpeMatch + expected string + }{ + { + name: "Equals", + matches: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendor:product:2.2.0:*:*:*:*:target:*:*", + }, + }, + expected: "= 2.2.0", + }, + { + name: "VersionEndExcluding", + matches: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", + VersionEndExcluding: strRef("2.3.0"), + }, + }, + expected: "< 2.3.0", + }, + { + name: "VersionEndIncluding", + matches: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", + VersionEndIncluding: strRef("2.3.0"), + }, + }, + expected: "<= 2.3.0", + }, + { + name: "VersionStartExcluding", + matches: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", + VersionStartExcluding: strRef("2.3.0"), + }, + }, + expected: "> 2.3.0", + }, + { + name: "VersionStartIncluding", + matches: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", + VersionStartIncluding: strRef("2.3.0"), + }, + }, + expected: ">= 2.3.0", + }, + { + name: "Version Range", + matches: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", + VersionStartIncluding: strRef("2.3.0"), + VersionEndIncluding: strRef("2.5.0"), + }, + }, + expected: ">= 2.3.0, <= 2.5.0", + }, + { + name: "Multiple Version Ranges", + matches: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", + VersionStartIncluding: strRef("2.3.0"), + VersionEndIncluding: strRef("2.5.0"), + }, + { + Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", + VersionStartExcluding: strRef("3.3.0"), + VersionEndExcluding: strRef("3.5.0"), + }, + }, + expected: ">= 2.3.0, <= 2.5.0 || > 3.3.0, < 3.5.0", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actual := buildConstraints(test.matches) + + if actual != test.expected { + dmp := diffmatchpatch.New() + diffs := dmp.DiffMain(actual, test.expected, true) + t.Errorf("Expected: %q", test.expected) + t.Errorf("Got : %q", actual) + t.Errorf("Diff : %q", dmp.DiffPrettyText(diffs)) + } + }) + } + +} diff --git a/pkg/process/v1/transformers/nvd/unique_pkg_tracker.go b/pkg/process/v1/transformers/nvd/unique_pkg_tracker.go new file mode 100644 index 00000000..2b7e405d --- /dev/null +++ b/pkg/process/v1/transformers/nvd/unique_pkg_tracker.go @@ -0,0 +1,64 @@ +package nvd + +import ( + "sort" + + "github.com/anchore/grype-db/pkg/provider/unmarshal/nvd" +) + +type uniquePkgTracker map[pkgCandidate][]nvd.CpeMatch + +func newUniquePkgTracker() uniquePkgTracker { + return make(uniquePkgTracker) +} + +func (s uniquePkgTracker) Diff(other uniquePkgTracker) (missing []pkgCandidate, extra []pkgCandidate) { + for k := range s { + if !other.Contains(k) { + missing = append(missing, k) + } + } + + for k := range other { + if !s.Contains(k) { + extra = append(extra, k) + } + } + + return +} + +func (s uniquePkgTracker) Matches(i pkgCandidate) []nvd.CpeMatch { + return s[i] +} + +func (s uniquePkgTracker) Add(i pkgCandidate, match nvd.CpeMatch) { + if _, ok := s[i]; !ok { + s[i] = make([]nvd.CpeMatch, 0) + } + s[i] = append(s[i], match) +} + +func (s uniquePkgTracker) Remove(i pkgCandidate) { + delete(s, i) +} + +func (s uniquePkgTracker) Contains(i pkgCandidate) bool { + _, ok := s[i] + return ok +} + +func (s uniquePkgTracker) All() []pkgCandidate { + res := make([]pkgCandidate, len(s)) + idx := 0 + for k := range s { + res[idx] = k + idx++ + } + + sort.SliceStable(res, func(i, j int) bool { + return res[i].String() < res[j].String() + }) + + return res +} diff --git a/pkg/process/v1/transformers/os/test-fixtures/alpine-3.9.json b/pkg/process/v1/transformers/os/test-fixtures/alpine-3.9.json new file mode 100644 index 00000000..b9d84395 --- /dev/null +++ b/pkg/process/v1/transformers/os/test-fixtures/alpine-3.9.json @@ -0,0 +1,28 @@ +[ + { + "Vulnerability": { + "CVSS": [], + "Description": "", + "FixedIn": [ + { + "Name": "xen", + "NamespaceName": "alpine:3.9", + "Version": "4.11.1-r0", + "VersionFormat": "apk" + } + ], + "Link": "http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-19967", + "Metadata": { + "NVD": { + "CVSSv2": { + "Score": 4.9, + "Vectors": "AV:L/AC:L/Au:N/C:N/I:N/A:C" + } + } + }, + "Name": "CVE-2018-19967", + "NamespaceName": "alpine:3.9", + "Severity": "Medium" + } + } +] \ No newline at end of file diff --git a/pkg/process/v1/transformers/os/test-fixtures/amzn.json b/pkg/process/v1/transformers/os/test-fixtures/amzn.json new file mode 100644 index 00000000..a862c32e --- /dev/null +++ b/pkg/process/v1/transformers/os/test-fixtures/amzn.json @@ -0,0 +1,49 @@ +[ + { + "Vulnerability": { + "Description": "", + "FixedIn": [ + { + "Name": "389-ds-base", + "NamespaceName": "amzn:2", + "Version": "1.3.8.4-15.amzn2.0.1", + "VersionFormat": "rpm" + }, + { + "Name": "389-ds-base-debuginfo", + "NamespaceName": "amzn:2", + "Version": "1.3.8.4-15.amzn2.0.1", + "VersionFormat": "rpm" + }, + { + "Name": "389-ds-base-devel", + "NamespaceName": "amzn:2", + "Version": "1.3.8.4-15.amzn2.0.1", + "VersionFormat": "rpm" + }, + { + "Name": "389-ds-base-libs", + "NamespaceName": "amzn:2", + "Version": "1.3.8.4-15.amzn2.0.1", + "VersionFormat": "rpm" + }, + { + "Name": "389-ds-base-snmp", + "NamespaceName": "amzn:2", + "Version": "1.3.8.4-15.amzn2.0.1", + "VersionFormat": "rpm" + } + ], + "Link": "https://alas.aws.amazon.com/AL2/ALAS-2018-1106.html", + "Metadata": { + "CVE": [ + + {"Name": "CVE-2018-14648"} + ] + }, + "Name": "ALAS-2018-1106", + "NamespaceName": "amzn:2", + "Severity": "Medium" + } + } +] \ No newline at end of file diff --git a/pkg/process/v1/transformers/os/test-fixtures/debian-8-multiple-entries-for-same-package.json b/pkg/process/v1/transformers/os/test-fixtures/debian-8-multiple-entries-for-same-package.json new file mode 100644 index 00000000..5025b56e --- /dev/null +++ b/pkg/process/v1/transformers/os/test-fixtures/debian-8-multiple-entries-for-same-package.json @@ -0,0 +1,62 @@ +[ + { + "Vulnerability": { + "CVSS": [], + "Description": "", + "FixedIn": [ + { + "Name": "rsyslog", + "NamespaceName": "debian:8", + "VendorAdvisory": { + "AdvisorySummary": [], + "NoAdvisory": false + }, + "Version": "5.7.4-1", + "VersionFormat": "dpkg" + } + ], + "Link": "https://security-tracker.debian.org/tracker/CVE-2011-4623", + "Metadata": { + "NVD": { + "CVSSv2": { + "Score": 2.1, + "Vectors": "AV:L/AC:L/Au:N/C:N/I:N/A:P" + } + } + }, + "Name": "CVE-2011-4623", + "NamespaceName": "debian:8", + "Severity": "Low" + } + }, + { + "Vulnerability": { + "CVSS": [], + "Description": "", + "FixedIn": [ + { + "Name": "rsyslog", + "NamespaceName": "debian:8", + "VendorAdvisory": { + "AdvisorySummary": [], + "NoAdvisory": false + }, + "Version": "3.18.6-1", + "VersionFormat": "dpkg" + } + ], + "Link": "https://security-tracker.debian.org/tracker/CVE-2008-5618", + "Metadata": { + "NVD": { + "CVSSv2": { + "Score": 5, + "Vectors": "AV:N/AC:L/Au:N/C:N/I:N/A:P" + } + } + }, + "Name": "CVE-2008-5618", + "NamespaceName": "debian:8", + "Severity": "Low" + } + } +] \ No newline at end of file diff --git a/pkg/process/v1/transformers/os/test-fixtures/debian-8.json b/pkg/process/v1/transformers/os/test-fixtures/debian-8.json new file mode 100644 index 00000000..a758f13c --- /dev/null +++ b/pkg/process/v1/transformers/os/test-fixtures/debian-8.json @@ -0,0 +1,62 @@ +[ + { + "Vulnerability": { + "CVSS": [], + "Description": "", + "FixedIn": [ + { + "Name": "asterisk", + "NamespaceName": "debian:8", + "VendorAdvisory": { + "AdvisorySummary": [], + "NoAdvisory": false + }, + "Version": "1:1.6.2.0~rc3-1", + "VersionFormat": "dpkg" + }, + { + "Name": "auth2db", + "NamespaceName": "debian:8", + "VendorAdvisory": { + "AdvisorySummary": [], + "NoAdvisory": false + }, + "Version": "0.2.5-2+dfsg-1", + "VersionFormat": "dpkg" + }, + { + "Name": "exaile", + "NamespaceName": "debian:8", + "VendorAdvisory": { + "AdvisorySummary": [], + "NoAdvisory": false + }, + "Version": "0.2.14+debian-2.2", + "VersionFormat": "dpkg" + }, + { + "Name": "wordpress", + "NamespaceName": "debian:8", + "VendorAdvisory": { + "AdvisorySummary": [], + "NoAdvisory": false + }, + "Version": "", + "VersionFormat": "dpkg" + } + ], + "Link": "https://security-tracker.debian.org/tracker/CVE-2008-7220", + "Metadata": { + "NVD": { + "CVSSv2": { + "Score": 7.5, + "Vectors": "AV:N/AC:L/Au:N/C:P/I:P/A:P" + } + } + }, + "Name": "CVE-2008-7220", + "NamespaceName": "debian:8", + "Severity": "High" + } + } +] \ No newline at end of file diff --git a/pkg/process/v1/transformers/os/test-fixtures/ol-8-modules.json b/pkg/process/v1/transformers/os/test-fixtures/ol-8-modules.json new file mode 100644 index 00000000..f1d7372b --- /dev/null +++ b/pkg/process/v1/transformers/os/test-fixtures/ol-8-modules.json @@ -0,0 +1,36 @@ +[ + { + "Vulnerability": { + "CVSS": [], + "Description": "A flaw was found in PostgreSQL, where some PostgreSQL extensions did not use the search_path safely in their installation script. This flaw allows an attacker with sufficient privileges to trick an administrator into executing a specially crafted script during the extension's installation or update. The highest threat from this vulnerability is to confidentiality, integrity, as well as system availability.", + "FixedIn": [ + { + "Module": "postgresql:10", + "Name": "postgresql", + "NamespaceName": "ol:8", + "Version": "0:10.14-1.module+el8.2.0+7801+be0fed80", + "VersionFormat": "rpm" + }, + { + "Module": "postgresql:12", + "Name": "postgresql", + "NamespaceName": "ol:8", + "Version": "0:12.5-1.module+el8.3.0+9042+664538f4", + "VersionFormat": "rpm" + }, + { + "Module": "postgresql:9.6", + "Name": "postgresql", + "NamespaceName": "ol:8", + "Version": "0:9.6.20-1.module+el8.3.0+8938+7f0e88b6", + "VersionFormat": "rpm" + } + ], + "Link": "https://access.redhat.com/security/cve/CVE-2020-14350", + "Metadata": {}, + "Name": "CVE-2020-14350", + "NamespaceName": "ol:8", + "Severity": "Medium" + } + } +] \ No newline at end of file diff --git a/pkg/process/v1/transformers/os/test-fixtures/ol-8.json b/pkg/process/v1/transformers/os/test-fixtures/ol-8.json new file mode 100644 index 00000000..09439ece --- /dev/null +++ b/pkg/process/v1/transformers/os/test-fixtures/ol-8.json @@ -0,0 +1,42 @@ +[ + { + "Vulnerability": { + "CVSS": [], + "Description": "", + "FixedIn": [ + { + "Name": "libexif", + "NamespaceName": "ol:8", + "Version": "0:0.6.21-17.el8_2", + "VersionFormat": "rpm" + }, + { + "Name": "libexif-devel", + "NamespaceName": "ol:8", + "Version": "0:0.6.21-17.el8_2", + "VersionFormat": "rpm" + }, + { + "Name": "libexif-dummy", + "NamespaceName": "ol:8", + "Version": "None", + "VersionFormat": "rpm" + } + ], + "Link": "http://linux.oracle.com/errata/ELSA-2020-2550.html", + "Metadata": { + "CVE": [ + { + "Link": "http://linux.oracle.com/cve/CVE-2020-13112.html", + "Name": "CVE-2020-13112" + } + ], + "Issued": "2020-06-15", + "RefId": "ELSA-2020-2550" + }, + "Name": "ELSA-2020-2550", + "NamespaceName": "ol:8", + "Severity": "Medium" + } + } +] \ No newline at end of file diff --git a/pkg/process/v1/transformers/os/test-fixtures/rhel-8-modules.json b/pkg/process/v1/transformers/os/test-fixtures/rhel-8-modules.json new file mode 100644 index 00000000..c0400ad5 --- /dev/null +++ b/pkg/process/v1/transformers/os/test-fixtures/rhel-8-modules.json @@ -0,0 +1,75 @@ +[ + { + "Vulnerability": { + "CVSS": [ + { + "base_metrics": { + "base_score": 7.1, + "base_severity": "High", + "exploitability_score": 1.2, + "impact_score": 5.9 + }, + "status": "verified", + "vector_string": "CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:U/C:H/I:H/A:H", + "version": "3.1" + } + ], + "Description": "A flaw was found in PostgreSQL, where some PostgreSQL extensions did not use the search_path safely in their installation script. This flaw allows an attacker with sufficient privileges to trick an administrator into executing a specially crafted script during the extension's installation or update. The highest threat from this vulnerability is to confidentiality, integrity, as well as system availability.", + "FixedIn": [ + { + "Module": "postgresql:10", + "Name": "postgresql", + "NamespaceName": "rhel:8", + "VendorAdvisory": { + "AdvisorySummary": [ + { + "ID": "RHSA-2020:3669", + "Link": "https://access.redhat.com/errata/RHSA-2020:3669" + } + ], + "NoAdvisory": false + }, + "Version": "0:10.14-1.module+el8.2.0+7801+be0fed80", + "VersionFormat": "rpm" + }, + { + "Module": "postgresql:12", + "Name": "postgresql", + "NamespaceName": "rhel:8", + "VendorAdvisory": { + "AdvisorySummary": [ + { + "ID": "RHSA-2020:5620", + "Link": "https://access.redhat.com/errata/RHSA-2020:5620" + } + ], + "NoAdvisory": false + }, + "Version": "0:12.5-1.module+el8.3.0+9042+664538f4", + "VersionFormat": "rpm" + }, + { + "Module": "postgresql:9.6", + "Name": "postgresql", + "NamespaceName": "rhel:8", + "VendorAdvisory": { + "AdvisorySummary": [ + { + "ID": "RHSA-2020:5619", + "Link": "https://access.redhat.com/errata/RHSA-2020:5619" + } + ], + "NoAdvisory": false + }, + "Version": "0:9.6.20-1.module+el8.3.0+8938+7f0e88b6", + "VersionFormat": "rpm" + } + ], + "Link": "https://access.redhat.com/security/cve/CVE-2020-14350", + "Metadata": {}, + "Name": "CVE-2020-14350", + "NamespaceName": "rhel:8", + "Severity": "Medium" + } + } +] \ No newline at end of file diff --git a/pkg/process/v1/transformers/os/test-fixtures/rhel-8.json b/pkg/process/v1/transformers/os/test-fixtures/rhel-8.json new file mode 100644 index 00000000..2779708c --- /dev/null +++ b/pkg/process/v1/transformers/os/test-fixtures/rhel-8.json @@ -0,0 +1,57 @@ +[ + { + "Vulnerability": { + "CVSS": [ + { + "base_metrics": { + "base_score": 8.8, + "base_severity": "High", + "exploitability_score": 2.8, + "impact_score": 5.9 + }, + "status": "verified", + "vector_string": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H", + "version": "3.1" + } + ], + "Description": "A flaw was found in Mozilla Firefox. A race condition can occur while running the nsDocShell destructor causing a use-after-free memory issue. The highest threat from this vulnerability is to data confidentiality and integrity as well as system availability.", + "FixedIn": [ + { + "Name": "firefox", + "NamespaceName": "rhel:8", + "VendorAdvisory": { + "AdvisorySummary": [ + { + "ID": "RHSA-2020:1341", + "Link": "https://access.redhat.com/errata/RHSA-2020:1341" + } + ], + "NoAdvisory": false + }, + "Version": "0:68.6.1-1.el8_1", + "VersionFormat": "rpm" + }, + { + "Name": "thunderbird", + "NamespaceName": "rhel:8", + "VendorAdvisory": { + "AdvisorySummary": [ + { + "ID": "RHSA-2020:1495", + "Link": "https://access.redhat.com/errata/RHSA-2020:1495" + } + ], + "NoAdvisory": false + }, + "Version": "0:68.7.0-1.el8_1", + "VersionFormat": "rpm" + } + ], + "Link": "https://access.redhat.com/security/cve/CVE-2020-6819", + "Metadata": {}, + "Name": "CVE-2020-6819", + "NamespaceName": "rhel:8", + "Severity": "Critical" + } + } +] \ No newline at end of file diff --git a/pkg/process/v1/transformers/os/test-fixtures/unmarshal-test.json b/pkg/process/v1/transformers/os/test-fixtures/unmarshal-test.json new file mode 100644 index 00000000..edc6d25b --- /dev/null +++ b/pkg/process/v1/transformers/os/test-fixtures/unmarshal-test.json @@ -0,0 +1,104 @@ +[ + { + "Vulnerability": { + "Description": "", + "FixedIn": [ + { + "Name": "389-ds-base", + "NamespaceName": "amzn:2", + "Version": "1.3.8.4-15.amzn2.0.1", + "VersionFormat": "rpm" + }, + { + "Name": "389-ds-base-debuginfo", + "NamespaceName": "amzn:2", + "Version": "1.3.8.4-15.amzn2.0.1", + "VersionFormat": "rpm" + }, + { + "Name": "389-ds-base-devel", + "NamespaceName": "amzn:2", + "Version": "1.3.8.4-15.amzn2.0.1", + "VersionFormat": "rpm" + }, + { + "Name": "389-ds-base-libs", + "NamespaceName": "amzn:2", + "Version": "1.3.8.4-15.amzn2.0.1", + "VersionFormat": "rpm" + }, + { + "Name": "389-ds-base-snmp", + "NamespaceName": "amzn:2", + "Version": "1.3.8.4-15.amzn2.0.1", + "VersionFormat": "rpm" + } + ], + "Link": "https://alas.aws.amazon.com/AL2/ALAS-2018-1106.html", + "Metadata": { + "CVE": [ + {"Name": "CVE-2018-14648"} + ] + }, + "Name": "ALAS-2018-1106", + "NamespaceName": "amzn:2", + "Severity": "Medium" + } + }, + { + "Vulnerability": { + "Description": "", + "FixedIn": [ + { + "Name": "kernel-livepatch-4.14.173-137.228", + "NamespaceName": "amzn:2", + "Version": "1.0-3.amzn2", + "VersionFormat": "rpm" + }, + { + "Name": "kernel-livepatch-4.14.173-137.228-debuginfo", + "NamespaceName": "amzn:2", + "Version": "1.0-3.amzn2", + "VersionFormat": "rpm" + } + ], + "Link": "https://alas.aws.amazon.com/AL2/ALASLIVEPATCH-2020-012.html", + "Metadata": { + "CVE": [ + {"Name": "CVE-2020-12657"} + ] + }, + "Name": "ALASLIVEPATCH-2020-012", + "NamespaceName": "amzn:2", + "Severity": "High" + } + }, + { + "Vulnerability": { + "Description": "", + "FixedIn": [ + { + "Name": "kernel-livepatch-4.14.171-136.231", + "NamespaceName": "amzn:2", + "Version": "1.0-5.amzn2", + "VersionFormat": "rpm" + }, + { + "Name": "kernel-livepatch-4.14.171-136.231-debuginfo", + "NamespaceName": "amzn:2", + "Version": "1.0-5.amzn2", + "VersionFormat": "rpm" + } + ], + "Link": "https://alas.aws.amazon.com/AL2/ALASLIVEPATCH-2020-011.html", + "Metadata": { + "CVE": [ + {"Name": "CVE-2020-12657"} + ] + }, + "Name": "ALASLIVEPATCH-2020-011", + "NamespaceName": "amzn:2", + "Severity": "High" + } + } +] \ No newline at end of file diff --git a/pkg/process/v1/transformers/os/transform.go b/pkg/process/v1/transformers/os/transform.go new file mode 100644 index 00000000..13048c91 --- /dev/null +++ b/pkg/process/v1/transformers/os/transform.go @@ -0,0 +1,103 @@ +package os + +import ( + "fmt" + "strings" + + "github.com/anchore/grype-db/pkg/data" + "github.com/anchore/grype-db/pkg/process/common" + "github.com/anchore/grype-db/pkg/process/v1/transformers" + "github.com/anchore/grype-db/pkg/provider/unmarshal" + grypeDB "github.com/anchore/grype/grype/db/v1" +) + +const ( + // TODO: tech debt from a previous design + feed = "vulnerabilities" +) + +func Transform(vulnerability unmarshal.OSVulnerability) ([]data.Entry, error) { + group := vulnerability.Vulnerability.NamespaceName + + var allVulns []grypeDB.Vulnerability + + recordSource := grypeDB.RecordSource(feed, group) + vulnerability.Vulnerability.FixedIn = vulnerability.Vulnerability.FixedIn.FilterToHighestModularity() + + // there may be multiple packages indicated within the FixedIn field, we should make + // separate vulnerability entries (one for each name|namespace combo) while merging + // constraint ranges as they are found. + for _, advisory := range vulnerability.Vulnerability.FixedIn { + constraint, err := enforceConstraint(advisory.Version, advisory.VersionFormat) + if err != nil { + return nil, err + } + + // create vulnerability entry + vuln := grypeDB.Vulnerability{ + ID: vulnerability.Vulnerability.Name, + RecordSource: recordSource, + VersionConstraint: constraint, + VersionFormat: advisory.VersionFormat, + PackageName: advisory.Name, + Namespace: advisory.NamespaceName, + ProxyVulnerabilities: []string{}, + FixedInVersion: common.CleanFixedInVersion(advisory.Version), + } + + // associate related vulnerabilities + // note: an example of multiple CVEs for a record is centos:5 RHSA-2007:0055 which maps to CVE-2007-0002 and CVE-2007-1466 + for _, ref := range vulnerability.Vulnerability.Metadata.CVE { + vuln.ProxyVulnerabilities = append(vuln.ProxyVulnerabilities, ref.Name) + } + + allVulns = append(allVulns, vuln) + } + + var cvssV2 *grypeDB.Cvss + if vulnerability.Vulnerability.Metadata.NVD.CVSSv2.Vectors != "" { + cvssV2 = &grypeDB.Cvss{ + BaseScore: vulnerability.Vulnerability.Metadata.NVD.CVSSv2.Score, + ExploitabilityScore: 0, + ImpactScore: 0, + Vector: vulnerability.Vulnerability.Metadata.NVD.CVSSv2.Vectors, + } + } + + // find all URLs related to the vulnerability + links := []string{vulnerability.Vulnerability.Link} + if vulnerability.Vulnerability.Metadata.CVE != nil { + for _, cve := range vulnerability.Vulnerability.Metadata.CVE { + if cve.Link != "" { + links = append(links, cve.Link) + } + } + } + + // create vulnerability metadata entry (a single entry keyed off of the vulnerability ID) + metadata := grypeDB.VulnerabilityMetadata{ + ID: vulnerability.Vulnerability.Name, + RecordSource: recordSource, + Severity: vulnerability.Vulnerability.Severity, + Links: links, + Description: vulnerability.Vulnerability.Description, + CvssV2: cvssV2, + } + + return transformers.NewEntries(allVulns, metadata), nil +} + +func enforceConstraint(constraint, format string) (string, error) { + constraint = common.CleanConstraint(constraint) + if len(constraint) == 0 { + return "", nil + } + switch strings.ToLower(format) { + case "dpkg", "rpm", "apk": + // the passed constraint is a fixed version + return fmt.Sprintf("< %s", constraint), nil + case "semver": + return common.EnforceSemVerConstraint(constraint), nil + } + return "", fmt.Errorf("unable to enforce constraint='%s' format='%s'", constraint, format) +} diff --git a/pkg/process/v1/transformers/os/transform_test.go b/pkg/process/v1/transformers/os/transform_test.go new file mode 100644 index 00000000..a37ddd14 --- /dev/null +++ b/pkg/process/v1/transformers/os/transform_test.go @@ -0,0 +1,421 @@ +package os + +import ( + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/require" + "os" + "testing" + + testUtils "github.com/anchore/grype-db/pkg/process/tests" + "github.com/anchore/grype-db/pkg/provider/unmarshal" + grypeDB "github.com/anchore/grype/grype/db/v1" + "github.com/stretchr/testify/assert" +) + +func TestUnmarshalVulnerabilitiesEntries(t *testing.T) { + f, err := os.Open("test-fixtures/unmarshal-test.json") + require.NoError(t, err) + defer testUtils.CloseFile(f) + + entries, err := unmarshal.OSVulnerabilityEntries(f) + require.NoError(t, err) + + require.Len(t, entries, 3) +} + +func TestParseVulnerabilitiesEntry(t *testing.T) { + tests := []struct { + name string + numEntries int + fixture string + vulns []grypeDB.Vulnerability + metadata grypeDB.VulnerabilityMetadata + }{ + { + name: "Amazon", + numEntries: 1, + fixture: "test-fixtures/amzn.json", + vulns: []grypeDB.Vulnerability{ + { + ID: "ALAS-2018-1106", + RecordSource: "vulnerabilities:amzn:2", + VersionConstraint: "< 1.3.8.4-15.amzn2.0.1", + VersionFormat: "rpm", // TODO: this should reference a format, yes? (not a string) + ProxyVulnerabilities: []string{"CVE-2018-14648"}, + PackageName: "389-ds-base", + Namespace: "amzn:2", + FixedInVersion: "1.3.8.4-15.amzn2.0.1", + }, + { + ID: "ALAS-2018-1106", + RecordSource: "vulnerabilities:amzn:2", + VersionConstraint: "< 1.3.8.4-15.amzn2.0.1", + VersionFormat: "rpm", // TODO: this should reference a format, yes? (not a string) + ProxyVulnerabilities: []string{"CVE-2018-14648"}, + PackageName: "389-ds-base-debuginfo", + Namespace: "amzn:2", + FixedInVersion: "1.3.8.4-15.amzn2.0.1", + }, + { + ID: "ALAS-2018-1106", + RecordSource: "vulnerabilities:amzn:2", + VersionConstraint: "< 1.3.8.4-15.amzn2.0.1", + VersionFormat: "rpm", // TODO: this should reference a format, yes? (not a string) + ProxyVulnerabilities: []string{"CVE-2018-14648"}, + PackageName: "389-ds-base-devel", + Namespace: "amzn:2", + FixedInVersion: "1.3.8.4-15.amzn2.0.1", + }, + { + ID: "ALAS-2018-1106", + RecordSource: "vulnerabilities:amzn:2", + VersionConstraint: "< 1.3.8.4-15.amzn2.0.1", + VersionFormat: "rpm", // TODO: this should reference a format, yes? (not a string) + ProxyVulnerabilities: []string{"CVE-2018-14648"}, + PackageName: "389-ds-base-libs", + Namespace: "amzn:2", + FixedInVersion: "1.3.8.4-15.amzn2.0.1", + }, + { + ID: "ALAS-2018-1106", + RecordSource: "vulnerabilities:amzn:2", + VersionConstraint: "< 1.3.8.4-15.amzn2.0.1", + VersionFormat: "rpm", // TODO: this should reference a format, yes? (not a string) + ProxyVulnerabilities: []string{"CVE-2018-14648"}, + PackageName: "389-ds-base-snmp", + Namespace: "amzn:2", + FixedInVersion: "1.3.8.4-15.amzn2.0.1", + }, + }, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "ALAS-2018-1106", + RecordSource: "vulnerabilities:amzn:2", + Severity: "Medium", + Links: []string{"https://alas.aws.amazon.com/AL2/ALAS-2018-1106.html"}, + }, + }, + { + name: "Debian", + numEntries: 1, + fixture: "test-fixtures/debian-8.json", + vulns: []grypeDB.Vulnerability{ + { + ID: "CVE-2008-7220", + RecordSource: "vulnerabilities:debian:8", + PackageName: "asterisk", + VersionConstraint: "< 1:1.6.2.0~rc3-1", + VersionFormat: "dpkg", // TODO: this should reference a format, yes? (not a string) + ProxyVulnerabilities: []string{}, + Namespace: "debian:8", + FixedInVersion: "1:1.6.2.0~rc3-1", + }, + { + ID: "CVE-2008-7220", + RecordSource: "vulnerabilities:debian:8", + PackageName: "auth2db", + VersionConstraint: "< 0.2.5-2+dfsg-1", + VersionFormat: "dpkg", // TODO: this should reference a format, yes? (not a string) + ProxyVulnerabilities: []string{}, + Namespace: "debian:8", + FixedInVersion: "0.2.5-2+dfsg-1", + }, + { + ID: "CVE-2008-7220", + RecordSource: "vulnerabilities:debian:8", + PackageName: "exaile", + VersionConstraint: "< 0.2.14+debian-2.2", + VersionFormat: "dpkg", // TODO: this should reference a format, yes? (not a string) + ProxyVulnerabilities: []string{}, + Namespace: "debian:8", + FixedInVersion: "0.2.14+debian-2.2", + }, + { + ID: "CVE-2008-7220", + RecordSource: "vulnerabilities:debian:8", + PackageName: "wordpress", + VersionConstraint: "", + VersionFormat: "dpkg", // TODO: this should reference a format, yes? (not a string) + ProxyVulnerabilities: []string{}, + Namespace: "debian:8", + }, + }, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "CVE-2008-7220", + RecordSource: "vulnerabilities:debian:8", + Severity: "High", + Links: []string{"https://security-tracker.debian.org/tracker/CVE-2008-7220"}, + Description: "", + CvssV2: &grypeDB.Cvss{ + BaseScore: 7.5, + Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P", + }, + }, + }, + { + name: "RHEL", + numEntries: 1, + fixture: "test-fixtures/rhel-8.json", + vulns: []grypeDB.Vulnerability{ + { + ID: "CVE-2020-6819", + RecordSource: "vulnerabilities:rhel:8", + PackageName: "firefox", + VersionConstraint: "< 0:68.6.1-1.el8_1", + VersionFormat: "rpm", // TODO: this should reference a format, yes? (not a string) + ProxyVulnerabilities: []string{}, + Namespace: "rhel:8", + FixedInVersion: "0:68.6.1-1.el8_1", + }, + { + ID: "CVE-2020-6819", + RecordSource: "vulnerabilities:rhel:8", + PackageName: "thunderbird", + VersionConstraint: "< 0:68.7.0-1.el8_1", + VersionFormat: "rpm", // TODO: this should reference a format, yes? (not a string) + ProxyVulnerabilities: []string{}, + Namespace: "rhel:8", + FixedInVersion: "0:68.7.0-1.el8_1", + }, + }, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "CVE-2020-6819", + RecordSource: "vulnerabilities:rhel:8", + Severity: "Critical", + Links: []string{"https://access.redhat.com/security/cve/CVE-2020-6819"}, + Description: "A flaw was found in Mozilla Firefox. A race condition can occur while running the nsDocShell destructor causing a use-after-free memory issue. The highest threat from this vulnerability is to data confidentiality and integrity as well as system availability.", + }, + }, + { + name: "RHEL with modularity", + numEntries: 1, + fixture: "test-fixtures/rhel-8-modules.json", + vulns: []grypeDB.Vulnerability{ + { + ID: "CVE-2020-14350", + RecordSource: "vulnerabilities:rhel:8", + PackageName: "postgresql", + VersionConstraint: "< 0:12.5-1.module+el8.3.0+9042+664538f4", + VersionFormat: "rpm", + ProxyVulnerabilities: []string{}, + Namespace: "rhel:8", + FixedInVersion: "0:12.5-1.module+el8.3.0+9042+664538f4", + }, + }, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "CVE-2020-14350", + RecordSource: "vulnerabilities:rhel:8", + Severity: "Medium", + Links: []string{"https://access.redhat.com/security/cve/CVE-2020-14350"}, + Description: "A flaw was found in PostgreSQL, where some PostgreSQL extensions did not use the search_path safely in their installation script. This flaw allows an attacker with sufficient privileges to trick an administrator into executing a specially crafted script during the extension's installation or update. The highest threat from this vulnerability is to confidentiality, integrity, as well as system availability.", + }, + }, + { + name: "Alpine", + numEntries: 1, + fixture: "test-fixtures/alpine-3.9.json", + vulns: []grypeDB.Vulnerability{ + { + ID: "CVE-2018-19967", + RecordSource: "vulnerabilities:alpine:3.9", + PackageName: "xen", + VersionConstraint: "< 4.11.1-r0", + VersionFormat: "apk", // TODO: this should reference a format, yes? (not a string) + ProxyVulnerabilities: []string{}, + Namespace: "alpine:3.9", + FixedInVersion: "4.11.1-r0", + }, + }, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "CVE-2018-19967", + RecordSource: "vulnerabilities:alpine:3.9", + Severity: "Medium", + Links: []string{"http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-19967"}, + Description: "", + CvssV2: &grypeDB.Cvss{ + BaseScore: 4.9, + ExploitabilityScore: 0, + ImpactScore: 0, + Vector: "AV:L/AC:L/Au:N/C:N/I:N/A:C", + }, + }, + }, + { + name: "Oracle", + numEntries: 1, + fixture: "test-fixtures/ol-8.json", + vulns: []grypeDB.Vulnerability{ + { + ID: "ELSA-2020-2550", + RecordSource: "vulnerabilities:ol:8", + PackageName: "libexif", + VersionConstraint: "< 0:0.6.21-17.el8_2", + VersionFormat: "rpm", // TODO: this should reference a format, yes? (not a string) + ProxyVulnerabilities: []string{"CVE-2020-13112"}, + Namespace: "ol:8", + FixedInVersion: "0:0.6.21-17.el8_2", + }, + { + ID: "ELSA-2020-2550", + RecordSource: "vulnerabilities:ol:8", + PackageName: "libexif-devel", + VersionConstraint: "< 0:0.6.21-17.el8_2", + VersionFormat: "rpm", // TODO: this should reference a format, yes? (not a string) + ProxyVulnerabilities: []string{"CVE-2020-13112"}, + Namespace: "ol:8", + FixedInVersion: "0:0.6.21-17.el8_2", + }, + { + ID: "ELSA-2020-2550", + RecordSource: "vulnerabilities:ol:8", + PackageName: "libexif-dummy", + VersionConstraint: "", + VersionFormat: "rpm", // TODO: this should reference a format, yes? (not a string) + ProxyVulnerabilities: []string{"CVE-2020-13112"}, + Namespace: "ol:8", + FixedInVersion: "", + }, + }, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "ELSA-2020-2550", + RecordSource: "vulnerabilities:ol:8", + Severity: "Medium", + Links: []string{"http://linux.oracle.com/errata/ELSA-2020-2550.html", "http://linux.oracle.com/cve/CVE-2020-13112.html"}, + }, + }, + { + name: "Oracle Linux 8 with modularity", + numEntries: 1, + fixture: "test-fixtures/ol-8-modules.json", + vulns: []grypeDB.Vulnerability{ + { + ID: "CVE-2020-14350", + RecordSource: "vulnerabilities:ol:8", + PackageName: "postgresql", + VersionConstraint: "< 0:12.5-1.module+el8.3.0+9042+664538f4", + VersionFormat: "rpm", + ProxyVulnerabilities: []string{}, + Namespace: "ol:8", + FixedInVersion: "0:12.5-1.module+el8.3.0+9042+664538f4", + }, + }, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "CVE-2020-14350", + RecordSource: "vulnerabilities:ol:8", + Severity: "Medium", + Links: []string{"https://access.redhat.com/security/cve/CVE-2020-14350"}, + Description: "A flaw was found in PostgreSQL, where some PostgreSQL extensions did not use the search_path safely in their installation script. This flaw allows an attacker with sufficient privileges to trick an administrator into executing a specially crafted script during the extension's installation or update. The highest threat from this vulnerability is to confidentiality, integrity, as well as system availability.", + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + f, err := os.Open(test.fixture) + require.NoError(t, err) + t.Cleanup(func() { + assert.NoError(t, f.Close()) + }) + + entries, err := unmarshal.OSVulnerabilityEntries(f) + assert.NoError(t, err) + assert.Len(t, entries, 1) + + entry := entries[0] + + dataEntries, err := Transform(entry) + assert.NoError(t, err) + + var vulns []grypeDB.Vulnerability + for _, entry := range dataEntries { + switch vuln := entry.Data.(type) { + case grypeDB.Vulnerability: + vulns = append(vulns, vuln) + case grypeDB.VulnerabilityMetadata: + assert.Equal(t, test.metadata, vuln) + default: + t.Fatalf("unexpected condition: data entry does not have a vulnerability or a metadata") + } + } + + if diff := cmp.Diff(test.vulns, vulns); diff != "" { + t.Errorf("vulnerabilities do not match (-want +got):\n%s", diff) + } + + }) + } + +} + +func TestParseVulnerabilitiesAllEntries(t *testing.T) { + + tests := []struct { + name string + numEntries int + fixture string + vulns []grypeDB.Vulnerability + }{ + { + name: "Debian", + numEntries: 2, + fixture: "test-fixtures/debian-8-multiple-entries-for-same-package.json", + vulns: []grypeDB.Vulnerability{ + { + ID: "CVE-2011-4623", + RecordSource: "vulnerabilities:debian:8", + PackageName: "rsyslog", + VersionConstraint: "< 5.7.4-1", + VersionFormat: "dpkg", // TODO: this should reference a format, yes? (not a string) + ProxyVulnerabilities: []string{}, + Namespace: "debian:8", + FixedInVersion: "5.7.4-1", + }, + { + ID: "CVE-2008-5618", + RecordSource: "vulnerabilities:debian:8", + PackageName: "rsyslog", + VersionConstraint: "< 3.18.6-1", + VersionFormat: "dpkg", // TODO: this should reference a format, yes? (not a string) + ProxyVulnerabilities: []string{}, + Namespace: "debian:8", + FixedInVersion: "3.18.6-1", + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + f, err := os.Open(test.fixture) + require.NoError(t, err) + t.Cleanup(func() { + assert.NoError(t, f.Close()) + }) + + entries, err := unmarshal.OSVulnerabilityEntries(f) + assert.NoError(t, err) + assert.Len(t, entries, len(test.vulns)) + + var vulns []grypeDB.Vulnerability + for _, entry := range entries { + assert.NoError(t, err) + dataEntries, err := Transform(entry) + assert.NoError(t, err) + + for _, entry := range dataEntries { + switch vuln := entry.Data.(type) { + case grypeDB.Vulnerability: + vulns = append(vulns, vuln) + case grypeDB.VulnerabilityMetadata: + default: + t.Fatalf("unexpected condition: data entry does not have a vulnerability or a metadata") + } + } + } + + if diff := cmp.Diff(test.vulns, vulns); diff != "" { + t.Errorf("vulnerabilities do not match (-want +got):\n%s", diff) + } + }) + } + +} diff --git a/pkg/process/v1/writer.go b/pkg/process/v1/writer.go new file mode 100644 index 00000000..5598d939 --- /dev/null +++ b/pkg/process/v1/writer.go @@ -0,0 +1,124 @@ +package v1 + +import ( + "crypto/sha256" + "fmt" + "path" + "path/filepath" + "strings" + "time" + + "github.com/anchore/grype-db/internal/log" + + "github.com/anchore/grype-db/internal/file" + "github.com/anchore/grype-db/pkg/data" + "github.com/anchore/grype/grype/db" + grypeDB "github.com/anchore/grype/grype/db/v1" + grypeDBStore "github.com/anchore/grype/grype/db/v1/store" + "github.com/spf13/afero" +) + +var _ data.Writer = (*writer)(nil) + +type writer struct { + dbPath string + store grypeDB.Store +} + +func NewWriter(directory string, dataAge time.Time) (data.Writer, error) { + dbPath := path.Join(directory, grypeDB.VulnerabilityStoreFileName) + theStore, err := grypeDBStore.New(dbPath, true) + if err != nil { + return nil, fmt.Errorf("unable to create writer: %w", err) + } + + if err := theStore.SetID(grypeDB.NewID(dataAge)); err != nil { + return nil, fmt.Errorf("unable to set DB ID: %w", err) + } + + return &writer{ + dbPath: dbPath, + store: theStore, + }, nil +} + +func (w writer) Write(entries ...data.Entry) error { + for _, entry := range entries { + if entry.DBSchemaVersion != grypeDB.SchemaVersion { + return fmt.Errorf("wrong schema version: want %+v got %+v", grypeDB.SchemaVersion, entry.DBSchemaVersion) + } + switch row := entry.Data.(type) { + case grypeDB.Vulnerability: + if err := w.store.AddVulnerability(row); err != nil { + return fmt.Errorf("unable to write vulnerability to store: %w", err) + } + case grypeDB.VulnerabilityMetadata: + normalizeSeverity(&row, w.store) + if err := w.store.AddVulnerabilityMetadata(row); err != nil { + return fmt.Errorf("unable to write vulnerability metadata to store: %w", err) + } + default: + return fmt.Errorf("data entry does not have a vulnerability or a metadata: %T", row) + } + } + + return nil +} + +func (w writer) metadata() (*db.Metadata, error) { + hashStr, err := file.ContentDigest(afero.NewOsFs(), w.dbPath, sha256.New()) + if err != nil { + return nil, fmt.Errorf("failed to hash database file (%s): %w", w.dbPath, err) + } + + storeID, err := w.store.GetID() + if err != nil { + return nil, fmt.Errorf("failed to fetch store ID: %w", err) + } + + metadata := db.Metadata{ + Built: storeID.BuildTimestamp, + Version: storeID.SchemaVersion, + Checksum: "sha256:" + hashStr, + } + return &metadata, nil +} + +func (w writer) Close() error { + w.store.Close() + metadata, err := w.metadata() + if err != nil { + return err + } + + metadataPath := path.Join(filepath.Dir(w.dbPath), db.MetadataFileName) + + return metadata.Write(metadataPath) +} + +func normalizeSeverity(metadata *grypeDB.VulnerabilityMetadata, reader grypeDB.VulnerabilityMetadataStoreReader) { + if metadata.Severity != "" && strings.ToLower(metadata.Severity) != "unknown" { + return + } + if !strings.HasPrefix(strings.ToLower(metadata.ID), "cve") { + return + } + if strings.Contains(metadata.RecordSource, grypeDB.NVDNamespace) { + return + } + m, err := reader.GetVulnerabilityMetadata(metadata.ID, grypeDB.NVDNamespace) + if err != nil { + log.WithFields("id", metadata.ID, "error", err).Warn("error fetching vulnerability metadata from NVD namespace") + return + } + if m == nil { + log.WithFields("id", metadata.ID).Debug("unable to find vulnerability metadata from NVD namespace") + return + } + + newSeverity := string(data.ParseSeverity(m.Severity)) + + log.WithFields("id", metadata.ID, "record-source", metadata.RecordSource, "from", metadata.Severity, "to", newSeverity).Trace("overriding irrelevant severity with data from NVD record") + + metadata.Severity = newSeverity +} diff --git a/pkg/process/v1/writer_test.go b/pkg/process/v1/writer_test.go new file mode 100644 index 00000000..95b3e2eb --- /dev/null +++ b/pkg/process/v1/writer_test.go @@ -0,0 +1,116 @@ +package v1 + +import ( + "errors" + "github.com/anchore/grype-db/pkg/data" + grypeDB "github.com/anchore/grype/grype/db/v1" + "github.com/stretchr/testify/assert" + "testing" +) + +var _ grypeDB.VulnerabilityMetadataStoreReader = (*mockReader)(nil) + +type mockReader struct { + metadata *grypeDB.VulnerabilityMetadata + err error +} + +func newMockReader(sev string) *mockReader { + return &mockReader{ + metadata: &grypeDB.VulnerabilityMetadata{ + Severity: sev, + RecordSource: "nvdv2:cves", + }, + } +} + +func newDeadMockReader() *mockReader { + return &mockReader{ + err: errors.New("dead"), + } +} + +func (m mockReader) GetVulnerabilityMetadata(_, _ string) (*grypeDB.VulnerabilityMetadata, error) { + return m.metadata, m.err +} + +func (m mockReader) GetAllVulnerabilityMetadata() (*[]grypeDB.VulnerabilityMetadata, error) { + panic("implement me") +} + +func Test_normalizeSeverity(t *testing.T) { + + tests := []struct { + name string + initialSeverity string + recordSource string + cveID string + reader grypeDB.VulnerabilityMetadataStoreReader + expected data.Severity + }{ + { + name: "skip missing metadata", + initialSeverity: "", + recordSource: "test", + reader: &mockReader{}, + expected: "", + }, + { + name: "skip non-cve records metadata", + cveID: "GHSA-1234-1234-1234", + initialSeverity: "", + recordSource: "test", + reader: newDeadMockReader(), // should not be used + expected: "", + }, + { + name: "override empty severity", + initialSeverity: "", + recordSource: "test", + reader: newMockReader("low"), + expected: data.SeverityLow, + }, + { + name: "override unknown severity", + initialSeverity: "unknown", + recordSource: "test", + reader: newMockReader("low"), + expected: data.SeverityLow, + }, + { + name: "ignore record with severity already set", + initialSeverity: "Low", + recordSource: "test", + reader: newMockReader("critical"), // should not be used + expected: data.SeverityLow, + }, + { + name: "ignore nvd records", + initialSeverity: "Low", + recordSource: "nvdv2:cves", + reader: newDeadMockReader(), // should not be used + expected: data.SeverityLow, + }, + { + name: "db errors should not fail or modify the record", + initialSeverity: "", + recordSource: "test", + reader: newDeadMockReader(), + expected: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + record := &grypeDB.VulnerabilityMetadata{ + ID: "cve-2020-0000", + Severity: tt.initialSeverity, + RecordSource: tt.recordSource, + } + if tt.cveID != "" { + record.ID = tt.cveID + } + normalizeSeverity(record, tt.reader) + assert.Equal(t, string(tt.expected), record.Severity) + }) + } +} diff --git a/pkg/process/v2/processors.go b/pkg/process/v2/processors.go new file mode 100644 index 00000000..19eb1ead --- /dev/null +++ b/pkg/process/v2/processors.go @@ -0,0 +1,17 @@ +package v2 + +import ( + "github.com/anchore/grype-db/pkg/data" + "github.com/anchore/grype-db/pkg/process/processors" + "github.com/anchore/grype-db/pkg/process/v2/transformers/github" + "github.com/anchore/grype-db/pkg/process/v2/transformers/nvd" + "github.com/anchore/grype-db/pkg/process/v2/transformers/os" +) + +func Processors() []data.Processor { + return []data.Processor{ + processors.NewGitHubProcessor(github.Transform), + processors.NewNVDProcessor(nvd.Transform), + processors.NewOSProcessor(os.Transform), + } +} diff --git a/pkg/process/v2/transformers/entry.go b/pkg/process/v2/transformers/entry.go new file mode 100644 index 00000000..6109add5 --- /dev/null +++ b/pkg/process/v2/transformers/entry.go @@ -0,0 +1,22 @@ +package transformers + +import ( + "github.com/anchore/grype-db/pkg/data" + grypeDB "github.com/anchore/grype/grype/db/v2" +) + +func NewEntries(vs []grypeDB.Vulnerability, metadata grypeDB.VulnerabilityMetadata) []data.Entry { + entries := []data.Entry{ + { + DBSchemaVersion: grypeDB.SchemaVersion, + Data: metadata, + }, + } + for _, vuln := range vs { + entries = append(entries, data.Entry{ + DBSchemaVersion: grypeDB.SchemaVersion, + Data: vuln, + }) + } + return entries +} diff --git a/pkg/process/v2/transformers/github/test-fixtures/github-github-npm-0.json b/pkg/process/v2/transformers/github/test-fixtures/github-github-npm-0.json new file mode 100644 index 00000000..b0a7d1e9 --- /dev/null +++ b/pkg/process/v2/transformers/github/test-fixtures/github-github-npm-0.json @@ -0,0 +1,31 @@ + +{ + "Advisory": { + "CVE": [ + "CVE-2020-14000" + ], + "FixedIn": [ + { + "ecosystem": "npm", + "identifier": "0.2.0-prerelease.20200714185213", + "name": "scratch-vm", + "namespace": "github:npm", + "range": "<= 0.2.0-prerelease.20200709173451" + } + ], + "Metadata": { + "CVE": [ + "CVE-2020-14000" + ] + }, + "Severity": "High", + "Summary": "Remote Code Execution in scratch-vm", + "ghsaId": "GHSA-vc9j-fhvv-8vrf", + "namespace": "github:npm", + "url": "https://github.com/advisories/GHSA-vc9j-fhvv-8vrf", + "withdrawn": null + }, + "Vulnerability": {} +} + + diff --git a/pkg/process/v2/transformers/github/test-fixtures/github-github-python-0.json b/pkg/process/v2/transformers/github/test-fixtures/github-github-python-0.json new file mode 100644 index 00000000..ad14aa60 --- /dev/null +++ b/pkg/process/v2/transformers/github/test-fixtures/github-github-python-0.json @@ -0,0 +1,58 @@ +[ + { + "Advisory": { + "CVE": [ + "CVE-2018-8768" + ], + "FixedIn": [ + { + "ecosystem": "python", + "identifier": "5.4.1", + "name": "notebook", + "namespace": "github:python", + "range": "< 5.4.1" + } + ], + "Metadata": { + "CVE": [ + "CVE-2018-8768" + ] + }, + "Severity": "Low", + "Summary": "Low severity vulnerability that affects notebook", + "ghsaId": "GHSA-6cwv-x26c-w2q4", + "namespace": "github:python", + "url": "https://github.com/advisories/GHSA-6cwv-x26c-w2q4", + "withdrawn": null + }, + "Vulnerability": {} + }, + { + "Advisory": { + "CVE": [ + "CVE-2017-5524" + ], + "FixedIn": [ + { + "ecosystem": "python", + "identifier": "4.3.12", + "name": "Plone", + "namespace": "github:python", + "range": ">= 4.0 < 4.3.12" + } + ], + "Metadata": { + "CVE": [ + "CVE-2017-5524" + ] + }, + "Severity": "Medium", + "Summary": "Moderate severity vulnerability that affects Plone", + "ghsaId": "GHSA-p5wr-vp8g-q5p4", + "namespace": "github:python", + "url": "https://github.com/advisories/GHSA-p5wr-vp8g-q5p4", + "withdrawn": null + }, + "Vulnerability": {} + } +] \ No newline at end of file diff --git a/pkg/process/v2/transformers/github/test-fixtures/github-github-python-1.json b/pkg/process/v2/transformers/github/test-fixtures/github-github-python-1.json new file mode 100644 index 00000000..bfa84922 --- /dev/null +++ b/pkg/process/v2/transformers/github/test-fixtures/github-github-python-1.json @@ -0,0 +1,43 @@ + +{ + "Advisory": { + "CVE": [ + "CVE-2017-5524" + ], + "FixedIn": [ + { + "ecosystem": "python", + "identifier": "4.3.12", + "name": "Plone", + "namespace": "github:python", + "range": ">= 4.0 < 4.3.12" + }, + { + "ecosystem": "python", + "identifier": "5.1b1", + "name": "Plone", + "namespace": "github:python", + "range": ">= 5.1a1 < 5.1b1" + }, + { + "ecosystem": "python", + "identifier": "5.0.7", + "name": "Plone", + "namespace": "github:python", + "range": ">= 5.0rc1 < 5.0.7" + } + ], + "Metadata": { + "CVE": [ + "CVE-2017-5524" + ] + }, + "Severity": "Medium", + "Summary": "Moderate severity vulnerability that affects Plone", + "ghsaId": "GHSA-p5wr-vp8g-q5p4", + "namespace": "github:python", + "url": "https://github.com/advisories/GHSA-p5wr-vp8g-q5p4", + "withdrawn": null + }, + "Vulnerability": {} +} diff --git a/pkg/process/v2/transformers/github/test-fixtures/github-withdrawn.json b/pkg/process/v2/transformers/github/test-fixtures/github-withdrawn.json new file mode 100644 index 00000000..04995e38 --- /dev/null +++ b/pkg/process/v2/transformers/github/test-fixtures/github-withdrawn.json @@ -0,0 +1,29 @@ + +{ + "Advisory": { + "CVE": [ + "CVE-2018-8768" + ], + "FixedIn": [ + { + "ecosystem": "python", + "identifier": "5.4.1", + "name": "notebook", + "namespace": "github:python", + "range": "< 5.4.1" + } + ], + "Metadata": { + "CVE": [ + "CVE-2018-8768" + ] + }, + "Severity": "Low", + "Summary": "Low severity vulnerability that affects notebook", + "ghsaId": "GHSA-6cwv-x26c-w2q4", + "namespace": "github:python", + "url": "https://github.com/advisories/GHSA-6cwv-x26c-w2q4", + "withdrawn": "2022-01-31T14:32:09Z" + }, + "Vulnerability": {} +} diff --git a/pkg/process/v2/transformers/github/test-fixtures/multiple-fixed-in-names.json b/pkg/process/v2/transformers/github/test-fixtures/multiple-fixed-in-names.json new file mode 100644 index 00000000..ac1ef982 --- /dev/null +++ b/pkg/process/v2/transformers/github/test-fixtures/multiple-fixed-in-names.json @@ -0,0 +1,43 @@ + +{ + "Advisory": { + "CVE": [ + "CVE-2017-5524" + ], + "FixedIn": [ + { + "ecosystem": "python", + "identifier": "4.3.12", + "name": "Plone", + "namespace": "github:python", + "range": ">= 4.0 < 4.3.12" + }, + { + "ecosystem": "python", + "identifier": "5.1b1", + "name": "Plone", + "namespace": "github:python", + "range": ">= 5.1a1 < 5.1b1" + }, + { + "ecosystem": "python", + "identifier": "5.0.7", + "name": "Plone-debug", + "namespace": "github:python", + "range": ">= 5.0rc1 < 5.0.7" + } + ], + "Metadata": { + "CVE": [ + "CVE-2017-5524" + ] + }, + "Severity": "Medium", + "Summary": "Moderate severity vulnerability that affects Plone", + "ghsaId": "GHSA-p5wr-vp8g-q5p4", + "namespace": "github:python", + "url": "https://github.com/advisories/GHSA-p5wr-vp8g-q5p4", + "withdrawn": null + }, + "Vulnerability": {} +} diff --git a/pkg/process/v2/transformers/github/transform.go b/pkg/process/v2/transformers/github/transform.go new file mode 100644 index 00000000..873ac1a9 --- /dev/null +++ b/pkg/process/v2/transformers/github/transform.go @@ -0,0 +1,65 @@ +package github + +import ( + "github.com/anchore/grype-db/pkg/data" + "github.com/anchore/grype-db/pkg/process/common" + "github.com/anchore/grype-db/pkg/process/v2/transformers" + "github.com/anchore/grype-db/pkg/provider/unmarshal" + grypeDB "github.com/anchore/grype/grype/db/v2" +) + +const ( + // TODO: tech debt from a previous design + feed = "github" +) + +func Transform(vulnerability unmarshal.GitHubAdvisory) ([]data.Entry, error) { + var allVulns []grypeDB.Vulnerability + + // Exclude entries marked as withdrawn + if vulnerability.Advisory.Withdrawn != nil { + return nil, nil + } + + recordSource := grypeDB.RecordSource(feed, vulnerability.Advisory.Namespace) + + // there may be multiple packages indicated within the FixedIn field, we should make + // separate vulnerability entries (one for each name|namespace combo) while merging + // constraint ranges as they are found. + for _, advisory := range vulnerability.Advisory.FixedIn { + constraint := common.EnforceSemVerConstraint(advisory.Range) + + var versionFormat string + switch vulnerability.Advisory.Namespace { + case "github:python": + versionFormat = "python" + default: + versionFormat = "unknown" + } + + // create vulnerability entry + vuln := grypeDB.Vulnerability{ + ID: vulnerability.Advisory.GhsaID, + RecordSource: recordSource, + VersionConstraint: constraint, + VersionFormat: versionFormat, // TODO: this should reference a format, yes? (not a string) + ProxyVulnerabilities: vulnerability.Advisory.CVE, + PackageName: advisory.Name, + Namespace: advisory.Namespace, + FixedInVersion: common.CleanFixedInVersion(advisory.Identifier), + } + + allVulns = append(allVulns, vuln) + } + + // create vulnerability metadata entry (a single entry keyed off of the vulnerability ID) + metadata := grypeDB.VulnerabilityMetadata{ + ID: vulnerability.Advisory.GhsaID, + RecordSource: recordSource, + Severity: vulnerability.Advisory.Severity, + Links: []string{vulnerability.Advisory.URL}, + Description: vulnerability.Advisory.Summary, + } + + return transformers.NewEntries(allVulns, metadata), nil +} diff --git a/pkg/process/v2/transformers/github/transform_test.go b/pkg/process/v2/transformers/github/transform_test.go new file mode 100644 index 00000000..83e70be5 --- /dev/null +++ b/pkg/process/v2/transformers/github/transform_test.go @@ -0,0 +1,169 @@ +package github + +import ( + "github.com/stretchr/testify/require" + "os" + "testing" + + testUtils "github.com/anchore/grype-db/pkg/process/tests" + "github.com/anchore/grype-db/pkg/provider/unmarshal" + grypeDB "github.com/anchore/grype/grype/db/v2" + "github.com/stretchr/testify/assert" +) + +func TestUnmarshalGitHubEntries(t *testing.T) { + f, err := os.Open("test-fixtures/github-github-python-0.json") + require.NoError(t, err) + t.Cleanup(func() { + assert.NoError(t, f.Close()) + }) + + entries, err := unmarshal.GitHubAdvisoryEntries(f) + require.NoError(t, err) + + require.Len(t, entries, 2) +} + +func TestParseGitHubEntry(t *testing.T) { + expectedVulns := []grypeDB.Vulnerability{ + { + ID: "GHSA-p5wr-vp8g-q5p4", + RecordSource: "github:python", + VersionConstraint: ">=4.0,<4.3.12", + VersionFormat: "python", // TODO: this should reference a format, yes? (not a string) + ProxyVulnerabilities: []string{"CVE-2017-5524"}, + PackageName: "Plone", + Namespace: "github:python", + FixedInVersion: "4.3.12", + }, + { + ID: "GHSA-p5wr-vp8g-q5p4", + RecordSource: "github:python", + VersionConstraint: ">=5.1a1,<5.1b1", + VersionFormat: "python", // TODO: this should reference a format, yes? (not a string) + ProxyVulnerabilities: []string{"CVE-2017-5524"}, + PackageName: "Plone", + Namespace: "github:python", + FixedInVersion: "5.1b1", + }, + { + ID: "GHSA-p5wr-vp8g-q5p4", + RecordSource: "github:python", + VersionConstraint: ">=5.0rc1,<5.0.7", + VersionFormat: "python", // TODO: this should reference a format, yes? (not a string) + ProxyVulnerabilities: []string{"CVE-2017-5524"}, + PackageName: "Plone", + Namespace: "github:python", + FixedInVersion: "5.0.7", + }, + } + + expectedMetadata := grypeDB.VulnerabilityMetadata{ + ID: "GHSA-p5wr-vp8g-q5p4", + RecordSource: "github:python", + Severity: "Medium", + Links: []string{"https://github.com/advisories/GHSA-p5wr-vp8g-q5p4"}, + Description: "Moderate severity vulnerability that affects Plone", + } + + f, err := os.Open("test-fixtures/github-github-python-1.json") + require.NoError(t, err) + defer testUtils.CloseFile(f) + + entries, err := unmarshal.GitHubAdvisoryEntries(f) + assert.NoError(t, err) + assert.Len(t, entries, 1) + + entry := entries[0] + + dataEntries, err := Transform(entry) + assert.NoError(t, err) + + var vulns []grypeDB.Vulnerability + for _, entry := range dataEntries { + switch vuln := entry.Data.(type) { + case grypeDB.Vulnerability: + vulns = append(vulns, vuln) + case grypeDB.VulnerabilityMetadata: + assert.Equal(t, expectedMetadata, vuln) + default: + t.Fatalf("unexpected condition: data entry does not have a vulnerability or a metadata") + } + } + + // check vulnerability + assert.Len(t, vulns, len(expectedVulns)) + + assert.ElementsMatch(t, expectedVulns, vulns) + +} + +func TestDefaultVersionFormatNpmGitHubEntry(t *testing.T) { + expectedVulns := []grypeDB.Vulnerability{ + { + ID: "GHSA-vc9j-fhvv-8vrf", + RecordSource: "github:npm", + VersionConstraint: "<=0.2.0-prerelease.20200709173451", + VersionFormat: "unknown", // TODO: this should reference a format, yes? (not a string) + ProxyVulnerabilities: []string{"CVE-2020-14000"}, + PackageName: "scratch-vm", + Namespace: "github:npm", + FixedInVersion: "0.2.0-prerelease.20200714185213", + }, + } + + expectedMetadata := grypeDB.VulnerabilityMetadata{ + ID: "GHSA-vc9j-fhvv-8vrf", + RecordSource: "github:npm", + Severity: "High", + Links: []string{"https://github.com/advisories/GHSA-vc9j-fhvv-8vrf"}, + Description: "Remote Code Execution in scratch-vm", + } + + f, err := os.Open("test-fixtures/github-github-npm-0.json") + require.NoError(t, err) + defer testUtils.CloseFile(f) + + entries, err := unmarshal.GitHubAdvisoryEntries(f) + assert.NoError(t, err) + assert.Len(t, entries, 1) + + entry := entries[0] + + dataEntries, err := Transform(entry) + assert.NoError(t, err) + + var vulns []grypeDB.Vulnerability + for _, entry := range dataEntries { + switch vuln := entry.Data.(type) { + case grypeDB.Vulnerability: + vulns = append(vulns, vuln) + case grypeDB.VulnerabilityMetadata: + assert.Equal(t, expectedMetadata, vuln) + default: + t.Fatalf("unexpected condition: data entry does not have a vulnerability or a metadata") + } + } + + // check vulnerability + assert.Len(t, vulns, len(expectedVulns)) + + assert.ElementsMatch(t, expectedVulns, vulns) +} + +func TestFilterWithdrawnEntries(t *testing.T) { + f, err := os.Open("test-fixtures/github-withdrawn.json") + require.NoError(t, err) + defer testUtils.CloseFile(f) + + entries, err := unmarshal.GitHubAdvisoryEntries(f) + require.NoError(t, err) + + require.Len(t, entries, 1) + + entry := entries[0] + + dataEntries, err := Transform(entry) + assert.NoError(t, err) + assert.Nil(t, dataEntries) +} diff --git a/pkg/process/v2/transformers/nvd/test-fixtures/compound-pkg.json b/pkg/process/v2/transformers/nvd/test-fixtures/compound-pkg.json new file mode 100644 index 00000000..8e658dcd --- /dev/null +++ b/pkg/process/v2/transformers/nvd/test-fixtures/compound-pkg.json @@ -0,0 +1,115 @@ +{ + "cve": { + "id": "CVE-2018-10189", + "sourceIdentifier": "cve@mitre.org", + "published": "2018-04-17T20:29:00.410", + "lastModified": "2018-05-23T14:41:49.073", + "vulnStatus": "Analyzed", + "descriptions": [ + { + "lang": "en", + "value": "An issue was discovered in Mautic 1.x and 2.x before 2.13.0. It is possible to systematically emulate tracking cookies per contact due to tracking the contact by their auto-incremented ID. Thus, a third party can manipulate the cookie value with +1 to systematically assume being tracked as each contact in Mautic. It is then possible to retrieve information about the contact through forms that have progressive profiling enabled." + }, + { + "lang": "es", + "value": "Se ha descubierto un problema en Mautic, en versiones 1.x y 2.x anteriores a la 2.13.0. Es posible emular de forma sistemática el rastreo de cookies por contacto debido al rastreo de contacto por su ID autoincrementada. Por lo tanto, un tercero puede manipular el valor de la cookie con un +1 para asumir sistemáticamente que se está rastreando como cada contacto en Mautic. Así, sería posible recuperar información sobre el contacto a través de formularios que tengan habilitada la generación de perfiles progresiva." + } + ], + "metrics": { + "cvssMetricV30": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "cvssData": { + "version": "3.0", + "vectorString": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N", + "attackVector": "NETWORK", + "attackComplexity": "LOW", + "privilegesRequired": "NONE", + "userInteraction": "NONE", + "scope": "UNCHANGED", + "confidentialityImpact": "HIGH", + "integrityImpact": "NONE", + "availabilityImpact": "NONE", + "baseScore": 7.5, + "baseSeverity": "HIGH" + }, + "exploitabilityScore": 3.9, + "impactScore": 3.6 + } + ], + "cvssMetricV2": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "cvssData": { + "version": "2.0", + "vectorString": "AV:N/AC:L/Au:N/C:P/I:N/A:N", + "accessVector": "NETWORK", + "accessComplexity": "LOW", + "authentication": "NONE", + "confidentialityImpact": "PARTIAL", + "integrityImpact": "NONE", + "availabilityImpact": "NONE", + "baseScore": 5.0 + }, + "baseSeverity": "MEDIUM", + "exploitabilityScore": 10.0, + "impactScore": 2.9, + "acInsufInfo": false, + "obtainAllPrivilege": false, + "obtainUserPrivilege": false, + "obtainOtherPrivilege": false, + "userInteractionRequired": false + } + ] + }, + "weaknesses": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "description": [ + { + "lang": "en", + "value": "CWE-200" + } + ] + } + ], + "configurations": [ + { + "nodes": [ + { + "operator": "OR", + "negate": false, + "cpeMatch": [ + { + "vulnerable": true, + "criteria": "cpe:2.3:a:mautic:mautic:*:*:*:*:*:*:*:*", + "versionStartIncluding": "1.0.0", + "versionEndIncluding": "1.4.1", + "matchCriteriaId": "5779710D-099E-40EE-8DF3-55BD3179A50C" + }, + { + "vulnerable": true, + "criteria": "cpe:2.3:a:mautic:mautic:*:*:*:*:*:*:*:*", + "versionStartIncluding": "2.0.0", + "versionEndExcluding": "2.13.0", + "matchCriteriaId": "4EFAEE48-4AEF-4F8C-95E0-6E8D848D900F" + } + ] + } + ] + } + ], + "references": [ + { + "url": "https://github.com/mautic/mautic/releases/tag/2.13.0", + "source": "cve@mitre.org", + "tags": [ + "Third Party Advisory" + ] + } + ] + } +} diff --git a/pkg/process/v2/transformers/nvd/test-fixtures/invalid_cpe.json b/pkg/process/v2/transformers/nvd/test-fixtures/invalid_cpe.json new file mode 100644 index 00000000..eac2ebd4 --- /dev/null +++ b/pkg/process/v2/transformers/nvd/test-fixtures/invalid_cpe.json @@ -0,0 +1,111 @@ +{ + "cve": { + "id": "CVE-2015-8978", + "sourceIdentifier": "cve@mitre.org", + "published": "2016-11-22T17:59:00.180", + "lastModified": "2016-11-28T19:50:59.600", + "vulnStatus": "Modified", + "descriptions": [ + { + "lang": "en", + "value": "In Soap Lite (aka the SOAP::Lite extension for Perl) 1.14 and earlier, an example attack consists of defining 10 or more XML entities, each defined as consisting of 10 of the previous entity, with the document consisting of a single instance of the largest entity, which expands to one billion copies of the first entity. The amount of computer memory used for handling an external SOAP call would likely exceed that available to the process parsing the XML." + }, + { + "lang": "es", + "value": "En Soap Lite (también conocido como la extensión SOAP::Lite para Perl) 1.14 y versiones anteriores, un ejemplo de ataque consiste en definir 10 o más entidades XML, cada una definida como consistente de 10 de la entidad anterior, con el documento consistente de una única instancia de la entidad más grande, que se expande a mil millones de copias de la primera entidad. La suma de la memoria del ordenador utilizada para manejar una llamada SOAP externa probablemente superaría el disponible para el proceso de análisis del XML." + } + ], + "metrics": { + "cvssMetricV30": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "cvssData": { + "version": "3.0", + "vectorString": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", + "attackVector": "NETWORK", + "attackComplexity": "LOW", + "privilegesRequired": "NONE", + "userInteraction": "NONE", + "scope": "UNCHANGED", + "confidentialityImpact": "NONE", + "integrityImpact": "NONE", + "availabilityImpact": "HIGH", + "baseScore": 7.5, + "baseSeverity": "HIGH" + }, + "exploitabilityScore": 3.9, + "impactScore": 3.6 + } + ], + "cvssMetricV2": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "cvssData": { + "version": "2.0", + "vectorString": "AV:N/AC:L/Au:N/C:N/I:N/A:P", + "accessVector": "NETWORK", + "accessComplexity": "LOW", + "authentication": "NONE", + "confidentialityImpact": "NONE", + "integrityImpact": "NONE", + "availabilityImpact": "PARTIAL", + "baseScore": 5.0 + }, + "baseSeverity": "MEDIUM", + "exploitabilityScore": 10.0, + "impactScore": 2.9, + "acInsufInfo": false, + "obtainAllPrivilege": false, + "obtainUserPrivilege": false, + "obtainOtherPrivilege": false, + "userInteractionRequired": false + } + ] + }, + "weaknesses": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "description": [ + { + "lang": "en", + "value": "CWE-399" + } + ] + } + ], + "configurations": [ + { + "nodes": [ + { + "operator": "OR", + "negate": false, + "cpeMatch": [ + { + "vulnerable": true, + "criteria": "cpe:2.3:a:soap::lite_project:soap::lite:*:*:*:*:*:perl:*:*", + "versionEndIncluding": "1.14", + "matchCriteriaId": "FB4DACB9-2E9E-4CBE-825F-FC0303D8CC86" + } + ] + } + ] + } + ], + "references": [ + { + "url": "http://cpansearch.perl.org/src/PHRED/SOAP-Lite-1.20/Changes", + "source": "cve@mitre.org", + "tags": [ + "Vendor Advisory" + ] + }, + { + "url": "http://www.securityfocus.com/bid/94487", + "source": "cve@mitre.org" + } + ] + } +} diff --git a/pkg/process/v2/transformers/nvd/test-fixtures/single-package-multi-distro.json b/pkg/process/v2/transformers/nvd/test-fixtures/single-package-multi-distro.json new file mode 100644 index 00000000..ed108475 --- /dev/null +++ b/pkg/process/v2/transformers/nvd/test-fixtures/single-package-multi-distro.json @@ -0,0 +1,174 @@ +{ + "cve": { + "id": "CVE-2018-1000222", + "sourceIdentifier": "cve@mitre.org", + "published": "2018-08-20T20:29:01.347", + "lastModified": "2020-03-31T02:15:12.667", + "vulnStatus": "Modified", + "descriptions": [ + { + "lang": "en", + "value": "Libgd version 2.2.5 contains a Double Free Vulnerability vulnerability in gdImageBmpPtr Function that can result in Remote Code Execution . This attack appear to be exploitable via Specially Crafted Jpeg Image can trigger double free. This vulnerability appears to have been fixed in after commit ac16bdf2d41724b5a65255d4c28fb0ec46bc42f5." + }, + { + "lang": "es", + "value": "Libgd 2.2.5 contiene una vulnerabilidad de doble liberación (double free) en la función gdImageBmpPtr que puede resultar en la ejecución remota de código. Este ataque parece ser explotable mediante una imagen JPEG especialmente manipulada que desencadene una doble liberación (double free). La vulnerabilidad parece haber sido solucionada tras el commit con ID ac16bdf2d41724b5a65255d4c28fb0ec46bc42f5." + } + ], + "metrics": { + "cvssMetricV30": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "cvssData": { + "version": "3.0", + "vectorString": "CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H", + "attackVector": "NETWORK", + "attackComplexity": "LOW", + "privilegesRequired": "NONE", + "userInteraction": "REQUIRED", + "scope": "UNCHANGED", + "confidentialityImpact": "HIGH", + "integrityImpact": "HIGH", + "availabilityImpact": "HIGH", + "baseScore": 8.8, + "baseSeverity": "HIGH" + }, + "exploitabilityScore": 2.8, + "impactScore": 5.9 + } + ], + "cvssMetricV2": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "cvssData": { + "version": "2.0", + "vectorString": "AV:N/AC:M/Au:N/C:P/I:P/A:P", + "accessVector": "NETWORK", + "accessComplexity": "MEDIUM", + "authentication": "NONE", + "confidentialityImpact": "PARTIAL", + "integrityImpact": "PARTIAL", + "availabilityImpact": "PARTIAL", + "baseScore": 6.8 + }, + "baseSeverity": "MEDIUM", + "exploitabilityScore": 8.6, + "impactScore": 6.4, + "acInsufInfo": false, + "obtainAllPrivilege": false, + "obtainUserPrivilege": false, + "obtainOtherPrivilege": false, + "userInteractionRequired": true + } + ] + }, + "weaknesses": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "description": [ + { + "lang": "en", + "value": "CWE-415" + } + ] + } + ], + "configurations": [ + { + "nodes": [ + { + "operator": "OR", + "negate": false, + "cpeMatch": [ + { + "vulnerable": true, + "criteria": "cpe:2.3:a:libgd:libgd:2.2.5:*:*:*:*:*:*:*", + "matchCriteriaId": "C257CC1C-BF6A-4125-AA61-9C2D09096084" + } + ] + } + ] + }, + { + "nodes": [ + { + "operator": "OR", + "negate": false, + "cpeMatch": [ + { + "vulnerable": true, + "criteria": "cpe:2.3:o:canonical:ubuntu_linux:14.04:*:*:*:lts:*:*:*", + "matchCriteriaId": "B5A6F2F3-4894-4392-8296-3B8DD2679084" + }, + { + "vulnerable": true, + "criteria": "cpe:2.3:o:canonical:ubuntu_linux:16.04:*:*:*:lts:*:*:*", + "matchCriteriaId": "F7016A2A-8365-4F1A-89A2-7A19F2BCAE5B" + }, + { + "vulnerable": true, + "criteria": "cpe:2.3:o:canonical:ubuntu_linux:18.04:*:*:*:lts:*:*:*", + "matchCriteriaId": "23A7C53F-B80F-4E6A-AFA9-58EEA84BE11D" + } + ] + } + ] + }, + { + "nodes": [ + { + "operator": "OR", + "negate": false, + "cpeMatch": [ + { + "vulnerable": true, + "criteria": "cpe:2.3:o:debian:debian_linux:8.0:*:*:*:*:*:*:*", + "matchCriteriaId": "C11E6FB0-C8C0-4527-9AA0-CB9B316F8F43" + } + ] + } + ] + } + ], + "references": [ + { + "url": "https://github.com/libgd/libgd/issues/447", + "source": "cve@mitre.org", + "tags": [ + "Issue Tracking", + "Third Party Advisory" + ] + }, + { + "url": "https://lists.debian.org/debian-lts-announce/2019/01/msg00028.html", + "source": "cve@mitre.org", + "tags": [ + "Mailing List", + "Third Party Advisory" + ] + }, + { + "url": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/3CZ2QADQTKRHTGB2AHD7J4QQNDLBEMM6/", + "source": "cve@mitre.org" + }, + { + "url": "https://security.gentoo.org/glsa/201903-18", + "source": "cve@mitre.org", + "tags": [ + "Third Party Advisory" + ] + }, + { + "url": "https://usn.ubuntu.com/3755-1/", + "source": "cve@mitre.org", + "tags": [ + "Mitigation", + "Third Party Advisory" + ] + } + ] + } +} diff --git a/pkg/process/v2/transformers/nvd/test-fixtures/unmarshal-test.json b/pkg/process/v2/transformers/nvd/test-fixtures/unmarshal-test.json new file mode 100644 index 00000000..2dc698fa --- /dev/null +++ b/pkg/process/v2/transformers/nvd/test-fixtures/unmarshal-test.json @@ -0,0 +1,109 @@ +{ + "cve": { + "id": "CVE-2003-0349", + "sourceIdentifier": "cve@mitre.org", + "published": "2003-07-24T04:00:00.000", + "lastModified": "2018-10-12T21:32:41.083", + "vulnStatus": "Modified", + "descriptions": [ + { + "lang": "en", + "value": "Buffer overflow in the streaming media component for logging multicast requests in the ISAPI for the logging capability of Microsoft Windows Media Services (nsiislog.dll), as installed in IIS 5.0, allows remote attackers to execute arbitrary code via a large POST request to nsiislog.dll." + }, + { + "lang": "es", + "value": "Desbordamiento de búfer en el componente de secuenciamiento (streaming) de medios para registrar peticiones de multidifusión en la librería ISAPI de la capacidad de registro (logging) de Microsoft Windows Media Services (nsiislog.dll), como el instalado en IIS 5.9, permite a atacantes remotos ejecutar código arbitrario mediante una petición POST larga a nsiislog.dll." + } + ], + "metrics": { + "cvssMetricV2": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "cvssData": { + "version": "2.0", + "vectorString": "AV:N/AC:L/Au:N/C:P/I:P/A:P", + "accessVector": "NETWORK", + "accessComplexity": "LOW", + "authentication": "NONE", + "confidentialityImpact": "PARTIAL", + "integrityImpact": "PARTIAL", + "availabilityImpact": "PARTIAL", + "baseScore": 7.5 + }, + "baseSeverity": "HIGH", + "exploitabilityScore": 10.0, + "impactScore": 6.4, + "acInsufInfo": false, + "obtainAllPrivilege": false, + "obtainUserPrivilege": true, + "obtainOtherPrivilege": false, + "userInteractionRequired": false + } + ] + }, + "weaknesses": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "description": [ + { + "lang": "en", + "value": "NVD-CWE-Other" + } + ] + } + ], + "configurations": [ + { + "nodes": [ + { + "operator": "OR", + "negate": false, + "cpeMatch": [ + { + "vulnerable": true, + "criteria": "cpe:2.3:o:microsoft:windows_2000:*:*:*:*:*:*:*:*", + "matchCriteriaId": "4E545C63-FE9C-4CA1-AF0F-D999D84D2AFD" + } + ] + } + ] + } + ], + "references": [ + { + "url": "http://marc.info/?l=bugtraq&m=105665030925504&w=2", + "source": "cve@mitre.org" + }, + { + "url": "http://securitytracker.com/id?1007059", + "source": "cve@mitre.org" + }, + { + "url": "http://www.kb.cert.org/vuls/id/113716", + "source": "cve@mitre.org", + "tags": [ + "US Government Resource" + ] + }, + { + "url": "http://www.ntbugtraq.com/default.asp?pid=36&sid=1&A2=ind0306&L=NTBUGTRAQ&P=R4563", + "source": "cve@mitre.org", + "tags": [ + "Exploit", + "Patch", + "Vendor Advisory" + ] + }, + { + "url": "https://docs.microsoft.com/en-us/security-updates/securitybulletins/2003/ms03-022", + "source": "cve@mitre.org" + }, + { + "url": "https://oval.cisecurity.org/repository/search/definition/oval%3Aorg.mitre.oval%3Adef%3A938", + "source": "cve@mitre.org" + } + ] + } +} diff --git a/pkg/process/v2/transformers/nvd/test-fixtures/version-range.json b/pkg/process/v2/transformers/nvd/test-fixtures/version-range.json new file mode 100644 index 00000000..3df5b86d --- /dev/null +++ b/pkg/process/v2/transformers/nvd/test-fixtures/version-range.json @@ -0,0 +1,121 @@ +{ + "cve": { + "id": "CVE-2018-5487", + "sourceIdentifier": "security-alert@netapp.com", + "published": "2018-05-24T14:29:00.390", + "lastModified": "2018-07-05T13:52:30.627", + "vulnStatus": "Analyzed", + "descriptions": [ + { + "lang": "en", + "value": "NetApp OnCommand Unified Manager for Linux versions 7.2 through 7.3 ship with the Java Management Extension Remote Method Invocation (JMX RMI) service bound to the network, and are susceptible to unauthenticated remote code execution." + }, + { + "lang": "es", + "value": "NetApp OnCommand Unified Manager for Linux, de la versión 7.2 hasta la 7.3, se distribuye con el servicio Java Management Extension Remote Method Invocation (JMX RMI) enlazado a la red y es susceptible a la ejecución remota de código sin autenticación." + } + ], + "metrics": { + "cvssMetricV30": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "cvssData": { + "version": "3.0", + "vectorString": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + "attackVector": "NETWORK", + "attackComplexity": "LOW", + "privilegesRequired": "NONE", + "userInteraction": "NONE", + "scope": "UNCHANGED", + "confidentialityImpact": "HIGH", + "integrityImpact": "HIGH", + "availabilityImpact": "HIGH", + "baseScore": 9.8, + "baseSeverity": "CRITICAL" + }, + "exploitabilityScore": 3.9, + "impactScore": 5.9 + } + ], + "cvssMetricV2": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "cvssData": { + "version": "2.0", + "vectorString": "AV:N/AC:L/Au:N/C:P/I:P/A:P", + "accessVector": "NETWORK", + "accessComplexity": "LOW", + "authentication": "NONE", + "confidentialityImpact": "PARTIAL", + "integrityImpact": "PARTIAL", + "availabilityImpact": "PARTIAL", + "baseScore": 7.5 + }, + "baseSeverity": "HIGH", + "exploitabilityScore": 10.0, + "impactScore": 6.4, + "acInsufInfo": true, + "obtainAllPrivilege": false, + "obtainUserPrivilege": false, + "obtainOtherPrivilege": false, + "userInteractionRequired": false + } + ] + }, + "weaknesses": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "description": [ + { + "lang": "en", + "value": "CWE-20" + } + ] + } + ], + "configurations": [ + { + "operator": "AND", + "nodes": [ + { + "operator": "OR", + "negate": false, + "cpeMatch": [ + { + "vulnerable": true, + "criteria": "cpe:2.3:a:netapp:oncommand_unified_manager:*:*:*:*:*:*:*:*", + "versionStartIncluding": "7.2", + "versionEndIncluding": "7.3", + "matchCriteriaId": "A5949307-3E9B-441F-B008-81A0E0228DC0" + } + ] + }, + { + "operator": "OR", + "negate": false, + "cpeMatch": [ + { + "vulnerable": false, + "criteria": "cpe:2.3:o:linux:linux_kernel:-:*:*:*:*:*:*:*", + "matchCriteriaId": "703AF700-7A70-47E2-BC3A-7FD03B3CA9C1" + } + ] + } + ] + } + ], + "references": [ + { + "url": "https://security.netapp.com/advisory/ntap-20180523-0001/", + "source": "security-alert@netapp.com", + "tags": [ + "Patch", + "Vendor Advisory" + ] + } + ] + } +} diff --git a/pkg/process/v2/transformers/nvd/transform.go b/pkg/process/v2/transformers/nvd/transform.go new file mode 100644 index 00000000..a11a05b6 --- /dev/null +++ b/pkg/process/v2/transformers/nvd/transform.go @@ -0,0 +1,112 @@ +package nvd + +import ( + "strings" + + "github.com/anchore/grype-db/pkg/provider/unmarshal/nvd" + + "github.com/anchore/grype-db/internal" + "github.com/anchore/grype-db/pkg/data" + "github.com/anchore/grype-db/pkg/process/v2/transformers" + "github.com/anchore/grype-db/pkg/provider/unmarshal" + grypeDB "github.com/anchore/grype/grype/db/v2" +) + +const ( + // TODO: tech debt from a previous design + feed = "nvdv2" + group = "nvdv2:cves" +) + +func Transform(vulnerability unmarshal.NVDVulnerability) ([]data.Entry, error) { + var allVulns []grypeDB.Vulnerability + + recordSource := grypeDB.RecordSource(feed, group) + + uniquePkgs := findUniquePkgs(vulnerability.Configurations...) + + // extract all links + var links []string + for _, externalRefs := range vulnerability.References { + // TODO: should we capture other information here? + if externalRefs.URL != "" { + links = append(links, externalRefs.URL) + } + } + + // duplicate the vulnerabilities based on the set of unique packages the vulnerability is for + for _, p := range uniquePkgs.All() { + matches := uniquePkgs.Matches(p) + cpes := internal.NewStringSet() + for _, m := range matches { + cpes.Add(m.Criteria) + } + + // create vulnerability entry + vuln := grypeDB.Vulnerability{ + ID: vulnerability.ID, + RecordSource: recordSource, + VersionConstraint: buildConstraints(uniquePkgs.Matches(p)), + VersionFormat: "unknown", // TODO: derive this from the target software + PackageName: p.Product, + Namespace: "nvd", // should the vendor be here? or in other metadata? + ProxyVulnerabilities: []string{}, + CPEs: cpes.ToSlice(), + } + + allVulns = append(allVulns, vuln) + } + + // If all the CPEs are invalid and no vulnerabilities were generated then there is no point + // in creating metadata, so just return + if len(allVulns) == 0 { + return nil, nil + } + + // create vulnerability metadata entry (a single entry keyed off of the vulnerability ID) + allCVSS := vulnerability.CVSS() + + metadata := grypeDB.VulnerabilityMetadata{ + ID: vulnerability.ID, + RecordSource: recordSource, + Severity: nvd.CvssSummaries(allCVSS).Sorted().Severity(), + Links: links, + Description: vulnerability.Description(), + } + + for _, c := range allCVSS { + if strings.HasPrefix(c.Version, "2.") { + newCvss := &grypeDB.Cvss{ + BaseScore: c.BaseScore, + Vector: c.Vector, + } + if c.ExploitabilityScore != nil { + newCvss.ExploitabilityScore = *c.ExploitabilityScore + } + if c.ImpactScore != nil { + newCvss.ImpactScore = *c.ImpactScore + } + metadata.CvssV2 = newCvss + break + } + } + + for _, c := range allCVSS { + if strings.HasPrefix(c.Version, "3.") { + newCvss := &grypeDB.Cvss{ + BaseScore: c.BaseScore, + Vector: c.Vector, + } + if c.ExploitabilityScore != nil { + newCvss.ExploitabilityScore = *c.ExploitabilityScore + } + if c.ImpactScore != nil { + newCvss.ImpactScore = *c.ImpactScore + } + metadata.CvssV3 = newCvss + break + } + } + + return transformers.NewEntries(allVulns, metadata), nil +} diff --git a/pkg/process/v2/transformers/nvd/transform_test.go b/pkg/process/v2/transformers/nvd/transform_test.go new file mode 100644 index 00000000..65d657ee --- /dev/null +++ b/pkg/process/v2/transformers/nvd/transform_test.go @@ -0,0 +1,189 @@ +package nvd + +import ( + "github.com/stretchr/testify/require" + "os" + "testing" + + testUtils "github.com/anchore/grype-db/pkg/process/tests" + "github.com/anchore/grype-db/pkg/provider/unmarshal" + grypeDB "github.com/anchore/grype/grype/db/v2" + "github.com/go-test/deep" + "github.com/stretchr/testify/assert" +) + +func TestUnmarshalVulnerabilitiesEntries(t *testing.T) { + f, err := os.Open("test-fixtures/unmarshal-test.json") + require.NoError(t, err) + defer testUtils.CloseFile(f) + + entries, err := unmarshal.NvdVulnerabilityEntries(f) + require.NoError(t, err) + + require.Len(t, entries, 1) +} + +func TestParseVulnerabilitiesAllEntries(t *testing.T) { + tests := []struct { + name string + numEntries int + fixture string + vulns []grypeDB.Vulnerability + metadata grypeDB.VulnerabilityMetadata + }{ + { + name: "AppVersionRange", + numEntries: 1, + fixture: "test-fixtures/version-range.json", + vulns: []grypeDB.Vulnerability{ + { + ID: "CVE-2018-5487", + RecordSource: "nvdv2:cves", + PackageName: "oncommand_unified_manager", + VersionConstraint: ">= 7.2, <= 7.3", + VersionFormat: "unknown", // TODO: this should reference a format, yes? (not a string) + ProxyVulnerabilities: []string{}, + Namespace: "nvd", + CPEs: []string{"cpe:2.3:a:netapp:oncommand_unified_manager:*:*:*:*:*:*:*:*"}, + }, + }, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "CVE-2018-5487", + RecordSource: "nvdv2:cves", + Severity: "Critical", + Links: []string{"https://security.netapp.com/advisory/ntap-20180523-0001/"}, + Description: "NetApp OnCommand Unified Manager for Linux versions 7.2 through 7.3 ship with the Java Management Extension Remote Method Invocation (JMX RMI) service bound to the network, and are susceptible to unauthenticated remote code execution.", + CvssV2: &grypeDB.Cvss{ + BaseScore: 7.5, + ExploitabilityScore: 10, + ImpactScore: 6.4, + Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P", + }, + CvssV3: &grypeDB.Cvss{ + BaseScore: 9.8, + ExploitabilityScore: 3.9, + ImpactScore: 5.9, + Vector: "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + }, + }, + }, + { + name: "App+OS", + numEntries: 1, + fixture: "test-fixtures/single-package-multi-distro.json", + vulns: []grypeDB.Vulnerability{ + { + ID: "CVE-2018-1000222", + RecordSource: "nvdv2:cves", + PackageName: "libgd", + VersionConstraint: "= 2.2.5", + VersionFormat: "unknown", // TODO: this should reference a format, yes? (not a string) + ProxyVulnerabilities: []string{}, + Namespace: "nvd", + CPEs: []string{"cpe:2.3:a:libgd:libgd:2.2.5:*:*:*:*:*:*:*"}, + }, + // TODO: Question: should this match also the OS's? (as in the vulnerable_cpes list)... this seems wrong! + }, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "CVE-2018-1000222", + RecordSource: "nvdv2:cves", + Severity: "High", + Links: []string{"https://github.com/libgd/libgd/issues/447", "https://lists.debian.org/debian-lts-announce/2019/01/msg00028.html", "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/3CZ2QADQTKRHTGB2AHD7J4QQNDLBEMM6/", "https://security.gentoo.org/glsa/201903-18", "https://usn.ubuntu.com/3755-1/"}, + Description: "Libgd version 2.2.5 contains a Double Free Vulnerability vulnerability in gdImageBmpPtr Function that can result in Remote Code Execution . This attack appear to be exploitable via Specially Crafted Jpeg Image can trigger double free. This vulnerability appears to have been fixed in after commit ac16bdf2d41724b5a65255d4c28fb0ec46bc42f5.", + CvssV2: &grypeDB.Cvss{ + BaseScore: 6.8, + ExploitabilityScore: 8.6, + ImpactScore: 6.4, + Vector: "AV:N/AC:M/Au:N/C:P/I:P/A:P", + }, + CvssV3: &grypeDB.Cvss{ + BaseScore: 8.8, + ExploitabilityScore: 2.8, + ImpactScore: 5.9, + Vector: "CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H", + }, + }, + }, + { + name: "AppCompoundVersionRange", + numEntries: 1, + fixture: "test-fixtures/compound-pkg.json", + vulns: []grypeDB.Vulnerability{ + { + ID: "CVE-2018-10189", + RecordSource: "nvdv2:cves", + PackageName: "mautic", + VersionConstraint: ">= 1.0.0, <= 1.4.1 || >= 2.0.0, < 2.13.0", + VersionFormat: "unknown", // TODO: this should reference a format, yes? (not a string) + ProxyVulnerabilities: []string{}, + Namespace: "nvd", + CPEs: []string{"cpe:2.3:a:mautic:mautic:*:*:*:*:*:*:*:*"}, // note: entry was dedupicated + }, + }, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "CVE-2018-10189", + RecordSource: "nvdv2:cves", + Severity: "High", + Links: []string{"https://github.com/mautic/mautic/releases/tag/2.13.0"}, + Description: "An issue was discovered in Mautic 1.x and 2.x before 2.13.0. It is possible to systematically emulate tracking cookies per contact due to tracking the contact by their auto-incremented ID. Thus, a third party can manipulate the cookie value with +1 to systematically assume being tracked as each contact in Mautic. It is then possible to retrieve information about the contact through forms that have progressive profiling enabled.", + CvssV2: &grypeDB.Cvss{ + BaseScore: 5, + ExploitabilityScore: 10, + ImpactScore: 2.9, + Vector: "AV:N/AC:L/Au:N/C:P/I:N/A:N", + }, + CvssV3: &grypeDB.Cvss{ + BaseScore: 7.5, + ExploitabilityScore: 3.9, + ImpactScore: 3.6, + Vector: "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N", + }, + }, + }, + { + name: "InvalidCPE", + numEntries: 1, + fixture: "test-fixtures/invalid_cpe.json", + vulns: []grypeDB.Vulnerability{}, + metadata: grypeDB.VulnerabilityMetadata{}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + f, err := os.Open(test.fixture) + require.NoError(t, err) + t.Cleanup(func() { + assert.NoError(t, f.Close()) + }) + + entries, err := unmarshal.NvdVulnerabilityEntries(f) + assert.NoError(t, err) + + var vulns []grypeDB.Vulnerability + for _, entry := range entries { + dataEntries, err := Transform(entry.Cve) + assert.NoError(t, err) + + for _, entry := range dataEntries { + switch vuln := entry.Data.(type) { + case grypeDB.Vulnerability: + vulns = append(vulns, vuln) + case grypeDB.VulnerabilityMetadata: + // check metadata + if diff := deep.Equal(test.metadata, vuln); diff != nil { + for _, d := range diff { + t.Errorf("metadata diff: %+v", d) + } + } + default: + t.Fatalf("unexpected condition: data entry does not have a vulnerability or a metadata") + } + } + + } + + assert.ElementsMatch(t, test.vulns, vulns) + }) + } +} diff --git a/pkg/process/v2/transformers/nvd/unique_pkg.go b/pkg/process/v2/transformers/nvd/unique_pkg.go new file mode 100644 index 00000000..48791517 --- /dev/null +++ b/pkg/process/v2/transformers/nvd/unique_pkg.go @@ -0,0 +1,115 @@ +package nvd + +import ( + "fmt" + "strings" + + "github.com/anchore/grype-db/pkg/provider/unmarshal/nvd" + + "github.com/anchore/grype-db/internal/log" + "github.com/anchore/grype-db/pkg/process/common" + "github.com/umisama/go-cpe" +) + +const ( + ANY = "*" + NA = "-" +) + +type pkgCandidate struct { + Product string + Vendor string + TargetSoftware string +} + +func (p pkgCandidate) String() string { + return fmt.Sprintf("%s|%s|%s", p.Vendor, p.Product, p.TargetSoftware) +} + +func newPkgCandidate(match nvd.CpeMatch) (*pkgCandidate, error) { + // we are only interested in packages that are vulnerable (not related to secondary match conditioning) + if !match.Vulnerable { + return nil, nil + } + + c, err := cpe.NewItemFromFormattedString(match.Criteria) + if err != nil { + return nil, fmt.Errorf("unable to create uniquePkgEntry from '%s': %w", match.Criteria, err) + } + + // we are only interested in applications, not hardware or operating systems + if c.Part() != cpe.Application { + return nil, nil + } + + return &pkgCandidate{ + Product: c.Product().String(), + Vendor: c.Vendor().String(), + TargetSoftware: c.TargetSw().String(), + }, nil +} + +func findUniquePkgs(cfgs ...nvd.Configuration) uniquePkgTracker { + set := newUniquePkgTracker() + for _, c := range cfgs { + _findUniquePkgs(set, c.Nodes...) + } + return set +} + +func _findUniquePkgs(set uniquePkgTracker, ns ...nvd.Node) { + if len(ns) == 0 { + return + } + for _, node := range ns { + for _, match := range node.CpeMatch { + candidate, err := newPkgCandidate(match) + if err != nil { + // Do not halt all execution because of being unable to create + // a PkgCandidate. This can happen when a CPE is invalid which + // could avoid creating a database + log.Debugf("unable processing uniquePkg: %v", err) + continue + } + if candidate != nil { + set.Add(*candidate, match) + } + } + } +} + +func buildConstraints(matches []nvd.CpeMatch) string { + constraints := make([]string, 0) + for _, match := range matches { + constraints = append(constraints, buildConstraint(match)) + } + return common.OrConstraints(constraints...) +} + +func buildConstraint(match nvd.CpeMatch) string { + constraints := make([]string, 0) + if match.VersionStartIncluding != nil && *match.VersionStartIncluding != "" { + constraints = append(constraints, fmt.Sprintf(">= %s", *match.VersionStartIncluding)) + } else if match.VersionStartExcluding != nil && *match.VersionStartExcluding != "" { + constraints = append(constraints, fmt.Sprintf("> %s", *match.VersionStartExcluding)) + } + + if match.VersionEndIncluding != nil && *match.VersionEndIncluding != "" { + constraints = append(constraints, fmt.Sprintf("<= %s", *match.VersionEndIncluding)) + } else if match.VersionEndExcluding != nil && *match.VersionEndExcluding != "" { + constraints = append(constraints, fmt.Sprintf("< %s", *match.VersionEndExcluding)) + } + + if len(constraints) == 0 { + c, err := cpe.NewItemFromFormattedString(match.Criteria) + if err != nil { + return "" + } + version := c.Version().String() + if version != ANY && version != NA { + constraints = append(constraints, fmt.Sprintf("= %s", version)) + } + } + + return strings.Join(constraints, ", ") +} diff --git a/pkg/process/v2/transformers/nvd/unique_pkg_test.go b/pkg/process/v2/transformers/nvd/unique_pkg_test.go new file mode 100644 index 00000000..9a98731f --- /dev/null +++ b/pkg/process/v2/transformers/nvd/unique_pkg_test.go @@ -0,0 +1,352 @@ +package nvd + +import ( + "github.com/anchore/grype-db/pkg/provider/unmarshal/nvd" + "testing" + + "github.com/sergi/go-diff/diffmatchpatch" +) + +func newUniquePkgTrackerFromSlice(candidates []pkgCandidate) uniquePkgTracker { + set := newUniquePkgTracker() + for _, c := range candidates { + set[c] = nil + } + return set +} + +func TestFindUniquePkgs(t *testing.T) { + tests := []struct { + name string + nodes []nvd.Node + expected uniquePkgTracker + }{ + { + name: "simple-match", + nodes: []nvd.Node{ + { + CpeMatch: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendor:product:2.2.0:*:*:*:*:target:*:*", + Vulnerable: true, + }, + }, + }, + }, + expected: newUniquePkgTrackerFromSlice( + []pkgCandidate{ + { + Product: "product", + Vendor: "vendor", + TargetSoftware: "target", + }, + }), + }, + { + name: "skip-hw", + nodes: []nvd.Node{ + { + CpeMatch: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:h:vendor:product:2.2.0:*:*:*:*:target:*:*", + Vulnerable: true, + }, + }, + }, + }, + expected: newUniquePkgTrackerFromSlice([]pkgCandidate{}), + }, + { + name: "skip-os", + nodes: []nvd.Node{ + { + CpeMatch: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:o:vendor:product:2.2.0:*:*:*:*:target:*:*", + Vulnerable: true, + }, + }, + }, + }, + expected: newUniquePkgTrackerFromSlice([]pkgCandidate{}), + }, + { + name: "duplicate-by-product", + nodes: []nvd.Node{ + { + CpeMatch: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendor:productA:3.3.3:*:*:*:*:target:*:*", + Vulnerable: true, + }, + { + Criteria: "cpe:2.3:a:vendor:productB:2.2.0:*:*:*:*:target:*:*", + Vulnerable: true, + }, + }, + Operator: "OR", + }, + }, + expected: newUniquePkgTrackerFromSlice( + []pkgCandidate{ + { + Product: "productA", + Vendor: "vendor", + TargetSoftware: "target", + }, + { + Product: "productB", + Vendor: "vendor", + TargetSoftware: "target", + }, + }), + }, + { + name: "duplicate-by-target", + nodes: []nvd.Node{ + { + CpeMatch: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendor:product:3.3.3:*:*:*:*:targetA:*:*", + Vulnerable: true, + }, + { + Criteria: "cpe:2.3:a:vendor:product:2.2.0:*:*:*:*:targetB:*:*", + Vulnerable: true, + }, + }, + Operator: "OR", + }, + }, + expected: newUniquePkgTrackerFromSlice( + []pkgCandidate{ + { + Product: "product", + Vendor: "vendor", + TargetSoftware: "targetA", + }, + { + Product: "product", + Vendor: "vendor", + TargetSoftware: "targetB", + }, + }), + }, + { + name: "duplicate-by-vendor", + nodes: []nvd.Node{ + { + CpeMatch: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendorA:product:3.3.3:*:*:*:*:target:*:*", + Vulnerable: true, + }, + { + Criteria: "cpe:2.3:a:vendorB:product:2.2.0:*:*:*:*:target:*:*", + Vulnerable: true, + }, + }, + Operator: "OR", + }, + }, + expected: newUniquePkgTrackerFromSlice( + []pkgCandidate{ + { + Product: "product", + Vendor: "vendorA", + TargetSoftware: "target", + }, + { + Product: "product", + Vendor: "vendorB", + TargetSoftware: "target", + }, + }), + }, + { + name: "de-duplicate-case", + nodes: []nvd.Node{ + { + CpeMatch: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendor:product:3.3.3:A:B:C:D:target:E:F", + Vulnerable: true, + }, + { + Criteria: "cpe:2.3:a:vendor:product:2.2.0:Q:R:S:T:target:U:V", + Vulnerable: true, + }, + }, + Operator: "OR", + }, + }, + expected: newUniquePkgTrackerFromSlice( + []pkgCandidate{ + { + Product: "product", + Vendor: "vendor", + TargetSoftware: "target", + }, + }), + }, + { + name: "duplicate-from-nested-nodes", + nodes: []nvd.Node{ + { + CpeMatch: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendorB:product:2.2.0:*:*:*:*:target:*:*", + Vulnerable: true, + }, + }, + Operator: "OR", + }, + { + CpeMatch: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendorA:product:2.2.0:*:*:*:*:target:*:*", + Vulnerable: true, + }, + }, + Operator: "OR", + }, + }, + expected: newUniquePkgTrackerFromSlice( + []pkgCandidate{ + { + Product: "product", + Vendor: "vendorA", + TargetSoftware: "target", + }, + { + Product: "product", + Vendor: "vendorB", + TargetSoftware: "target", + }, + }), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actual := findUniquePkgs(nvd.Configuration{Nodes: test.nodes}) + missing, extra := test.expected.Diff(actual) + if len(missing) != 0 { + for _, c := range missing { + t.Errorf("missing candidate: %+v", c) + } + } + + if len(extra) != 0 { + for _, c := range extra { + t.Errorf("extra candidate: %+v", c) + } + } + }) + } + +} + +func strRef(s string) *string { + return &s +} + +func TestBuildConstraints(t *testing.T) { + tests := []struct { + name string + matches []nvd.CpeMatch + expected string + }{ + { + name: "Equals", + matches: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendor:product:2.2.0:*:*:*:*:target:*:*", + }, + }, + expected: "= 2.2.0", + }, + { + name: "VersionEndExcluding", + matches: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", + VersionEndExcluding: strRef("2.3.0"), + }, + }, + expected: "< 2.3.0", + }, + { + name: "VersionEndIncluding", + matches: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", + VersionEndIncluding: strRef("2.3.0"), + }, + }, + expected: "<= 2.3.0", + }, + { + name: "VersionStartExcluding", + matches: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", + VersionStartExcluding: strRef("2.3.0"), + }, + }, + expected: "> 2.3.0", + }, + { + name: "VersionStartIncluding", + matches: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", + VersionStartIncluding: strRef("2.3.0"), + }, + }, + expected: ">= 2.3.0", + }, + { + name: "Version Range", + matches: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", + VersionStartIncluding: strRef("2.3.0"), + VersionEndIncluding: strRef("2.5.0"), + }, + }, + expected: ">= 2.3.0, <= 2.5.0", + }, + { + name: "Multiple Version Ranges", + matches: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", + VersionStartIncluding: strRef("2.3.0"), + VersionEndIncluding: strRef("2.5.0"), + }, + { + Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", + VersionStartExcluding: strRef("3.3.0"), + VersionEndExcluding: strRef("3.5.0"), + }, + }, + expected: ">= 2.3.0, <= 2.5.0 || > 3.3.0, < 3.5.0", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actual := buildConstraints(test.matches) + + if actual != test.expected { + dmp := diffmatchpatch.New() + diffs := dmp.DiffMain(actual, test.expected, true) + t.Errorf("Expected: %q", test.expected) + t.Errorf("Got : %q", actual) + t.Errorf("Diff : %q", dmp.DiffPrettyText(diffs)) + } + }) + } + +} diff --git a/pkg/process/v2/transformers/nvd/unique_pkg_tracker.go b/pkg/process/v2/transformers/nvd/unique_pkg_tracker.go new file mode 100644 index 00000000..2b7e405d --- /dev/null +++ b/pkg/process/v2/transformers/nvd/unique_pkg_tracker.go @@ -0,0 +1,64 @@ +package nvd + +import ( + "sort" + + "github.com/anchore/grype-db/pkg/provider/unmarshal/nvd" +) + +type uniquePkgTracker map[pkgCandidate][]nvd.CpeMatch + +func newUniquePkgTracker() uniquePkgTracker { + return make(uniquePkgTracker) +} + +func (s uniquePkgTracker) Diff(other uniquePkgTracker) (missing []pkgCandidate, extra []pkgCandidate) { + for k := range s { + if !other.Contains(k) { + missing = append(missing, k) + } + } + + for k := range other { + if !s.Contains(k) { + extra = append(extra, k) + } + } + + return +} + +func (s uniquePkgTracker) Matches(i pkgCandidate) []nvd.CpeMatch { + return s[i] +} + +func (s uniquePkgTracker) Add(i pkgCandidate, match nvd.CpeMatch) { + if _, ok := s[i]; !ok { + s[i] = make([]nvd.CpeMatch, 0) + } + s[i] = append(s[i], match) +} + +func (s uniquePkgTracker) Remove(i pkgCandidate) { + delete(s, i) +} + +func (s uniquePkgTracker) Contains(i pkgCandidate) bool { + _, ok := s[i] + return ok +} + +func (s uniquePkgTracker) All() []pkgCandidate { + res := make([]pkgCandidate, len(s)) + idx := 0 + for k := range s { + res[idx] = k + idx++ + } + + sort.SliceStable(res, func(i, j int) bool { + return res[i].String() < res[j].String() + }) + + return res +} diff --git a/pkg/process/v2/transformers/os/test-fixtures/alpine-3.9.json b/pkg/process/v2/transformers/os/test-fixtures/alpine-3.9.json new file mode 100644 index 00000000..b9d84395 --- /dev/null +++ b/pkg/process/v2/transformers/os/test-fixtures/alpine-3.9.json @@ -0,0 +1,28 @@ +[ + { + "Vulnerability": { + "CVSS": [], + "Description": "", + "FixedIn": [ + { + "Name": "xen", + "NamespaceName": "alpine:3.9", + "Version": "4.11.1-r0", + "VersionFormat": "apk" + } + ], + "Link": "http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-19967", + "Metadata": { + "NVD": { + "CVSSv2": { + "Score": 4.9, + "Vectors": "AV:L/AC:L/Au:N/C:N/I:N/A:C" + } + } + }, + "Name": "CVE-2018-19967", + "NamespaceName": "alpine:3.9", + "Severity": "Medium" + } + } +] \ No newline at end of file diff --git a/pkg/process/v2/transformers/os/test-fixtures/amzn.json b/pkg/process/v2/transformers/os/test-fixtures/amzn.json new file mode 100644 index 00000000..a862c32e --- /dev/null +++ b/pkg/process/v2/transformers/os/test-fixtures/amzn.json @@ -0,0 +1,49 @@ +[ + { + "Vulnerability": { + "Description": "", + "FixedIn": [ + { + "Name": "389-ds-base", + "NamespaceName": "amzn:2", + "Version": "1.3.8.4-15.amzn2.0.1", + "VersionFormat": "rpm" + }, + { + "Name": "389-ds-base-debuginfo", + "NamespaceName": "amzn:2", + "Version": "1.3.8.4-15.amzn2.0.1", + "VersionFormat": "rpm" + }, + { + "Name": "389-ds-base-devel", + "NamespaceName": "amzn:2", + "Version": "1.3.8.4-15.amzn2.0.1", + "VersionFormat": "rpm" + }, + { + "Name": "389-ds-base-libs", + "NamespaceName": "amzn:2", + "Version": "1.3.8.4-15.amzn2.0.1", + "VersionFormat": "rpm" + }, + { + "Name": "389-ds-base-snmp", + "NamespaceName": "amzn:2", + "Version": "1.3.8.4-15.amzn2.0.1", + "VersionFormat": "rpm" + } + ], + "Link": "https://alas.aws.amazon.com/AL2/ALAS-2018-1106.html", + "Metadata": { + "CVE": [ + + {"Name": "CVE-2018-14648"} + ] + }, + "Name": "ALAS-2018-1106", + "NamespaceName": "amzn:2", + "Severity": "Medium" + } + } +] \ No newline at end of file diff --git a/pkg/process/v2/transformers/os/test-fixtures/debian-8-multiple-entries-for-same-package.json b/pkg/process/v2/transformers/os/test-fixtures/debian-8-multiple-entries-for-same-package.json new file mode 100644 index 00000000..5025b56e --- /dev/null +++ b/pkg/process/v2/transformers/os/test-fixtures/debian-8-multiple-entries-for-same-package.json @@ -0,0 +1,62 @@ +[ + { + "Vulnerability": { + "CVSS": [], + "Description": "", + "FixedIn": [ + { + "Name": "rsyslog", + "NamespaceName": "debian:8", + "VendorAdvisory": { + "AdvisorySummary": [], + "NoAdvisory": false + }, + "Version": "5.7.4-1", + "VersionFormat": "dpkg" + } + ], + "Link": "https://security-tracker.debian.org/tracker/CVE-2011-4623", + "Metadata": { + "NVD": { + "CVSSv2": { + "Score": 2.1, + "Vectors": "AV:L/AC:L/Au:N/C:N/I:N/A:P" + } + } + }, + "Name": "CVE-2011-4623", + "NamespaceName": "debian:8", + "Severity": "Low" + } + }, + { + "Vulnerability": { + "CVSS": [], + "Description": "", + "FixedIn": [ + { + "Name": "rsyslog", + "NamespaceName": "debian:8", + "VendorAdvisory": { + "AdvisorySummary": [], + "NoAdvisory": false + }, + "Version": "3.18.6-1", + "VersionFormat": "dpkg" + } + ], + "Link": "https://security-tracker.debian.org/tracker/CVE-2008-5618", + "Metadata": { + "NVD": { + "CVSSv2": { + "Score": 5, + "Vectors": "AV:N/AC:L/Au:N/C:N/I:N/A:P" + } + } + }, + "Name": "CVE-2008-5618", + "NamespaceName": "debian:8", + "Severity": "Low" + } + } +] \ No newline at end of file diff --git a/pkg/process/v2/transformers/os/test-fixtures/debian-8.json b/pkg/process/v2/transformers/os/test-fixtures/debian-8.json new file mode 100644 index 00000000..a758f13c --- /dev/null +++ b/pkg/process/v2/transformers/os/test-fixtures/debian-8.json @@ -0,0 +1,62 @@ +[ + { + "Vulnerability": { + "CVSS": [], + "Description": "", + "FixedIn": [ + { + "Name": "asterisk", + "NamespaceName": "debian:8", + "VendorAdvisory": { + "AdvisorySummary": [], + "NoAdvisory": false + }, + "Version": "1:1.6.2.0~rc3-1", + "VersionFormat": "dpkg" + }, + { + "Name": "auth2db", + "NamespaceName": "debian:8", + "VendorAdvisory": { + "AdvisorySummary": [], + "NoAdvisory": false + }, + "Version": "0.2.5-2+dfsg-1", + "VersionFormat": "dpkg" + }, + { + "Name": "exaile", + "NamespaceName": "debian:8", + "VendorAdvisory": { + "AdvisorySummary": [], + "NoAdvisory": false + }, + "Version": "0.2.14+debian-2.2", + "VersionFormat": "dpkg" + }, + { + "Name": "wordpress", + "NamespaceName": "debian:8", + "VendorAdvisory": { + "AdvisorySummary": [], + "NoAdvisory": false + }, + "Version": "", + "VersionFormat": "dpkg" + } + ], + "Link": "https://security-tracker.debian.org/tracker/CVE-2008-7220", + "Metadata": { + "NVD": { + "CVSSv2": { + "Score": 7.5, + "Vectors": "AV:N/AC:L/Au:N/C:P/I:P/A:P" + } + } + }, + "Name": "CVE-2008-7220", + "NamespaceName": "debian:8", + "Severity": "High" + } + } +] \ No newline at end of file diff --git a/pkg/process/v2/transformers/os/test-fixtures/ol-8-modules.json b/pkg/process/v2/transformers/os/test-fixtures/ol-8-modules.json new file mode 100644 index 00000000..f1d7372b --- /dev/null +++ b/pkg/process/v2/transformers/os/test-fixtures/ol-8-modules.json @@ -0,0 +1,36 @@ +[ + { + "Vulnerability": { + "CVSS": [], + "Description": "A flaw was found in PostgreSQL, where some PostgreSQL extensions did not use the search_path safely in their installation script. This flaw allows an attacker with sufficient privileges to trick an administrator into executing a specially crafted script during the extension's installation or update. The highest threat from this vulnerability is to confidentiality, integrity, as well as system availability.", + "FixedIn": [ + { + "Module": "postgresql:10", + "Name": "postgresql", + "NamespaceName": "ol:8", + "Version": "0:10.14-1.module+el8.2.0+7801+be0fed80", + "VersionFormat": "rpm" + }, + { + "Module": "postgresql:12", + "Name": "postgresql", + "NamespaceName": "ol:8", + "Version": "0:12.5-1.module+el8.3.0+9042+664538f4", + "VersionFormat": "rpm" + }, + { + "Module": "postgresql:9.6", + "Name": "postgresql", + "NamespaceName": "ol:8", + "Version": "0:9.6.20-1.module+el8.3.0+8938+7f0e88b6", + "VersionFormat": "rpm" + } + ], + "Link": "https://access.redhat.com/security/cve/CVE-2020-14350", + "Metadata": {}, + "Name": "CVE-2020-14350", + "NamespaceName": "ol:8", + "Severity": "Medium" + } + } +] \ No newline at end of file diff --git a/pkg/process/v2/transformers/os/test-fixtures/ol-8.json b/pkg/process/v2/transformers/os/test-fixtures/ol-8.json new file mode 100644 index 00000000..09439ece --- /dev/null +++ b/pkg/process/v2/transformers/os/test-fixtures/ol-8.json @@ -0,0 +1,42 @@ +[ + { + "Vulnerability": { + "CVSS": [], + "Description": "", + "FixedIn": [ + { + "Name": "libexif", + "NamespaceName": "ol:8", + "Version": "0:0.6.21-17.el8_2", + "VersionFormat": "rpm" + }, + { + "Name": "libexif-devel", + "NamespaceName": "ol:8", + "Version": "0:0.6.21-17.el8_2", + "VersionFormat": "rpm" + }, + { + "Name": "libexif-dummy", + "NamespaceName": "ol:8", + "Version": "None", + "VersionFormat": "rpm" + } + ], + "Link": "http://linux.oracle.com/errata/ELSA-2020-2550.html", + "Metadata": { + "CVE": [ + { + "Link": "http://linux.oracle.com/cve/CVE-2020-13112.html", + "Name": "CVE-2020-13112" + } + ], + "Issued": "2020-06-15", + "RefId": "ELSA-2020-2550" + }, + "Name": "ELSA-2020-2550", + "NamespaceName": "ol:8", + "Severity": "Medium" + } + } +] \ No newline at end of file diff --git a/pkg/process/v2/transformers/os/test-fixtures/rhel-8-modules.json b/pkg/process/v2/transformers/os/test-fixtures/rhel-8-modules.json new file mode 100644 index 00000000..c0400ad5 --- /dev/null +++ b/pkg/process/v2/transformers/os/test-fixtures/rhel-8-modules.json @@ -0,0 +1,75 @@ +[ + { + "Vulnerability": { + "CVSS": [ + { + "base_metrics": { + "base_score": 7.1, + "base_severity": "High", + "exploitability_score": 1.2, + "impact_score": 5.9 + }, + "status": "verified", + "vector_string": "CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:U/C:H/I:H/A:H", + "version": "3.1" + } + ], + "Description": "A flaw was found in PostgreSQL, where some PostgreSQL extensions did not use the search_path safely in their installation script. This flaw allows an attacker with sufficient privileges to trick an administrator into executing a specially crafted script during the extension's installation or update. The highest threat from this vulnerability is to confidentiality, integrity, as well as system availability.", + "FixedIn": [ + { + "Module": "postgresql:10", + "Name": "postgresql", + "NamespaceName": "rhel:8", + "VendorAdvisory": { + "AdvisorySummary": [ + { + "ID": "RHSA-2020:3669", + "Link": "https://access.redhat.com/errata/RHSA-2020:3669" + } + ], + "NoAdvisory": false + }, + "Version": "0:10.14-1.module+el8.2.0+7801+be0fed80", + "VersionFormat": "rpm" + }, + { + "Module": "postgresql:12", + "Name": "postgresql", + "NamespaceName": "rhel:8", + "VendorAdvisory": { + "AdvisorySummary": [ + { + "ID": "RHSA-2020:5620", + "Link": "https://access.redhat.com/errata/RHSA-2020:5620" + } + ], + "NoAdvisory": false + }, + "Version": "0:12.5-1.module+el8.3.0+9042+664538f4", + "VersionFormat": "rpm" + }, + { + "Module": "postgresql:9.6", + "Name": "postgresql", + "NamespaceName": "rhel:8", + "VendorAdvisory": { + "AdvisorySummary": [ + { + "ID": "RHSA-2020:5619", + "Link": "https://access.redhat.com/errata/RHSA-2020:5619" + } + ], + "NoAdvisory": false + }, + "Version": "0:9.6.20-1.module+el8.3.0+8938+7f0e88b6", + "VersionFormat": "rpm" + } + ], + "Link": "https://access.redhat.com/security/cve/CVE-2020-14350", + "Metadata": {}, + "Name": "CVE-2020-14350", + "NamespaceName": "rhel:8", + "Severity": "Medium" + } + } +] \ No newline at end of file diff --git a/pkg/process/v2/transformers/os/test-fixtures/rhel-8.json b/pkg/process/v2/transformers/os/test-fixtures/rhel-8.json new file mode 100644 index 00000000..2779708c --- /dev/null +++ b/pkg/process/v2/transformers/os/test-fixtures/rhel-8.json @@ -0,0 +1,57 @@ +[ + { + "Vulnerability": { + "CVSS": [ + { + "base_metrics": { + "base_score": 8.8, + "base_severity": "High", + "exploitability_score": 2.8, + "impact_score": 5.9 + }, + "status": "verified", + "vector_string": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H", + "version": "3.1" + } + ], + "Description": "A flaw was found in Mozilla Firefox. A race condition can occur while running the nsDocShell destructor causing a use-after-free memory issue. The highest threat from this vulnerability is to data confidentiality and integrity as well as system availability.", + "FixedIn": [ + { + "Name": "firefox", + "NamespaceName": "rhel:8", + "VendorAdvisory": { + "AdvisorySummary": [ + { + "ID": "RHSA-2020:1341", + "Link": "https://access.redhat.com/errata/RHSA-2020:1341" + } + ], + "NoAdvisory": false + }, + "Version": "0:68.6.1-1.el8_1", + "VersionFormat": "rpm" + }, + { + "Name": "thunderbird", + "NamespaceName": "rhel:8", + "VendorAdvisory": { + "AdvisorySummary": [ + { + "ID": "RHSA-2020:1495", + "Link": "https://access.redhat.com/errata/RHSA-2020:1495" + } + ], + "NoAdvisory": false + }, + "Version": "0:68.7.0-1.el8_1", + "VersionFormat": "rpm" + } + ], + "Link": "https://access.redhat.com/security/cve/CVE-2020-6819", + "Metadata": {}, + "Name": "CVE-2020-6819", + "NamespaceName": "rhel:8", + "Severity": "Critical" + } + } +] \ No newline at end of file diff --git a/pkg/process/v2/transformers/os/test-fixtures/unmarshal-test.json b/pkg/process/v2/transformers/os/test-fixtures/unmarshal-test.json new file mode 100644 index 00000000..edc6d25b --- /dev/null +++ b/pkg/process/v2/transformers/os/test-fixtures/unmarshal-test.json @@ -0,0 +1,104 @@ +[ + { + "Vulnerability": { + "Description": "", + "FixedIn": [ + { + "Name": "389-ds-base", + "NamespaceName": "amzn:2", + "Version": "1.3.8.4-15.amzn2.0.1", + "VersionFormat": "rpm" + }, + { + "Name": "389-ds-base-debuginfo", + "NamespaceName": "amzn:2", + "Version": "1.3.8.4-15.amzn2.0.1", + "VersionFormat": "rpm" + }, + { + "Name": "389-ds-base-devel", + "NamespaceName": "amzn:2", + "Version": "1.3.8.4-15.amzn2.0.1", + "VersionFormat": "rpm" + }, + { + "Name": "389-ds-base-libs", + "NamespaceName": "amzn:2", + "Version": "1.3.8.4-15.amzn2.0.1", + "VersionFormat": "rpm" + }, + { + "Name": "389-ds-base-snmp", + "NamespaceName": "amzn:2", + "Version": "1.3.8.4-15.amzn2.0.1", + "VersionFormat": "rpm" + } + ], + "Link": "https://alas.aws.amazon.com/AL2/ALAS-2018-1106.html", + "Metadata": { + "CVE": [ + {"Name": "CVE-2018-14648"} + ] + }, + "Name": "ALAS-2018-1106", + "NamespaceName": "amzn:2", + "Severity": "Medium" + } + }, + { + "Vulnerability": { + "Description": "", + "FixedIn": [ + { + "Name": "kernel-livepatch-4.14.173-137.228", + "NamespaceName": "amzn:2", + "Version": "1.0-3.amzn2", + "VersionFormat": "rpm" + }, + { + "Name": "kernel-livepatch-4.14.173-137.228-debuginfo", + "NamespaceName": "amzn:2", + "Version": "1.0-3.amzn2", + "VersionFormat": "rpm" + } + ], + "Link": "https://alas.aws.amazon.com/AL2/ALASLIVEPATCH-2020-012.html", + "Metadata": { + "CVE": [ + {"Name": "CVE-2020-12657"} + ] + }, + "Name": "ALASLIVEPATCH-2020-012", + "NamespaceName": "amzn:2", + "Severity": "High" + } + }, + { + "Vulnerability": { + "Description": "", + "FixedIn": [ + { + "Name": "kernel-livepatch-4.14.171-136.231", + "NamespaceName": "amzn:2", + "Version": "1.0-5.amzn2", + "VersionFormat": "rpm" + }, + { + "Name": "kernel-livepatch-4.14.171-136.231-debuginfo", + "NamespaceName": "amzn:2", + "Version": "1.0-5.amzn2", + "VersionFormat": "rpm" + } + ], + "Link": "https://alas.aws.amazon.com/AL2/ALASLIVEPATCH-2020-011.html", + "Metadata": { + "CVE": [ + {"Name": "CVE-2020-12657"} + ] + }, + "Name": "ALASLIVEPATCH-2020-011", + "NamespaceName": "amzn:2", + "Severity": "High" + } + } +] \ No newline at end of file diff --git a/pkg/process/v2/transformers/os/transform.go b/pkg/process/v2/transformers/os/transform.go new file mode 100644 index 00000000..6fc496c8 --- /dev/null +++ b/pkg/process/v2/transformers/os/transform.go @@ -0,0 +1,103 @@ +package os + +import ( + "fmt" + "strings" + + "github.com/anchore/grype-db/pkg/data" + "github.com/anchore/grype-db/pkg/process/common" + "github.com/anchore/grype-db/pkg/process/v2/transformers" + "github.com/anchore/grype-db/pkg/provider/unmarshal" + grypeDB "github.com/anchore/grype/grype/db/v2" +) + +const ( + // TODO: tech debt from a previous design + feed = "vulnerabilities" +) + +func Transform(vulnerability unmarshal.OSVulnerability) ([]data.Entry, error) { + group := vulnerability.Vulnerability.NamespaceName + + var allVulns []grypeDB.Vulnerability + + recordSource := grypeDB.RecordSource(feed, group) + vulnerability.Vulnerability.FixedIn = vulnerability.Vulnerability.FixedIn.FilterToHighestModularity() + + // there may be multiple packages indicated within the FixedIn field, we should make + // separate vulnerability entries (one for each name|namespace combo) while merging + // constraint ranges as they are found. + for _, advisory := range vulnerability.Vulnerability.FixedIn { + constraint, err := enforceConstraint(advisory.Version, advisory.VersionFormat) + if err != nil { + return nil, err + } + + // create vulnerability entry + vuln := grypeDB.Vulnerability{ + ID: vulnerability.Vulnerability.Name, + RecordSource: recordSource, + VersionConstraint: constraint, + VersionFormat: advisory.VersionFormat, + PackageName: advisory.Name, + Namespace: advisory.NamespaceName, + ProxyVulnerabilities: []string{}, + FixedInVersion: common.CleanFixedInVersion(advisory.Version), + } + + // associate related vulnerabilities + // note: an example of multiple CVEs for a record is centos:5 RHSA-2007:0055 which maps to CVE-2007-0002 and CVE-2007-1466 + for _, ref := range vulnerability.Vulnerability.Metadata.CVE { + vuln.ProxyVulnerabilities = append(vuln.ProxyVulnerabilities, ref.Name) + } + + allVulns = append(allVulns, vuln) + } + + var cvssV2 *grypeDB.Cvss + if vulnerability.Vulnerability.Metadata.NVD.CVSSv2.Vectors != "" { + cvssV2 = &grypeDB.Cvss{ + BaseScore: vulnerability.Vulnerability.Metadata.NVD.CVSSv2.Score, + ExploitabilityScore: 0, + ImpactScore: 0, + Vector: vulnerability.Vulnerability.Metadata.NVD.CVSSv2.Vectors, + } + } + + // find all URLs related to the vulnerability + links := []string{vulnerability.Vulnerability.Link} + if vulnerability.Vulnerability.Metadata.CVE != nil { + for _, cve := range vulnerability.Vulnerability.Metadata.CVE { + if cve.Link != "" { + links = append(links, cve.Link) + } + } + } + + // create vulnerability metadata entry (a single entry keyed off of the vulnerability ID) + metadata := grypeDB.VulnerabilityMetadata{ + ID: vulnerability.Vulnerability.Name, + RecordSource: recordSource, + Severity: vulnerability.Vulnerability.Severity, + Links: links, + Description: vulnerability.Vulnerability.Description, + CvssV2: cvssV2, + } + + return transformers.NewEntries(allVulns, metadata), nil +} + +func enforceConstraint(constraint, format string) (string, error) { + constraint = common.CleanConstraint(constraint) + if len(constraint) == 0 { + return "", nil + } + switch strings.ToLower(format) { + case "dpkg", "rpm", "apk": + // the passed constraint is a fixed version + return fmt.Sprintf("< %s", constraint), nil + case "semver": + return common.EnforceSemVerConstraint(constraint), nil + } + return "", fmt.Errorf("unable to enforce constraint='%s' format='%s'", constraint, format) +} diff --git a/pkg/process/v2/transformers/os/transform_test.go b/pkg/process/v2/transformers/os/transform_test.go new file mode 100644 index 00000000..961be009 --- /dev/null +++ b/pkg/process/v2/transformers/os/transform_test.go @@ -0,0 +1,432 @@ +package os + +import ( + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/require" + "os" + "testing" + + testUtils "github.com/anchore/grype-db/pkg/process/tests" + "github.com/anchore/grype-db/pkg/provider/unmarshal" + grypeDB "github.com/anchore/grype/grype/db/v2" + "github.com/stretchr/testify/assert" +) + +func TestUnmarshalVulnerabilitiesEntries(t *testing.T) { + f, err := os.Open("test-fixtures/unmarshal-test.json") + require.NoError(t, err) + defer testUtils.CloseFile(f) + + entries, err := unmarshal.OSVulnerabilityEntries(f) + require.NoError(t, err) + + require.Len(t, entries, 3) +} + +func TestParseVulnerabilitiesEntry(t *testing.T) { + tests := []struct { + name string + numEntries int + fixture string + vulns []grypeDB.Vulnerability + metadata grypeDB.VulnerabilityMetadata + feed, group string + }{ + { + name: "Amazon", + numEntries: 1, + fixture: "test-fixtures/amzn.json", + feed: "vulnerabilities", + group: "amzn:2", + vulns: []grypeDB.Vulnerability{ + { + ID: "ALAS-2018-1106", + RecordSource: "vulnerabilities:amzn:2", + VersionConstraint: "< 1.3.8.4-15.amzn2.0.1", + VersionFormat: "rpm", // TODO: this should reference a format, yes? (not a string) + ProxyVulnerabilities: []string{"CVE-2018-14648"}, + PackageName: "389-ds-base", + Namespace: "amzn:2", + FixedInVersion: "1.3.8.4-15.amzn2.0.1", + }, + { + ID: "ALAS-2018-1106", + RecordSource: "vulnerabilities:amzn:2", + VersionConstraint: "< 1.3.8.4-15.amzn2.0.1", + VersionFormat: "rpm", // TODO: this should reference a format, yes? (not a string) + ProxyVulnerabilities: []string{"CVE-2018-14648"}, + PackageName: "389-ds-base-debuginfo", + Namespace: "amzn:2", + FixedInVersion: "1.3.8.4-15.amzn2.0.1", + }, + { + ID: "ALAS-2018-1106", + RecordSource: "vulnerabilities:amzn:2", + VersionConstraint: "< 1.3.8.4-15.amzn2.0.1", + VersionFormat: "rpm", // TODO: this should reference a format, yes? (not a string) + ProxyVulnerabilities: []string{"CVE-2018-14648"}, + PackageName: "389-ds-base-devel", + Namespace: "amzn:2", + FixedInVersion: "1.3.8.4-15.amzn2.0.1", + }, + { + ID: "ALAS-2018-1106", + RecordSource: "vulnerabilities:amzn:2", + VersionConstraint: "< 1.3.8.4-15.amzn2.0.1", + VersionFormat: "rpm", // TODO: this should reference a format, yes? (not a string) + ProxyVulnerabilities: []string{"CVE-2018-14648"}, + PackageName: "389-ds-base-libs", + Namespace: "amzn:2", + FixedInVersion: "1.3.8.4-15.amzn2.0.1", + }, + { + ID: "ALAS-2018-1106", + RecordSource: "vulnerabilities:amzn:2", + VersionConstraint: "< 1.3.8.4-15.amzn2.0.1", + VersionFormat: "rpm", // TODO: this should reference a format, yes? (not a string) + ProxyVulnerabilities: []string{"CVE-2018-14648"}, + PackageName: "389-ds-base-snmp", + Namespace: "amzn:2", + FixedInVersion: "1.3.8.4-15.amzn2.0.1", + }, + }, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "ALAS-2018-1106", + RecordSource: "vulnerabilities:amzn:2", + Severity: "Medium", + Links: []string{"https://alas.aws.amazon.com/AL2/ALAS-2018-1106.html"}, + }, + }, + { + name: "Debian", + numEntries: 1, + fixture: "test-fixtures/debian-8.json", + feed: "vulnerabilities", + group: "debian:8", + vulns: []grypeDB.Vulnerability{ + { + ID: "CVE-2008-7220", + RecordSource: "vulnerabilities:debian:8", + PackageName: "asterisk", + VersionConstraint: "< 1:1.6.2.0~rc3-1", + VersionFormat: "dpkg", // TODO: this should reference a format, yes? (not a string) + ProxyVulnerabilities: []string{}, + Namespace: "debian:8", + FixedInVersion: "1:1.6.2.0~rc3-1", + }, + { + ID: "CVE-2008-7220", + RecordSource: "vulnerabilities:debian:8", + PackageName: "auth2db", + VersionConstraint: "< 0.2.5-2+dfsg-1", + VersionFormat: "dpkg", // TODO: this should reference a format, yes? (not a string) + ProxyVulnerabilities: []string{}, + Namespace: "debian:8", + FixedInVersion: "0.2.5-2+dfsg-1", + }, + { + ID: "CVE-2008-7220", + RecordSource: "vulnerabilities:debian:8", + PackageName: "exaile", + VersionConstraint: "< 0.2.14+debian-2.2", + VersionFormat: "dpkg", // TODO: this should reference a format, yes? (not a string) + ProxyVulnerabilities: []string{}, + Namespace: "debian:8", + FixedInVersion: "0.2.14+debian-2.2", + }, + { + ID: "CVE-2008-7220", + RecordSource: "vulnerabilities:debian:8", + PackageName: "wordpress", + VersionConstraint: "", + VersionFormat: "dpkg", // TODO: this should reference a format, yes? (not a string) + ProxyVulnerabilities: []string{}, + Namespace: "debian:8", + }, + }, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "CVE-2008-7220", + RecordSource: "vulnerabilities:debian:8", + Severity: "High", + Links: []string{"https://security-tracker.debian.org/tracker/CVE-2008-7220"}, + Description: "", + CvssV2: &grypeDB.Cvss{ + BaseScore: 7.5, + Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P", + }, + }, + }, + { + name: "RHEL", + numEntries: 1, + fixture: "test-fixtures/rhel-8.json", + feed: "vulnerabilities", + group: "rhel:8", + vulns: []grypeDB.Vulnerability{ + { + ID: "CVE-2020-6819", + RecordSource: "vulnerabilities:rhel:8", + PackageName: "firefox", + VersionConstraint: "< 0:68.6.1-1.el8_1", + VersionFormat: "rpm", // TODO: this should reference a format, yes? (not a string) + ProxyVulnerabilities: []string{}, + Namespace: "rhel:8", + FixedInVersion: "0:68.6.1-1.el8_1", + }, + { + ID: "CVE-2020-6819", + RecordSource: "vulnerabilities:rhel:8", + PackageName: "thunderbird", + VersionConstraint: "< 0:68.7.0-1.el8_1", + VersionFormat: "rpm", // TODO: this should reference a format, yes? (not a string) + ProxyVulnerabilities: []string{}, + Namespace: "rhel:8", + FixedInVersion: "0:68.7.0-1.el8_1", + }, + }, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "CVE-2020-6819", + RecordSource: "vulnerabilities:rhel:8", + Severity: "Critical", + Links: []string{"https://access.redhat.com/security/cve/CVE-2020-6819"}, + Description: "A flaw was found in Mozilla Firefox. A race condition can occur while running the nsDocShell destructor causing a use-after-free memory issue. The highest threat from this vulnerability is to data confidentiality and integrity as well as system availability.", + }, + }, + { + name: "RHEL with modularity", + numEntries: 1, + fixture: "test-fixtures/rhel-8-modules.json", + feed: "vulnerabilities", + group: "rhel:8", + vulns: []grypeDB.Vulnerability{ + { + ID: "CVE-2020-14350", + RecordSource: "vulnerabilities:rhel:8", + PackageName: "postgresql", + VersionConstraint: "< 0:12.5-1.module+el8.3.0+9042+664538f4", + VersionFormat: "rpm", + ProxyVulnerabilities: []string{}, + Namespace: "rhel:8", + FixedInVersion: "0:12.5-1.module+el8.3.0+9042+664538f4", + }, + }, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "CVE-2020-14350", + RecordSource: "vulnerabilities:rhel:8", + Severity: "Medium", + Links: []string{"https://access.redhat.com/security/cve/CVE-2020-14350"}, + Description: "A flaw was found in PostgreSQL, where some PostgreSQL extensions did not use the search_path safely in their installation script. This flaw allows an attacker with sufficient privileges to trick an administrator into executing a specially crafted script during the extension's installation or update. The highest threat from this vulnerability is to confidentiality, integrity, as well as system availability.", + }, + }, + { + name: "Alpine", + numEntries: 1, + fixture: "test-fixtures/alpine-3.9.json", + feed: "vulnerabilities", + group: "alpine:3.9", + vulns: []grypeDB.Vulnerability{ + { + ID: "CVE-2018-19967", + RecordSource: "vulnerabilities:alpine:3.9", + PackageName: "xen", + VersionConstraint: "< 4.11.1-r0", + VersionFormat: "apk", // TODO: this should reference a format, yes? (not a string) + ProxyVulnerabilities: []string{}, + Namespace: "alpine:3.9", + FixedInVersion: "4.11.1-r0", + }, + }, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "CVE-2018-19967", + RecordSource: "vulnerabilities:alpine:3.9", + Severity: "Medium", + Links: []string{"http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-19967"}, + Description: "", + CvssV2: &grypeDB.Cvss{ + BaseScore: 4.9, + ExploitabilityScore: 0, + ImpactScore: 0, + Vector: "AV:L/AC:L/Au:N/C:N/I:N/A:C", + }, + }, + }, + { + name: "Oracle", + numEntries: 1, + fixture: "test-fixtures/ol-8.json", + vulns: []grypeDB.Vulnerability{ + { + ID: "ELSA-2020-2550", + RecordSource: "vulnerabilities:ol:8", + PackageName: "libexif", + VersionConstraint: "< 0:0.6.21-17.el8_2", + VersionFormat: "rpm", // TODO: this should reference a format, yes? (not a string) + ProxyVulnerabilities: []string{"CVE-2020-13112"}, + Namespace: "ol:8", + FixedInVersion: "0:0.6.21-17.el8_2", + }, + { + ID: "ELSA-2020-2550", + RecordSource: "vulnerabilities:ol:8", + PackageName: "libexif-devel", + VersionConstraint: "< 0:0.6.21-17.el8_2", + VersionFormat: "rpm", // TODO: this should reference a format, yes? (not a string) + ProxyVulnerabilities: []string{"CVE-2020-13112"}, + Namespace: "ol:8", + FixedInVersion: "0:0.6.21-17.el8_2", + }, + { + ID: "ELSA-2020-2550", + RecordSource: "vulnerabilities:ol:8", + PackageName: "libexif-dummy", + VersionConstraint: "", + VersionFormat: "rpm", // TODO: this should reference a format, yes? (not a string) + ProxyVulnerabilities: []string{"CVE-2020-13112"}, + Namespace: "ol:8", + FixedInVersion: "", + }, + }, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "ELSA-2020-2550", + RecordSource: "vulnerabilities:ol:8", + Severity: "Medium", + Links: []string{"http://linux.oracle.com/errata/ELSA-2020-2550.html", "http://linux.oracle.com/cve/CVE-2020-13112.html"}, + }, + }, + { + name: "Oracle Linux 8 with modularity", + numEntries: 1, + fixture: "test-fixtures/ol-8-modules.json", + vulns: []grypeDB.Vulnerability{ + { + ID: "CVE-2020-14350", + RecordSource: "vulnerabilities:ol:8", + PackageName: "postgresql", + VersionConstraint: "< 0:12.5-1.module+el8.3.0+9042+664538f4", + VersionFormat: "rpm", + ProxyVulnerabilities: []string{}, + Namespace: "ol:8", + FixedInVersion: "0:12.5-1.module+el8.3.0+9042+664538f4", + }, + }, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "CVE-2020-14350", + RecordSource: "vulnerabilities:ol:8", + Severity: "Medium", + Links: []string{"https://access.redhat.com/security/cve/CVE-2020-14350"}, + Description: "A flaw was found in PostgreSQL, where some PostgreSQL extensions did not use the search_path safely in their installation script. This flaw allows an attacker with sufficient privileges to trick an administrator into executing a specially crafted script during the extension's installation or update. The highest threat from this vulnerability is to confidentiality, integrity, as well as system availability.", + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + f, err := os.Open(test.fixture) + require.NoError(t, err) + t.Cleanup(func() { + assert.NoError(t, f.Close()) + }) + + entries, err := unmarshal.OSVulnerabilityEntries(f) + assert.NoError(t, err) + assert.Len(t, entries, 1) + + entry := entries[0] + + dataEntries, err := Transform(entry) + assert.NoError(t, err) + + var vulns []grypeDB.Vulnerability + for _, entry := range dataEntries { + switch vuln := entry.Data.(type) { + case grypeDB.Vulnerability: + vulns = append(vulns, vuln) + case grypeDB.VulnerabilityMetadata: + assert.Equal(t, test.metadata, vuln) + default: + t.Fatalf("unexpected condition: data entry does not have a vulnerability or a metadata") + } + } + + if diff := cmp.Diff(test.vulns, vulns); diff != "" { + t.Errorf("vulnerabilities do not match (-want +got):\n%s", diff) + } + + }) + } + +} + +func TestParseVulnerabilitiesAllEntries(t *testing.T) { + + tests := []struct { + name string + numEntries int + fixture string + vulns []grypeDB.Vulnerability + }{ + { + name: "Debian", + numEntries: 2, + fixture: "test-fixtures/debian-8-multiple-entries-for-same-package.json", + vulns: []grypeDB.Vulnerability{ + { + ID: "CVE-2011-4623", + RecordSource: "vulnerabilities:debian:8", + PackageName: "rsyslog", + VersionConstraint: "< 5.7.4-1", + VersionFormat: "dpkg", // TODO: this should reference a format, yes? (not a string) + ProxyVulnerabilities: []string{}, + Namespace: "debian:8", + FixedInVersion: "5.7.4-1", + }, + { + ID: "CVE-2008-5618", + RecordSource: "vulnerabilities:debian:8", + PackageName: "rsyslog", + VersionConstraint: "< 3.18.6-1", + VersionFormat: "dpkg", // TODO: this should reference a format, yes? (not a string) + ProxyVulnerabilities: []string{}, + Namespace: "debian:8", + FixedInVersion: "3.18.6-1", + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + f, err := os.Open(test.fixture) + require.NoError(t, err) + t.Cleanup(func() { + assert.NoError(t, f.Close()) + }) + + entries, err := unmarshal.OSVulnerabilityEntries(f) + assert.NoError(t, err) + assert.Len(t, entries, len(test.vulns)) + + var vulns []grypeDB.Vulnerability + for _, entry := range entries { + assert.NoError(t, err) + dataEntries, err := Transform(entry) + assert.NoError(t, err) + + for _, entry := range dataEntries { + switch vuln := entry.Data.(type) { + case grypeDB.Vulnerability: + vulns = append(vulns, vuln) + case grypeDB.VulnerabilityMetadata: + default: + t.Fatalf("unexpected condition: data entry does not have a vulnerability or a metadata") + } + } + } + + if diff := cmp.Diff(test.vulns, vulns); diff != "" { + t.Errorf("vulnerabilities do not match (-want +got):\n%s", diff) + } + }) + } + +} diff --git a/pkg/process/v2/writer.go b/pkg/process/v2/writer.go new file mode 100644 index 00000000..9399bd98 --- /dev/null +++ b/pkg/process/v2/writer.go @@ -0,0 +1,124 @@ +package v2 + +import ( + "crypto/sha256" + "fmt" + "path" + "path/filepath" + "strings" + "time" + + "github.com/anchore/grype-db/internal/log" + + "github.com/anchore/grype-db/internal/file" + "github.com/anchore/grype-db/pkg/data" + "github.com/anchore/grype/grype/db" + grypeDB "github.com/anchore/grype/grype/db/v2" + grypeDBStore "github.com/anchore/grype/grype/db/v2/store" + "github.com/spf13/afero" +) + +var _ data.Writer = (*writer)(nil) + +type writer struct { + dbPath string + store grypeDB.Store +} + +func NewWriter(directory string, dataAge time.Time) (data.Writer, error) { + dbPath := path.Join(directory, grypeDB.VulnerabilityStoreFileName) + theStore, err := grypeDBStore.New(dbPath, true) + if err != nil { + return nil, fmt.Errorf("unable to create writer: %w", err) + } + + if err := theStore.SetID(grypeDB.NewID(dataAge)); err != nil { + return nil, fmt.Errorf("unable to set DB ID: %w", err) + } + + return &writer{ + dbPath: dbPath, + store: theStore, + }, nil +} + +func (w writer) Write(entries ...data.Entry) error { + for _, entry := range entries { + if entry.DBSchemaVersion != grypeDB.SchemaVersion { + return fmt.Errorf("wrong schema version: want %+v got %+v", grypeDB.SchemaVersion, entry.DBSchemaVersion) + } + switch row := entry.Data.(type) { + case grypeDB.Vulnerability: + if err := w.store.AddVulnerability(row); err != nil { + return fmt.Errorf("unable to write vulnerability to store: %w", err) + } + case grypeDB.VulnerabilityMetadata: + normalizeSeverity(&row, w.store) + if err := w.store.AddVulnerabilityMetadata(row); err != nil { + return fmt.Errorf("unable to write vulnerability metadata to store: %w", err) + } + default: + return fmt.Errorf("data entry does not have a vulnerability or a metadata: %T", row) + } + } + + return nil +} + +func (w writer) metadata() (*db.Metadata, error) { + hashStr, err := file.ContentDigest(afero.NewOsFs(), w.dbPath, sha256.New()) + if err != nil { + return nil, fmt.Errorf("failed to hash database file (%s): %w", w.dbPath, err) + } + + storeID, err := w.store.GetID() + if err != nil { + return nil, fmt.Errorf("failed to fetch store ID: %w", err) + } + + metadata := db.Metadata{ + Built: storeID.BuildTimestamp, + Version: storeID.SchemaVersion, + Checksum: "sha256:" + hashStr, + } + return &metadata, nil +} + +func (w writer) Close() error { + w.store.Close() + metadata, err := w.metadata() + if err != nil { + return err + } + + metadataPath := path.Join(filepath.Dir(w.dbPath), db.MetadataFileName) + + return metadata.Write(metadataPath) +} + +func normalizeSeverity(metadata *grypeDB.VulnerabilityMetadata, reader grypeDB.VulnerabilityMetadataStoreReader) { + if metadata.Severity != "" && strings.ToLower(metadata.Severity) != "unknown" { + return + } + if !strings.HasPrefix(strings.ToLower(metadata.ID), "cve") { + return + } + if strings.Contains(metadata.RecordSource, grypeDB.NVDNamespace) { + return + } + m, err := reader.GetVulnerabilityMetadata(metadata.ID, grypeDB.NVDNamespace) + if err != nil { + log.WithFields("id", metadata.ID, "error", err).Warn("error fetching vulnerability metadata from NVD namespace") + return + } + if m == nil { + log.WithFields("id", metadata.ID).Debug("unable to find vulnerability metadata from NVD namespace") + return + } + + newSeverity := string(data.ParseSeverity(m.Severity)) + + log.WithFields("id", metadata.ID, "record-source", metadata.RecordSource, "from", metadata.Severity, "to", newSeverity).Trace("overriding irrelevant severity with data from NVD record") + + metadata.Severity = newSeverity +} diff --git a/pkg/process/v2/writer_test.go b/pkg/process/v2/writer_test.go new file mode 100644 index 00000000..2666d0e8 --- /dev/null +++ b/pkg/process/v2/writer_test.go @@ -0,0 +1,116 @@ +package v2 + +import ( + "errors" + "github.com/anchore/grype-db/pkg/data" + grypeDB "github.com/anchore/grype/grype/db/v2" + "github.com/stretchr/testify/assert" + "testing" +) + +var _ grypeDB.VulnerabilityMetadataStoreReader = (*mockReader)(nil) + +type mockReader struct { + metadata *grypeDB.VulnerabilityMetadata + err error +} + +func newMockReader(sev string) *mockReader { + return &mockReader{ + metadata: &grypeDB.VulnerabilityMetadata{ + Severity: sev, + RecordSource: "nvdv2:cves", + }, + } +} + +func newDeadMockReader() *mockReader { + return &mockReader{ + err: errors.New("dead"), + } +} + +func (m mockReader) GetVulnerabilityMetadata(_, _ string) (*grypeDB.VulnerabilityMetadata, error) { + return m.metadata, m.err +} + +func (m mockReader) GetAllVulnerabilityMetadata() (*[]grypeDB.VulnerabilityMetadata, error) { + panic("implement me") +} + +func Test_normalizeSeverity(t *testing.T) { + + tests := []struct { + name string + initialSeverity string + recordSource string + cveID string + reader grypeDB.VulnerabilityMetadataStoreReader + expected data.Severity + }{ + { + name: "skip missing metadata", + initialSeverity: "", + recordSource: "test", + reader: &mockReader{}, + expected: "", + }, + { + name: "skip non-cve records metadata", + cveID: "GHSA-1234-1234-1234", + initialSeverity: "", + recordSource: "test", + reader: newDeadMockReader(), // should not be used + expected: "", + }, + { + name: "override empty severity", + initialSeverity: "", + recordSource: "test", + reader: newMockReader("low"), + expected: data.SeverityLow, + }, + { + name: "override unknown severity", + initialSeverity: "unknown", + recordSource: "test", + reader: newMockReader("low"), + expected: data.SeverityLow, + }, + { + name: "ignore record with severity already set", + initialSeverity: "Low", + recordSource: "test", + reader: newMockReader("critical"), // should not be used + expected: data.SeverityLow, + }, + { + name: "ignore nvd records", + initialSeverity: "Low", + recordSource: "nvdv2:cves", + reader: newDeadMockReader(), // should not be used + expected: data.SeverityLow, + }, + { + name: "db errors should not fail or modify the record", + initialSeverity: "", + recordSource: "test", + reader: newDeadMockReader(), + expected: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + record := &grypeDB.VulnerabilityMetadata{ + ID: "cve-2020-0000", + Severity: tt.initialSeverity, + RecordSource: tt.recordSource, + } + if tt.cveID != "" { + record.ID = tt.cveID + } + normalizeSeverity(record, tt.reader) + assert.Equal(t, string(tt.expected), record.Severity) + }) + } +} diff --git a/pkg/process/v3/processors.go b/pkg/process/v3/processors.go new file mode 100644 index 00000000..e7d51f4e --- /dev/null +++ b/pkg/process/v3/processors.go @@ -0,0 +1,19 @@ +package v3 + +import ( + "github.com/anchore/grype-db/pkg/data" + "github.com/anchore/grype-db/pkg/process/processors" + "github.com/anchore/grype-db/pkg/process/v3/transformers/github" + "github.com/anchore/grype-db/pkg/process/v3/transformers/msrc" + "github.com/anchore/grype-db/pkg/process/v3/transformers/nvd" + "github.com/anchore/grype-db/pkg/process/v3/transformers/os" +) + +func Processors() []data.Processor { + return []data.Processor{ + processors.NewGitHubProcessor(github.Transform), + processors.NewMSRCProcessor(msrc.Transform), + processors.NewNVDProcessor(nvd.Transform), + processors.NewOSProcessor(os.Transform), + } +} diff --git a/pkg/process/v3/transformers/entry.go b/pkg/process/v3/transformers/entry.go new file mode 100644 index 00000000..ae8f3715 --- /dev/null +++ b/pkg/process/v3/transformers/entry.go @@ -0,0 +1,22 @@ +package transformers + +import ( + "github.com/anchore/grype-db/pkg/data" + grypeDB "github.com/anchore/grype/grype/db/v3" +) + +func NewEntries(vs []grypeDB.Vulnerability, metadata grypeDB.VulnerabilityMetadata) []data.Entry { + entries := []data.Entry{ + { + DBSchemaVersion: grypeDB.SchemaVersion, + Data: metadata, + }, + } + for _, vuln := range vs { + entries = append(entries, data.Entry{ + DBSchemaVersion: grypeDB.SchemaVersion, + Data: vuln, + }) + } + return entries +} diff --git a/pkg/process/v3/transformers/github/test-fixtures/github-github-npm-0.json b/pkg/process/v3/transformers/github/test-fixtures/github-github-npm-0.json new file mode 100644 index 00000000..b0a7d1e9 --- /dev/null +++ b/pkg/process/v3/transformers/github/test-fixtures/github-github-npm-0.json @@ -0,0 +1,31 @@ + +{ + "Advisory": { + "CVE": [ + "CVE-2020-14000" + ], + "FixedIn": [ + { + "ecosystem": "npm", + "identifier": "0.2.0-prerelease.20200714185213", + "name": "scratch-vm", + "namespace": "github:npm", + "range": "<= 0.2.0-prerelease.20200709173451" + } + ], + "Metadata": { + "CVE": [ + "CVE-2020-14000" + ] + }, + "Severity": "High", + "Summary": "Remote Code Execution in scratch-vm", + "ghsaId": "GHSA-vc9j-fhvv-8vrf", + "namespace": "github:npm", + "url": "https://github.com/advisories/GHSA-vc9j-fhvv-8vrf", + "withdrawn": null + }, + "Vulnerability": {} +} + + diff --git a/pkg/process/v3/transformers/github/test-fixtures/github-github-python-0.json b/pkg/process/v3/transformers/github/test-fixtures/github-github-python-0.json new file mode 100644 index 00000000..ad14aa60 --- /dev/null +++ b/pkg/process/v3/transformers/github/test-fixtures/github-github-python-0.json @@ -0,0 +1,58 @@ +[ + { + "Advisory": { + "CVE": [ + "CVE-2018-8768" + ], + "FixedIn": [ + { + "ecosystem": "python", + "identifier": "5.4.1", + "name": "notebook", + "namespace": "github:python", + "range": "< 5.4.1" + } + ], + "Metadata": { + "CVE": [ + "CVE-2018-8768" + ] + }, + "Severity": "Low", + "Summary": "Low severity vulnerability that affects notebook", + "ghsaId": "GHSA-6cwv-x26c-w2q4", + "namespace": "github:python", + "url": "https://github.com/advisories/GHSA-6cwv-x26c-w2q4", + "withdrawn": null + }, + "Vulnerability": {} + }, + { + "Advisory": { + "CVE": [ + "CVE-2017-5524" + ], + "FixedIn": [ + { + "ecosystem": "python", + "identifier": "4.3.12", + "name": "Plone", + "namespace": "github:python", + "range": ">= 4.0 < 4.3.12" + } + ], + "Metadata": { + "CVE": [ + "CVE-2017-5524" + ] + }, + "Severity": "Medium", + "Summary": "Moderate severity vulnerability that affects Plone", + "ghsaId": "GHSA-p5wr-vp8g-q5p4", + "namespace": "github:python", + "url": "https://github.com/advisories/GHSA-p5wr-vp8g-q5p4", + "withdrawn": null + }, + "Vulnerability": {} + } +] \ No newline at end of file diff --git a/pkg/process/v3/transformers/github/test-fixtures/github-github-python-1.json b/pkg/process/v3/transformers/github/test-fixtures/github-github-python-1.json new file mode 100644 index 00000000..bfa84922 --- /dev/null +++ b/pkg/process/v3/transformers/github/test-fixtures/github-github-python-1.json @@ -0,0 +1,43 @@ + +{ + "Advisory": { + "CVE": [ + "CVE-2017-5524" + ], + "FixedIn": [ + { + "ecosystem": "python", + "identifier": "4.3.12", + "name": "Plone", + "namespace": "github:python", + "range": ">= 4.0 < 4.3.12" + }, + { + "ecosystem": "python", + "identifier": "5.1b1", + "name": "Plone", + "namespace": "github:python", + "range": ">= 5.1a1 < 5.1b1" + }, + { + "ecosystem": "python", + "identifier": "5.0.7", + "name": "Plone", + "namespace": "github:python", + "range": ">= 5.0rc1 < 5.0.7" + } + ], + "Metadata": { + "CVE": [ + "CVE-2017-5524" + ] + }, + "Severity": "Medium", + "Summary": "Moderate severity vulnerability that affects Plone", + "ghsaId": "GHSA-p5wr-vp8g-q5p4", + "namespace": "github:python", + "url": "https://github.com/advisories/GHSA-p5wr-vp8g-q5p4", + "withdrawn": null + }, + "Vulnerability": {} +} diff --git a/pkg/process/v3/transformers/github/test-fixtures/github-withdrawn.json b/pkg/process/v3/transformers/github/test-fixtures/github-withdrawn.json new file mode 100644 index 00000000..04995e38 --- /dev/null +++ b/pkg/process/v3/transformers/github/test-fixtures/github-withdrawn.json @@ -0,0 +1,29 @@ + +{ + "Advisory": { + "CVE": [ + "CVE-2018-8768" + ], + "FixedIn": [ + { + "ecosystem": "python", + "identifier": "5.4.1", + "name": "notebook", + "namespace": "github:python", + "range": "< 5.4.1" + } + ], + "Metadata": { + "CVE": [ + "CVE-2018-8768" + ] + }, + "Severity": "Low", + "Summary": "Low severity vulnerability that affects notebook", + "ghsaId": "GHSA-6cwv-x26c-w2q4", + "namespace": "github:python", + "url": "https://github.com/advisories/GHSA-6cwv-x26c-w2q4", + "withdrawn": "2022-01-31T14:32:09Z" + }, + "Vulnerability": {} +} diff --git a/pkg/process/v3/transformers/github/test-fixtures/multiple-fixed-in-names.json b/pkg/process/v3/transformers/github/test-fixtures/multiple-fixed-in-names.json new file mode 100644 index 00000000..ac1ef982 --- /dev/null +++ b/pkg/process/v3/transformers/github/test-fixtures/multiple-fixed-in-names.json @@ -0,0 +1,43 @@ + +{ + "Advisory": { + "CVE": [ + "CVE-2017-5524" + ], + "FixedIn": [ + { + "ecosystem": "python", + "identifier": "4.3.12", + "name": "Plone", + "namespace": "github:python", + "range": ">= 4.0 < 4.3.12" + }, + { + "ecosystem": "python", + "identifier": "5.1b1", + "name": "Plone", + "namespace": "github:python", + "range": ">= 5.1a1 < 5.1b1" + }, + { + "ecosystem": "python", + "identifier": "5.0.7", + "name": "Plone-debug", + "namespace": "github:python", + "range": ">= 5.0rc1 < 5.0.7" + } + ], + "Metadata": { + "CVE": [ + "CVE-2017-5524" + ] + }, + "Severity": "Medium", + "Summary": "Moderate severity vulnerability that affects Plone", + "ghsaId": "GHSA-p5wr-vp8g-q5p4", + "namespace": "github:python", + "url": "https://github.com/advisories/GHSA-p5wr-vp8g-q5p4", + "withdrawn": null + }, + "Vulnerability": {} +} diff --git a/pkg/process/v3/transformers/github/transform.go b/pkg/process/v3/transformers/github/transform.go new file mode 100644 index 00000000..f9ec70cf --- /dev/null +++ b/pkg/process/v3/transformers/github/transform.go @@ -0,0 +1,99 @@ +package github + +import ( + "github.com/anchore/grype-db/pkg/data" + "github.com/anchore/grype-db/pkg/process/common" + "github.com/anchore/grype-db/pkg/process/v3/transformers" + "github.com/anchore/grype-db/pkg/provider/unmarshal" + grypeDB "github.com/anchore/grype/grype/db/v3" +) + +const ( + // TODO: tech debt from a previous design + feed = "github" +) + +func Transform(vulnerability unmarshal.GitHubAdvisory) ([]data.Entry, error) { + var allVulns []grypeDB.Vulnerability + + // Exclude entries marked as withdrawn + if vulnerability.Advisory.Withdrawn != nil { + return nil, nil + } + + recordSource := grypeDB.RecordSource(feed, vulnerability.Advisory.Namespace) + entryNamespace, err := grypeDB.NamespaceForFeedGroup(feed, vulnerability.Advisory.Namespace) + if err != nil { + return nil, err + } + + // there may be multiple packages indicated within the FixedIn field, we should make + // separate vulnerability entries (one for each name|namespaces combo) while merging + // constraint ranges as they are found. + for idx, fixedInEntry := range vulnerability.Advisory.FixedIn { + constraint := common.EnforceSemVerConstraint(fixedInEntry.Range) + + var versionFormat string + switch vulnerability.Advisory.Namespace { + case "github:python": + versionFormat = "python" + default: + versionFormat = "unknown" + } + + // create vulnerability entry + allVulns = append(allVulns, grypeDB.Vulnerability{ + ID: vulnerability.Advisory.GhsaID, + VersionConstraint: constraint, + VersionFormat: versionFormat, + RelatedVulnerabilities: getRelatedVulnerabilities(vulnerability), + PackageName: fixedInEntry.Name, + Namespace: entryNamespace, + Fix: getFix(vulnerability, idx), + }) + } + + // create vulnerability metadata entry (a single entry keyed off of the vulnerability ID) + metadata := grypeDB.VulnerabilityMetadata{ + ID: vulnerability.Advisory.GhsaID, + DataSource: vulnerability.Advisory.URL, + Namespace: entryNamespace, + RecordSource: recordSource, + Severity: vulnerability.Advisory.Severity, + URLs: []string{vulnerability.Advisory.URL}, + Description: vulnerability.Advisory.Summary, + } + + return transformers.NewEntries(allVulns, metadata), nil +} + +func getFix(entry unmarshal.GitHubAdvisory, idx int) grypeDB.Fix { + fixedInEntry := entry.Advisory.FixedIn[idx] + + var fixedInVersions []string + fixedInVersion := common.CleanFixedInVersion(fixedInEntry.Identifier) + if fixedInVersion != "" { + fixedInVersions = append(fixedInVersions, fixedInVersion) + } + + fixState := grypeDB.NotFixedState + if len(fixedInVersions) > 0 { + fixState = grypeDB.FixedState + } + + return grypeDB.Fix{ + Versions: fixedInVersions, + State: fixState, + } +} + +func getRelatedVulnerabilities(entry unmarshal.GitHubAdvisory) []grypeDB.VulnerabilityReference { + vulns := make([]grypeDB.VulnerabilityReference, len(entry.Advisory.CVE)) + for idx, cve := range entry.Advisory.CVE { + vulns[idx] = grypeDB.VulnerabilityReference{ + ID: cve, + Namespace: grypeDB.NVDNamespace, + } + } + return vulns +} diff --git a/pkg/process/v3/transformers/github/transform_test.go b/pkg/process/v3/transformers/github/transform_test.go new file mode 100644 index 00000000..ad8bce61 --- /dev/null +++ b/pkg/process/v3/transformers/github/transform_test.go @@ -0,0 +1,197 @@ +package github + +import ( + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/require" + "os" + "testing" + + testUtils "github.com/anchore/grype-db/pkg/process/tests" + "github.com/anchore/grype-db/pkg/provider/unmarshal" + grypeDB "github.com/anchore/grype/grype/db/v3" + "github.com/stretchr/testify/assert" +) + +func TestUnmarshalGitHubEntries(t *testing.T) { + f, err := os.Open("test-fixtures/github-github-python-0.json") + require.NoError(t, err) + defer testUtils.CloseFile(f) + + entries, err := unmarshal.GitHubAdvisoryEntries(f) + require.NoError(t, err) + + assert.Len(t, entries, 2) + +} + +func TestParseGitHubEntry(t *testing.T) { + expectedVulns := []grypeDB.Vulnerability{ + { + ID: "GHSA-p5wr-vp8g-q5p4", + VersionConstraint: ">=4.0,<4.3.12", + VersionFormat: "python", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2017-5524", + Namespace: grypeDB.NVDNamespace, + }, + }, + PackageName: "Plone", + Namespace: "github:python", + Fix: grypeDB.Fix{ + Versions: []string{"4.3.12"}, + State: grypeDB.FixedState, + }, + }, + { + ID: "GHSA-p5wr-vp8g-q5p4", + VersionConstraint: ">=5.1a1,<5.1b1", + VersionFormat: "python", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2017-5524", + Namespace: grypeDB.NVDNamespace, + }, + }, + PackageName: "Plone", + Namespace: "github:python", + Fix: grypeDB.Fix{ + Versions: []string{"5.1b1"}, + State: grypeDB.FixedState, + }, + }, + { + ID: "GHSA-p5wr-vp8g-q5p4", + VersionConstraint: ">=5.0rc1,<5.0.7", + VersionFormat: "python", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2017-5524", + Namespace: grypeDB.NVDNamespace, + }, + }, + PackageName: "Plone", + Namespace: "github:python", + Fix: grypeDB.Fix{ + Versions: []string{"5.0.7"}, + State: grypeDB.FixedState, + }, + }, + } + + expectedMetadata := grypeDB.VulnerabilityMetadata{ + ID: "GHSA-p5wr-vp8g-q5p4", + Namespace: "github:python", + RecordSource: "github:github:python", + DataSource: "https://github.com/advisories/GHSA-p5wr-vp8g-q5p4", + Severity: "Medium", + URLs: []string{"https://github.com/advisories/GHSA-p5wr-vp8g-q5p4"}, + Description: "Moderate severity vulnerability that affects Plone", + } + + f, err := os.Open("test-fixtures/github-github-python-1.json") + require.NoError(t, err) + defer testUtils.CloseFile(f) + + entries, err := unmarshal.GitHubAdvisoryEntries(f) + require.NoError(t, err) + + require.Len(t, entries, 1) + entry := entries[0] + + dataEntries, err := Transform(entry) + assert.NoError(t, err) + + var vulns []grypeDB.Vulnerability + for _, entry := range dataEntries { + switch vuln := entry.Data.(type) { + case grypeDB.Vulnerability: + vulns = append(vulns, vuln) + case grypeDB.VulnerabilityMetadata: + assert.Equal(t, expectedMetadata, vuln) + default: + t.Fatalf("unexpected condition: data entry does not have a vulnerability or a metadata") + } + } + + // check vulnerability + assert.Len(t, vulns, len(expectedVulns)) + + if diff := cmp.Diff(expectedVulns, vulns); diff != "" { + t.Errorf("vulnerabilities do not match (-want +got):\n%s", diff) + } +} + +func TestDefaultVersionFormatNpmGitHubEntry(t *testing.T) { + expectedVuln := grypeDB.Vulnerability{ + ID: "GHSA-vc9j-fhvv-8vrf", + VersionConstraint: "<=0.2.0-prerelease.20200709173451", + VersionFormat: "unknown", // TODO: this should reference a format, yes? (not a string) + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2020-14000", + Namespace: grypeDB.NVDNamespace, + }, + }, + PackageName: "scratch-vm", + Namespace: "github:npm", + Fix: grypeDB.Fix{ + Versions: []string{"0.2.0-prerelease.20200714185213"}, + State: grypeDB.FixedState, + }, + } + + expectedMetadata := grypeDB.VulnerabilityMetadata{ + ID: "GHSA-vc9j-fhvv-8vrf", + Namespace: "github:npm", + RecordSource: "github:github:npm", + DataSource: "https://github.com/advisories/GHSA-vc9j-fhvv-8vrf", + Severity: "High", + URLs: []string{"https://github.com/advisories/GHSA-vc9j-fhvv-8vrf"}, + Description: "Remote Code Execution in scratch-vm", + } + + f, err := os.Open("test-fixtures/github-github-npm-0.json") + require.NoError(t, err) + defer testUtils.CloseFile(f) + + entries, err := unmarshal.GitHubAdvisoryEntries(f) + require.NoError(t, err) + + require.Len(t, entries, 1) + + entry := entries[0] + + dataEntries, err := Transform(entry) + assert.NoError(t, err) + + for _, entry := range dataEntries { + switch vuln := entry.Data.(type) { + case grypeDB.Vulnerability: + assert.Equal(t, expectedVuln, vuln) + case grypeDB.VulnerabilityMetadata: + assert.Equal(t, expectedMetadata, vuln) + default: + t.Fatalf("unexpected condition: data entry does not have a vulnerability or a metadata") + } + } + + // check vulnerability + assert.Len(t, dataEntries, 2) +} + +func TestFilterWithdrawnEntries(t *testing.T) { + f, err := os.Open("test-fixtures/github-withdrawn.json") + require.NoError(t, err) + defer testUtils.CloseFile(f) + + entries, err := unmarshal.GitHubAdvisoryEntries(f) + require.NoError(t, err) + + require.Len(t, entries, 1) + entry := entries[0] + + dataEntries, err := Transform(entry) + assert.NoError(t, err) + assert.Nil(t, dataEntries) +} diff --git a/pkg/process/v3/transformers/msrc/test-fixtures/microsoft-msrc-0.json b/pkg/process/v3/transformers/msrc/test-fixtures/microsoft-msrc-0.json new file mode 100644 index 00000000..474b23b2 --- /dev/null +++ b/pkg/process/v3/transformers/msrc/test-fixtures/microsoft-msrc-0.json @@ -0,0 +1,194 @@ +[ + { + "cvss": { + "base_score": 7.8, + "temporal_score": 7, + "vector": "CVSS:3.0/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H/E:P/RL:O/RC:C" + }, + "fixed_in": [ + { + "id": "4493470", + "is_first": true, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4493470", + "https://support.microsoft.com/help/4493470" + ] + }, + { + "id": "4494440", + "is_first": false, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4494440", + "https://support.microsoft.com/help/4494440" + ] + }, + { + "id": "4503267", + "is_first": false, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4503267", + "https://support.microsoft.com/en-us/help/4503267" + ] + }, + { + "id": "4507460", + "is_first": false, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4507460", + "https://support.microsoft.com/help/4507460" + ] + }, + { + "id": "4512517", + "is_first": false, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4512517", + "https://support.microsoft.com/help/4512517" + ] + }, + { + "id": "4516044", + "is_first": false, + "is_latest": true, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4516044", + "https://support.microsoft.com/help/4516044" + ] + } + ], + "id": "CVE-2019-0671", + "link": "https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2019-0671", + "product": { + "family": "Windows", + "id": "10852", + "name": "Windows 10 Version 1607 for 32-bit Systems" + }, + "severity": "High", + "summary": "Microsoft Office Access Connectivity Engine Remote Code Execution Vulnerability", + "vulnerable": [ + "4480961", + "4483229", + "4487026", + "4489882" + ] + }, +{ + "cvss": { + "base_score": 4.4, + "temporal_score": 4, + "vector": "CVSS:3.0/AV:L/AC:H/PR:L/UI:R/S:U/C:N/I:N/A:H/E:P/RL:O/RC:C" + }, + "fixed_in": [ + { + "id": "4093119", + "is_first": true, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4093119" + ] + }, + { + "id": "4103723", + "is_first": false, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4103723" + ] + }, + { + "id": "4284880", + "is_first": false, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4284880" + ] + }, + { + "id": "4338814", + "is_first": false, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4338814" + ] + }, + { + "id": "4343887", + "is_first": false, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4343887" + ] + }, + { + "id": "4345418", + "is_first": false, + "is_latest": true, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4345418" + ] + }, + { + "id": "4457131", + "is_first": false, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4457131" + ] + }, + { + "id": "4462917", + "is_first": false, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4462917" + ] + }, + { + "id": "4467691", + "is_first": false, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4467691" + ] + }, + { + "id": "4471321", + "is_first": false, + "is_latest": true, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4471321" + ] + } + ], + "id": "CVE-2018-8116", + "link": "https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2018-8116", + "product": { + "family": "Windows", + "id": "10852", + "name": "Windows 10 Version 1607 for 32-bit Systems" + }, + "severity": "Medium", + "summary": "Microsoft Graphics Component Denial of Service Vulnerability", + "vulnerable": [ + "3213986", + "4013429", + "4015217", + "4019472", + "4022715", + "4025339", + "4034658", + "4038782", + "4041691", + "4048953", + "4053579", + "4056890", + "4074590", + "4088787" + ] + } +] diff --git a/pkg/process/v3/transformers/msrc/transform.go b/pkg/process/v3/transformers/msrc/transform.go new file mode 100644 index 00000000..d1a1ea48 --- /dev/null +++ b/pkg/process/v3/transformers/msrc/transform.go @@ -0,0 +1,91 @@ +package msrc + +import ( + "fmt" + + "github.com/anchore/grype-db/pkg/data" + "github.com/anchore/grype-db/pkg/process/common" + "github.com/anchore/grype-db/pkg/process/v3/transformers" + "github.com/anchore/grype-db/pkg/provider/unmarshal" + grypeDB "github.com/anchore/grype/grype/db/v3" +) + +const ( + // TODO: tech debt from a previous design + feed = "microsoft" + groupPrefix = "msrc" +) + +// Transform gets called by the parser, which consumes entries from the JSON files previously pulled. Each VulnDBVulnerability represents +// a single unmarshalled entry from the feed service +func Transform(vulnerability unmarshal.MSRCVulnerability) ([]data.Entry, error) { + group := fmt.Sprintf("%s:%s", groupPrefix, vulnerability.Product.ID) + recordSource := grypeDB.RecordSource(feed, group) + entryNamespace, err := grypeDB.NamespaceForFeedGroup(feed, group) + if err != nil { + return nil, err + } + + // In anchore-enterprise windows analyzer, "base" represents unpatched windows images (images with no KBs). + // If a vulnerability exists for a Microsoft Product ID and the image has no KBs (which are patches), + // then the image must be vulnerable to the image. + //nolint:gocritic + versionConstraint := append(vulnerability.Vulnerable, "base") + + allVulns := []grypeDB.Vulnerability{ + { + ID: vulnerability.ID, + VersionConstraint: common.OrConstraints(versionConstraint...), + VersionFormat: "kb", + PackageName: vulnerability.Product.ID, + Namespace: entryNamespace, + Fix: getFix(vulnerability), + }, + } + + // create vulnerability metadata entry (a single entry keyed off of the vulnerability ID) + metadata := grypeDB.VulnerabilityMetadata{ + ID: vulnerability.ID, + DataSource: vulnerability.Link, + Namespace: entryNamespace, + RecordSource: recordSource, + Severity: vulnerability.Severity, + URLs: []string{vulnerability.Link}, + // There is no description for vulnerabilities from the feed service + // summary gives something like "windows information disclosure vulnerability" + //Description: vulnerability.Summary, + Cvss: []grypeDB.Cvss{ + { + Metrics: grypeDB.CvssMetrics{BaseScore: vulnerability.Cvss.BaseScore}, + Vector: vulnerability.Cvss.Vector, + }, + }, + } + + return transformers.NewEntries(allVulns, metadata), nil +} + +func getFix(entry unmarshal.MSRCVulnerability) grypeDB.Fix { + fixedInVersion := fixedInKB(entry) + fixState := grypeDB.FixedState + + if fixedInVersion == "" { + fixState = grypeDB.NotFixedState + } + + return grypeDB.Fix{ + Versions: []string{fixedInVersion}, + State: fixState, + } +} + +// fixedInKB finds the "latest" patch (KB id) amongst the available microsoft patches and returns it +// if the "latest" patch cannot be found, an error is returned +func fixedInKB(vulnerability unmarshal.MSRCVulnerability) string { + for _, fixedIn := range vulnerability.FixedIn { + if fixedIn.IsLatest { + return fixedIn.ID + } + } + return "" +} diff --git a/pkg/process/v3/transformers/msrc/transform_test.go b/pkg/process/v3/transformers/msrc/transform_test.go new file mode 100644 index 00000000..04b5bec0 --- /dev/null +++ b/pkg/process/v3/transformers/msrc/transform_test.go @@ -0,0 +1,119 @@ +package msrc + +import ( + "github.com/stretchr/testify/require" + "os" + "testing" + + testUtils "github.com/anchore/grype-db/pkg/process/tests" + "github.com/anchore/grype-db/pkg/provider/unmarshal" + grypeDB "github.com/anchore/grype/grype/db/v3" + "github.com/stretchr/testify/assert" +) + +func TestUnmarshalMsrcVulnerabilities(t *testing.T) { + f, err := os.Open("test-fixtures/microsoft-msrc-0.json") + require.NoError(t, err) + defer testUtils.CloseFile(f) + + entries, err := unmarshal.MSRCVulnerabilityEntries(f) + require.NoError(t, err) + + assert.Equal(t, len(entries), 2) +} + +func TestParseMSRCEntry(t *testing.T) { + expectedVulns := []struct { + vulnerability grypeDB.Vulnerability + metadata grypeDB.VulnerabilityMetadata + }{ + { + vulnerability: grypeDB.Vulnerability{ + ID: "CVE-2019-0671", + VersionConstraint: `4480961 || 4483229 || 4487026 || 4489882 || base`, + VersionFormat: "kb", + PackageName: "10852", + Namespace: "msrc:10852", + Fix: grypeDB.Fix{ + Versions: []string{"4516044"}, + State: grypeDB.FixedState, + }, + }, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "CVE-2019-0671", + Severity: "High", + DataSource: "https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2019-0671", + URLs: []string{"https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2019-0671"}, + Description: "", + RecordSource: "microsoft:msrc:10852", + Namespace: "msrc:10852", + Cvss: []grypeDB.Cvss{ + { + Metrics: grypeDB.CvssMetrics{ + BaseScore: 7.8, + ImpactScore: nil, + }, + Vector: "CVSS:3.0/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H/E:P/RL:O/RC:C", + }, + }, + }, + }, + { + vulnerability: grypeDB.Vulnerability{ + ID: "CVE-2018-8116", + VersionConstraint: `3213986 || 4013429 || 4015217 || 4019472 || 4022715 || 4025339 || 4034658 || 4038782 || 4041691 || 4048953 || 4053579 || 4056890 || 4074590 || 4088787 || base`, + VersionFormat: "kb", + PackageName: "10852", + Namespace: "msrc:10852", + Fix: grypeDB.Fix{ + Versions: []string{"4345418"}, + State: grypeDB.FixedState, + }, + }, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "CVE-2018-8116", + Namespace: "msrc:10852", + DataSource: "https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2018-8116", + RecordSource: "microsoft:msrc:10852", + Severity: "Medium", + URLs: []string{"https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2018-8116"}, + Description: "", + Cvss: []grypeDB.Cvss{ + { + Metrics: grypeDB.CvssMetrics{ + BaseScore: 4.4, + ImpactScore: nil, + }, + Vector: "CVSS:3.0/AV:L/AC:H/PR:L/UI:R/S:U/C:N/I:N/A:H/E:P/RL:O/RC:C", + }, + }, + }, + }, + } + + f, err := os.Open("test-fixtures/microsoft-msrc-0.json") + require.NoError(t, err) + defer testUtils.CloseFile(f) + + entries, err := unmarshal.MSRCVulnerabilityEntries(f) + require.NoError(t, err) + + require.Equal(t, len(entries), 2) + + for idx, entry := range entries { + dataEntries, err := Transform(entry) + require.NoError(t, err) + assert.Len(t, dataEntries, 2) + expected := expectedVulns[idx] + for _, entry := range dataEntries { + switch vuln := entry.Data.(type) { + case grypeDB.Vulnerability: + assert.Equal(t, expected.vulnerability, vuln) + case grypeDB.VulnerabilityMetadata: + assert.Equal(t, expected.metadata, vuln) + default: + t.Fatalf("unexpected condition: data entry does not have a vulnerability or a metadata") + } + } + } +} diff --git a/pkg/process/v3/transformers/nvd/test-fixtures/compound-pkg.json b/pkg/process/v3/transformers/nvd/test-fixtures/compound-pkg.json new file mode 100644 index 00000000..8e658dcd --- /dev/null +++ b/pkg/process/v3/transformers/nvd/test-fixtures/compound-pkg.json @@ -0,0 +1,115 @@ +{ + "cve": { + "id": "CVE-2018-10189", + "sourceIdentifier": "cve@mitre.org", + "published": "2018-04-17T20:29:00.410", + "lastModified": "2018-05-23T14:41:49.073", + "vulnStatus": "Analyzed", + "descriptions": [ + { + "lang": "en", + "value": "An issue was discovered in Mautic 1.x and 2.x before 2.13.0. It is possible to systematically emulate tracking cookies per contact due to tracking the contact by their auto-incremented ID. Thus, a third party can manipulate the cookie value with +1 to systematically assume being tracked as each contact in Mautic. It is then possible to retrieve information about the contact through forms that have progressive profiling enabled." + }, + { + "lang": "es", + "value": "Se ha descubierto un problema en Mautic, en versiones 1.x y 2.x anteriores a la 2.13.0. Es posible emular de forma sistemática el rastreo de cookies por contacto debido al rastreo de contacto por su ID autoincrementada. Por lo tanto, un tercero puede manipular el valor de la cookie con un +1 para asumir sistemáticamente que se está rastreando como cada contacto en Mautic. Así, sería posible recuperar información sobre el contacto a través de formularios que tengan habilitada la generación de perfiles progresiva." + } + ], + "metrics": { + "cvssMetricV30": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "cvssData": { + "version": "3.0", + "vectorString": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N", + "attackVector": "NETWORK", + "attackComplexity": "LOW", + "privilegesRequired": "NONE", + "userInteraction": "NONE", + "scope": "UNCHANGED", + "confidentialityImpact": "HIGH", + "integrityImpact": "NONE", + "availabilityImpact": "NONE", + "baseScore": 7.5, + "baseSeverity": "HIGH" + }, + "exploitabilityScore": 3.9, + "impactScore": 3.6 + } + ], + "cvssMetricV2": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "cvssData": { + "version": "2.0", + "vectorString": "AV:N/AC:L/Au:N/C:P/I:N/A:N", + "accessVector": "NETWORK", + "accessComplexity": "LOW", + "authentication": "NONE", + "confidentialityImpact": "PARTIAL", + "integrityImpact": "NONE", + "availabilityImpact": "NONE", + "baseScore": 5.0 + }, + "baseSeverity": "MEDIUM", + "exploitabilityScore": 10.0, + "impactScore": 2.9, + "acInsufInfo": false, + "obtainAllPrivilege": false, + "obtainUserPrivilege": false, + "obtainOtherPrivilege": false, + "userInteractionRequired": false + } + ] + }, + "weaknesses": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "description": [ + { + "lang": "en", + "value": "CWE-200" + } + ] + } + ], + "configurations": [ + { + "nodes": [ + { + "operator": "OR", + "negate": false, + "cpeMatch": [ + { + "vulnerable": true, + "criteria": "cpe:2.3:a:mautic:mautic:*:*:*:*:*:*:*:*", + "versionStartIncluding": "1.0.0", + "versionEndIncluding": "1.4.1", + "matchCriteriaId": "5779710D-099E-40EE-8DF3-55BD3179A50C" + }, + { + "vulnerable": true, + "criteria": "cpe:2.3:a:mautic:mautic:*:*:*:*:*:*:*:*", + "versionStartIncluding": "2.0.0", + "versionEndExcluding": "2.13.0", + "matchCriteriaId": "4EFAEE48-4AEF-4F8C-95E0-6E8D848D900F" + } + ] + } + ] + } + ], + "references": [ + { + "url": "https://github.com/mautic/mautic/releases/tag/2.13.0", + "source": "cve@mitre.org", + "tags": [ + "Third Party Advisory" + ] + } + ] + } +} diff --git a/pkg/process/v3/transformers/nvd/test-fixtures/invalid_cpe.json b/pkg/process/v3/transformers/nvd/test-fixtures/invalid_cpe.json new file mode 100644 index 00000000..eac2ebd4 --- /dev/null +++ b/pkg/process/v3/transformers/nvd/test-fixtures/invalid_cpe.json @@ -0,0 +1,111 @@ +{ + "cve": { + "id": "CVE-2015-8978", + "sourceIdentifier": "cve@mitre.org", + "published": "2016-11-22T17:59:00.180", + "lastModified": "2016-11-28T19:50:59.600", + "vulnStatus": "Modified", + "descriptions": [ + { + "lang": "en", + "value": "In Soap Lite (aka the SOAP::Lite extension for Perl) 1.14 and earlier, an example attack consists of defining 10 or more XML entities, each defined as consisting of 10 of the previous entity, with the document consisting of a single instance of the largest entity, which expands to one billion copies of the first entity. The amount of computer memory used for handling an external SOAP call would likely exceed that available to the process parsing the XML." + }, + { + "lang": "es", + "value": "En Soap Lite (también conocido como la extensión SOAP::Lite para Perl) 1.14 y versiones anteriores, un ejemplo de ataque consiste en definir 10 o más entidades XML, cada una definida como consistente de 10 de la entidad anterior, con el documento consistente de una única instancia de la entidad más grande, que se expande a mil millones de copias de la primera entidad. La suma de la memoria del ordenador utilizada para manejar una llamada SOAP externa probablemente superaría el disponible para el proceso de análisis del XML." + } + ], + "metrics": { + "cvssMetricV30": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "cvssData": { + "version": "3.0", + "vectorString": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", + "attackVector": "NETWORK", + "attackComplexity": "LOW", + "privilegesRequired": "NONE", + "userInteraction": "NONE", + "scope": "UNCHANGED", + "confidentialityImpact": "NONE", + "integrityImpact": "NONE", + "availabilityImpact": "HIGH", + "baseScore": 7.5, + "baseSeverity": "HIGH" + }, + "exploitabilityScore": 3.9, + "impactScore": 3.6 + } + ], + "cvssMetricV2": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "cvssData": { + "version": "2.0", + "vectorString": "AV:N/AC:L/Au:N/C:N/I:N/A:P", + "accessVector": "NETWORK", + "accessComplexity": "LOW", + "authentication": "NONE", + "confidentialityImpact": "NONE", + "integrityImpact": "NONE", + "availabilityImpact": "PARTIAL", + "baseScore": 5.0 + }, + "baseSeverity": "MEDIUM", + "exploitabilityScore": 10.0, + "impactScore": 2.9, + "acInsufInfo": false, + "obtainAllPrivilege": false, + "obtainUserPrivilege": false, + "obtainOtherPrivilege": false, + "userInteractionRequired": false + } + ] + }, + "weaknesses": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "description": [ + { + "lang": "en", + "value": "CWE-399" + } + ] + } + ], + "configurations": [ + { + "nodes": [ + { + "operator": "OR", + "negate": false, + "cpeMatch": [ + { + "vulnerable": true, + "criteria": "cpe:2.3:a:soap::lite_project:soap::lite:*:*:*:*:*:perl:*:*", + "versionEndIncluding": "1.14", + "matchCriteriaId": "FB4DACB9-2E9E-4CBE-825F-FC0303D8CC86" + } + ] + } + ] + } + ], + "references": [ + { + "url": "http://cpansearch.perl.org/src/PHRED/SOAP-Lite-1.20/Changes", + "source": "cve@mitre.org", + "tags": [ + "Vendor Advisory" + ] + }, + { + "url": "http://www.securityfocus.com/bid/94487", + "source": "cve@mitre.org" + } + ] + } +} diff --git a/pkg/process/v3/transformers/nvd/test-fixtures/single-package-multi-distro.json b/pkg/process/v3/transformers/nvd/test-fixtures/single-package-multi-distro.json new file mode 100644 index 00000000..ed108475 --- /dev/null +++ b/pkg/process/v3/transformers/nvd/test-fixtures/single-package-multi-distro.json @@ -0,0 +1,174 @@ +{ + "cve": { + "id": "CVE-2018-1000222", + "sourceIdentifier": "cve@mitre.org", + "published": "2018-08-20T20:29:01.347", + "lastModified": "2020-03-31T02:15:12.667", + "vulnStatus": "Modified", + "descriptions": [ + { + "lang": "en", + "value": "Libgd version 2.2.5 contains a Double Free Vulnerability vulnerability in gdImageBmpPtr Function that can result in Remote Code Execution . This attack appear to be exploitable via Specially Crafted Jpeg Image can trigger double free. This vulnerability appears to have been fixed in after commit ac16bdf2d41724b5a65255d4c28fb0ec46bc42f5." + }, + { + "lang": "es", + "value": "Libgd 2.2.5 contiene una vulnerabilidad de doble liberación (double free) en la función gdImageBmpPtr que puede resultar en la ejecución remota de código. Este ataque parece ser explotable mediante una imagen JPEG especialmente manipulada que desencadene una doble liberación (double free). La vulnerabilidad parece haber sido solucionada tras el commit con ID ac16bdf2d41724b5a65255d4c28fb0ec46bc42f5." + } + ], + "metrics": { + "cvssMetricV30": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "cvssData": { + "version": "3.0", + "vectorString": "CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H", + "attackVector": "NETWORK", + "attackComplexity": "LOW", + "privilegesRequired": "NONE", + "userInteraction": "REQUIRED", + "scope": "UNCHANGED", + "confidentialityImpact": "HIGH", + "integrityImpact": "HIGH", + "availabilityImpact": "HIGH", + "baseScore": 8.8, + "baseSeverity": "HIGH" + }, + "exploitabilityScore": 2.8, + "impactScore": 5.9 + } + ], + "cvssMetricV2": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "cvssData": { + "version": "2.0", + "vectorString": "AV:N/AC:M/Au:N/C:P/I:P/A:P", + "accessVector": "NETWORK", + "accessComplexity": "MEDIUM", + "authentication": "NONE", + "confidentialityImpact": "PARTIAL", + "integrityImpact": "PARTIAL", + "availabilityImpact": "PARTIAL", + "baseScore": 6.8 + }, + "baseSeverity": "MEDIUM", + "exploitabilityScore": 8.6, + "impactScore": 6.4, + "acInsufInfo": false, + "obtainAllPrivilege": false, + "obtainUserPrivilege": false, + "obtainOtherPrivilege": false, + "userInteractionRequired": true + } + ] + }, + "weaknesses": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "description": [ + { + "lang": "en", + "value": "CWE-415" + } + ] + } + ], + "configurations": [ + { + "nodes": [ + { + "operator": "OR", + "negate": false, + "cpeMatch": [ + { + "vulnerable": true, + "criteria": "cpe:2.3:a:libgd:libgd:2.2.5:*:*:*:*:*:*:*", + "matchCriteriaId": "C257CC1C-BF6A-4125-AA61-9C2D09096084" + } + ] + } + ] + }, + { + "nodes": [ + { + "operator": "OR", + "negate": false, + "cpeMatch": [ + { + "vulnerable": true, + "criteria": "cpe:2.3:o:canonical:ubuntu_linux:14.04:*:*:*:lts:*:*:*", + "matchCriteriaId": "B5A6F2F3-4894-4392-8296-3B8DD2679084" + }, + { + "vulnerable": true, + "criteria": "cpe:2.3:o:canonical:ubuntu_linux:16.04:*:*:*:lts:*:*:*", + "matchCriteriaId": "F7016A2A-8365-4F1A-89A2-7A19F2BCAE5B" + }, + { + "vulnerable": true, + "criteria": "cpe:2.3:o:canonical:ubuntu_linux:18.04:*:*:*:lts:*:*:*", + "matchCriteriaId": "23A7C53F-B80F-4E6A-AFA9-58EEA84BE11D" + } + ] + } + ] + }, + { + "nodes": [ + { + "operator": "OR", + "negate": false, + "cpeMatch": [ + { + "vulnerable": true, + "criteria": "cpe:2.3:o:debian:debian_linux:8.0:*:*:*:*:*:*:*", + "matchCriteriaId": "C11E6FB0-C8C0-4527-9AA0-CB9B316F8F43" + } + ] + } + ] + } + ], + "references": [ + { + "url": "https://github.com/libgd/libgd/issues/447", + "source": "cve@mitre.org", + "tags": [ + "Issue Tracking", + "Third Party Advisory" + ] + }, + { + "url": "https://lists.debian.org/debian-lts-announce/2019/01/msg00028.html", + "source": "cve@mitre.org", + "tags": [ + "Mailing List", + "Third Party Advisory" + ] + }, + { + "url": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/3CZ2QADQTKRHTGB2AHD7J4QQNDLBEMM6/", + "source": "cve@mitre.org" + }, + { + "url": "https://security.gentoo.org/glsa/201903-18", + "source": "cve@mitre.org", + "tags": [ + "Third Party Advisory" + ] + }, + { + "url": "https://usn.ubuntu.com/3755-1/", + "source": "cve@mitre.org", + "tags": [ + "Mitigation", + "Third Party Advisory" + ] + } + ] + } +} diff --git a/pkg/process/v3/transformers/nvd/test-fixtures/unmarshal-test.json b/pkg/process/v3/transformers/nvd/test-fixtures/unmarshal-test.json new file mode 100644 index 00000000..2dc698fa --- /dev/null +++ b/pkg/process/v3/transformers/nvd/test-fixtures/unmarshal-test.json @@ -0,0 +1,109 @@ +{ + "cve": { + "id": "CVE-2003-0349", + "sourceIdentifier": "cve@mitre.org", + "published": "2003-07-24T04:00:00.000", + "lastModified": "2018-10-12T21:32:41.083", + "vulnStatus": "Modified", + "descriptions": [ + { + "lang": "en", + "value": "Buffer overflow in the streaming media component for logging multicast requests in the ISAPI for the logging capability of Microsoft Windows Media Services (nsiislog.dll), as installed in IIS 5.0, allows remote attackers to execute arbitrary code via a large POST request to nsiislog.dll." + }, + { + "lang": "es", + "value": "Desbordamiento de búfer en el componente de secuenciamiento (streaming) de medios para registrar peticiones de multidifusión en la librería ISAPI de la capacidad de registro (logging) de Microsoft Windows Media Services (nsiislog.dll), como el instalado en IIS 5.9, permite a atacantes remotos ejecutar código arbitrario mediante una petición POST larga a nsiislog.dll." + } + ], + "metrics": { + "cvssMetricV2": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "cvssData": { + "version": "2.0", + "vectorString": "AV:N/AC:L/Au:N/C:P/I:P/A:P", + "accessVector": "NETWORK", + "accessComplexity": "LOW", + "authentication": "NONE", + "confidentialityImpact": "PARTIAL", + "integrityImpact": "PARTIAL", + "availabilityImpact": "PARTIAL", + "baseScore": 7.5 + }, + "baseSeverity": "HIGH", + "exploitabilityScore": 10.0, + "impactScore": 6.4, + "acInsufInfo": false, + "obtainAllPrivilege": false, + "obtainUserPrivilege": true, + "obtainOtherPrivilege": false, + "userInteractionRequired": false + } + ] + }, + "weaknesses": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "description": [ + { + "lang": "en", + "value": "NVD-CWE-Other" + } + ] + } + ], + "configurations": [ + { + "nodes": [ + { + "operator": "OR", + "negate": false, + "cpeMatch": [ + { + "vulnerable": true, + "criteria": "cpe:2.3:o:microsoft:windows_2000:*:*:*:*:*:*:*:*", + "matchCriteriaId": "4E545C63-FE9C-4CA1-AF0F-D999D84D2AFD" + } + ] + } + ] + } + ], + "references": [ + { + "url": "http://marc.info/?l=bugtraq&m=105665030925504&w=2", + "source": "cve@mitre.org" + }, + { + "url": "http://securitytracker.com/id?1007059", + "source": "cve@mitre.org" + }, + { + "url": "http://www.kb.cert.org/vuls/id/113716", + "source": "cve@mitre.org", + "tags": [ + "US Government Resource" + ] + }, + { + "url": "http://www.ntbugtraq.com/default.asp?pid=36&sid=1&A2=ind0306&L=NTBUGTRAQ&P=R4563", + "source": "cve@mitre.org", + "tags": [ + "Exploit", + "Patch", + "Vendor Advisory" + ] + }, + { + "url": "https://docs.microsoft.com/en-us/security-updates/securitybulletins/2003/ms03-022", + "source": "cve@mitre.org" + }, + { + "url": "https://oval.cisecurity.org/repository/search/definition/oval%3Aorg.mitre.oval%3Adef%3A938", + "source": "cve@mitre.org" + } + ] + } +} diff --git a/pkg/process/v3/transformers/nvd/test-fixtures/version-range.json b/pkg/process/v3/transformers/nvd/test-fixtures/version-range.json new file mode 100644 index 00000000..3df5b86d --- /dev/null +++ b/pkg/process/v3/transformers/nvd/test-fixtures/version-range.json @@ -0,0 +1,121 @@ +{ + "cve": { + "id": "CVE-2018-5487", + "sourceIdentifier": "security-alert@netapp.com", + "published": "2018-05-24T14:29:00.390", + "lastModified": "2018-07-05T13:52:30.627", + "vulnStatus": "Analyzed", + "descriptions": [ + { + "lang": "en", + "value": "NetApp OnCommand Unified Manager for Linux versions 7.2 through 7.3 ship with the Java Management Extension Remote Method Invocation (JMX RMI) service bound to the network, and are susceptible to unauthenticated remote code execution." + }, + { + "lang": "es", + "value": "NetApp OnCommand Unified Manager for Linux, de la versión 7.2 hasta la 7.3, se distribuye con el servicio Java Management Extension Remote Method Invocation (JMX RMI) enlazado a la red y es susceptible a la ejecución remota de código sin autenticación." + } + ], + "metrics": { + "cvssMetricV30": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "cvssData": { + "version": "3.0", + "vectorString": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + "attackVector": "NETWORK", + "attackComplexity": "LOW", + "privilegesRequired": "NONE", + "userInteraction": "NONE", + "scope": "UNCHANGED", + "confidentialityImpact": "HIGH", + "integrityImpact": "HIGH", + "availabilityImpact": "HIGH", + "baseScore": 9.8, + "baseSeverity": "CRITICAL" + }, + "exploitabilityScore": 3.9, + "impactScore": 5.9 + } + ], + "cvssMetricV2": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "cvssData": { + "version": "2.0", + "vectorString": "AV:N/AC:L/Au:N/C:P/I:P/A:P", + "accessVector": "NETWORK", + "accessComplexity": "LOW", + "authentication": "NONE", + "confidentialityImpact": "PARTIAL", + "integrityImpact": "PARTIAL", + "availabilityImpact": "PARTIAL", + "baseScore": 7.5 + }, + "baseSeverity": "HIGH", + "exploitabilityScore": 10.0, + "impactScore": 6.4, + "acInsufInfo": true, + "obtainAllPrivilege": false, + "obtainUserPrivilege": false, + "obtainOtherPrivilege": false, + "userInteractionRequired": false + } + ] + }, + "weaknesses": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "description": [ + { + "lang": "en", + "value": "CWE-20" + } + ] + } + ], + "configurations": [ + { + "operator": "AND", + "nodes": [ + { + "operator": "OR", + "negate": false, + "cpeMatch": [ + { + "vulnerable": true, + "criteria": "cpe:2.3:a:netapp:oncommand_unified_manager:*:*:*:*:*:*:*:*", + "versionStartIncluding": "7.2", + "versionEndIncluding": "7.3", + "matchCriteriaId": "A5949307-3E9B-441F-B008-81A0E0228DC0" + } + ] + }, + { + "operator": "OR", + "negate": false, + "cpeMatch": [ + { + "vulnerable": false, + "criteria": "cpe:2.3:o:linux:linux_kernel:-:*:*:*:*:*:*:*", + "matchCriteriaId": "703AF700-7A70-47E2-BC3A-7FD03B3CA9C1" + } + ] + } + ] + } + ], + "references": [ + { + "url": "https://security.netapp.com/advisory/ntap-20180523-0001/", + "source": "security-alert@netapp.com", + "tags": [ + "Patch", + "Vendor Advisory" + ] + } + ] + } +} diff --git a/pkg/process/v3/transformers/nvd/transform.go b/pkg/process/v3/transformers/nvd/transform.go new file mode 100644 index 00000000..3b3f484c --- /dev/null +++ b/pkg/process/v3/transformers/nvd/transform.go @@ -0,0 +1,95 @@ +package nvd + +import ( + "fmt" + + "github.com/anchore/grype-db/internal" + "github.com/anchore/grype-db/pkg/data" + "github.com/anchore/grype-db/pkg/process/v3/transformers" + "github.com/anchore/grype-db/pkg/provider/unmarshal" + "github.com/anchore/grype-db/pkg/provider/unmarshal/nvd" + grypeDB "github.com/anchore/grype/grype/db/v3" +) + +const ( + // TODO: tech debt from a previous design + feed = "nvdv2" + group = "nvdv2:cves" +) + +func Transform(vulnerability unmarshal.NVDVulnerability) ([]data.Entry, error) { + recordSource := grypeDB.RecordSource(feed, group) + entryNamespace, err := grypeDB.NamespaceForFeedGroup(feed, group) + if err != nil { + return nil, err + } + + uniquePkgs := findUniquePkgs(vulnerability.Configurations...) + + if err != nil { + return nil, fmt.Errorf("unable to parse NVD entry: %w", err) + } + + // extract all links + var links []string + for _, externalRefs := range vulnerability.References { + // TODO: should we capture other information here? + if externalRefs.URL != "" { + links = append(links, externalRefs.URL) + } + } + + // duplicate the vulnerabilities based on the set of unique packages the vulnerability is for + var allVulns []grypeDB.Vulnerability + for _, p := range uniquePkgs.All() { + matches := uniquePkgs.Matches(p) + cpes := internal.NewStringSet() + for _, m := range matches { + cpes.Add(m.Criteria) + } + + // create vulnerability entry + allVulns = append(allVulns, grypeDB.Vulnerability{ + ID: vulnerability.ID, + VersionConstraint: buildConstraints(uniquePkgs.Matches(p)), + VersionFormat: "unknown", + PackageName: p.Product, + Namespace: entryNamespace, + CPEs: cpes.ToSlice(), + Fix: grypeDB.Fix{ + State: grypeDB.UnknownFixState, + }, + }) + } + + // create vulnerability metadata entry (a single entry keyed off of the vulnerability ID) + allCVSS := vulnerability.CVSS() + metadata := grypeDB.VulnerabilityMetadata{ + ID: vulnerability.ID, + DataSource: "https://nvd.nist.gov/vuln/detail/" + vulnerability.ID, + Namespace: entryNamespace, + RecordSource: recordSource, + Severity: nvd.CvssSummaries(allCVSS).Sorted().Severity(), + URLs: links, + Description: vulnerability.Description(), + Cvss: getCvss(allCVSS...), + } + + return transformers.NewEntries(allVulns, metadata), nil +} + +func getCvss(cvss ...nvd.CvssSummary) []grypeDB.Cvss { + var results []grypeDB.Cvss + for _, c := range cvss { + results = append(results, grypeDB.Cvss{ + Version: c.Version, + Vector: c.Vector, + Metrics: grypeDB.CvssMetrics{ + BaseScore: c.BaseScore, + ExploitabilityScore: c.ExploitabilityScore, + ImpactScore: c.ImpactScore, + }, + }) + } + return results +} diff --git a/pkg/process/v3/transformers/nvd/transform_test.go b/pkg/process/v3/transformers/nvd/transform_test.go new file mode 100644 index 00000000..e5460517 --- /dev/null +++ b/pkg/process/v3/transformers/nvd/transform_test.go @@ -0,0 +1,256 @@ +package nvd + +import ( + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/require" + "os" + "testing" + + testUtils "github.com/anchore/grype-db/pkg/process/tests" + "github.com/anchore/grype-db/pkg/provider/unmarshal" + grypeDB "github.com/anchore/grype/grype/db/v3" + "github.com/go-test/deep" + "github.com/stretchr/testify/assert" +) + +func TestUnmarshalNVDVulnerabilitiesEntries(t *testing.T) { + f, err := os.Open("test-fixtures/unmarshal-test.json") + require.NoError(t, err) + defer testUtils.CloseFile(f) + + entries, err := unmarshal.NvdVulnerabilityEntries(f) + require.NoError(t, err) + + assert.Len(t, entries, 1) +} + +func TestParseAllNVDVulnerabilityEntries(t *testing.T) { + + tests := []struct { + name string + numEntries int + fixture string + vulns []grypeDB.Vulnerability + metadata grypeDB.VulnerabilityMetadata + }{ + { + name: "AppVersionRange", + numEntries: 1, + fixture: "test-fixtures/version-range.json", + vulns: []grypeDB.Vulnerability{ + { + ID: "CVE-2018-5487", + PackageName: "oncommand_unified_manager", + VersionConstraint: ">= 7.2, <= 7.3", + VersionFormat: "unknown", // TODO: this should reference a format, yes? (not a string) + Namespace: "nvd", + CPEs: []string{"cpe:2.3:a:netapp:oncommand_unified_manager:*:*:*:*:*:*:*:*"}, + Fix: grypeDB.Fix{ + State: "unknown", + }, + }, + }, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "CVE-2018-5487", + DataSource: "https://nvd.nist.gov/vuln/detail/CVE-2018-5487", + Namespace: "nvd", + RecordSource: "nvdv2:nvdv2:cves", + Severity: "Critical", + URLs: []string{"https://security.netapp.com/advisory/ntap-20180523-0001/"}, + Description: "NetApp OnCommand Unified Manager for Linux versions 7.2 through 7.3 ship with the Java Management Extension Remote Method Invocation (JMX RMI) service bound to the network, and are susceptible to unauthenticated remote code execution.", + Cvss: []grypeDB.Cvss{ + { + Metrics: grypeDB.NewCvssMetrics( + 7.5, + 10, + 6.4, + ), + Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P", + Version: "2.0", + }, + { + Metrics: grypeDB.NewCvssMetrics( + 9.8, + 3.9, + 5.9, + ), + Vector: "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + Version: "3.0", + }, + }, + }, + }, + { + name: "App+OS", + numEntries: 1, + fixture: "test-fixtures/single-package-multi-distro.json", + vulns: []grypeDB.Vulnerability{ + { + ID: "CVE-2018-1000222", + PackageName: "libgd", + VersionConstraint: "= 2.2.5", + VersionFormat: "unknown", // TODO: this should reference a format, yes? (not a string) + Namespace: "nvd", + CPEs: []string{"cpe:2.3:a:libgd:libgd:2.2.5:*:*:*:*:*:*:*"}, + Fix: grypeDB.Fix{ + State: "unknown", + }, + }, + // TODO: Question: should this match also the OS's? (as in the vulnerable_cpes list)... this seems wrong! + }, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "CVE-2018-1000222", + DataSource: "https://nvd.nist.gov/vuln/detail/CVE-2018-1000222", + Namespace: "nvd", + RecordSource: "nvdv2:nvdv2:cves", + Severity: "High", + URLs: []string{"https://github.com/libgd/libgd/issues/447", "https://lists.debian.org/debian-lts-announce/2019/01/msg00028.html", "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/3CZ2QADQTKRHTGB2AHD7J4QQNDLBEMM6/", "https://security.gentoo.org/glsa/201903-18", "https://usn.ubuntu.com/3755-1/"}, + Description: "Libgd version 2.2.5 contains a Double Free Vulnerability vulnerability in gdImageBmpPtr Function that can result in Remote Code Execution . This attack appear to be exploitable via Specially Crafted Jpeg Image can trigger double free. This vulnerability appears to have been fixed in after commit ac16bdf2d41724b5a65255d4c28fb0ec46bc42f5.", + Cvss: []grypeDB.Cvss{ + { + Metrics: grypeDB.NewCvssMetrics( + 6.8, + 8.6, + 6.4, + ), + Vector: "AV:N/AC:M/Au:N/C:P/I:P/A:P", + Version: "2.0", + }, + { + Metrics: grypeDB.NewCvssMetrics( + 8.8, + 2.8, + 5.9, + ), + Vector: "CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H", + Version: "3.0", + }, + }, + }, + }, + { + name: "AppCompoundVersionRange", + numEntries: 1, + fixture: "test-fixtures/compound-pkg.json", + vulns: []grypeDB.Vulnerability{ + { + ID: "CVE-2018-10189", + PackageName: "mautic", + VersionConstraint: ">= 1.0.0, <= 1.4.1 || >= 2.0.0, < 2.13.0", + VersionFormat: "unknown", + Namespace: "nvd", + CPEs: []string{"cpe:2.3:a:mautic:mautic:*:*:*:*:*:*:*:*"}, // note: entry was dedupicated + Fix: grypeDB.Fix{ + State: "unknown", + }, + }, + }, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "CVE-2018-10189", + DataSource: "https://nvd.nist.gov/vuln/detail/CVE-2018-10189", + Namespace: "nvd", + RecordSource: "nvdv2:nvdv2:cves", + Severity: "High", + URLs: []string{"https://github.com/mautic/mautic/releases/tag/2.13.0"}, + Description: "An issue was discovered in Mautic 1.x and 2.x before 2.13.0. It is possible to systematically emulate tracking cookies per contact due to tracking the contact by their auto-incremented ID. Thus, a third party can manipulate the cookie value with +1 to systematically assume being tracked as each contact in Mautic. It is then possible to retrieve information about the contact through forms that have progressive profiling enabled.", + Cvss: []grypeDB.Cvss{ + { + Metrics: grypeDB.NewCvssMetrics( + 5, + 10, + 2.9, + ), + Vector: "AV:N/AC:L/Au:N/C:P/I:N/A:N", + Version: "2.0", + }, + { + Metrics: grypeDB.NewCvssMetrics( + 7.5, + 3.9, + 3.6, + ), + Vector: "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N", + Version: "3.0", + }, + }, + }, + }, + { + // we always keep the metadata even though there are no vulnerability entries for it + name: "InvalidCPE", + numEntries: 1, + fixture: "test-fixtures/invalid_cpe.json", + vulns: nil, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "CVE-2015-8978", + Namespace: "nvd", + DataSource: "https://nvd.nist.gov/vuln/detail/CVE-2015-8978", + RecordSource: "nvdv2:nvdv2:cves", + Severity: "High", + URLs: []string{ + "http://cpansearch.perl.org/src/PHRED/SOAP-Lite-1.20/Changes", + "http://www.securityfocus.com/bid/94487", + }, + Description: "In Soap Lite (aka the SOAP::Lite extension for Perl) 1.14 and earlier, an example attack consists of defining 10 or more XML entities, each defined as consisting of 10 of the previous entity, with the document consisting of a single instance of the largest entity, which expands to one billion copies of the first entity. The amount of computer memory used for handling an external SOAP call would likely exceed that available to the process parsing the XML.", + Cvss: []grypeDB.Cvss{ + { + Metrics: grypeDB.NewCvssMetrics( + 5, + 10, + 2.9, + ), + Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P", + Version: "2.0", + }, + { + Metrics: grypeDB.NewCvssMetrics( + 7.5, + 3.9, + 3.6, + ), + Vector: "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", + Version: "3.0", + }, + }, + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + f, err := os.Open(test.fixture) + require.NoError(t, err) + t.Cleanup(func() { + assert.NoError(t, f.Close()) + }) + + entries, err := unmarshal.NvdVulnerabilityEntries(f) + require.NoError(t, err) + + var vulns []grypeDB.Vulnerability + for _, entry := range entries { + dataEntries, err := Transform(entry.Cve) + require.NoError(t, err) + + for _, entry := range dataEntries { + switch vuln := entry.Data.(type) { + case grypeDB.Vulnerability: + vulns = append(vulns, vuln) + case grypeDB.VulnerabilityMetadata: + // check metadata + if diff := deep.Equal(test.metadata, vuln); diff != nil { + for _, d := range diff { + t.Errorf("metadata diff: %+v", d) + } + } + default: + t.Fatalf("unexpected condition: data entry does not have a vulnerability or a metadata") + } + } + } + + if diff := cmp.Diff(test.vulns, vulns); diff != "" { + t.Errorf("vulnerabilities do not match (-want +got):\n%s", diff) + } + }) + } +} diff --git a/pkg/process/v3/transformers/nvd/unique_pkg.go b/pkg/process/v3/transformers/nvd/unique_pkg.go new file mode 100644 index 00000000..3d680d50 --- /dev/null +++ b/pkg/process/v3/transformers/nvd/unique_pkg.go @@ -0,0 +1,115 @@ +package nvd + +import ( + "fmt" + "strings" + + "github.com/anchore/grype-db/pkg/provider/unmarshal/nvd" + + "github.com/anchore/grype-db/internal/log" + "github.com/anchore/grype-db/pkg/process/common" + "github.com/umisama/go-cpe" +) + +const ( + ANY = "*" + NA = "-" +) + +type pkgCandidate struct { + Product string + Vendor string + TargetSoftware string +} + +func (p pkgCandidate) String() string { + return fmt.Sprintf("%s|%s|%s", p.Vendor, p.Product, p.TargetSoftware) +} + +func newPkgCandidate(match nvd.CpeMatch) (*pkgCandidate, error) { + // we are only interested in packages that are vulnerable (not related to secondary match conditioning) + if !match.Vulnerable { + return nil, nil + } + + c, err := cpe.NewItemFromFormattedString(match.Criteria) + if err != nil { + return nil, fmt.Errorf("unable to create uniquePkgEntry from '%s': %w", match.Criteria, err) + } + + // we are only interested in applications, not hardware or operating systems + if c.Part() != cpe.Application { + return nil, nil + } + + return &pkgCandidate{ + Product: c.Product().String(), + Vendor: c.Vendor().String(), + TargetSoftware: c.TargetSw().String(), + }, nil +} + +func findUniquePkgs(cfgs ...nvd.Configuration) uniquePkgTracker { + set := newUniquePkgTracker() + for _, c := range cfgs { + _findUniquePkgs(set, c.Nodes...) + } + return set +} + +func _findUniquePkgs(set uniquePkgTracker, ns ...nvd.Node) { + if len(ns) == 0 { + return + } + for _, node := range ns { + for _, match := range node.CpeMatch { + candidate, err := newPkgCandidate(match) + if err != nil { + // Do not halt all execution because of being unable to create + // a PkgCandidate. This can happen when a CPE is invalid which + // could avoid creating a database + log.Debugf("unable processing pkg: %v", err) + continue + } + if candidate != nil { + set.Add(*candidate, match) + } + } + } +} + +func buildConstraints(matches []nvd.CpeMatch) string { + constraints := make([]string, 0) + for _, match := range matches { + constraints = append(constraints, buildConstraint(match)) + } + return common.OrConstraints(constraints...) +} + +func buildConstraint(match nvd.CpeMatch) string { + constraints := make([]string, 0) + if match.VersionStartIncluding != nil && *match.VersionStartIncluding != "" { + constraints = append(constraints, fmt.Sprintf(">= %s", *match.VersionStartIncluding)) + } else if match.VersionStartExcluding != nil && *match.VersionStartExcluding != "" { + constraints = append(constraints, fmt.Sprintf("> %s", *match.VersionStartExcluding)) + } + + if match.VersionEndIncluding != nil && *match.VersionEndIncluding != "" { + constraints = append(constraints, fmt.Sprintf("<= %s", *match.VersionEndIncluding)) + } else if match.VersionEndExcluding != nil && *match.VersionEndExcluding != "" { + constraints = append(constraints, fmt.Sprintf("< %s", *match.VersionEndExcluding)) + } + + if len(constraints) == 0 { + c, err := cpe.NewItemFromFormattedString(match.Criteria) + if err != nil { + return "" + } + version := c.Version().String() + if version != ANY && version != NA { + constraints = append(constraints, fmt.Sprintf("= %s", version)) + } + } + + return strings.Join(constraints, ", ") +} diff --git a/pkg/process/v3/transformers/nvd/unique_pkg_test.go b/pkg/process/v3/transformers/nvd/unique_pkg_test.go new file mode 100644 index 00000000..21ef9e0c --- /dev/null +++ b/pkg/process/v3/transformers/nvd/unique_pkg_test.go @@ -0,0 +1,352 @@ +package nvd + +import ( + "github.com/anchore/grype-db/pkg/provider/unmarshal/nvd" + "testing" + + "github.com/sergi/go-diff/diffmatchpatch" +) + +func newUniquePkgTrackerFromSlice(candidates []pkgCandidate) uniquePkgTracker { + set := newUniquePkgTracker() + for _, c := range candidates { + set[c] = nil + } + return set +} + +func TestFindUniquePkgs(t *testing.T) { + tests := []struct { + name string + nodes []nvd.Node + expected uniquePkgTracker + }{ + { + name: "simple-match", + nodes: []nvd.Node{ + { + CpeMatch: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendor:product:2.2.0:*:*:*:*:target:*:*", + Vulnerable: true, + }, + }, + }, + }, + expected: newUniquePkgTrackerFromSlice( + []pkgCandidate{ + { + Product: "product", + Vendor: "vendor", + TargetSoftware: "target", + }, + }), + }, + { + name: "skip-hw", + nodes: []nvd.Node{ + { + CpeMatch: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:h:vendor:product:2.2.0:*:*:*:*:target:*:*", + Vulnerable: true, + }, + }, + }, + }, + expected: newUniquePkgTrackerFromSlice([]pkgCandidate{}), + }, + { + name: "skip-os", + nodes: []nvd.Node{ + { + CpeMatch: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:o:vendor:product:2.2.0:*:*:*:*:target:*:*", + Vulnerable: true, + }, + }, + }, + }, + expected: newUniquePkgTrackerFromSlice([]pkgCandidate{}), + }, + { + name: "duplicate-by-product", + nodes: []nvd.Node{ + { + CpeMatch: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendor:productA:3.3.3:*:*:*:*:target:*:*", + Vulnerable: true, + }, + { + Criteria: "cpe:2.3:a:vendor:productB:2.2.0:*:*:*:*:target:*:*", + Vulnerable: true, + }, + }, + Operator: "OR", + }, + }, + expected: newUniquePkgTrackerFromSlice( + []pkgCandidate{ + { + Product: "productA", + Vendor: "vendor", + TargetSoftware: "target", + }, + { + Product: "productB", + Vendor: "vendor", + TargetSoftware: "target", + }, + }), + }, + { + name: "duplicate-by-target", + nodes: []nvd.Node{ + { + CpeMatch: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendor:product:3.3.3:*:*:*:*:targetA:*:*", + Vulnerable: true, + }, + { + Criteria: "cpe:2.3:a:vendor:product:2.2.0:*:*:*:*:targetB:*:*", + Vulnerable: true, + }, + }, + Operator: "OR", + }, + }, + expected: newUniquePkgTrackerFromSlice( + []pkgCandidate{ + { + Product: "product", + Vendor: "vendor", + TargetSoftware: "targetA", + }, + { + Product: "product", + Vendor: "vendor", + TargetSoftware: "targetB", + }, + }), + }, + { + name: "duplicate-by-vendor", + nodes: []nvd.Node{ + { + CpeMatch: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendorA:product:3.3.3:*:*:*:*:target:*:*", + Vulnerable: true, + }, + { + Criteria: "cpe:2.3:a:vendorB:product:2.2.0:*:*:*:*:target:*:*", + Vulnerable: true, + }, + }, + Operator: "OR", + }, + }, + expected: newUniquePkgTrackerFromSlice( + []pkgCandidate{ + { + Product: "product", + Vendor: "vendorA", + TargetSoftware: "target", + }, + { + Product: "product", + Vendor: "vendorB", + TargetSoftware: "target", + }, + }), + }, + { + name: "de-duplicate-case", + nodes: []nvd.Node{ + { + CpeMatch: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendor:product:3.3.3:A:B:C:D:target:E:F", + Vulnerable: true, + }, + { + Criteria: "cpe:2.3:a:vendor:product:2.2.0:Q:R:S:T:target:U:V", + Vulnerable: true, + }, + }, + Operator: "OR", + }, + }, + expected: newUniquePkgTrackerFromSlice( + []pkgCandidate{ + { + Product: "product", + Vendor: "vendor", + TargetSoftware: "target", + }, + }), + }, + { + name: "duplicate-from-nested-nodes", + nodes: []nvd.Node{ + { + CpeMatch: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendorB:product:2.2.0:*:*:*:*:target:*:*", + Vulnerable: true, + }, + }, + Operator: "OR", + }, + { + CpeMatch: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendorA:product:2.2.0:*:*:*:*:target:*:*", + Vulnerable: true, + }, + }, + Operator: "OR", + }, + }, + expected: newUniquePkgTrackerFromSlice( + []pkgCandidate{ + { + Product: "product", + Vendor: "vendorA", + TargetSoftware: "target", + }, + { + Product: "product", + Vendor: "vendorB", + TargetSoftware: "target", + }, + }), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actual := findUniquePkgs(nvd.Configuration{Nodes: test.nodes}) + missing, extra := test.expected.Diff(actual) + if len(missing) != 0 { + for _, c := range missing { + t.Errorf("missing candidate: %+v", c) + } + } + + if len(extra) != 0 { + for _, c := range extra { + t.Errorf("extra candidate: %+v", c) + } + } + }) + } + +} + +func strRef(s string) *string { + return &s +} + +func TestBuildConstraints(t *testing.T) { + tests := []struct { + name string + matches []nvd.CpeMatch + expected string + }{ + { + name: "Equals", + matches: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendor:product:2.2.0:*:*:*:*:target:*:*", + }, + }, + expected: "= 2.2.0", + }, + { + name: "VersionEndExcluding", + matches: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", + VersionEndExcluding: strRef("2.3.0"), + }, + }, + expected: "< 2.3.0", + }, + { + name: "VersionEndIncluding", + matches: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", + VersionEndIncluding: strRef("2.3.0"), + }, + }, + expected: "<= 2.3.0", + }, + { + name: "VersionStartExcluding", + matches: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", + VersionStartExcluding: strRef("2.3.0"), + }, + }, + expected: "> 2.3.0", + }, + { + name: "VersionStartIncluding", + matches: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", + VersionStartIncluding: strRef("2.3.0"), + }, + }, + expected: ">= 2.3.0", + }, + { + name: "Version Range", + matches: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", + VersionStartIncluding: strRef("2.3.0"), + VersionEndIncluding: strRef("2.5.0"), + }, + }, + expected: ">= 2.3.0, <= 2.5.0", + }, + { + name: "Multiple Version Ranges", + matches: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", + VersionStartIncluding: strRef("2.3.0"), + VersionEndIncluding: strRef("2.5.0"), + }, + { + Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", + VersionStartExcluding: strRef("3.3.0"), + VersionEndExcluding: strRef("3.5.0"), + }, + }, + expected: ">= 2.3.0, <= 2.5.0 || > 3.3.0, < 3.5.0", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actual := buildConstraints(test.matches) + + if actual != test.expected { + dmp := diffmatchpatch.New() + diffs := dmp.DiffMain(actual, test.expected, true) + t.Errorf("Expected: '%s'", test.expected) + t.Errorf("Got : '%s'", actual) + t.Errorf("Diff : '%s'", dmp.DiffPrettyText(diffs)) + } + }) + } + +} diff --git a/pkg/process/v3/transformers/nvd/unique_pkg_tracker.go b/pkg/process/v3/transformers/nvd/unique_pkg_tracker.go new file mode 100644 index 00000000..2b7e405d --- /dev/null +++ b/pkg/process/v3/transformers/nvd/unique_pkg_tracker.go @@ -0,0 +1,64 @@ +package nvd + +import ( + "sort" + + "github.com/anchore/grype-db/pkg/provider/unmarshal/nvd" +) + +type uniquePkgTracker map[pkgCandidate][]nvd.CpeMatch + +func newUniquePkgTracker() uniquePkgTracker { + return make(uniquePkgTracker) +} + +func (s uniquePkgTracker) Diff(other uniquePkgTracker) (missing []pkgCandidate, extra []pkgCandidate) { + for k := range s { + if !other.Contains(k) { + missing = append(missing, k) + } + } + + for k := range other { + if !s.Contains(k) { + extra = append(extra, k) + } + } + + return +} + +func (s uniquePkgTracker) Matches(i pkgCandidate) []nvd.CpeMatch { + return s[i] +} + +func (s uniquePkgTracker) Add(i pkgCandidate, match nvd.CpeMatch) { + if _, ok := s[i]; !ok { + s[i] = make([]nvd.CpeMatch, 0) + } + s[i] = append(s[i], match) +} + +func (s uniquePkgTracker) Remove(i pkgCandidate) { + delete(s, i) +} + +func (s uniquePkgTracker) Contains(i pkgCandidate) bool { + _, ok := s[i] + return ok +} + +func (s uniquePkgTracker) All() []pkgCandidate { + res := make([]pkgCandidate, len(s)) + idx := 0 + for k := range s { + res[idx] = k + idx++ + } + + sort.SliceStable(res, func(i, j int) bool { + return res[i].String() < res[j].String() + }) + + return res +} diff --git a/pkg/process/v3/transformers/os/test-fixtures/alpine-3.9.json b/pkg/process/v3/transformers/os/test-fixtures/alpine-3.9.json new file mode 100644 index 00000000..b9d84395 --- /dev/null +++ b/pkg/process/v3/transformers/os/test-fixtures/alpine-3.9.json @@ -0,0 +1,28 @@ +[ + { + "Vulnerability": { + "CVSS": [], + "Description": "", + "FixedIn": [ + { + "Name": "xen", + "NamespaceName": "alpine:3.9", + "Version": "4.11.1-r0", + "VersionFormat": "apk" + } + ], + "Link": "http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-19967", + "Metadata": { + "NVD": { + "CVSSv2": { + "Score": 4.9, + "Vectors": "AV:L/AC:L/Au:N/C:N/I:N/A:C" + } + } + }, + "Name": "CVE-2018-19967", + "NamespaceName": "alpine:3.9", + "Severity": "Medium" + } + } +] \ No newline at end of file diff --git a/pkg/process/v3/transformers/os/test-fixtures/amzn.json b/pkg/process/v3/transformers/os/test-fixtures/amzn.json new file mode 100644 index 00000000..a862c32e --- /dev/null +++ b/pkg/process/v3/transformers/os/test-fixtures/amzn.json @@ -0,0 +1,49 @@ +[ + { + "Vulnerability": { + "Description": "", + "FixedIn": [ + { + "Name": "389-ds-base", + "NamespaceName": "amzn:2", + "Version": "1.3.8.4-15.amzn2.0.1", + "VersionFormat": "rpm" + }, + { + "Name": "389-ds-base-debuginfo", + "NamespaceName": "amzn:2", + "Version": "1.3.8.4-15.amzn2.0.1", + "VersionFormat": "rpm" + }, + { + "Name": "389-ds-base-devel", + "NamespaceName": "amzn:2", + "Version": "1.3.8.4-15.amzn2.0.1", + "VersionFormat": "rpm" + }, + { + "Name": "389-ds-base-libs", + "NamespaceName": "amzn:2", + "Version": "1.3.8.4-15.amzn2.0.1", + "VersionFormat": "rpm" + }, + { + "Name": "389-ds-base-snmp", + "NamespaceName": "amzn:2", + "Version": "1.3.8.4-15.amzn2.0.1", + "VersionFormat": "rpm" + } + ], + "Link": "https://alas.aws.amazon.com/AL2/ALAS-2018-1106.html", + "Metadata": { + "CVE": [ + + {"Name": "CVE-2018-14648"} + ] + }, + "Name": "ALAS-2018-1106", + "NamespaceName": "amzn:2", + "Severity": "Medium" + } + } +] \ No newline at end of file diff --git a/pkg/process/v3/transformers/os/test-fixtures/debian-8-multiple-entries-for-same-package.json b/pkg/process/v3/transformers/os/test-fixtures/debian-8-multiple-entries-for-same-package.json new file mode 100644 index 00000000..5025b56e --- /dev/null +++ b/pkg/process/v3/transformers/os/test-fixtures/debian-8-multiple-entries-for-same-package.json @@ -0,0 +1,62 @@ +[ + { + "Vulnerability": { + "CVSS": [], + "Description": "", + "FixedIn": [ + { + "Name": "rsyslog", + "NamespaceName": "debian:8", + "VendorAdvisory": { + "AdvisorySummary": [], + "NoAdvisory": false + }, + "Version": "5.7.4-1", + "VersionFormat": "dpkg" + } + ], + "Link": "https://security-tracker.debian.org/tracker/CVE-2011-4623", + "Metadata": { + "NVD": { + "CVSSv2": { + "Score": 2.1, + "Vectors": "AV:L/AC:L/Au:N/C:N/I:N/A:P" + } + } + }, + "Name": "CVE-2011-4623", + "NamespaceName": "debian:8", + "Severity": "Low" + } + }, + { + "Vulnerability": { + "CVSS": [], + "Description": "", + "FixedIn": [ + { + "Name": "rsyslog", + "NamespaceName": "debian:8", + "VendorAdvisory": { + "AdvisorySummary": [], + "NoAdvisory": false + }, + "Version": "3.18.6-1", + "VersionFormat": "dpkg" + } + ], + "Link": "https://security-tracker.debian.org/tracker/CVE-2008-5618", + "Metadata": { + "NVD": { + "CVSSv2": { + "Score": 5, + "Vectors": "AV:N/AC:L/Au:N/C:N/I:N/A:P" + } + } + }, + "Name": "CVE-2008-5618", + "NamespaceName": "debian:8", + "Severity": "Low" + } + } +] \ No newline at end of file diff --git a/pkg/process/v3/transformers/os/test-fixtures/debian-8.json b/pkg/process/v3/transformers/os/test-fixtures/debian-8.json new file mode 100644 index 00000000..a758f13c --- /dev/null +++ b/pkg/process/v3/transformers/os/test-fixtures/debian-8.json @@ -0,0 +1,62 @@ +[ + { + "Vulnerability": { + "CVSS": [], + "Description": "", + "FixedIn": [ + { + "Name": "asterisk", + "NamespaceName": "debian:8", + "VendorAdvisory": { + "AdvisorySummary": [], + "NoAdvisory": false + }, + "Version": "1:1.6.2.0~rc3-1", + "VersionFormat": "dpkg" + }, + { + "Name": "auth2db", + "NamespaceName": "debian:8", + "VendorAdvisory": { + "AdvisorySummary": [], + "NoAdvisory": false + }, + "Version": "0.2.5-2+dfsg-1", + "VersionFormat": "dpkg" + }, + { + "Name": "exaile", + "NamespaceName": "debian:8", + "VendorAdvisory": { + "AdvisorySummary": [], + "NoAdvisory": false + }, + "Version": "0.2.14+debian-2.2", + "VersionFormat": "dpkg" + }, + { + "Name": "wordpress", + "NamespaceName": "debian:8", + "VendorAdvisory": { + "AdvisorySummary": [], + "NoAdvisory": false + }, + "Version": "", + "VersionFormat": "dpkg" + } + ], + "Link": "https://security-tracker.debian.org/tracker/CVE-2008-7220", + "Metadata": { + "NVD": { + "CVSSv2": { + "Score": 7.5, + "Vectors": "AV:N/AC:L/Au:N/C:P/I:P/A:P" + } + } + }, + "Name": "CVE-2008-7220", + "NamespaceName": "debian:8", + "Severity": "High" + } + } +] \ No newline at end of file diff --git a/pkg/process/v3/transformers/os/test-fixtures/ol-8-modules.json b/pkg/process/v3/transformers/os/test-fixtures/ol-8-modules.json new file mode 100644 index 00000000..f1d7372b --- /dev/null +++ b/pkg/process/v3/transformers/os/test-fixtures/ol-8-modules.json @@ -0,0 +1,36 @@ +[ + { + "Vulnerability": { + "CVSS": [], + "Description": "A flaw was found in PostgreSQL, where some PostgreSQL extensions did not use the search_path safely in their installation script. This flaw allows an attacker with sufficient privileges to trick an administrator into executing a specially crafted script during the extension's installation or update. The highest threat from this vulnerability is to confidentiality, integrity, as well as system availability.", + "FixedIn": [ + { + "Module": "postgresql:10", + "Name": "postgresql", + "NamespaceName": "ol:8", + "Version": "0:10.14-1.module+el8.2.0+7801+be0fed80", + "VersionFormat": "rpm" + }, + { + "Module": "postgresql:12", + "Name": "postgresql", + "NamespaceName": "ol:8", + "Version": "0:12.5-1.module+el8.3.0+9042+664538f4", + "VersionFormat": "rpm" + }, + { + "Module": "postgresql:9.6", + "Name": "postgresql", + "NamespaceName": "ol:8", + "Version": "0:9.6.20-1.module+el8.3.0+8938+7f0e88b6", + "VersionFormat": "rpm" + } + ], + "Link": "https://access.redhat.com/security/cve/CVE-2020-14350", + "Metadata": {}, + "Name": "CVE-2020-14350", + "NamespaceName": "ol:8", + "Severity": "Medium" + } + } +] \ No newline at end of file diff --git a/pkg/process/v3/transformers/os/test-fixtures/ol-8.json b/pkg/process/v3/transformers/os/test-fixtures/ol-8.json new file mode 100644 index 00000000..09439ece --- /dev/null +++ b/pkg/process/v3/transformers/os/test-fixtures/ol-8.json @@ -0,0 +1,42 @@ +[ + { + "Vulnerability": { + "CVSS": [], + "Description": "", + "FixedIn": [ + { + "Name": "libexif", + "NamespaceName": "ol:8", + "Version": "0:0.6.21-17.el8_2", + "VersionFormat": "rpm" + }, + { + "Name": "libexif-devel", + "NamespaceName": "ol:8", + "Version": "0:0.6.21-17.el8_2", + "VersionFormat": "rpm" + }, + { + "Name": "libexif-dummy", + "NamespaceName": "ol:8", + "Version": "None", + "VersionFormat": "rpm" + } + ], + "Link": "http://linux.oracle.com/errata/ELSA-2020-2550.html", + "Metadata": { + "CVE": [ + { + "Link": "http://linux.oracle.com/cve/CVE-2020-13112.html", + "Name": "CVE-2020-13112" + } + ], + "Issued": "2020-06-15", + "RefId": "ELSA-2020-2550" + }, + "Name": "ELSA-2020-2550", + "NamespaceName": "ol:8", + "Severity": "Medium" + } + } +] \ No newline at end of file diff --git a/pkg/process/v3/transformers/os/test-fixtures/rhel-8-modules.json b/pkg/process/v3/transformers/os/test-fixtures/rhel-8-modules.json new file mode 100644 index 00000000..c0400ad5 --- /dev/null +++ b/pkg/process/v3/transformers/os/test-fixtures/rhel-8-modules.json @@ -0,0 +1,75 @@ +[ + { + "Vulnerability": { + "CVSS": [ + { + "base_metrics": { + "base_score": 7.1, + "base_severity": "High", + "exploitability_score": 1.2, + "impact_score": 5.9 + }, + "status": "verified", + "vector_string": "CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:U/C:H/I:H/A:H", + "version": "3.1" + } + ], + "Description": "A flaw was found in PostgreSQL, where some PostgreSQL extensions did not use the search_path safely in their installation script. This flaw allows an attacker with sufficient privileges to trick an administrator into executing a specially crafted script during the extension's installation or update. The highest threat from this vulnerability is to confidentiality, integrity, as well as system availability.", + "FixedIn": [ + { + "Module": "postgresql:10", + "Name": "postgresql", + "NamespaceName": "rhel:8", + "VendorAdvisory": { + "AdvisorySummary": [ + { + "ID": "RHSA-2020:3669", + "Link": "https://access.redhat.com/errata/RHSA-2020:3669" + } + ], + "NoAdvisory": false + }, + "Version": "0:10.14-1.module+el8.2.0+7801+be0fed80", + "VersionFormat": "rpm" + }, + { + "Module": "postgresql:12", + "Name": "postgresql", + "NamespaceName": "rhel:8", + "VendorAdvisory": { + "AdvisorySummary": [ + { + "ID": "RHSA-2020:5620", + "Link": "https://access.redhat.com/errata/RHSA-2020:5620" + } + ], + "NoAdvisory": false + }, + "Version": "0:12.5-1.module+el8.3.0+9042+664538f4", + "VersionFormat": "rpm" + }, + { + "Module": "postgresql:9.6", + "Name": "postgresql", + "NamespaceName": "rhel:8", + "VendorAdvisory": { + "AdvisorySummary": [ + { + "ID": "RHSA-2020:5619", + "Link": "https://access.redhat.com/errata/RHSA-2020:5619" + } + ], + "NoAdvisory": false + }, + "Version": "0:9.6.20-1.module+el8.3.0+8938+7f0e88b6", + "VersionFormat": "rpm" + } + ], + "Link": "https://access.redhat.com/security/cve/CVE-2020-14350", + "Metadata": {}, + "Name": "CVE-2020-14350", + "NamespaceName": "rhel:8", + "Severity": "Medium" + } + } +] \ No newline at end of file diff --git a/pkg/process/v3/transformers/os/test-fixtures/rhel-8.json b/pkg/process/v3/transformers/os/test-fixtures/rhel-8.json new file mode 100644 index 00000000..2779708c --- /dev/null +++ b/pkg/process/v3/transformers/os/test-fixtures/rhel-8.json @@ -0,0 +1,57 @@ +[ + { + "Vulnerability": { + "CVSS": [ + { + "base_metrics": { + "base_score": 8.8, + "base_severity": "High", + "exploitability_score": 2.8, + "impact_score": 5.9 + }, + "status": "verified", + "vector_string": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H", + "version": "3.1" + } + ], + "Description": "A flaw was found in Mozilla Firefox. A race condition can occur while running the nsDocShell destructor causing a use-after-free memory issue. The highest threat from this vulnerability is to data confidentiality and integrity as well as system availability.", + "FixedIn": [ + { + "Name": "firefox", + "NamespaceName": "rhel:8", + "VendorAdvisory": { + "AdvisorySummary": [ + { + "ID": "RHSA-2020:1341", + "Link": "https://access.redhat.com/errata/RHSA-2020:1341" + } + ], + "NoAdvisory": false + }, + "Version": "0:68.6.1-1.el8_1", + "VersionFormat": "rpm" + }, + { + "Name": "thunderbird", + "NamespaceName": "rhel:8", + "VendorAdvisory": { + "AdvisorySummary": [ + { + "ID": "RHSA-2020:1495", + "Link": "https://access.redhat.com/errata/RHSA-2020:1495" + } + ], + "NoAdvisory": false + }, + "Version": "0:68.7.0-1.el8_1", + "VersionFormat": "rpm" + } + ], + "Link": "https://access.redhat.com/security/cve/CVE-2020-6819", + "Metadata": {}, + "Name": "CVE-2020-6819", + "NamespaceName": "rhel:8", + "Severity": "Critical" + } + } +] \ No newline at end of file diff --git a/pkg/process/v3/transformers/os/test-fixtures/unmarshal-test.json b/pkg/process/v3/transformers/os/test-fixtures/unmarshal-test.json new file mode 100644 index 00000000..edc6d25b --- /dev/null +++ b/pkg/process/v3/transformers/os/test-fixtures/unmarshal-test.json @@ -0,0 +1,104 @@ +[ + { + "Vulnerability": { + "Description": "", + "FixedIn": [ + { + "Name": "389-ds-base", + "NamespaceName": "amzn:2", + "Version": "1.3.8.4-15.amzn2.0.1", + "VersionFormat": "rpm" + }, + { + "Name": "389-ds-base-debuginfo", + "NamespaceName": "amzn:2", + "Version": "1.3.8.4-15.amzn2.0.1", + "VersionFormat": "rpm" + }, + { + "Name": "389-ds-base-devel", + "NamespaceName": "amzn:2", + "Version": "1.3.8.4-15.amzn2.0.1", + "VersionFormat": "rpm" + }, + { + "Name": "389-ds-base-libs", + "NamespaceName": "amzn:2", + "Version": "1.3.8.4-15.amzn2.0.1", + "VersionFormat": "rpm" + }, + { + "Name": "389-ds-base-snmp", + "NamespaceName": "amzn:2", + "Version": "1.3.8.4-15.amzn2.0.1", + "VersionFormat": "rpm" + } + ], + "Link": "https://alas.aws.amazon.com/AL2/ALAS-2018-1106.html", + "Metadata": { + "CVE": [ + {"Name": "CVE-2018-14648"} + ] + }, + "Name": "ALAS-2018-1106", + "NamespaceName": "amzn:2", + "Severity": "Medium" + } + }, + { + "Vulnerability": { + "Description": "", + "FixedIn": [ + { + "Name": "kernel-livepatch-4.14.173-137.228", + "NamespaceName": "amzn:2", + "Version": "1.0-3.amzn2", + "VersionFormat": "rpm" + }, + { + "Name": "kernel-livepatch-4.14.173-137.228-debuginfo", + "NamespaceName": "amzn:2", + "Version": "1.0-3.amzn2", + "VersionFormat": "rpm" + } + ], + "Link": "https://alas.aws.amazon.com/AL2/ALASLIVEPATCH-2020-012.html", + "Metadata": { + "CVE": [ + {"Name": "CVE-2020-12657"} + ] + }, + "Name": "ALASLIVEPATCH-2020-012", + "NamespaceName": "amzn:2", + "Severity": "High" + } + }, + { + "Vulnerability": { + "Description": "", + "FixedIn": [ + { + "Name": "kernel-livepatch-4.14.171-136.231", + "NamespaceName": "amzn:2", + "Version": "1.0-5.amzn2", + "VersionFormat": "rpm" + }, + { + "Name": "kernel-livepatch-4.14.171-136.231-debuginfo", + "NamespaceName": "amzn:2", + "Version": "1.0-5.amzn2", + "VersionFormat": "rpm" + } + ], + "Link": "https://alas.aws.amazon.com/AL2/ALASLIVEPATCH-2020-011.html", + "Metadata": { + "CVE": [ + {"Name": "CVE-2020-12657"} + ] + }, + "Name": "ALASLIVEPATCH-2020-011", + "NamespaceName": "amzn:2", + "Severity": "High" + } + } +] \ No newline at end of file diff --git a/pkg/process/v3/transformers/os/transform.go b/pkg/process/v3/transformers/os/transform.go new file mode 100644 index 00000000..945c2ba3 --- /dev/null +++ b/pkg/process/v3/transformers/os/transform.go @@ -0,0 +1,167 @@ +package os + +import ( + "fmt" + "strings" + + "github.com/anchore/grype-db/pkg/data" + "github.com/anchore/grype-db/pkg/process/common" + "github.com/anchore/grype-db/pkg/process/v3/transformers" + "github.com/anchore/grype-db/pkg/provider/unmarshal" + grypeDB "github.com/anchore/grype/grype/db/v3" +) + +const ( + // TODO: tech debt from a previous design + feed = "vulnerabilities" +) + +func Transform(vulnerability unmarshal.OSVulnerability) ([]data.Entry, error) { + group := vulnerability.Vulnerability.NamespaceName + + var allVulns []grypeDB.Vulnerability + + recordSource := grypeDB.RecordSource(feed, group) + entryNamespace, err := grypeDB.NamespaceForFeedGroup(feed, group) + if err != nil { + return nil, err + } + + vulnerability.Vulnerability.FixedIn = vulnerability.Vulnerability.FixedIn.FilterToHighestModularity() + + // there may be multiple packages indicated within the FixedIn field, we should make + // separate vulnerability entries (one for each name|namespace combo) while merging + // constraint ranges as they are found. + for idx, fixedInEntry := range vulnerability.Vulnerability.FixedIn { + constraint, err := enforceConstraint(fixedInEntry.Version, fixedInEntry.VersionFormat) + if err != nil { + return nil, err + } + + // create vulnerability entry + allVulns = append(allVulns, grypeDB.Vulnerability{ + ID: vulnerability.Vulnerability.Name, + VersionConstraint: constraint, + VersionFormat: fixedInEntry.VersionFormat, + PackageName: fixedInEntry.Name, + Namespace: entryNamespace, + RelatedVulnerabilities: getRelatedVulnerabilities(vulnerability), + Fix: getFix(vulnerability, idx), + Advisories: getAdvisories(vulnerability, idx), + }) + } + + // create vulnerability metadata entry (a single entry keyed off of the vulnerability ID) + metadata := grypeDB.VulnerabilityMetadata{ + ID: vulnerability.Vulnerability.Name, + Namespace: entryNamespace, + DataSource: vulnerability.Vulnerability.Link, + RecordSource: recordSource, + Severity: vulnerability.Vulnerability.Severity, + URLs: getLinks(vulnerability), + Description: vulnerability.Vulnerability.Description, + Cvss: getCvss(vulnerability), + } + + return transformers.NewEntries(allVulns, metadata), nil +} + +func getLinks(entry unmarshal.OSVulnerability) []string { + // find all URLs related to the vulnerability + links := []string{entry.Vulnerability.Link} + if entry.Vulnerability.Metadata.CVE != nil { + for _, cve := range entry.Vulnerability.Metadata.CVE { + if cve.Link != "" { + links = append(links, cve.Link) + } + } + } + return links +} + +func getCvss(entry unmarshal.OSVulnerability) (cvss []grypeDB.Cvss) { + for _, vendorCvss := range entry.Vulnerability.CVSS { + cvss = append(cvss, grypeDB.Cvss{ + Version: vendorCvss.Version, + Vector: vendorCvss.VectorString, + Metrics: grypeDB.NewCvssMetrics( + vendorCvss.BaseMetrics.BaseScore, + vendorCvss.BaseMetrics.ExploitabilityScore, + vendorCvss.BaseMetrics.ImpactScore, + ), + VendorMetadata: transformers.VendorBaseMetrics{ + BaseSeverity: vendorCvss.BaseMetrics.BaseSeverity, + Status: vendorCvss.Status, + }, + }) + } + return cvss +} + +func getAdvisories(entry unmarshal.OSVulnerability, idx int) (advisories []grypeDB.Advisory) { + fixedInEntry := entry.Vulnerability.FixedIn[idx] + + for _, advisory := range fixedInEntry.VendorAdvisory.AdvisorySummary { + advisories = append(advisories, grypeDB.Advisory{ + ID: advisory.ID, + Link: advisory.Link, + }) + } + return advisories +} + +func getFix(entry unmarshal.OSVulnerability, idx int) grypeDB.Fix { + fixedInEntry := entry.Vulnerability.FixedIn[idx] + + var fixedInVersions []string + fixedInVersion := common.CleanFixedInVersion(fixedInEntry.Version) + if fixedInVersion != "" { + fixedInVersions = append(fixedInVersions, fixedInVersion) + } + + fixState := grypeDB.NotFixedState + if len(fixedInVersions) > 0 { + fixState = grypeDB.FixedState + } else if fixedInEntry.VendorAdvisory.NoAdvisory { + fixState = grypeDB.WontFixState + } + + return grypeDB.Fix{ + Versions: fixedInVersions, + State: fixState, + } +} + +func getRelatedVulnerabilities(entry unmarshal.OSVulnerability) (vulns []grypeDB.VulnerabilityReference) { + // associate related vulnerabilities from the NVD namespace + if strings.HasPrefix(entry.Vulnerability.Name, "CVE") { + vulns = append(vulns, grypeDB.VulnerabilityReference{ + ID: entry.Vulnerability.Name, + Namespace: grypeDB.NVDNamespace, + }) + } + + // note: an example of multiple CVEs for a record is centos:5 RHSA-2007:0055 which maps to CVE-2007-0002 and CVE-2007-1466 + for _, ref := range entry.Vulnerability.Metadata.CVE { + vulns = append(vulns, grypeDB.VulnerabilityReference{ + ID: ref.Name, + Namespace: grypeDB.NVDNamespace, + }) + } + return vulns +} + +func enforceConstraint(constraint, format string) (string, error) { + constraint = common.CleanConstraint(constraint) + if len(constraint) == 0 { + return "", nil + } + switch strings.ToLower(format) { + case "dpkg", "rpm", "apk": + // the passed constraint is a fixed version + return fmt.Sprintf("< %s", constraint), nil + case "semver": + return common.EnforceSemVerConstraint(constraint), nil + } + return "", fmt.Errorf("unable to enforce constraint='%s' format='%s'", constraint, format) +} diff --git a/pkg/process/v3/transformers/os/transform_test.go b/pkg/process/v3/transformers/os/transform_test.go new file mode 100644 index 00000000..0f32a53b --- /dev/null +++ b/pkg/process/v3/transformers/os/transform_test.go @@ -0,0 +1,616 @@ +package os + +import ( + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/require" + "os" + "testing" + + testUtils "github.com/anchore/grype-db/pkg/process/tests" + "github.com/anchore/grype-db/pkg/process/v3/transformers" + "github.com/anchore/grype-db/pkg/provider/unmarshal" + grypeDB "github.com/anchore/grype/grype/db/v3" + "github.com/stretchr/testify/assert" +) + +func TestUnmarshalOSVulnerabilitiesEntries(t *testing.T) { + f, err := os.Open("test-fixtures/unmarshal-test.json") + require.NoError(t, err) + defer testUtils.CloseFile(f) + + entries, err := unmarshal.OSVulnerabilityEntries(f) + require.NoError(t, err) + + assert.Len(t, entries, 3) + +} + +func TestParseVulnerabilitiesEntry(t *testing.T) { + tests := []struct { + name string + numEntries int + fixture string + vulns []grypeDB.Vulnerability + metadata grypeDB.VulnerabilityMetadata + feed, group string + }{ + { + name: "Amazon", + numEntries: 1, + fixture: "test-fixtures/amzn.json", + feed: "vulnerabilities", + group: "amzn:2", + vulns: []grypeDB.Vulnerability{ + { + ID: "ALAS-2018-1106", + VersionConstraint: "< 1.3.8.4-15.amzn2.0.1", + VersionFormat: "rpm", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2018-14648", + Namespace: grypeDB.NVDNamespace, + }, + }, + PackageName: "389-ds-base", + Namespace: "amzn:2", + Fix: grypeDB.Fix{ + Versions: []string{"1.3.8.4-15.amzn2.0.1"}, + State: grypeDB.FixedState, + }, + }, + { + ID: "ALAS-2018-1106", + VersionConstraint: "< 1.3.8.4-15.amzn2.0.1", + VersionFormat: "rpm", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2018-14648", + Namespace: grypeDB.NVDNamespace, + }, + }, + PackageName: "389-ds-base-debuginfo", + Namespace: "amzn:2", + Fix: grypeDB.Fix{ + Versions: []string{"1.3.8.4-15.amzn2.0.1"}, + State: grypeDB.FixedState, + }, + }, + { + ID: "ALAS-2018-1106", + VersionConstraint: "< 1.3.8.4-15.amzn2.0.1", + VersionFormat: "rpm", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2018-14648", + Namespace: grypeDB.NVDNamespace, + }, + }, + PackageName: "389-ds-base-devel", + Namespace: "amzn:2", + Fix: grypeDB.Fix{ + Versions: []string{"1.3.8.4-15.amzn2.0.1"}, + State: grypeDB.FixedState, + }, + }, + { + ID: "ALAS-2018-1106", + VersionConstraint: "< 1.3.8.4-15.amzn2.0.1", + VersionFormat: "rpm", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2018-14648", + Namespace: grypeDB.NVDNamespace, + }, + }, + PackageName: "389-ds-base-libs", + Namespace: "amzn:2", + Fix: grypeDB.Fix{ + Versions: []string{"1.3.8.4-15.amzn2.0.1"}, + State: grypeDB.FixedState, + }, + }, + { + ID: "ALAS-2018-1106", + VersionConstraint: "< 1.3.8.4-15.amzn2.0.1", + VersionFormat: "rpm", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2018-14648", + Namespace: grypeDB.NVDNamespace, + }, + }, + PackageName: "389-ds-base-snmp", + Namespace: "amzn:2", + Fix: grypeDB.Fix{ + Versions: []string{"1.3.8.4-15.amzn2.0.1"}, + State: grypeDB.FixedState, + }, + }, + }, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "ALAS-2018-1106", + Namespace: "amzn:2", + DataSource: "https://alas.aws.amazon.com/AL2/ALAS-2018-1106.html", + RecordSource: "vulnerabilities:amzn:2", + Severity: "Medium", + URLs: []string{"https://alas.aws.amazon.com/AL2/ALAS-2018-1106.html"}, + }, + }, + { + name: "Debian", + numEntries: 1, + fixture: "test-fixtures/debian-8.json", + feed: "vulnerabilities", + group: "debian:8", + vulns: []grypeDB.Vulnerability{ + { + ID: "CVE-2008-7220", + PackageName: "asterisk", + VersionConstraint: "< 1:1.6.2.0~rc3-1", + VersionFormat: "dpkg", + Namespace: "debian:8", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2008-7220", + Namespace: "nvd", + }, + }, + Fix: grypeDB.Fix{ + Versions: []string{"1:1.6.2.0~rc3-1"}, + State: grypeDB.FixedState, + }, + }, + { + ID: "CVE-2008-7220", + PackageName: "auth2db", + VersionConstraint: "< 0.2.5-2+dfsg-1", + VersionFormat: "dpkg", + Namespace: "debian:8", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2008-7220", + Namespace: "nvd", + }, + }, + Fix: grypeDB.Fix{ + Versions: []string{"0.2.5-2+dfsg-1"}, + State: grypeDB.FixedState, + }, + }, + { + ID: "CVE-2008-7220", + PackageName: "exaile", + VersionConstraint: "< 0.2.14+debian-2.2", + VersionFormat: "dpkg", + Namespace: "debian:8", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2008-7220", + Namespace: "nvd", + }, + }, + Fix: grypeDB.Fix{ + Versions: []string{"0.2.14+debian-2.2"}, + State: grypeDB.FixedState, + }, + }, + { + ID: "CVE-2008-7220", + PackageName: "wordpress", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2008-7220", + Namespace: "nvd", + }, + }, + Fix: grypeDB.Fix{ + State: grypeDB.NotFixedState, + }, + VersionConstraint: "", + VersionFormat: "dpkg", + Namespace: "debian:8", + }, + }, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "CVE-2008-7220", + Namespace: "debian:8", + DataSource: "https://security-tracker.debian.org/tracker/CVE-2008-7220", + RecordSource: "vulnerabilities:debian:8", + Severity: "High", + URLs: []string{"https://security-tracker.debian.org/tracker/CVE-2008-7220"}, + Description: "", + }, + }, + { + name: "RHEL", + numEntries: 1, + fixture: "test-fixtures/rhel-8.json", + feed: "vulnerabilities", + group: "rhel:8", + vulns: []grypeDB.Vulnerability{ + { + ID: "CVE-2020-6819", + PackageName: "firefox", + VersionConstraint: "< 0:68.6.1-1.el8_1", + VersionFormat: "rpm", + Namespace: "rhel:8", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2020-6819", + Namespace: "nvd", + }, + }, + Fix: grypeDB.Fix{ + Versions: []string{"0:68.6.1-1.el8_1"}, + State: grypeDB.FixedState, + }, + Advisories: []grypeDB.Advisory{ + { + ID: "RHSA-2020:1341", + Link: "https://access.redhat.com/errata/RHSA-2020:1341", + }, + }, + }, + { + ID: "CVE-2020-6819", + PackageName: "thunderbird", + VersionConstraint: "< 0:68.7.0-1.el8_1", + VersionFormat: "rpm", + Namespace: "rhel:8", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2020-6819", + Namespace: "nvd", + }, + }, + Fix: grypeDB.Fix{ + Versions: []string{"0:68.7.0-1.el8_1"}, + State: grypeDB.FixedState, + }, + Advisories: []grypeDB.Advisory{ + { + ID: "RHSA-2020:1495", + Link: "https://access.redhat.com/errata/RHSA-2020:1495", + }, + }, + }, + }, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "CVE-2020-6819", + DataSource: "https://access.redhat.com/security/cve/CVE-2020-6819", + Namespace: "rhel:8", + RecordSource: "vulnerabilities:rhel:8", + Severity: "Critical", + URLs: []string{"https://access.redhat.com/security/cve/CVE-2020-6819"}, + Description: "A flaw was found in Mozilla Firefox. A race condition can occur while running the nsDocShell destructor causing a use-after-free memory issue. The highest threat from this vulnerability is to data confidentiality and integrity as well as system availability.", + Cvss: []grypeDB.Cvss{ + { + Version: "3.1", + Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H", + Metrics: grypeDB.NewCvssMetrics( + 8.8, + 2.8, + 5.9, + ), + VendorMetadata: transformers.VendorBaseMetrics{ + Status: "verified", + BaseSeverity: "High", + }, + }, + }, + }, + }, + { + name: "RHEL with modularity", + numEntries: 1, + fixture: "test-fixtures/rhel-8-modules.json", + feed: "vulnerabilities", + group: "rhel:8", + vulns: []grypeDB.Vulnerability{ + { + ID: "CVE-2020-14350", + PackageName: "postgresql", + VersionConstraint: "< 0:12.5-1.module+el8.3.0+9042+664538f4", + VersionFormat: "rpm", + Namespace: "rhel:8", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2020-14350", + Namespace: "nvd", + }, + }, + Fix: grypeDB.Fix{ + Versions: []string{"0:12.5-1.module+el8.3.0+9042+664538f4"}, + State: grypeDB.FixedState, + }, + Advisories: []grypeDB.Advisory{ + { + ID: "RHSA-2020:5620", + Link: "https://access.redhat.com/errata/RHSA-2020:5620", + }, + }, + }, + }, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "CVE-2020-14350", + DataSource: "https://access.redhat.com/security/cve/CVE-2020-14350", + Namespace: "rhel:8", + RecordSource: "vulnerabilities:rhel:8", + Severity: "Medium", + URLs: []string{"https://access.redhat.com/security/cve/CVE-2020-14350"}, + Description: "A flaw was found in PostgreSQL, where some PostgreSQL extensions did not use the search_path safely in their installation script. This flaw allows an attacker with sufficient privileges to trick an administrator into executing a specially crafted script during the extension's installation or update. The highest threat from this vulnerability is to confidentiality, integrity, as well as system availability.", + Cvss: []grypeDB.Cvss{ + { + Version: "3.1", + Vector: "CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:U/C:H/I:H/A:H", + Metrics: grypeDB.NewCvssMetrics( + 7.1, + 1.2, + 5.9, + ), + VendorMetadata: transformers.VendorBaseMetrics{ + Status: "verified", + BaseSeverity: "High", + }, + }, + }, + }, + }, + { + name: "Alpine", + numEntries: 1, + fixture: "test-fixtures/alpine-3.9.json", + feed: "vulnerabilities", + group: "alpine:3.9", + vulns: []grypeDB.Vulnerability{ + { + ID: "CVE-2018-19967", + PackageName: "xen", + VersionConstraint: "< 4.11.1-r0", + VersionFormat: "apk", + Namespace: "alpine:3.9", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2018-19967", + Namespace: "nvd", + }, + }, + Fix: grypeDB.Fix{ + Versions: []string{"4.11.1-r0"}, + State: grypeDB.FixedState, + }, + }, + }, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "CVE-2018-19967", + DataSource: "http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-19967", + Namespace: "alpine:3.9", + RecordSource: "vulnerabilities:alpine:3.9", + Severity: "Medium", + URLs: []string{"http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-19967"}, + Description: "", + }, + }, + { + name: "Oracle", + numEntries: 1, + fixture: "test-fixtures/ol-8.json", + vulns: []grypeDB.Vulnerability{ + { + ID: "ELSA-2020-2550", + PackageName: "libexif", + VersionConstraint: "< 0:0.6.21-17.el8_2", + VersionFormat: "rpm", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2020-13112", + Namespace: grypeDB.NVDNamespace, + }, + }, + Namespace: "ol:8", + Fix: grypeDB.Fix{ + Versions: []string{"0:0.6.21-17.el8_2"}, + State: grypeDB.FixedState, + }, + }, + { + ID: "ELSA-2020-2550", + PackageName: "libexif-devel", + VersionConstraint: "< 0:0.6.21-17.el8_2", + VersionFormat: "rpm", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2020-13112", + Namespace: grypeDB.NVDNamespace, + }, + }, + Namespace: "ol:8", + Fix: grypeDB.Fix{ + Versions: []string{"0:0.6.21-17.el8_2"}, + State: grypeDB.FixedState, + }, + }, + { + ID: "ELSA-2020-2550", + PackageName: "libexif-dummy", + VersionConstraint: "", + VersionFormat: "rpm", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2020-13112", + Namespace: grypeDB.NVDNamespace, + }, + }, + Namespace: "ol:8", + Fix: grypeDB.Fix{ + Versions: nil, + State: grypeDB.NotFixedState, + }, + }, + }, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "ELSA-2020-2550", + DataSource: "http://linux.oracle.com/errata/ELSA-2020-2550.html", + Namespace: "ol:8", + RecordSource: "vulnerabilities:ol:8", + Severity: "Medium", + URLs: []string{"http://linux.oracle.com/errata/ELSA-2020-2550.html", "http://linux.oracle.com/cve/CVE-2020-13112.html"}, + }, + }, + { + name: "Oracle Linux 8 with modularity", + numEntries: 1, + fixture: "test-fixtures/ol-8-modules.json", + vulns: []grypeDB.Vulnerability{ + { + ID: "CVE-2020-14350", + PackageName: "postgresql", + VersionConstraint: "< 0:12.5-1.module+el8.3.0+9042+664538f4", + VersionFormat: "rpm", + Namespace: "ol:8", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2020-14350", + Namespace: "nvd", + }, + }, + Fix: grypeDB.Fix{ + Versions: []string{"0:12.5-1.module+el8.3.0+9042+664538f4"}, + State: grypeDB.FixedState, + }, + }, + }, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "CVE-2020-14350", + DataSource: "https://access.redhat.com/security/cve/CVE-2020-14350", + Namespace: "ol:8", + RecordSource: "vulnerabilities:ol:8", + Severity: "Medium", + URLs: []string{"https://access.redhat.com/security/cve/CVE-2020-14350"}, + Description: "A flaw was found in PostgreSQL, where some PostgreSQL extensions did not use the search_path safely in their installation script. This flaw allows an attacker with sufficient privileges to trick an administrator into executing a specially crafted script during the extension's installation or update. The highest threat from this vulnerability is to confidentiality, integrity, as well as system availability.", + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + f, err := os.Open(test.fixture) + require.NoError(t, err) + t.Cleanup(func() { + assert.NoError(t, f.Close()) + }) + + entries, err := unmarshal.OSVulnerabilityEntries(f) + require.NoError(t, err) + require.Len(t, entries, 1) + + entry := entries[0] + + dataEntries, err := Transform(entry) + require.NoError(t, err) + + var vulns []grypeDB.Vulnerability + for _, entry := range dataEntries { + switch vuln := entry.Data.(type) { + case grypeDB.Vulnerability: + vulns = append(vulns, vuln) + case grypeDB.VulnerabilityMetadata: + assert.Equal(t, test.metadata, vuln) + default: + t.Fatalf("unexpected condition: data entry does not have a vulnerability or a metadata") + } + } + + if diff := cmp.Diff(test.vulns, vulns); diff != "" { + t.Errorf("vulnerabilities do not match (-want +got):\n%s", diff) + } + + }) + } + +} + +func TestParseVulnerabilitiesAllEntries(t *testing.T) { + tests := []struct { + name string + numEntries int + fixture string + vulns []grypeDB.Vulnerability + }{ + { + name: "Debian", + numEntries: 2, + fixture: "test-fixtures/debian-8-multiple-entries-for-same-package.json", + vulns: []grypeDB.Vulnerability{ + { + ID: "CVE-2011-4623", + PackageName: "rsyslog", + VersionConstraint: "< 5.7.4-1", + VersionFormat: "dpkg", + Namespace: "debian:8", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2011-4623", + Namespace: "nvd", + }, + }, + Fix: grypeDB.Fix{ + Versions: []string{"5.7.4-1"}, + State: grypeDB.FixedState, + }, + }, + { + ID: "CVE-2008-5618", + PackageName: "rsyslog", + VersionConstraint: "< 3.18.6-1", + VersionFormat: "dpkg", + Namespace: "debian:8", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2008-5618", + Namespace: "nvd", + }, + }, + Fix: grypeDB.Fix{ + Versions: []string{"3.18.6-1"}, + State: grypeDB.FixedState, + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + f, err := os.Open(test.fixture) + require.NoError(t, err) + t.Cleanup(func() { + assert.NoError(t, f.Close()) + }) + + entries, err := unmarshal.OSVulnerabilityEntries(f) + require.NoError(t, err) + require.Len(t, entries, len(test.vulns)) + + var vulns []grypeDB.Vulnerability + for _, entry := range entries { + dataEntries, err := Transform(entry) + require.NoError(t, err) + + for _, entry := range dataEntries { + switch vuln := entry.Data.(type) { + case grypeDB.Vulnerability: + vulns = append(vulns, vuln) + case grypeDB.VulnerabilityMetadata: + default: + t.Fatalf("unexpected condition: data entry does not have a vulnerability or a metadata") + } + } + } + + if diff := cmp.Diff(test.vulns, vulns); diff != "" { + t.Errorf("vulnerabilities do not match (-want +got):\n%s", diff) + } + }) + } +} diff --git a/pkg/process/v3/transformers/vulnerability_metadata.go b/pkg/process/v3/transformers/vulnerability_metadata.go new file mode 100644 index 00000000..da214315 --- /dev/null +++ b/pkg/process/v3/transformers/vulnerability_metadata.go @@ -0,0 +1,8 @@ +package transformers + +// VendorBaseMetrics captures extra metrics that do not fit into a common CVSS +// struct, like Status and BaseSeverity +type VendorBaseMetrics struct { + BaseSeverity string + Status string +} diff --git a/pkg/process/v3/writer.go b/pkg/process/v3/writer.go new file mode 100644 index 00000000..d0a68ff1 --- /dev/null +++ b/pkg/process/v3/writer.go @@ -0,0 +1,125 @@ +package v3 + +import ( + "crypto/sha256" + "fmt" + "path" + "path/filepath" + "strings" + "time" + + "github.com/anchore/grype-db/internal/log" + + "github.com/anchore/grype-db/internal/file" + "github.com/anchore/grype-db/pkg/data" + "github.com/anchore/grype/grype/db" + grypeDB "github.com/anchore/grype/grype/db/v3" + grypeDBStore "github.com/anchore/grype/grype/db/v3/store" + "github.com/spf13/afero" +) + +var _ data.Writer = (*writer)(nil) + +type writer struct { + dbPath string + store grypeDB.Store +} + +func NewWriter(directory string, dataAge time.Time) (data.Writer, error) { + dbPath := path.Join(directory, grypeDB.VulnerabilityStoreFileName) + theStore, err := grypeDBStore.New(dbPath, true) + if err != nil { + return nil, fmt.Errorf("unable to create writer: %w", err) + } + + if err := theStore.SetID(grypeDB.NewID(dataAge)); err != nil { + return nil, fmt.Errorf("unable to set DB ID: %w", err) + } + + return &writer{ + dbPath: dbPath, + store: theStore, + }, nil +} + +func (w writer) Write(entries ...data.Entry) error { + for _, entry := range entries { + if entry.DBSchemaVersion != grypeDB.SchemaVersion { + return fmt.Errorf("wrong schema version: want %+v got %+v", grypeDB.SchemaVersion, entry.DBSchemaVersion) + } + + switch row := entry.Data.(type) { + case grypeDB.Vulnerability: + if err := w.store.AddVulnerability(row); err != nil { + return fmt.Errorf("unable to write vulnerability to store: %w", err) + } + case grypeDB.VulnerabilityMetadata: + normalizeSeverity(&row, w.store) + if err := w.store.AddVulnerabilityMetadata(row); err != nil { + return fmt.Errorf("unable to write vulnerability metadata to store: %w", err) + } + default: + return fmt.Errorf("data entry does not have a vulnerability or a metadata: %T", row) + } + } + + return nil +} + +func (w writer) metadata() (*db.Metadata, error) { + hashStr, err := file.ContentDigest(afero.NewOsFs(), w.dbPath, sha256.New()) + if err != nil { + return nil, fmt.Errorf("failed to hash database file (%s): %w", w.dbPath, err) + } + + storeID, err := w.store.GetID() + if err != nil { + return nil, fmt.Errorf("failed to fetch store ID: %w", err) + } + + metadata := db.Metadata{ + Built: storeID.BuildTimestamp, + Version: storeID.SchemaVersion, + Checksum: "sha256:" + hashStr, + } + return &metadata, nil +} + +func (w writer) Close() error { + w.store.Close() + metadata, err := w.metadata() + if err != nil { + return err + } + + metadataPath := path.Join(filepath.Dir(w.dbPath), db.MetadataFileName) + + return metadata.Write(metadataPath) +} + +func normalizeSeverity(metadata *grypeDB.VulnerabilityMetadata, reader grypeDB.VulnerabilityMetadataStoreReader) { + if metadata.Severity != "" && strings.ToLower(metadata.Severity) != "unknown" { + return + } + if !strings.HasPrefix(strings.ToLower(metadata.ID), "cve") { + return + } + if strings.HasPrefix(metadata.Namespace, grypeDB.NVDNamespace) { + return + } + m, err := reader.GetVulnerabilityMetadata(metadata.ID, grypeDB.NVDNamespace) + if err != nil { + log.WithFields("id", metadata.ID, "error", err).Warn("error fetching vulnerability metadata from NVD namespace") + return + } + if m == nil { + log.WithFields("id", metadata.ID).Debug("unable to find vulnerability metadata from NVD namespace") + return + } + + newSeverity := string(data.ParseSeverity(m.Severity)) + + log.WithFields("id", metadata.ID, "namespace", metadata.Namespace, "from", metadata.Severity, "to", newSeverity).Trace("overriding irrelevant severity with data from NVD record") + + metadata.Severity = newSeverity +} diff --git a/pkg/process/v3/writer_test.go b/pkg/process/v3/writer_test.go new file mode 100644 index 00000000..ee467f75 --- /dev/null +++ b/pkg/process/v3/writer_test.go @@ -0,0 +1,115 @@ +package v3 + +import ( + "errors" + "github.com/anchore/grype-db/pkg/data" + grypeDB "github.com/anchore/grype/grype/db/v3" + "github.com/stretchr/testify/assert" + "testing" +) + +var _ grypeDB.VulnerabilityMetadataStoreReader = (*mockReader)(nil) + +type mockReader struct { + metadata *grypeDB.VulnerabilityMetadata + err error +} + +func newMockReader(sev string) *mockReader { + return &mockReader{ + metadata: &grypeDB.VulnerabilityMetadata{ + Severity: sev, + Namespace: "nvd", + }, + } +} + +func newDeadMockReader() *mockReader { + return &mockReader{ + err: errors.New("dead"), + } +} + +func (m mockReader) GetVulnerabilityMetadata(_, _ string) (*grypeDB.VulnerabilityMetadata, error) { + return m.metadata, m.err +} + +func (m mockReader) GetAllVulnerabilityMetadata() (*[]grypeDB.VulnerabilityMetadata, error) { + panic("implement me") +} + +func Test_normalizeSeverity(t *testing.T) { + + tests := []struct { + name string + initialSeverity string + namespace string + cveID string + reader grypeDB.VulnerabilityMetadataStoreReader + expected data.Severity + }{ + { + name: "skip missing metadata", + initialSeverity: "", + namespace: "test", + reader: &mockReader{}, + expected: "", + }, + { + name: "skip non-cve records metadata", + cveID: "GHSA-1234-1234-1234", + initialSeverity: "", + namespace: "test", + reader: newDeadMockReader(), // should not be used + expected: "", + }, { + name: "override empty severity", + initialSeverity: "", + namespace: "test", + reader: newMockReader("low"), + expected: data.SeverityLow, + }, + { + name: "override unknown severity", + initialSeverity: "unknown", + namespace: "test", + reader: newMockReader("low"), + expected: data.SeverityLow, + }, + { + name: "ignore record with severity already set", + initialSeverity: "Low", + namespace: "test", + reader: newMockReader("critical"), // should not be used + expected: data.SeverityLow, + }, + { + name: "ignore nvd records", + initialSeverity: "Low", + namespace: "nvd", + reader: newDeadMockReader(), // should not be used + expected: data.SeverityLow, + }, + { + name: "db errors should not fail or modify the record", + initialSeverity: "", + namespace: "test", + reader: newDeadMockReader(), + expected: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + record := &grypeDB.VulnerabilityMetadata{ + ID: "cve-2020-0000", + Severity: tt.initialSeverity, + Namespace: tt.namespace, + } + if tt.cveID != "" { + record.ID = tt.cveID + } + normalizeSeverity(record, tt.reader) + assert.Equal(t, string(tt.expected), record.Severity) + }) + } +} diff --git a/pkg/process/v4/processors.go b/pkg/process/v4/processors.go new file mode 100644 index 00000000..9e0a69a1 --- /dev/null +++ b/pkg/process/v4/processors.go @@ -0,0 +1,21 @@ +package v4 + +import ( + "github.com/anchore/grype-db/pkg/data" + "github.com/anchore/grype-db/pkg/process/processors" + "github.com/anchore/grype-db/pkg/process/v4/transformers/github" + "github.com/anchore/grype-db/pkg/process/v4/transformers/matchexclusions" + "github.com/anchore/grype-db/pkg/process/v4/transformers/msrc" + "github.com/anchore/grype-db/pkg/process/v4/transformers/nvd" + "github.com/anchore/grype-db/pkg/process/v4/transformers/os" +) + +func Processors() []data.Processor { + return []data.Processor{ + processors.NewGitHubProcessor(github.Transform), + processors.NewMSRCProcessor(msrc.Transform), + processors.NewNVDProcessor(nvd.Transform), + processors.NewOSProcessor(os.Transform), + processors.NewMatchExclusionProcessor(matchexclusions.Transform), + } +} diff --git a/pkg/process/v4/transformers/entry.go b/pkg/process/v4/transformers/entry.go new file mode 100644 index 00000000..a60d5daf --- /dev/null +++ b/pkg/process/v4/transformers/entry.go @@ -0,0 +1,22 @@ +package transformers + +import ( + "github.com/anchore/grype-db/pkg/data" + grypeDB "github.com/anchore/grype/grype/db/v4" +) + +func NewEntries(vs []grypeDB.Vulnerability, metadata grypeDB.VulnerabilityMetadata) []data.Entry { + entries := []data.Entry{ + { + DBSchemaVersion: grypeDB.SchemaVersion, + Data: metadata, + }, + } + for _, vuln := range vs { + entries = append(entries, data.Entry{ + DBSchemaVersion: grypeDB.SchemaVersion, + Data: vuln, + }) + } + return entries +} diff --git a/pkg/process/v4/transformers/github/test-fixtures/github-github-npm-0.json b/pkg/process/v4/transformers/github/test-fixtures/github-github-npm-0.json new file mode 100644 index 00000000..b0a7d1e9 --- /dev/null +++ b/pkg/process/v4/transformers/github/test-fixtures/github-github-npm-0.json @@ -0,0 +1,31 @@ + +{ + "Advisory": { + "CVE": [ + "CVE-2020-14000" + ], + "FixedIn": [ + { + "ecosystem": "npm", + "identifier": "0.2.0-prerelease.20200714185213", + "name": "scratch-vm", + "namespace": "github:npm", + "range": "<= 0.2.0-prerelease.20200709173451" + } + ], + "Metadata": { + "CVE": [ + "CVE-2020-14000" + ] + }, + "Severity": "High", + "Summary": "Remote Code Execution in scratch-vm", + "ghsaId": "GHSA-vc9j-fhvv-8vrf", + "namespace": "github:npm", + "url": "https://github.com/advisories/GHSA-vc9j-fhvv-8vrf", + "withdrawn": null + }, + "Vulnerability": {} +} + + diff --git a/pkg/process/v4/transformers/github/test-fixtures/github-github-python-0.json b/pkg/process/v4/transformers/github/test-fixtures/github-github-python-0.json new file mode 100644 index 00000000..ad14aa60 --- /dev/null +++ b/pkg/process/v4/transformers/github/test-fixtures/github-github-python-0.json @@ -0,0 +1,58 @@ +[ + { + "Advisory": { + "CVE": [ + "CVE-2018-8768" + ], + "FixedIn": [ + { + "ecosystem": "python", + "identifier": "5.4.1", + "name": "notebook", + "namespace": "github:python", + "range": "< 5.4.1" + } + ], + "Metadata": { + "CVE": [ + "CVE-2018-8768" + ] + }, + "Severity": "Low", + "Summary": "Low severity vulnerability that affects notebook", + "ghsaId": "GHSA-6cwv-x26c-w2q4", + "namespace": "github:python", + "url": "https://github.com/advisories/GHSA-6cwv-x26c-w2q4", + "withdrawn": null + }, + "Vulnerability": {} + }, + { + "Advisory": { + "CVE": [ + "CVE-2017-5524" + ], + "FixedIn": [ + { + "ecosystem": "python", + "identifier": "4.3.12", + "name": "Plone", + "namespace": "github:python", + "range": ">= 4.0 < 4.3.12" + } + ], + "Metadata": { + "CVE": [ + "CVE-2017-5524" + ] + }, + "Severity": "Medium", + "Summary": "Moderate severity vulnerability that affects Plone", + "ghsaId": "GHSA-p5wr-vp8g-q5p4", + "namespace": "github:python", + "url": "https://github.com/advisories/GHSA-p5wr-vp8g-q5p4", + "withdrawn": null + }, + "Vulnerability": {} + } +] \ No newline at end of file diff --git a/pkg/process/v4/transformers/github/test-fixtures/github-github-python-1.json b/pkg/process/v4/transformers/github/test-fixtures/github-github-python-1.json new file mode 100644 index 00000000..bfa84922 --- /dev/null +++ b/pkg/process/v4/transformers/github/test-fixtures/github-github-python-1.json @@ -0,0 +1,43 @@ + +{ + "Advisory": { + "CVE": [ + "CVE-2017-5524" + ], + "FixedIn": [ + { + "ecosystem": "python", + "identifier": "4.3.12", + "name": "Plone", + "namespace": "github:python", + "range": ">= 4.0 < 4.3.12" + }, + { + "ecosystem": "python", + "identifier": "5.1b1", + "name": "Plone", + "namespace": "github:python", + "range": ">= 5.1a1 < 5.1b1" + }, + { + "ecosystem": "python", + "identifier": "5.0.7", + "name": "Plone", + "namespace": "github:python", + "range": ">= 5.0rc1 < 5.0.7" + } + ], + "Metadata": { + "CVE": [ + "CVE-2017-5524" + ] + }, + "Severity": "Medium", + "Summary": "Moderate severity vulnerability that affects Plone", + "ghsaId": "GHSA-p5wr-vp8g-q5p4", + "namespace": "github:python", + "url": "https://github.com/advisories/GHSA-p5wr-vp8g-q5p4", + "withdrawn": null + }, + "Vulnerability": {} +} diff --git a/pkg/process/v4/transformers/github/test-fixtures/github-withdrawn.json b/pkg/process/v4/transformers/github/test-fixtures/github-withdrawn.json new file mode 100644 index 00000000..04995e38 --- /dev/null +++ b/pkg/process/v4/transformers/github/test-fixtures/github-withdrawn.json @@ -0,0 +1,29 @@ + +{ + "Advisory": { + "CVE": [ + "CVE-2018-8768" + ], + "FixedIn": [ + { + "ecosystem": "python", + "identifier": "5.4.1", + "name": "notebook", + "namespace": "github:python", + "range": "< 5.4.1" + } + ], + "Metadata": { + "CVE": [ + "CVE-2018-8768" + ] + }, + "Severity": "Low", + "Summary": "Low severity vulnerability that affects notebook", + "ghsaId": "GHSA-6cwv-x26c-w2q4", + "namespace": "github:python", + "url": "https://github.com/advisories/GHSA-6cwv-x26c-w2q4", + "withdrawn": "2022-01-31T14:32:09Z" + }, + "Vulnerability": {} +} diff --git a/pkg/process/v4/transformers/github/test-fixtures/multiple-fixed-in-names.json b/pkg/process/v4/transformers/github/test-fixtures/multiple-fixed-in-names.json new file mode 100644 index 00000000..ac1ef982 --- /dev/null +++ b/pkg/process/v4/transformers/github/test-fixtures/multiple-fixed-in-names.json @@ -0,0 +1,43 @@ + +{ + "Advisory": { + "CVE": [ + "CVE-2017-5524" + ], + "FixedIn": [ + { + "ecosystem": "python", + "identifier": "4.3.12", + "name": "Plone", + "namespace": "github:python", + "range": ">= 4.0 < 4.3.12" + }, + { + "ecosystem": "python", + "identifier": "5.1b1", + "name": "Plone", + "namespace": "github:python", + "range": ">= 5.1a1 < 5.1b1" + }, + { + "ecosystem": "python", + "identifier": "5.0.7", + "name": "Plone-debug", + "namespace": "github:python", + "range": ">= 5.0rc1 < 5.0.7" + } + ], + "Metadata": { + "CVE": [ + "CVE-2017-5524" + ] + }, + "Severity": "Medium", + "Summary": "Moderate severity vulnerability that affects Plone", + "ghsaId": "GHSA-p5wr-vp8g-q5p4", + "namespace": "github:python", + "url": "https://github.com/advisories/GHSA-p5wr-vp8g-q5p4", + "withdrawn": null + }, + "Vulnerability": {} +} diff --git a/pkg/process/v4/transformers/github/transform.go b/pkg/process/v4/transformers/github/transform.go new file mode 100644 index 00000000..b34a2305 --- /dev/null +++ b/pkg/process/v4/transformers/github/transform.go @@ -0,0 +1,139 @@ +package github + +import ( + "fmt" + "strings" + + "github.com/anchore/grype/grype/db/v4/namespace" + + "github.com/anchore/grype-db/pkg/data" + "github.com/anchore/grype-db/pkg/process/common" + "github.com/anchore/grype-db/pkg/process/v4/transformers" + "github.com/anchore/grype-db/pkg/provider/unmarshal" + grypeDB "github.com/anchore/grype/grype/db/v4" + syftPkg "github.com/anchore/syft/syft/pkg" +) + +const ( + // TODO: tech debt from a previous design + feed = "github" +) + +func buildGrypeNamespace(feed, group string) (namespace.Namespace, error) { + if feed != "github" { + return nil, fmt.Errorf("unable to determine grype namespace for enterprise feed=%s, group=%s", feed, group) + } + + feedGroupComponents := strings.Split(group, ":") + + if len(feedGroupComponents) < 2 { + return nil, fmt.Errorf("unable to determine grype namespace for enterprise feed=%s, group=%s", feed, group) + } + + feedGroupLang := feedGroupComponents[1] + syftLanguage := syftPkg.LanguageByName(feedGroupLang) + + if syftLanguage == syftPkg.UnknownLanguage { + // For now map nuget to dotnet as the language. + if feedGroupLang == "nuget" { + syftLanguage = syftPkg.Dotnet + } else { + return nil, fmt.Errorf("unable to determine grype namespace for enterprise feed=%s, group=%s", feed, group) + } + } + + ns, err := namespace.FromString(fmt.Sprintf("github:language:%s", string(syftLanguage))) + + if err != nil { + return nil, err + } + + return ns, nil +} + +func Transform(vulnerability unmarshal.GitHubAdvisory) ([]data.Entry, error) { + var allVulns []grypeDB.Vulnerability + + // Exclude entries marked as withdrawn + if vulnerability.Advisory.Withdrawn != nil { + return nil, nil + } + + recordSource := fmt.Sprintf("%s:%s", feed, vulnerability.Advisory.Namespace) + grypeNamespace, err := buildGrypeNamespace(feed, vulnerability.Advisory.Namespace) + if err != nil { + return nil, err + } + + entryNamespace := grypeNamespace.String() + + // there may be multiple packages indicated within the FixedIn field, we should make + // separate vulnerability entries (one for each name|namespaces combo) while merging + // constraint ranges as they are found. + for idx, fixedInEntry := range vulnerability.Advisory.FixedIn { + constraint := common.EnforceSemVerConstraint(fixedInEntry.Range) + + var versionFormat string + switch entryNamespace { + case "github:language:python": + versionFormat = "python" + default: + versionFormat = "unknown" + } + + // create vulnerability entry + allVulns = append(allVulns, grypeDB.Vulnerability{ + ID: vulnerability.Advisory.GhsaID, + VersionConstraint: constraint, + VersionFormat: versionFormat, + RelatedVulnerabilities: getRelatedVulnerabilities(vulnerability), + PackageName: grypeNamespace.Resolver().Normalize(fixedInEntry.Name), + Namespace: entryNamespace, + Fix: getFix(vulnerability, idx), + }) + } + + // create vulnerability metadata entry (a single entry keyed off of the vulnerability ID) + metadata := grypeDB.VulnerabilityMetadata{ + ID: vulnerability.Advisory.GhsaID, + DataSource: vulnerability.Advisory.URL, + Namespace: entryNamespace, + RecordSource: recordSource, + Severity: vulnerability.Advisory.Severity, + URLs: []string{vulnerability.Advisory.URL}, + Description: vulnerability.Advisory.Summary, + } + + return transformers.NewEntries(allVulns, metadata), nil +} + +func getFix(entry unmarshal.GitHubAdvisory, idx int) grypeDB.Fix { + fixedInEntry := entry.Advisory.FixedIn[idx] + + var fixedInVersions []string + fixedInVersion := common.CleanFixedInVersion(fixedInEntry.Identifier) + if fixedInVersion != "" { + fixedInVersions = append(fixedInVersions, fixedInVersion) + } + + fixState := grypeDB.NotFixedState + if len(fixedInVersions) > 0 { + fixState = grypeDB.FixedState + } + + return grypeDB.Fix{ + Versions: fixedInVersions, + State: fixState, + } +} + +func getRelatedVulnerabilities(entry unmarshal.GitHubAdvisory) []grypeDB.VulnerabilityReference { + vulns := make([]grypeDB.VulnerabilityReference, len(entry.Advisory.CVE)) + for idx, cve := range entry.Advisory.CVE { + vulns[idx] = grypeDB.VulnerabilityReference{ + ID: cve, + Namespace: "nvd:cpe", + } + } + return vulns +} diff --git a/pkg/process/v4/transformers/github/transform_test.go b/pkg/process/v4/transformers/github/transform_test.go new file mode 100644 index 00000000..417fcd47 --- /dev/null +++ b/pkg/process/v4/transformers/github/transform_test.go @@ -0,0 +1,254 @@ +package github + +import ( + "github.com/anchore/grype/grype/db/v4/namespace" + "github.com/anchore/grype/grype/db/v4/namespace/language" + syftPkg "github.com/anchore/syft/syft/pkg" + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/require" + "os" + "testing" + + testUtils "github.com/anchore/grype-db/pkg/process/tests" + "github.com/anchore/grype-db/pkg/provider/unmarshal" + grypeDB "github.com/anchore/grype/grype/db/v4" + "github.com/stretchr/testify/assert" +) + +func TestBuildGrypeNamespace(t *testing.T) { + tests := []struct { + feed string + group string + namespace namespace.Namespace + }{ + { + feed: "github", + group: "github:python", + namespace: language.NewNamespace("github", syftPkg.Python, ""), + }, + { + feed: "github", + group: "github:composer", + namespace: language.NewNamespace("github", syftPkg.PHP, ""), + }, + { + feed: "github", + group: "github:gem", + namespace: language.NewNamespace("github", syftPkg.Ruby, ""), + }, + { + feed: "github", + group: "github:npm", + namespace: language.NewNamespace("github", syftPkg.JavaScript, ""), + }, + { + feed: "github", + group: "github:go", + namespace: language.NewNamespace("github", syftPkg.Go, ""), + }, + { + feed: "github", + group: "github:nuget", + namespace: language.NewNamespace("github", syftPkg.Dotnet, ""), + }, + { + feed: "github", + group: "github:rust", + namespace: language.NewNamespace("github", syftPkg.Rust, ""), + }, + } + + for _, test := range tests { + ns, err := buildGrypeNamespace(test.feed, test.group) + + assert.NoError(t, err) + assert.Equal(t, test.namespace, ns) + } +} + +func TestUnmarshalGitHubEntries(t *testing.T) { + f, err := os.Open("test-fixtures/github-github-python-0.json") + require.NoError(t, err) + defer testUtils.CloseFile(f) + + entries, err := unmarshal.GitHubAdvisoryEntries(f) + require.NoError(t, err) + + assert.Len(t, entries, 2) + +} + +func TestParseGitHubEntry(t *testing.T) { + expectedVulns := []grypeDB.Vulnerability{ + { + ID: "GHSA-p5wr-vp8g-q5p4", + VersionConstraint: ">=4.0,<4.3.12", + VersionFormat: "python", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2017-5524", + Namespace: "nvd:cpe", + }, + }, + PackageName: "plone", + Namespace: "github:language:python", + Fix: grypeDB.Fix{ + Versions: []string{"4.3.12"}, + State: grypeDB.FixedState, + }, + }, + { + ID: "GHSA-p5wr-vp8g-q5p4", + VersionConstraint: ">=5.1a1,<5.1b1", + VersionFormat: "python", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2017-5524", + Namespace: "nvd:cpe", + }, + }, + PackageName: "plone", + Namespace: "github:language:python", + Fix: grypeDB.Fix{ + Versions: []string{"5.1b1"}, + State: grypeDB.FixedState, + }, + }, + { + ID: "GHSA-p5wr-vp8g-q5p4", + VersionConstraint: ">=5.0rc1,<5.0.7", + VersionFormat: "python", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2017-5524", + Namespace: "nvd:cpe", + }, + }, + PackageName: "plone", + Namespace: "github:language:python", + Fix: grypeDB.Fix{ + Versions: []string{"5.0.7"}, + State: grypeDB.FixedState, + }, + }, + } + + expectedMetadata := grypeDB.VulnerabilityMetadata{ + ID: "GHSA-p5wr-vp8g-q5p4", + Namespace: "github:language:python", + RecordSource: "github:github:python", + DataSource: "https://github.com/advisories/GHSA-p5wr-vp8g-q5p4", + Severity: "Medium", + URLs: []string{"https://github.com/advisories/GHSA-p5wr-vp8g-q5p4"}, + Description: "Moderate severity vulnerability that affects Plone", + } + + f, err := os.Open("test-fixtures/github-github-python-1.json") + require.NoError(t, err) + defer testUtils.CloseFile(f) + + entries, err := unmarshal.GitHubAdvisoryEntries(f) + require.NoError(t, err) + + require.Len(t, entries, 1) + + entry := entries[0] + + dataEntries, err := Transform(entry) + require.NoError(t, err) + + var vulns []grypeDB.Vulnerability + for _, entry := range dataEntries { + switch vuln := entry.Data.(type) { + case grypeDB.Vulnerability: + vulns = append(vulns, vuln) + case grypeDB.VulnerabilityMetadata: + assert.Equal(t, expectedMetadata, vuln) + default: + t.Fatalf("unexpected condition: data entry does not have a vulnerability or a metadata") + } + } + + // check vulnerability + assert.Len(t, vulns, len(expectedVulns)) + + if diff := cmp.Diff(expectedVulns, vulns); diff != "" { + t.Errorf("vulnerabilities do not match (-want +got):\n%s", diff) + } + +} + +func TestDefaultVersionFormatNpmGitHubEntry(t *testing.T) { + expectedVuln := grypeDB.Vulnerability{ + ID: "GHSA-vc9j-fhvv-8vrf", + VersionConstraint: "<=0.2.0-prerelease.20200709173451", + VersionFormat: "unknown", // TODO: this should reference a format, yes? (not a string) + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2020-14000", + Namespace: "nvd:cpe", + }, + }, + PackageName: "scratch-vm", + Namespace: "github:language:javascript", + Fix: grypeDB.Fix{ + Versions: []string{"0.2.0-prerelease.20200714185213"}, + State: grypeDB.FixedState, + }, + } + + expectedMetadata := grypeDB.VulnerabilityMetadata{ + ID: "GHSA-vc9j-fhvv-8vrf", + Namespace: "github:language:javascript", + RecordSource: "github:github:npm", + DataSource: "https://github.com/advisories/GHSA-vc9j-fhvv-8vrf", + Severity: "High", + URLs: []string{"https://github.com/advisories/GHSA-vc9j-fhvv-8vrf"}, + Description: "Remote Code Execution in scratch-vm", + } + + f, err := os.Open("test-fixtures/github-github-npm-0.json") + require.NoError(t, err) + defer testUtils.CloseFile(f) + + entries, err := unmarshal.GitHubAdvisoryEntries(f) + require.NoError(t, err) + + require.Len(t, entries, 1) + + entry := entries[0] + + dataEntries, err := Transform(entry) + assert.NoError(t, err) + + for _, entry := range dataEntries { + switch vuln := entry.Data.(type) { + case grypeDB.Vulnerability: + assert.Equal(t, expectedVuln, vuln) + case grypeDB.VulnerabilityMetadata: + assert.Equal(t, expectedMetadata, vuln) + default: + t.Fatalf("unexpected condition: data entry does not have a vulnerability or a metadata") + } + } + + // check vulnerability + assert.Len(t, dataEntries, 2) +} + +func TestFilterWithdrawnEntries(t *testing.T) { + f, err := os.Open("test-fixtures/github-withdrawn.json") + require.NoError(t, err) + defer testUtils.CloseFile(f) + + entries, err := unmarshal.GitHubAdvisoryEntries(f) + require.NoError(t, err) + + require.Len(t, entries, 1) + + entry := entries[0] + + dataEntries, err := Transform(entry) + assert.NoError(t, err) + assert.Nil(t, dataEntries) +} diff --git a/pkg/process/v4/transformers/matchexclusions/transform.go b/pkg/process/v4/transformers/matchexclusions/transform.go new file mode 100644 index 00000000..f5985213 --- /dev/null +++ b/pkg/process/v4/transformers/matchexclusions/transform.go @@ -0,0 +1,43 @@ +package matchexclusions + +import ( + "github.com/anchore/grype-db/pkg/data" + "github.com/anchore/grype-db/pkg/provider/unmarshal" + + grypeDB "github.com/anchore/grype/grype/db/v4" +) + +func Transform(matchExclusion unmarshal.MatchExclusion) ([]data.Entry, error) { + exclusion := grypeDB.VulnerabilityMatchExclusion{ + ID: matchExclusion.ID, + Constraints: nil, + Justification: matchExclusion.Justification, + } + + for _, c := range matchExclusion.Constraints { + constraint := &grypeDB.VulnerabilityMatchExclusionConstraint{ + Vulnerability: grypeDB.VulnerabilityExclusionConstraint{ + Namespace: c.Vulnerability.Namespace, + FixState: grypeDB.FixState(c.Vulnerability.FixState), + }, + Package: grypeDB.PackageExclusionConstraint{ + Name: c.Package.Name, + Language: c.Package.Language, + Type: c.Package.Type, + Version: c.Package.Version, + Location: c.Package.Location, + }, + } + + exclusion.Constraints = append(exclusion.Constraints, *constraint) + } + + entries := []data.Entry{ + { + DBSchemaVersion: grypeDB.SchemaVersion, + Data: exclusion, + }, + } + + return entries, nil +} diff --git a/pkg/process/v4/transformers/msrc/test-fixtures/microsoft-msrc-0.json b/pkg/process/v4/transformers/msrc/test-fixtures/microsoft-msrc-0.json new file mode 100644 index 00000000..474b23b2 --- /dev/null +++ b/pkg/process/v4/transformers/msrc/test-fixtures/microsoft-msrc-0.json @@ -0,0 +1,194 @@ +[ + { + "cvss": { + "base_score": 7.8, + "temporal_score": 7, + "vector": "CVSS:3.0/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H/E:P/RL:O/RC:C" + }, + "fixed_in": [ + { + "id": "4493470", + "is_first": true, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4493470", + "https://support.microsoft.com/help/4493470" + ] + }, + { + "id": "4494440", + "is_first": false, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4494440", + "https://support.microsoft.com/help/4494440" + ] + }, + { + "id": "4503267", + "is_first": false, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4503267", + "https://support.microsoft.com/en-us/help/4503267" + ] + }, + { + "id": "4507460", + "is_first": false, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4507460", + "https://support.microsoft.com/help/4507460" + ] + }, + { + "id": "4512517", + "is_first": false, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4512517", + "https://support.microsoft.com/help/4512517" + ] + }, + { + "id": "4516044", + "is_first": false, + "is_latest": true, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4516044", + "https://support.microsoft.com/help/4516044" + ] + } + ], + "id": "CVE-2019-0671", + "link": "https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2019-0671", + "product": { + "family": "Windows", + "id": "10852", + "name": "Windows 10 Version 1607 for 32-bit Systems" + }, + "severity": "High", + "summary": "Microsoft Office Access Connectivity Engine Remote Code Execution Vulnerability", + "vulnerable": [ + "4480961", + "4483229", + "4487026", + "4489882" + ] + }, +{ + "cvss": { + "base_score": 4.4, + "temporal_score": 4, + "vector": "CVSS:3.0/AV:L/AC:H/PR:L/UI:R/S:U/C:N/I:N/A:H/E:P/RL:O/RC:C" + }, + "fixed_in": [ + { + "id": "4093119", + "is_first": true, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4093119" + ] + }, + { + "id": "4103723", + "is_first": false, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4103723" + ] + }, + { + "id": "4284880", + "is_first": false, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4284880" + ] + }, + { + "id": "4338814", + "is_first": false, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4338814" + ] + }, + { + "id": "4343887", + "is_first": false, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4343887" + ] + }, + { + "id": "4345418", + "is_first": false, + "is_latest": true, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4345418" + ] + }, + { + "id": "4457131", + "is_first": false, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4457131" + ] + }, + { + "id": "4462917", + "is_first": false, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4462917" + ] + }, + { + "id": "4467691", + "is_first": false, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4467691" + ] + }, + { + "id": "4471321", + "is_first": false, + "is_latest": true, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4471321" + ] + } + ], + "id": "CVE-2018-8116", + "link": "https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2018-8116", + "product": { + "family": "Windows", + "id": "10852", + "name": "Windows 10 Version 1607 for 32-bit Systems" + }, + "severity": "Medium", + "summary": "Microsoft Graphics Component Denial of Service Vulnerability", + "vulnerable": [ + "3213986", + "4013429", + "4015217", + "4019472", + "4022715", + "4025339", + "4034658", + "4038782", + "4041691", + "4048953", + "4053579", + "4056890", + "4074590", + "4088787" + ] + } +] diff --git a/pkg/process/v4/transformers/msrc/transform.go b/pkg/process/v4/transformers/msrc/transform.go new file mode 100644 index 00000000..b15dd881 --- /dev/null +++ b/pkg/process/v4/transformers/msrc/transform.go @@ -0,0 +1,114 @@ +package msrc + +import ( + "fmt" + "strings" + + "github.com/anchore/grype-db/pkg/data" + "github.com/anchore/grype-db/pkg/process/common" + "github.com/anchore/grype-db/pkg/process/v4/transformers" + "github.com/anchore/grype-db/pkg/provider/unmarshal" + grypeDB "github.com/anchore/grype/grype/db/v4" + "github.com/anchore/grype/grype/db/v4/namespace" + "github.com/anchore/grype/grype/distro" +) + +const ( + // TODO: tech debt from a previous design + feed = "microsoft" + groupPrefix = "msrc" +) + +func buildGrypeNamespace(feed, group string) (namespace.Namespace, error) { + if feed != "microsoft" || !strings.HasPrefix(group, "msrc:") { + return nil, fmt.Errorf("invalid source for feed=%s, group=%s", feed, group) + } + components := strings.Split(group, ":") + + if len(components) != 2 { + return nil, fmt.Errorf("invalid source for feed=%s, group=%s", feed, group) + } + ns, err := namespace.FromString(fmt.Sprintf("msrc:distro:%s:%s", distro.Windows, components[1])) + + if err != nil { + return nil, err + } + + return ns, nil +} + +// Transform gets called by the parser, which consumes entries from the JSON files previously pulled. Each VulnDBVulnerability represents +// a single unmarshalled entry from the feed service +func Transform(vulnerability unmarshal.MSRCVulnerability) ([]data.Entry, error) { + group := fmt.Sprintf("%s:%s", groupPrefix, vulnerability.Product.ID) + recordSource := fmt.Sprintf("%s:%s", feed, group) + grypeNamespace, err := buildGrypeNamespace(feed, group) + if err != nil { + return nil, err + } + + entryNamespace := grypeNamespace.String() + + // In anchore-enterprise windows analyzer, "base" represents unpatched windows images (images with no KBs). + // If a vulnerability exists for a Microsoft Product ID and the image has no KBs (which are patches), + // then the image must be vulnerable to the image. + //nolint:gocritic + versionConstraint := append(vulnerability.Vulnerable, "base") + + allVulns := []grypeDB.Vulnerability{ + { + ID: vulnerability.ID, + VersionConstraint: common.OrConstraints(versionConstraint...), + VersionFormat: "kb", + PackageName: grypeNamespace.Resolver().Normalize(vulnerability.Product.ID), + Namespace: entryNamespace, + Fix: getFix(vulnerability), + }, + } + + // create vulnerability metadata entry (a single entry keyed off of the vulnerability ID) + metadata := grypeDB.VulnerabilityMetadata{ + ID: vulnerability.ID, + DataSource: vulnerability.Link, + Namespace: entryNamespace, + RecordSource: recordSource, + Severity: vulnerability.Severity, + URLs: []string{vulnerability.Link}, + // There is no description for vulnerabilities from the feed service + // summary gives something like "windows information disclosure vulnerability" + //Description: vulnerability.Summary, + Cvss: []grypeDB.Cvss{ + { + Metrics: grypeDB.CvssMetrics{BaseScore: vulnerability.Cvss.BaseScore}, + Vector: vulnerability.Cvss.Vector, + }, + }, + } + + return transformers.NewEntries(allVulns, metadata), nil +} + +func getFix(entry unmarshal.MSRCVulnerability) grypeDB.Fix { + fixedInVersion := fixedInKB(entry) + fixState := grypeDB.FixedState + + if fixedInVersion == "" { + fixState = grypeDB.NotFixedState + } + + return grypeDB.Fix{ + Versions: []string{fixedInVersion}, + State: fixState, + } +} + +// fixedInKB finds the "latest" patch (KB id) amongst the available microsoft patches and returns it +// if the "latest" patch cannot be found, an error is returned +func fixedInKB(vulnerability unmarshal.MSRCVulnerability) string { + for _, fixedIn := range vulnerability.FixedIn { + if fixedIn.IsLatest { + return fixedIn.ID + } + } + return "" +} diff --git a/pkg/process/v4/transformers/msrc/transform_test.go b/pkg/process/v4/transformers/msrc/transform_test.go new file mode 100644 index 00000000..bafa2056 --- /dev/null +++ b/pkg/process/v4/transformers/msrc/transform_test.go @@ -0,0 +1,119 @@ +package msrc + +import ( + "github.com/stretchr/testify/require" + "os" + "testing" + + testUtils "github.com/anchore/grype-db/pkg/process/tests" + "github.com/anchore/grype-db/pkg/provider/unmarshal" + grypeDB "github.com/anchore/grype/grype/db/v4" + "github.com/stretchr/testify/assert" +) + +func TestUnmarshalMsrcVulnerabilities(t *testing.T) { + f, err := os.Open("test-fixtures/microsoft-msrc-0.json") + require.NoError(t, err) + defer testUtils.CloseFile(f) + + entries, err := unmarshal.MSRCVulnerabilityEntries(f) + require.NoError(t, err) + + assert.Equal(t, len(entries), 2) +} + +func TestParseMSRCEntry(t *testing.T) { + expectedVulns := []struct { + vulnerability grypeDB.Vulnerability + metadata grypeDB.VulnerabilityMetadata + }{ + { + vulnerability: grypeDB.Vulnerability{ + ID: "CVE-2019-0671", + VersionConstraint: `4480961 || 4483229 || 4487026 || 4489882 || base`, + VersionFormat: "kb", + PackageName: "10852", + Namespace: "msrc:distro:windows:10852", + Fix: grypeDB.Fix{ + Versions: []string{"4516044"}, + State: grypeDB.FixedState, + }, + }, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "CVE-2019-0671", + Severity: "High", + DataSource: "https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2019-0671", + URLs: []string{"https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2019-0671"}, + Description: "", + RecordSource: "microsoft:msrc:10852", + Namespace: "msrc:distro:windows:10852", + Cvss: []grypeDB.Cvss{ + { + Metrics: grypeDB.CvssMetrics{ + BaseScore: 7.8, + ImpactScore: nil, + }, + Vector: "CVSS:3.0/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H/E:P/RL:O/RC:C", + }, + }, + }, + }, + { + vulnerability: grypeDB.Vulnerability{ + ID: "CVE-2018-8116", + VersionConstraint: `3213986 || 4013429 || 4015217 || 4019472 || 4022715 || 4025339 || 4034658 || 4038782 || 4041691 || 4048953 || 4053579 || 4056890 || 4074590 || 4088787 || base`, + VersionFormat: "kb", + PackageName: "10852", + Namespace: "msrc:distro:windows:10852", + Fix: grypeDB.Fix{ + Versions: []string{"4345418"}, + State: grypeDB.FixedState, + }, + }, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "CVE-2018-8116", + Namespace: "msrc:distro:windows:10852", + DataSource: "https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2018-8116", + RecordSource: "microsoft:msrc:10852", + Severity: "Medium", + URLs: []string{"https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2018-8116"}, + Description: "", + Cvss: []grypeDB.Cvss{ + { + Metrics: grypeDB.CvssMetrics{ + BaseScore: 4.4, + ImpactScore: nil, + }, + Vector: "CVSS:3.0/AV:L/AC:H/PR:L/UI:R/S:U/C:N/I:N/A:H/E:P/RL:O/RC:C", + }, + }, + }, + }, + } + + f, err := os.Open("test-fixtures/microsoft-msrc-0.json") + require.NoError(t, err) + defer testUtils.CloseFile(f) + + entries, err := unmarshal.MSRCVulnerabilityEntries(f) + require.NoError(t, err) + + require.Equal(t, len(entries), 2) + + for idx, entry := range entries { + dataEntries, err := Transform(entry) + require.NoError(t, err) + assert.Len(t, dataEntries, 2) + expected := expectedVulns[idx] + for _, entry := range dataEntries { + switch vuln := entry.Data.(type) { + case grypeDB.Vulnerability: + assert.Equal(t, expected.vulnerability, vuln) + case grypeDB.VulnerabilityMetadata: + assert.Equal(t, expected.metadata, vuln) + default: + t.Fatalf("unexpected condition: data entry does not have a vulnerability or a metadata") + } + } + } +} diff --git a/pkg/process/v4/transformers/nvd/test-fixtures/compound-pkg.json b/pkg/process/v4/transformers/nvd/test-fixtures/compound-pkg.json new file mode 100644 index 00000000..8e658dcd --- /dev/null +++ b/pkg/process/v4/transformers/nvd/test-fixtures/compound-pkg.json @@ -0,0 +1,115 @@ +{ + "cve": { + "id": "CVE-2018-10189", + "sourceIdentifier": "cve@mitre.org", + "published": "2018-04-17T20:29:00.410", + "lastModified": "2018-05-23T14:41:49.073", + "vulnStatus": "Analyzed", + "descriptions": [ + { + "lang": "en", + "value": "An issue was discovered in Mautic 1.x and 2.x before 2.13.0. It is possible to systematically emulate tracking cookies per contact due to tracking the contact by their auto-incremented ID. Thus, a third party can manipulate the cookie value with +1 to systematically assume being tracked as each contact in Mautic. It is then possible to retrieve information about the contact through forms that have progressive profiling enabled." + }, + { + "lang": "es", + "value": "Se ha descubierto un problema en Mautic, en versiones 1.x y 2.x anteriores a la 2.13.0. Es posible emular de forma sistemática el rastreo de cookies por contacto debido al rastreo de contacto por su ID autoincrementada. Por lo tanto, un tercero puede manipular el valor de la cookie con un +1 para asumir sistemáticamente que se está rastreando como cada contacto en Mautic. Así, sería posible recuperar información sobre el contacto a través de formularios que tengan habilitada la generación de perfiles progresiva." + } + ], + "metrics": { + "cvssMetricV30": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "cvssData": { + "version": "3.0", + "vectorString": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N", + "attackVector": "NETWORK", + "attackComplexity": "LOW", + "privilegesRequired": "NONE", + "userInteraction": "NONE", + "scope": "UNCHANGED", + "confidentialityImpact": "HIGH", + "integrityImpact": "NONE", + "availabilityImpact": "NONE", + "baseScore": 7.5, + "baseSeverity": "HIGH" + }, + "exploitabilityScore": 3.9, + "impactScore": 3.6 + } + ], + "cvssMetricV2": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "cvssData": { + "version": "2.0", + "vectorString": "AV:N/AC:L/Au:N/C:P/I:N/A:N", + "accessVector": "NETWORK", + "accessComplexity": "LOW", + "authentication": "NONE", + "confidentialityImpact": "PARTIAL", + "integrityImpact": "NONE", + "availabilityImpact": "NONE", + "baseScore": 5.0 + }, + "baseSeverity": "MEDIUM", + "exploitabilityScore": 10.0, + "impactScore": 2.9, + "acInsufInfo": false, + "obtainAllPrivilege": false, + "obtainUserPrivilege": false, + "obtainOtherPrivilege": false, + "userInteractionRequired": false + } + ] + }, + "weaknesses": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "description": [ + { + "lang": "en", + "value": "CWE-200" + } + ] + } + ], + "configurations": [ + { + "nodes": [ + { + "operator": "OR", + "negate": false, + "cpeMatch": [ + { + "vulnerable": true, + "criteria": "cpe:2.3:a:mautic:mautic:*:*:*:*:*:*:*:*", + "versionStartIncluding": "1.0.0", + "versionEndIncluding": "1.4.1", + "matchCriteriaId": "5779710D-099E-40EE-8DF3-55BD3179A50C" + }, + { + "vulnerable": true, + "criteria": "cpe:2.3:a:mautic:mautic:*:*:*:*:*:*:*:*", + "versionStartIncluding": "2.0.0", + "versionEndExcluding": "2.13.0", + "matchCriteriaId": "4EFAEE48-4AEF-4F8C-95E0-6E8D848D900F" + } + ] + } + ] + } + ], + "references": [ + { + "url": "https://github.com/mautic/mautic/releases/tag/2.13.0", + "source": "cve@mitre.org", + "tags": [ + "Third Party Advisory" + ] + } + ] + } +} diff --git a/pkg/process/v4/transformers/nvd/test-fixtures/invalid_cpe.json b/pkg/process/v4/transformers/nvd/test-fixtures/invalid_cpe.json new file mode 100644 index 00000000..eac2ebd4 --- /dev/null +++ b/pkg/process/v4/transformers/nvd/test-fixtures/invalid_cpe.json @@ -0,0 +1,111 @@ +{ + "cve": { + "id": "CVE-2015-8978", + "sourceIdentifier": "cve@mitre.org", + "published": "2016-11-22T17:59:00.180", + "lastModified": "2016-11-28T19:50:59.600", + "vulnStatus": "Modified", + "descriptions": [ + { + "lang": "en", + "value": "In Soap Lite (aka the SOAP::Lite extension for Perl) 1.14 and earlier, an example attack consists of defining 10 or more XML entities, each defined as consisting of 10 of the previous entity, with the document consisting of a single instance of the largest entity, which expands to one billion copies of the first entity. The amount of computer memory used for handling an external SOAP call would likely exceed that available to the process parsing the XML." + }, + { + "lang": "es", + "value": "En Soap Lite (también conocido como la extensión SOAP::Lite para Perl) 1.14 y versiones anteriores, un ejemplo de ataque consiste en definir 10 o más entidades XML, cada una definida como consistente de 10 de la entidad anterior, con el documento consistente de una única instancia de la entidad más grande, que se expande a mil millones de copias de la primera entidad. La suma de la memoria del ordenador utilizada para manejar una llamada SOAP externa probablemente superaría el disponible para el proceso de análisis del XML." + } + ], + "metrics": { + "cvssMetricV30": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "cvssData": { + "version": "3.0", + "vectorString": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", + "attackVector": "NETWORK", + "attackComplexity": "LOW", + "privilegesRequired": "NONE", + "userInteraction": "NONE", + "scope": "UNCHANGED", + "confidentialityImpact": "NONE", + "integrityImpact": "NONE", + "availabilityImpact": "HIGH", + "baseScore": 7.5, + "baseSeverity": "HIGH" + }, + "exploitabilityScore": 3.9, + "impactScore": 3.6 + } + ], + "cvssMetricV2": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "cvssData": { + "version": "2.0", + "vectorString": "AV:N/AC:L/Au:N/C:N/I:N/A:P", + "accessVector": "NETWORK", + "accessComplexity": "LOW", + "authentication": "NONE", + "confidentialityImpact": "NONE", + "integrityImpact": "NONE", + "availabilityImpact": "PARTIAL", + "baseScore": 5.0 + }, + "baseSeverity": "MEDIUM", + "exploitabilityScore": 10.0, + "impactScore": 2.9, + "acInsufInfo": false, + "obtainAllPrivilege": false, + "obtainUserPrivilege": false, + "obtainOtherPrivilege": false, + "userInteractionRequired": false + } + ] + }, + "weaknesses": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "description": [ + { + "lang": "en", + "value": "CWE-399" + } + ] + } + ], + "configurations": [ + { + "nodes": [ + { + "operator": "OR", + "negate": false, + "cpeMatch": [ + { + "vulnerable": true, + "criteria": "cpe:2.3:a:soap::lite_project:soap::lite:*:*:*:*:*:perl:*:*", + "versionEndIncluding": "1.14", + "matchCriteriaId": "FB4DACB9-2E9E-4CBE-825F-FC0303D8CC86" + } + ] + } + ] + } + ], + "references": [ + { + "url": "http://cpansearch.perl.org/src/PHRED/SOAP-Lite-1.20/Changes", + "source": "cve@mitre.org", + "tags": [ + "Vendor Advisory" + ] + }, + { + "url": "http://www.securityfocus.com/bid/94487", + "source": "cve@mitre.org" + } + ] + } +} diff --git a/pkg/process/v4/transformers/nvd/test-fixtures/single-package-multi-distro.json b/pkg/process/v4/transformers/nvd/test-fixtures/single-package-multi-distro.json new file mode 100644 index 00000000..ed108475 --- /dev/null +++ b/pkg/process/v4/transformers/nvd/test-fixtures/single-package-multi-distro.json @@ -0,0 +1,174 @@ +{ + "cve": { + "id": "CVE-2018-1000222", + "sourceIdentifier": "cve@mitre.org", + "published": "2018-08-20T20:29:01.347", + "lastModified": "2020-03-31T02:15:12.667", + "vulnStatus": "Modified", + "descriptions": [ + { + "lang": "en", + "value": "Libgd version 2.2.5 contains a Double Free Vulnerability vulnerability in gdImageBmpPtr Function that can result in Remote Code Execution . This attack appear to be exploitable via Specially Crafted Jpeg Image can trigger double free. This vulnerability appears to have been fixed in after commit ac16bdf2d41724b5a65255d4c28fb0ec46bc42f5." + }, + { + "lang": "es", + "value": "Libgd 2.2.5 contiene una vulnerabilidad de doble liberación (double free) en la función gdImageBmpPtr que puede resultar en la ejecución remota de código. Este ataque parece ser explotable mediante una imagen JPEG especialmente manipulada que desencadene una doble liberación (double free). La vulnerabilidad parece haber sido solucionada tras el commit con ID ac16bdf2d41724b5a65255d4c28fb0ec46bc42f5." + } + ], + "metrics": { + "cvssMetricV30": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "cvssData": { + "version": "3.0", + "vectorString": "CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H", + "attackVector": "NETWORK", + "attackComplexity": "LOW", + "privilegesRequired": "NONE", + "userInteraction": "REQUIRED", + "scope": "UNCHANGED", + "confidentialityImpact": "HIGH", + "integrityImpact": "HIGH", + "availabilityImpact": "HIGH", + "baseScore": 8.8, + "baseSeverity": "HIGH" + }, + "exploitabilityScore": 2.8, + "impactScore": 5.9 + } + ], + "cvssMetricV2": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "cvssData": { + "version": "2.0", + "vectorString": "AV:N/AC:M/Au:N/C:P/I:P/A:P", + "accessVector": "NETWORK", + "accessComplexity": "MEDIUM", + "authentication": "NONE", + "confidentialityImpact": "PARTIAL", + "integrityImpact": "PARTIAL", + "availabilityImpact": "PARTIAL", + "baseScore": 6.8 + }, + "baseSeverity": "MEDIUM", + "exploitabilityScore": 8.6, + "impactScore": 6.4, + "acInsufInfo": false, + "obtainAllPrivilege": false, + "obtainUserPrivilege": false, + "obtainOtherPrivilege": false, + "userInteractionRequired": true + } + ] + }, + "weaknesses": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "description": [ + { + "lang": "en", + "value": "CWE-415" + } + ] + } + ], + "configurations": [ + { + "nodes": [ + { + "operator": "OR", + "negate": false, + "cpeMatch": [ + { + "vulnerable": true, + "criteria": "cpe:2.3:a:libgd:libgd:2.2.5:*:*:*:*:*:*:*", + "matchCriteriaId": "C257CC1C-BF6A-4125-AA61-9C2D09096084" + } + ] + } + ] + }, + { + "nodes": [ + { + "operator": "OR", + "negate": false, + "cpeMatch": [ + { + "vulnerable": true, + "criteria": "cpe:2.3:o:canonical:ubuntu_linux:14.04:*:*:*:lts:*:*:*", + "matchCriteriaId": "B5A6F2F3-4894-4392-8296-3B8DD2679084" + }, + { + "vulnerable": true, + "criteria": "cpe:2.3:o:canonical:ubuntu_linux:16.04:*:*:*:lts:*:*:*", + "matchCriteriaId": "F7016A2A-8365-4F1A-89A2-7A19F2BCAE5B" + }, + { + "vulnerable": true, + "criteria": "cpe:2.3:o:canonical:ubuntu_linux:18.04:*:*:*:lts:*:*:*", + "matchCriteriaId": "23A7C53F-B80F-4E6A-AFA9-58EEA84BE11D" + } + ] + } + ] + }, + { + "nodes": [ + { + "operator": "OR", + "negate": false, + "cpeMatch": [ + { + "vulnerable": true, + "criteria": "cpe:2.3:o:debian:debian_linux:8.0:*:*:*:*:*:*:*", + "matchCriteriaId": "C11E6FB0-C8C0-4527-9AA0-CB9B316F8F43" + } + ] + } + ] + } + ], + "references": [ + { + "url": "https://github.com/libgd/libgd/issues/447", + "source": "cve@mitre.org", + "tags": [ + "Issue Tracking", + "Third Party Advisory" + ] + }, + { + "url": "https://lists.debian.org/debian-lts-announce/2019/01/msg00028.html", + "source": "cve@mitre.org", + "tags": [ + "Mailing List", + "Third Party Advisory" + ] + }, + { + "url": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/3CZ2QADQTKRHTGB2AHD7J4QQNDLBEMM6/", + "source": "cve@mitre.org" + }, + { + "url": "https://security.gentoo.org/glsa/201903-18", + "source": "cve@mitre.org", + "tags": [ + "Third Party Advisory" + ] + }, + { + "url": "https://usn.ubuntu.com/3755-1/", + "source": "cve@mitre.org", + "tags": [ + "Mitigation", + "Third Party Advisory" + ] + } + ] + } +} diff --git a/pkg/process/v4/transformers/nvd/test-fixtures/unmarshal-test.json b/pkg/process/v4/transformers/nvd/test-fixtures/unmarshal-test.json new file mode 100644 index 00000000..2dc698fa --- /dev/null +++ b/pkg/process/v4/transformers/nvd/test-fixtures/unmarshal-test.json @@ -0,0 +1,109 @@ +{ + "cve": { + "id": "CVE-2003-0349", + "sourceIdentifier": "cve@mitre.org", + "published": "2003-07-24T04:00:00.000", + "lastModified": "2018-10-12T21:32:41.083", + "vulnStatus": "Modified", + "descriptions": [ + { + "lang": "en", + "value": "Buffer overflow in the streaming media component for logging multicast requests in the ISAPI for the logging capability of Microsoft Windows Media Services (nsiislog.dll), as installed in IIS 5.0, allows remote attackers to execute arbitrary code via a large POST request to nsiislog.dll." + }, + { + "lang": "es", + "value": "Desbordamiento de búfer en el componente de secuenciamiento (streaming) de medios para registrar peticiones de multidifusión en la librería ISAPI de la capacidad de registro (logging) de Microsoft Windows Media Services (nsiislog.dll), como el instalado en IIS 5.9, permite a atacantes remotos ejecutar código arbitrario mediante una petición POST larga a nsiislog.dll." + } + ], + "metrics": { + "cvssMetricV2": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "cvssData": { + "version": "2.0", + "vectorString": "AV:N/AC:L/Au:N/C:P/I:P/A:P", + "accessVector": "NETWORK", + "accessComplexity": "LOW", + "authentication": "NONE", + "confidentialityImpact": "PARTIAL", + "integrityImpact": "PARTIAL", + "availabilityImpact": "PARTIAL", + "baseScore": 7.5 + }, + "baseSeverity": "HIGH", + "exploitabilityScore": 10.0, + "impactScore": 6.4, + "acInsufInfo": false, + "obtainAllPrivilege": false, + "obtainUserPrivilege": true, + "obtainOtherPrivilege": false, + "userInteractionRequired": false + } + ] + }, + "weaknesses": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "description": [ + { + "lang": "en", + "value": "NVD-CWE-Other" + } + ] + } + ], + "configurations": [ + { + "nodes": [ + { + "operator": "OR", + "negate": false, + "cpeMatch": [ + { + "vulnerable": true, + "criteria": "cpe:2.3:o:microsoft:windows_2000:*:*:*:*:*:*:*:*", + "matchCriteriaId": "4E545C63-FE9C-4CA1-AF0F-D999D84D2AFD" + } + ] + } + ] + } + ], + "references": [ + { + "url": "http://marc.info/?l=bugtraq&m=105665030925504&w=2", + "source": "cve@mitre.org" + }, + { + "url": "http://securitytracker.com/id?1007059", + "source": "cve@mitre.org" + }, + { + "url": "http://www.kb.cert.org/vuls/id/113716", + "source": "cve@mitre.org", + "tags": [ + "US Government Resource" + ] + }, + { + "url": "http://www.ntbugtraq.com/default.asp?pid=36&sid=1&A2=ind0306&L=NTBUGTRAQ&P=R4563", + "source": "cve@mitre.org", + "tags": [ + "Exploit", + "Patch", + "Vendor Advisory" + ] + }, + { + "url": "https://docs.microsoft.com/en-us/security-updates/securitybulletins/2003/ms03-022", + "source": "cve@mitre.org" + }, + { + "url": "https://oval.cisecurity.org/repository/search/definition/oval%3Aorg.mitre.oval%3Adef%3A938", + "source": "cve@mitre.org" + } + ] + } +} diff --git a/pkg/process/v4/transformers/nvd/test-fixtures/version-range.json b/pkg/process/v4/transformers/nvd/test-fixtures/version-range.json new file mode 100644 index 00000000..3df5b86d --- /dev/null +++ b/pkg/process/v4/transformers/nvd/test-fixtures/version-range.json @@ -0,0 +1,121 @@ +{ + "cve": { + "id": "CVE-2018-5487", + "sourceIdentifier": "security-alert@netapp.com", + "published": "2018-05-24T14:29:00.390", + "lastModified": "2018-07-05T13:52:30.627", + "vulnStatus": "Analyzed", + "descriptions": [ + { + "lang": "en", + "value": "NetApp OnCommand Unified Manager for Linux versions 7.2 through 7.3 ship with the Java Management Extension Remote Method Invocation (JMX RMI) service bound to the network, and are susceptible to unauthenticated remote code execution." + }, + { + "lang": "es", + "value": "NetApp OnCommand Unified Manager for Linux, de la versión 7.2 hasta la 7.3, se distribuye con el servicio Java Management Extension Remote Method Invocation (JMX RMI) enlazado a la red y es susceptible a la ejecución remota de código sin autenticación." + } + ], + "metrics": { + "cvssMetricV30": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "cvssData": { + "version": "3.0", + "vectorString": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + "attackVector": "NETWORK", + "attackComplexity": "LOW", + "privilegesRequired": "NONE", + "userInteraction": "NONE", + "scope": "UNCHANGED", + "confidentialityImpact": "HIGH", + "integrityImpact": "HIGH", + "availabilityImpact": "HIGH", + "baseScore": 9.8, + "baseSeverity": "CRITICAL" + }, + "exploitabilityScore": 3.9, + "impactScore": 5.9 + } + ], + "cvssMetricV2": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "cvssData": { + "version": "2.0", + "vectorString": "AV:N/AC:L/Au:N/C:P/I:P/A:P", + "accessVector": "NETWORK", + "accessComplexity": "LOW", + "authentication": "NONE", + "confidentialityImpact": "PARTIAL", + "integrityImpact": "PARTIAL", + "availabilityImpact": "PARTIAL", + "baseScore": 7.5 + }, + "baseSeverity": "HIGH", + "exploitabilityScore": 10.0, + "impactScore": 6.4, + "acInsufInfo": true, + "obtainAllPrivilege": false, + "obtainUserPrivilege": false, + "obtainOtherPrivilege": false, + "userInteractionRequired": false + } + ] + }, + "weaknesses": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "description": [ + { + "lang": "en", + "value": "CWE-20" + } + ] + } + ], + "configurations": [ + { + "operator": "AND", + "nodes": [ + { + "operator": "OR", + "negate": false, + "cpeMatch": [ + { + "vulnerable": true, + "criteria": "cpe:2.3:a:netapp:oncommand_unified_manager:*:*:*:*:*:*:*:*", + "versionStartIncluding": "7.2", + "versionEndIncluding": "7.3", + "matchCriteriaId": "A5949307-3E9B-441F-B008-81A0E0228DC0" + } + ] + }, + { + "operator": "OR", + "negate": false, + "cpeMatch": [ + { + "vulnerable": false, + "criteria": "cpe:2.3:o:linux:linux_kernel:-:*:*:*:*:*:*:*", + "matchCriteriaId": "703AF700-7A70-47E2-BC3A-7FD03B3CA9C1" + } + ] + } + ] + } + ], + "references": [ + { + "url": "https://security.netapp.com/advisory/ntap-20180523-0001/", + "source": "security-alert@netapp.com", + "tags": [ + "Patch", + "Vendor Advisory" + ] + } + ] + } +} diff --git a/pkg/process/v4/transformers/nvd/transform.go b/pkg/process/v4/transformers/nvd/transform.go new file mode 100644 index 00000000..c7bf18bc --- /dev/null +++ b/pkg/process/v4/transformers/nvd/transform.go @@ -0,0 +1,109 @@ +package nvd + +import ( + "fmt" + + "github.com/anchore/grype-db/internal" + "github.com/anchore/grype-db/pkg/data" + "github.com/anchore/grype-db/pkg/process/v4/transformers" + "github.com/anchore/grype-db/pkg/provider/unmarshal" + "github.com/anchore/grype-db/pkg/provider/unmarshal/nvd" + grypeDB "github.com/anchore/grype/grype/db/v4" + "github.com/anchore/grype/grype/db/v4/namespace" +) + +const ( + // TODO: tech debt from a previous design + feed = "nvdv2" + group = "nvdv2:cves" +) + +func buildGrypeNamespace(feed, group string) (namespace.Namespace, error) { + if feed != "nvdv2" || group != "nvdv2:cves" { + return nil, fmt.Errorf("invalid source for feed=%s, group=%s", feed, group) + } + + ns, err := namespace.FromString("nvd:cpe") + + if err != nil { + return nil, err + } + + return ns, nil +} + +func Transform(vulnerability unmarshal.NVDVulnerability) ([]data.Entry, error) { + recordSource := fmt.Sprintf("%s:%s", feed, group) + grypeNamespace, err := buildGrypeNamespace(feed, group) + if err != nil { + return nil, err + } + + entryNamespace := grypeNamespace.String() + + uniquePkgs := findUniquePkgs(vulnerability.Configurations...) + + // extract all links + var links []string + for _, externalRefs := range vulnerability.References { + // TODO: should we capture other information here? + if externalRefs.URL != "" { + links = append(links, externalRefs.URL) + } + } + + // duplicate the vulnerabilities based on the set of unique packages the vulnerability is for + var allVulns []grypeDB.Vulnerability + for _, p := range uniquePkgs.All() { + matches := uniquePkgs.Matches(p) + cpes := internal.NewStringSet() + for _, m := range matches { + cpes.Add(grypeNamespace.Resolver().Normalize(m.Criteria)) + } + + // create vulnerability entry + allVulns = append(allVulns, grypeDB.Vulnerability{ + ID: vulnerability.ID, + VersionConstraint: buildConstraints(uniquePkgs.Matches(p)), + VersionFormat: "unknown", + PackageName: grypeNamespace.Resolver().Normalize(p.Product), + Namespace: entryNamespace, + CPEs: cpes.ToSlice(), + Fix: grypeDB.Fix{ + State: grypeDB.UnknownFixState, + }, + }) + } + + // create vulnerability metadata entry (a single entry keyed off of the vulnerability ID) + + allCVSS := vulnerability.CVSS() + metadata := grypeDB.VulnerabilityMetadata{ + ID: vulnerability.ID, + DataSource: "https://nvd.nist.gov/vuln/detail/" + vulnerability.ID, + Namespace: entryNamespace, + RecordSource: recordSource, + Severity: nvd.CvssSummaries(allCVSS).Sorted().Severity(), + URLs: links, + Description: vulnerability.Description(), + Cvss: getCvss(allCVSS...), + } + + return transformers.NewEntries(allVulns, metadata), nil +} + +func getCvss(cvss ...nvd.CvssSummary) []grypeDB.Cvss { + var results []grypeDB.Cvss + for _, c := range cvss { + results = append(results, grypeDB.Cvss{ + Version: c.Version, + Vector: c.Vector, + Metrics: grypeDB.CvssMetrics{ + BaseScore: c.BaseScore, + ExploitabilityScore: c.ExploitabilityScore, + ImpactScore: c.ImpactScore, + }, + }) + } + return results +} diff --git a/pkg/process/v4/transformers/nvd/transform_test.go b/pkg/process/v4/transformers/nvd/transform_test.go new file mode 100644 index 00000000..d92acc41 --- /dev/null +++ b/pkg/process/v4/transformers/nvd/transform_test.go @@ -0,0 +1,256 @@ +package nvd + +import ( + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/require" + "os" + "testing" + + testUtils "github.com/anchore/grype-db/pkg/process/tests" + "github.com/anchore/grype-db/pkg/provider/unmarshal" + grypeDB "github.com/anchore/grype/grype/db/v4" + "github.com/go-test/deep" + "github.com/stretchr/testify/assert" +) + +func TestUnmarshalNVDVulnerabilitiesEntries(t *testing.T) { + f, err := os.Open("test-fixtures/unmarshal-test.json") + require.NoError(t, err) + defer testUtils.CloseFile(f) + + entries, err := unmarshal.NvdVulnerabilityEntries(f) + require.NoError(t, err) + + assert.Len(t, entries, 1) +} + +func TestParseAllNVDVulnerabilityEntries(t *testing.T) { + + tests := []struct { + name string + numEntries int + fixture string + vulns []grypeDB.Vulnerability + metadata grypeDB.VulnerabilityMetadata + }{ + { + name: "AppVersionRange", + numEntries: 1, + fixture: "test-fixtures/version-range.json", + vulns: []grypeDB.Vulnerability{ + { + ID: "CVE-2018-5487", + PackageName: "oncommand_unified_manager", + VersionConstraint: ">= 7.2, <= 7.3", + VersionFormat: "unknown", // TODO: this should reference a format, yes? (not a string) + Namespace: "nvd:cpe", + CPEs: []string{"cpe:2.3:a:netapp:oncommand_unified_manager:*:*:*:*:*:*:*:*"}, + Fix: grypeDB.Fix{ + State: "unknown", + }, + }, + }, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "CVE-2018-5487", + DataSource: "https://nvd.nist.gov/vuln/detail/CVE-2018-5487", + Namespace: "nvd:cpe", + RecordSource: "nvdv2:nvdv2:cves", + Severity: "Critical", + URLs: []string{"https://security.netapp.com/advisory/ntap-20180523-0001/"}, + Description: "NetApp OnCommand Unified Manager for Linux versions 7.2 through 7.3 ship with the Java Management Extension Remote Method Invocation (JMX RMI) service bound to the network, and are susceptible to unauthenticated remote code execution.", + Cvss: []grypeDB.Cvss{ + { + Metrics: grypeDB.NewCvssMetrics( + 7.5, + 10, + 6.4, + ), + Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P", + Version: "2.0", + }, + { + Metrics: grypeDB.NewCvssMetrics( + 9.8, + 3.9, + 5.9, + ), + Vector: "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + Version: "3.0", + }, + }, + }, + }, + { + name: "App+OS", + numEntries: 1, + fixture: "test-fixtures/single-package-multi-distro.json", + vulns: []grypeDB.Vulnerability{ + { + ID: "CVE-2018-1000222", + PackageName: "libgd", + VersionConstraint: "= 2.2.5", + VersionFormat: "unknown", // TODO: this should reference a format, yes? (not a string) + Namespace: "nvd:cpe", + CPEs: []string{"cpe:2.3:a:libgd:libgd:2.2.5:*:*:*:*:*:*:*"}, + Fix: grypeDB.Fix{ + State: "unknown", + }, + }, + // TODO: Question: should this match also the OS's? (as in the vulnerable_cpes list)... this seems wrong! + }, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "CVE-2018-1000222", + DataSource: "https://nvd.nist.gov/vuln/detail/CVE-2018-1000222", + Namespace: "nvd:cpe", + RecordSource: "nvdv2:nvdv2:cves", + Severity: "High", + URLs: []string{"https://github.com/libgd/libgd/issues/447", "https://lists.debian.org/debian-lts-announce/2019/01/msg00028.html", "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/3CZ2QADQTKRHTGB2AHD7J4QQNDLBEMM6/", "https://security.gentoo.org/glsa/201903-18", "https://usn.ubuntu.com/3755-1/"}, + Description: "Libgd version 2.2.5 contains a Double Free Vulnerability vulnerability in gdImageBmpPtr Function that can result in Remote Code Execution . This attack appear to be exploitable via Specially Crafted Jpeg Image can trigger double free. This vulnerability appears to have been fixed in after commit ac16bdf2d41724b5a65255d4c28fb0ec46bc42f5.", + Cvss: []grypeDB.Cvss{ + { + Metrics: grypeDB.NewCvssMetrics( + 6.8, + 8.6, + 6.4, + ), + Vector: "AV:N/AC:M/Au:N/C:P/I:P/A:P", + Version: "2.0", + }, + { + Metrics: grypeDB.NewCvssMetrics( + 8.8, + 2.8, + 5.9, + ), + Vector: "CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H", + Version: "3.0", + }, + }, + }, + }, + { + name: "AppCompoundVersionRange", + numEntries: 1, + fixture: "test-fixtures/compound-pkg.json", + vulns: []grypeDB.Vulnerability{ + { + ID: "CVE-2018-10189", + PackageName: "mautic", + VersionConstraint: ">= 1.0.0, <= 1.4.1 || >= 2.0.0, < 2.13.0", + VersionFormat: "unknown", + Namespace: "nvd:cpe", + CPEs: []string{"cpe:2.3:a:mautic:mautic:*:*:*:*:*:*:*:*"}, // note: entry was dedupicated + Fix: grypeDB.Fix{ + State: "unknown", + }, + }, + }, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "CVE-2018-10189", + DataSource: "https://nvd.nist.gov/vuln/detail/CVE-2018-10189", + Namespace: "nvd:cpe", + RecordSource: "nvdv2:nvdv2:cves", + Severity: "High", + URLs: []string{"https://github.com/mautic/mautic/releases/tag/2.13.0"}, + Description: "An issue was discovered in Mautic 1.x and 2.x before 2.13.0. It is possible to systematically emulate tracking cookies per contact due to tracking the contact by their auto-incremented ID. Thus, a third party can manipulate the cookie value with +1 to systematically assume being tracked as each contact in Mautic. It is then possible to retrieve information about the contact through forms that have progressive profiling enabled.", + Cvss: []grypeDB.Cvss{ + { + Metrics: grypeDB.NewCvssMetrics( + 5, + 10, + 2.9, + ), + Vector: "AV:N/AC:L/Au:N/C:P/I:N/A:N", + Version: "2.0", + }, + { + Metrics: grypeDB.NewCvssMetrics( + 7.5, + 3.9, + 3.6, + ), + Vector: "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N", + Version: "3.0", + }, + }, + }, + }, + { + // we always keep the metadata even though there are no vulnerability entries for it + name: "InvalidCPE", + numEntries: 1, + fixture: "test-fixtures/invalid_cpe.json", + vulns: nil, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "CVE-2015-8978", + Namespace: "nvd:cpe", + DataSource: "https://nvd.nist.gov/vuln/detail/CVE-2015-8978", + RecordSource: "nvdv2:nvdv2:cves", + Severity: "High", + URLs: []string{ + "http://cpansearch.perl.org/src/PHRED/SOAP-Lite-1.20/Changes", + "http://www.securityfocus.com/bid/94487", + }, + Description: "In Soap Lite (aka the SOAP::Lite extension for Perl) 1.14 and earlier, an example attack consists of defining 10 or more XML entities, each defined as consisting of 10 of the previous entity, with the document consisting of a single instance of the largest entity, which expands to one billion copies of the first entity. The amount of computer memory used for handling an external SOAP call would likely exceed that available to the process parsing the XML.", + Cvss: []grypeDB.Cvss{ + { + Metrics: grypeDB.NewCvssMetrics( + 5, + 10, + 2.9, + ), + Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P", + Version: "2.0", + }, + { + Metrics: grypeDB.NewCvssMetrics( + 7.5, + 3.9, + 3.6, + ), + Vector: "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", + Version: "3.0", + }, + }, + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + f, err := os.Open(test.fixture) + require.NoError(t, err) + t.Cleanup(func() { + assert.NoError(t, f.Close()) + }) + + entries, err := unmarshal.NvdVulnerabilityEntries(f) + require.NoError(t, err) + + var vulns []grypeDB.Vulnerability + for _, entry := range entries { + dataEntries, err := Transform(entry.Cve) + require.NoError(t, err) + + for _, entry := range dataEntries { + switch vuln := entry.Data.(type) { + case grypeDB.Vulnerability: + vulns = append(vulns, vuln) + case grypeDB.VulnerabilityMetadata: + // check metadata + if diff := deep.Equal(test.metadata, vuln); diff != nil { + for _, d := range diff { + t.Errorf("metadata diff: %+v", d) + } + } + default: + t.Fatalf("unexpected condition: data entry does not have a vulnerability or a metadata") + } + } + } + + if diff := cmp.Diff(test.vulns, vulns); diff != "" { + t.Errorf("vulnerabilities do not match (-want +got):\n%s", diff) + } + }) + } +} diff --git a/pkg/process/v4/transformers/nvd/unique_pkg.go b/pkg/process/v4/transformers/nvd/unique_pkg.go new file mode 100644 index 00000000..48791517 --- /dev/null +++ b/pkg/process/v4/transformers/nvd/unique_pkg.go @@ -0,0 +1,115 @@ +package nvd + +import ( + "fmt" + "strings" + + "github.com/anchore/grype-db/pkg/provider/unmarshal/nvd" + + "github.com/anchore/grype-db/internal/log" + "github.com/anchore/grype-db/pkg/process/common" + "github.com/umisama/go-cpe" +) + +const ( + ANY = "*" + NA = "-" +) + +type pkgCandidate struct { + Product string + Vendor string + TargetSoftware string +} + +func (p pkgCandidate) String() string { + return fmt.Sprintf("%s|%s|%s", p.Vendor, p.Product, p.TargetSoftware) +} + +func newPkgCandidate(match nvd.CpeMatch) (*pkgCandidate, error) { + // we are only interested in packages that are vulnerable (not related to secondary match conditioning) + if !match.Vulnerable { + return nil, nil + } + + c, err := cpe.NewItemFromFormattedString(match.Criteria) + if err != nil { + return nil, fmt.Errorf("unable to create uniquePkgEntry from '%s': %w", match.Criteria, err) + } + + // we are only interested in applications, not hardware or operating systems + if c.Part() != cpe.Application { + return nil, nil + } + + return &pkgCandidate{ + Product: c.Product().String(), + Vendor: c.Vendor().String(), + TargetSoftware: c.TargetSw().String(), + }, nil +} + +func findUniquePkgs(cfgs ...nvd.Configuration) uniquePkgTracker { + set := newUniquePkgTracker() + for _, c := range cfgs { + _findUniquePkgs(set, c.Nodes...) + } + return set +} + +func _findUniquePkgs(set uniquePkgTracker, ns ...nvd.Node) { + if len(ns) == 0 { + return + } + for _, node := range ns { + for _, match := range node.CpeMatch { + candidate, err := newPkgCandidate(match) + if err != nil { + // Do not halt all execution because of being unable to create + // a PkgCandidate. This can happen when a CPE is invalid which + // could avoid creating a database + log.Debugf("unable processing uniquePkg: %v", err) + continue + } + if candidate != nil { + set.Add(*candidate, match) + } + } + } +} + +func buildConstraints(matches []nvd.CpeMatch) string { + constraints := make([]string, 0) + for _, match := range matches { + constraints = append(constraints, buildConstraint(match)) + } + return common.OrConstraints(constraints...) +} + +func buildConstraint(match nvd.CpeMatch) string { + constraints := make([]string, 0) + if match.VersionStartIncluding != nil && *match.VersionStartIncluding != "" { + constraints = append(constraints, fmt.Sprintf(">= %s", *match.VersionStartIncluding)) + } else if match.VersionStartExcluding != nil && *match.VersionStartExcluding != "" { + constraints = append(constraints, fmt.Sprintf("> %s", *match.VersionStartExcluding)) + } + + if match.VersionEndIncluding != nil && *match.VersionEndIncluding != "" { + constraints = append(constraints, fmt.Sprintf("<= %s", *match.VersionEndIncluding)) + } else if match.VersionEndExcluding != nil && *match.VersionEndExcluding != "" { + constraints = append(constraints, fmt.Sprintf("< %s", *match.VersionEndExcluding)) + } + + if len(constraints) == 0 { + c, err := cpe.NewItemFromFormattedString(match.Criteria) + if err != nil { + return "" + } + version := c.Version().String() + if version != ANY && version != NA { + constraints = append(constraints, fmt.Sprintf("= %s", version)) + } + } + + return strings.Join(constraints, ", ") +} diff --git a/pkg/process/v4/transformers/nvd/unique_pkg_test.go b/pkg/process/v4/transformers/nvd/unique_pkg_test.go new file mode 100644 index 00000000..21ef9e0c --- /dev/null +++ b/pkg/process/v4/transformers/nvd/unique_pkg_test.go @@ -0,0 +1,352 @@ +package nvd + +import ( + "github.com/anchore/grype-db/pkg/provider/unmarshal/nvd" + "testing" + + "github.com/sergi/go-diff/diffmatchpatch" +) + +func newUniquePkgTrackerFromSlice(candidates []pkgCandidate) uniquePkgTracker { + set := newUniquePkgTracker() + for _, c := range candidates { + set[c] = nil + } + return set +} + +func TestFindUniquePkgs(t *testing.T) { + tests := []struct { + name string + nodes []nvd.Node + expected uniquePkgTracker + }{ + { + name: "simple-match", + nodes: []nvd.Node{ + { + CpeMatch: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendor:product:2.2.0:*:*:*:*:target:*:*", + Vulnerable: true, + }, + }, + }, + }, + expected: newUniquePkgTrackerFromSlice( + []pkgCandidate{ + { + Product: "product", + Vendor: "vendor", + TargetSoftware: "target", + }, + }), + }, + { + name: "skip-hw", + nodes: []nvd.Node{ + { + CpeMatch: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:h:vendor:product:2.2.0:*:*:*:*:target:*:*", + Vulnerable: true, + }, + }, + }, + }, + expected: newUniquePkgTrackerFromSlice([]pkgCandidate{}), + }, + { + name: "skip-os", + nodes: []nvd.Node{ + { + CpeMatch: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:o:vendor:product:2.2.0:*:*:*:*:target:*:*", + Vulnerable: true, + }, + }, + }, + }, + expected: newUniquePkgTrackerFromSlice([]pkgCandidate{}), + }, + { + name: "duplicate-by-product", + nodes: []nvd.Node{ + { + CpeMatch: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendor:productA:3.3.3:*:*:*:*:target:*:*", + Vulnerable: true, + }, + { + Criteria: "cpe:2.3:a:vendor:productB:2.2.0:*:*:*:*:target:*:*", + Vulnerable: true, + }, + }, + Operator: "OR", + }, + }, + expected: newUniquePkgTrackerFromSlice( + []pkgCandidate{ + { + Product: "productA", + Vendor: "vendor", + TargetSoftware: "target", + }, + { + Product: "productB", + Vendor: "vendor", + TargetSoftware: "target", + }, + }), + }, + { + name: "duplicate-by-target", + nodes: []nvd.Node{ + { + CpeMatch: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendor:product:3.3.3:*:*:*:*:targetA:*:*", + Vulnerable: true, + }, + { + Criteria: "cpe:2.3:a:vendor:product:2.2.0:*:*:*:*:targetB:*:*", + Vulnerable: true, + }, + }, + Operator: "OR", + }, + }, + expected: newUniquePkgTrackerFromSlice( + []pkgCandidate{ + { + Product: "product", + Vendor: "vendor", + TargetSoftware: "targetA", + }, + { + Product: "product", + Vendor: "vendor", + TargetSoftware: "targetB", + }, + }), + }, + { + name: "duplicate-by-vendor", + nodes: []nvd.Node{ + { + CpeMatch: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendorA:product:3.3.3:*:*:*:*:target:*:*", + Vulnerable: true, + }, + { + Criteria: "cpe:2.3:a:vendorB:product:2.2.0:*:*:*:*:target:*:*", + Vulnerable: true, + }, + }, + Operator: "OR", + }, + }, + expected: newUniquePkgTrackerFromSlice( + []pkgCandidate{ + { + Product: "product", + Vendor: "vendorA", + TargetSoftware: "target", + }, + { + Product: "product", + Vendor: "vendorB", + TargetSoftware: "target", + }, + }), + }, + { + name: "de-duplicate-case", + nodes: []nvd.Node{ + { + CpeMatch: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendor:product:3.3.3:A:B:C:D:target:E:F", + Vulnerable: true, + }, + { + Criteria: "cpe:2.3:a:vendor:product:2.2.0:Q:R:S:T:target:U:V", + Vulnerable: true, + }, + }, + Operator: "OR", + }, + }, + expected: newUniquePkgTrackerFromSlice( + []pkgCandidate{ + { + Product: "product", + Vendor: "vendor", + TargetSoftware: "target", + }, + }), + }, + { + name: "duplicate-from-nested-nodes", + nodes: []nvd.Node{ + { + CpeMatch: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendorB:product:2.2.0:*:*:*:*:target:*:*", + Vulnerable: true, + }, + }, + Operator: "OR", + }, + { + CpeMatch: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendorA:product:2.2.0:*:*:*:*:target:*:*", + Vulnerable: true, + }, + }, + Operator: "OR", + }, + }, + expected: newUniquePkgTrackerFromSlice( + []pkgCandidate{ + { + Product: "product", + Vendor: "vendorA", + TargetSoftware: "target", + }, + { + Product: "product", + Vendor: "vendorB", + TargetSoftware: "target", + }, + }), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actual := findUniquePkgs(nvd.Configuration{Nodes: test.nodes}) + missing, extra := test.expected.Diff(actual) + if len(missing) != 0 { + for _, c := range missing { + t.Errorf("missing candidate: %+v", c) + } + } + + if len(extra) != 0 { + for _, c := range extra { + t.Errorf("extra candidate: %+v", c) + } + } + }) + } + +} + +func strRef(s string) *string { + return &s +} + +func TestBuildConstraints(t *testing.T) { + tests := []struct { + name string + matches []nvd.CpeMatch + expected string + }{ + { + name: "Equals", + matches: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendor:product:2.2.0:*:*:*:*:target:*:*", + }, + }, + expected: "= 2.2.0", + }, + { + name: "VersionEndExcluding", + matches: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", + VersionEndExcluding: strRef("2.3.0"), + }, + }, + expected: "< 2.3.0", + }, + { + name: "VersionEndIncluding", + matches: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", + VersionEndIncluding: strRef("2.3.0"), + }, + }, + expected: "<= 2.3.0", + }, + { + name: "VersionStartExcluding", + matches: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", + VersionStartExcluding: strRef("2.3.0"), + }, + }, + expected: "> 2.3.0", + }, + { + name: "VersionStartIncluding", + matches: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", + VersionStartIncluding: strRef("2.3.0"), + }, + }, + expected: ">= 2.3.0", + }, + { + name: "Version Range", + matches: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", + VersionStartIncluding: strRef("2.3.0"), + VersionEndIncluding: strRef("2.5.0"), + }, + }, + expected: ">= 2.3.0, <= 2.5.0", + }, + { + name: "Multiple Version Ranges", + matches: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", + VersionStartIncluding: strRef("2.3.0"), + VersionEndIncluding: strRef("2.5.0"), + }, + { + Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", + VersionStartExcluding: strRef("3.3.0"), + VersionEndExcluding: strRef("3.5.0"), + }, + }, + expected: ">= 2.3.0, <= 2.5.0 || > 3.3.0, < 3.5.0", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actual := buildConstraints(test.matches) + + if actual != test.expected { + dmp := diffmatchpatch.New() + diffs := dmp.DiffMain(actual, test.expected, true) + t.Errorf("Expected: '%s'", test.expected) + t.Errorf("Got : '%s'", actual) + t.Errorf("Diff : '%s'", dmp.DiffPrettyText(diffs)) + } + }) + } + +} diff --git a/pkg/process/v4/transformers/nvd/unique_pkg_tracker.go b/pkg/process/v4/transformers/nvd/unique_pkg_tracker.go new file mode 100644 index 00000000..2b7e405d --- /dev/null +++ b/pkg/process/v4/transformers/nvd/unique_pkg_tracker.go @@ -0,0 +1,64 @@ +package nvd + +import ( + "sort" + + "github.com/anchore/grype-db/pkg/provider/unmarshal/nvd" +) + +type uniquePkgTracker map[pkgCandidate][]nvd.CpeMatch + +func newUniquePkgTracker() uniquePkgTracker { + return make(uniquePkgTracker) +} + +func (s uniquePkgTracker) Diff(other uniquePkgTracker) (missing []pkgCandidate, extra []pkgCandidate) { + for k := range s { + if !other.Contains(k) { + missing = append(missing, k) + } + } + + for k := range other { + if !s.Contains(k) { + extra = append(extra, k) + } + } + + return +} + +func (s uniquePkgTracker) Matches(i pkgCandidate) []nvd.CpeMatch { + return s[i] +} + +func (s uniquePkgTracker) Add(i pkgCandidate, match nvd.CpeMatch) { + if _, ok := s[i]; !ok { + s[i] = make([]nvd.CpeMatch, 0) + } + s[i] = append(s[i], match) +} + +func (s uniquePkgTracker) Remove(i pkgCandidate) { + delete(s, i) +} + +func (s uniquePkgTracker) Contains(i pkgCandidate) bool { + _, ok := s[i] + return ok +} + +func (s uniquePkgTracker) All() []pkgCandidate { + res := make([]pkgCandidate, len(s)) + idx := 0 + for k := range s { + res[idx] = k + idx++ + } + + sort.SliceStable(res, func(i, j int) bool { + return res[i].String() < res[j].String() + }) + + return res +} diff --git a/pkg/process/v4/transformers/os/test-fixtures/alpine-3.9.json b/pkg/process/v4/transformers/os/test-fixtures/alpine-3.9.json new file mode 100644 index 00000000..b9d84395 --- /dev/null +++ b/pkg/process/v4/transformers/os/test-fixtures/alpine-3.9.json @@ -0,0 +1,28 @@ +[ + { + "Vulnerability": { + "CVSS": [], + "Description": "", + "FixedIn": [ + { + "Name": "xen", + "NamespaceName": "alpine:3.9", + "Version": "4.11.1-r0", + "VersionFormat": "apk" + } + ], + "Link": "http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-19967", + "Metadata": { + "NVD": { + "CVSSv2": { + "Score": 4.9, + "Vectors": "AV:L/AC:L/Au:N/C:N/I:N/A:C" + } + } + }, + "Name": "CVE-2018-19967", + "NamespaceName": "alpine:3.9", + "Severity": "Medium" + } + } +] \ No newline at end of file diff --git a/pkg/process/v4/transformers/os/test-fixtures/amzn.json b/pkg/process/v4/transformers/os/test-fixtures/amzn.json new file mode 100644 index 00000000..a862c32e --- /dev/null +++ b/pkg/process/v4/transformers/os/test-fixtures/amzn.json @@ -0,0 +1,49 @@ +[ + { + "Vulnerability": { + "Description": "", + "FixedIn": [ + { + "Name": "389-ds-base", + "NamespaceName": "amzn:2", + "Version": "1.3.8.4-15.amzn2.0.1", + "VersionFormat": "rpm" + }, + { + "Name": "389-ds-base-debuginfo", + "NamespaceName": "amzn:2", + "Version": "1.3.8.4-15.amzn2.0.1", + "VersionFormat": "rpm" + }, + { + "Name": "389-ds-base-devel", + "NamespaceName": "amzn:2", + "Version": "1.3.8.4-15.amzn2.0.1", + "VersionFormat": "rpm" + }, + { + "Name": "389-ds-base-libs", + "NamespaceName": "amzn:2", + "Version": "1.3.8.4-15.amzn2.0.1", + "VersionFormat": "rpm" + }, + { + "Name": "389-ds-base-snmp", + "NamespaceName": "amzn:2", + "Version": "1.3.8.4-15.amzn2.0.1", + "VersionFormat": "rpm" + } + ], + "Link": "https://alas.aws.amazon.com/AL2/ALAS-2018-1106.html", + "Metadata": { + "CVE": [ + + {"Name": "CVE-2018-14648"} + ] + }, + "Name": "ALAS-2018-1106", + "NamespaceName": "amzn:2", + "Severity": "Medium" + } + } +] \ No newline at end of file diff --git a/pkg/process/v4/transformers/os/test-fixtures/debian-8-multiple-entries-for-same-package.json b/pkg/process/v4/transformers/os/test-fixtures/debian-8-multiple-entries-for-same-package.json new file mode 100644 index 00000000..5025b56e --- /dev/null +++ b/pkg/process/v4/transformers/os/test-fixtures/debian-8-multiple-entries-for-same-package.json @@ -0,0 +1,62 @@ +[ + { + "Vulnerability": { + "CVSS": [], + "Description": "", + "FixedIn": [ + { + "Name": "rsyslog", + "NamespaceName": "debian:8", + "VendorAdvisory": { + "AdvisorySummary": [], + "NoAdvisory": false + }, + "Version": "5.7.4-1", + "VersionFormat": "dpkg" + } + ], + "Link": "https://security-tracker.debian.org/tracker/CVE-2011-4623", + "Metadata": { + "NVD": { + "CVSSv2": { + "Score": 2.1, + "Vectors": "AV:L/AC:L/Au:N/C:N/I:N/A:P" + } + } + }, + "Name": "CVE-2011-4623", + "NamespaceName": "debian:8", + "Severity": "Low" + } + }, + { + "Vulnerability": { + "CVSS": [], + "Description": "", + "FixedIn": [ + { + "Name": "rsyslog", + "NamespaceName": "debian:8", + "VendorAdvisory": { + "AdvisorySummary": [], + "NoAdvisory": false + }, + "Version": "3.18.6-1", + "VersionFormat": "dpkg" + } + ], + "Link": "https://security-tracker.debian.org/tracker/CVE-2008-5618", + "Metadata": { + "NVD": { + "CVSSv2": { + "Score": 5, + "Vectors": "AV:N/AC:L/Au:N/C:N/I:N/A:P" + } + } + }, + "Name": "CVE-2008-5618", + "NamespaceName": "debian:8", + "Severity": "Low" + } + } +] \ No newline at end of file diff --git a/pkg/process/v4/transformers/os/test-fixtures/debian-8.json b/pkg/process/v4/transformers/os/test-fixtures/debian-8.json new file mode 100644 index 00000000..a758f13c --- /dev/null +++ b/pkg/process/v4/transformers/os/test-fixtures/debian-8.json @@ -0,0 +1,62 @@ +[ + { + "Vulnerability": { + "CVSS": [], + "Description": "", + "FixedIn": [ + { + "Name": "asterisk", + "NamespaceName": "debian:8", + "VendorAdvisory": { + "AdvisorySummary": [], + "NoAdvisory": false + }, + "Version": "1:1.6.2.0~rc3-1", + "VersionFormat": "dpkg" + }, + { + "Name": "auth2db", + "NamespaceName": "debian:8", + "VendorAdvisory": { + "AdvisorySummary": [], + "NoAdvisory": false + }, + "Version": "0.2.5-2+dfsg-1", + "VersionFormat": "dpkg" + }, + { + "Name": "exaile", + "NamespaceName": "debian:8", + "VendorAdvisory": { + "AdvisorySummary": [], + "NoAdvisory": false + }, + "Version": "0.2.14+debian-2.2", + "VersionFormat": "dpkg" + }, + { + "Name": "wordpress", + "NamespaceName": "debian:8", + "VendorAdvisory": { + "AdvisorySummary": [], + "NoAdvisory": false + }, + "Version": "", + "VersionFormat": "dpkg" + } + ], + "Link": "https://security-tracker.debian.org/tracker/CVE-2008-7220", + "Metadata": { + "NVD": { + "CVSSv2": { + "Score": 7.5, + "Vectors": "AV:N/AC:L/Au:N/C:P/I:P/A:P" + } + } + }, + "Name": "CVE-2008-7220", + "NamespaceName": "debian:8", + "Severity": "High" + } + } +] \ No newline at end of file diff --git a/pkg/process/v4/transformers/os/test-fixtures/ol-8-modules.json b/pkg/process/v4/transformers/os/test-fixtures/ol-8-modules.json new file mode 100644 index 00000000..f1d7372b --- /dev/null +++ b/pkg/process/v4/transformers/os/test-fixtures/ol-8-modules.json @@ -0,0 +1,36 @@ +[ + { + "Vulnerability": { + "CVSS": [], + "Description": "A flaw was found in PostgreSQL, where some PostgreSQL extensions did not use the search_path safely in their installation script. This flaw allows an attacker with sufficient privileges to trick an administrator into executing a specially crafted script during the extension's installation or update. The highest threat from this vulnerability is to confidentiality, integrity, as well as system availability.", + "FixedIn": [ + { + "Module": "postgresql:10", + "Name": "postgresql", + "NamespaceName": "ol:8", + "Version": "0:10.14-1.module+el8.2.0+7801+be0fed80", + "VersionFormat": "rpm" + }, + { + "Module": "postgresql:12", + "Name": "postgresql", + "NamespaceName": "ol:8", + "Version": "0:12.5-1.module+el8.3.0+9042+664538f4", + "VersionFormat": "rpm" + }, + { + "Module": "postgresql:9.6", + "Name": "postgresql", + "NamespaceName": "ol:8", + "Version": "0:9.6.20-1.module+el8.3.0+8938+7f0e88b6", + "VersionFormat": "rpm" + } + ], + "Link": "https://access.redhat.com/security/cve/CVE-2020-14350", + "Metadata": {}, + "Name": "CVE-2020-14350", + "NamespaceName": "ol:8", + "Severity": "Medium" + } + } +] \ No newline at end of file diff --git a/pkg/process/v4/transformers/os/test-fixtures/ol-8.json b/pkg/process/v4/transformers/os/test-fixtures/ol-8.json new file mode 100644 index 00000000..09439ece --- /dev/null +++ b/pkg/process/v4/transformers/os/test-fixtures/ol-8.json @@ -0,0 +1,42 @@ +[ + { + "Vulnerability": { + "CVSS": [], + "Description": "", + "FixedIn": [ + { + "Name": "libexif", + "NamespaceName": "ol:8", + "Version": "0:0.6.21-17.el8_2", + "VersionFormat": "rpm" + }, + { + "Name": "libexif-devel", + "NamespaceName": "ol:8", + "Version": "0:0.6.21-17.el8_2", + "VersionFormat": "rpm" + }, + { + "Name": "libexif-dummy", + "NamespaceName": "ol:8", + "Version": "None", + "VersionFormat": "rpm" + } + ], + "Link": "http://linux.oracle.com/errata/ELSA-2020-2550.html", + "Metadata": { + "CVE": [ + { + "Link": "http://linux.oracle.com/cve/CVE-2020-13112.html", + "Name": "CVE-2020-13112" + } + ], + "Issued": "2020-06-15", + "RefId": "ELSA-2020-2550" + }, + "Name": "ELSA-2020-2550", + "NamespaceName": "ol:8", + "Severity": "Medium" + } + } +] \ No newline at end of file diff --git a/pkg/process/v4/transformers/os/test-fixtures/rhel-8-modules.json b/pkg/process/v4/transformers/os/test-fixtures/rhel-8-modules.json new file mode 100644 index 00000000..c0400ad5 --- /dev/null +++ b/pkg/process/v4/transformers/os/test-fixtures/rhel-8-modules.json @@ -0,0 +1,75 @@ +[ + { + "Vulnerability": { + "CVSS": [ + { + "base_metrics": { + "base_score": 7.1, + "base_severity": "High", + "exploitability_score": 1.2, + "impact_score": 5.9 + }, + "status": "verified", + "vector_string": "CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:U/C:H/I:H/A:H", + "version": "3.1" + } + ], + "Description": "A flaw was found in PostgreSQL, where some PostgreSQL extensions did not use the search_path safely in their installation script. This flaw allows an attacker with sufficient privileges to trick an administrator into executing a specially crafted script during the extension's installation or update. The highest threat from this vulnerability is to confidentiality, integrity, as well as system availability.", + "FixedIn": [ + { + "Module": "postgresql:10", + "Name": "postgresql", + "NamespaceName": "rhel:8", + "VendorAdvisory": { + "AdvisorySummary": [ + { + "ID": "RHSA-2020:3669", + "Link": "https://access.redhat.com/errata/RHSA-2020:3669" + } + ], + "NoAdvisory": false + }, + "Version": "0:10.14-1.module+el8.2.0+7801+be0fed80", + "VersionFormat": "rpm" + }, + { + "Module": "postgresql:12", + "Name": "postgresql", + "NamespaceName": "rhel:8", + "VendorAdvisory": { + "AdvisorySummary": [ + { + "ID": "RHSA-2020:5620", + "Link": "https://access.redhat.com/errata/RHSA-2020:5620" + } + ], + "NoAdvisory": false + }, + "Version": "0:12.5-1.module+el8.3.0+9042+664538f4", + "VersionFormat": "rpm" + }, + { + "Module": "postgresql:9.6", + "Name": "postgresql", + "NamespaceName": "rhel:8", + "VendorAdvisory": { + "AdvisorySummary": [ + { + "ID": "RHSA-2020:5619", + "Link": "https://access.redhat.com/errata/RHSA-2020:5619" + } + ], + "NoAdvisory": false + }, + "Version": "0:9.6.20-1.module+el8.3.0+8938+7f0e88b6", + "VersionFormat": "rpm" + } + ], + "Link": "https://access.redhat.com/security/cve/CVE-2020-14350", + "Metadata": {}, + "Name": "CVE-2020-14350", + "NamespaceName": "rhel:8", + "Severity": "Medium" + } + } +] \ No newline at end of file diff --git a/pkg/process/v4/transformers/os/test-fixtures/rhel-8.json b/pkg/process/v4/transformers/os/test-fixtures/rhel-8.json new file mode 100644 index 00000000..2779708c --- /dev/null +++ b/pkg/process/v4/transformers/os/test-fixtures/rhel-8.json @@ -0,0 +1,57 @@ +[ + { + "Vulnerability": { + "CVSS": [ + { + "base_metrics": { + "base_score": 8.8, + "base_severity": "High", + "exploitability_score": 2.8, + "impact_score": 5.9 + }, + "status": "verified", + "vector_string": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H", + "version": "3.1" + } + ], + "Description": "A flaw was found in Mozilla Firefox. A race condition can occur while running the nsDocShell destructor causing a use-after-free memory issue. The highest threat from this vulnerability is to data confidentiality and integrity as well as system availability.", + "FixedIn": [ + { + "Name": "firefox", + "NamespaceName": "rhel:8", + "VendorAdvisory": { + "AdvisorySummary": [ + { + "ID": "RHSA-2020:1341", + "Link": "https://access.redhat.com/errata/RHSA-2020:1341" + } + ], + "NoAdvisory": false + }, + "Version": "0:68.6.1-1.el8_1", + "VersionFormat": "rpm" + }, + { + "Name": "thunderbird", + "NamespaceName": "rhel:8", + "VendorAdvisory": { + "AdvisorySummary": [ + { + "ID": "RHSA-2020:1495", + "Link": "https://access.redhat.com/errata/RHSA-2020:1495" + } + ], + "NoAdvisory": false + }, + "Version": "0:68.7.0-1.el8_1", + "VersionFormat": "rpm" + } + ], + "Link": "https://access.redhat.com/security/cve/CVE-2020-6819", + "Metadata": {}, + "Name": "CVE-2020-6819", + "NamespaceName": "rhel:8", + "Severity": "Critical" + } + } +] \ No newline at end of file diff --git a/pkg/process/v4/transformers/os/test-fixtures/unmarshal-test.json b/pkg/process/v4/transformers/os/test-fixtures/unmarshal-test.json new file mode 100644 index 00000000..edc6d25b --- /dev/null +++ b/pkg/process/v4/transformers/os/test-fixtures/unmarshal-test.json @@ -0,0 +1,104 @@ +[ + { + "Vulnerability": { + "Description": "", + "FixedIn": [ + { + "Name": "389-ds-base", + "NamespaceName": "amzn:2", + "Version": "1.3.8.4-15.amzn2.0.1", + "VersionFormat": "rpm" + }, + { + "Name": "389-ds-base-debuginfo", + "NamespaceName": "amzn:2", + "Version": "1.3.8.4-15.amzn2.0.1", + "VersionFormat": "rpm" + }, + { + "Name": "389-ds-base-devel", + "NamespaceName": "amzn:2", + "Version": "1.3.8.4-15.amzn2.0.1", + "VersionFormat": "rpm" + }, + { + "Name": "389-ds-base-libs", + "NamespaceName": "amzn:2", + "Version": "1.3.8.4-15.amzn2.0.1", + "VersionFormat": "rpm" + }, + { + "Name": "389-ds-base-snmp", + "NamespaceName": "amzn:2", + "Version": "1.3.8.4-15.amzn2.0.1", + "VersionFormat": "rpm" + } + ], + "Link": "https://alas.aws.amazon.com/AL2/ALAS-2018-1106.html", + "Metadata": { + "CVE": [ + {"Name": "CVE-2018-14648"} + ] + }, + "Name": "ALAS-2018-1106", + "NamespaceName": "amzn:2", + "Severity": "Medium" + } + }, + { + "Vulnerability": { + "Description": "", + "FixedIn": [ + { + "Name": "kernel-livepatch-4.14.173-137.228", + "NamespaceName": "amzn:2", + "Version": "1.0-3.amzn2", + "VersionFormat": "rpm" + }, + { + "Name": "kernel-livepatch-4.14.173-137.228-debuginfo", + "NamespaceName": "amzn:2", + "Version": "1.0-3.amzn2", + "VersionFormat": "rpm" + } + ], + "Link": "https://alas.aws.amazon.com/AL2/ALASLIVEPATCH-2020-012.html", + "Metadata": { + "CVE": [ + {"Name": "CVE-2020-12657"} + ] + }, + "Name": "ALASLIVEPATCH-2020-012", + "NamespaceName": "amzn:2", + "Severity": "High" + } + }, + { + "Vulnerability": { + "Description": "", + "FixedIn": [ + { + "Name": "kernel-livepatch-4.14.171-136.231", + "NamespaceName": "amzn:2", + "Version": "1.0-5.amzn2", + "VersionFormat": "rpm" + }, + { + "Name": "kernel-livepatch-4.14.171-136.231-debuginfo", + "NamespaceName": "amzn:2", + "Version": "1.0-5.amzn2", + "VersionFormat": "rpm" + } + ], + "Link": "https://alas.aws.amazon.com/AL2/ALASLIVEPATCH-2020-011.html", + "Metadata": { + "CVE": [ + {"Name": "CVE-2020-12657"} + ] + }, + "Name": "ALASLIVEPATCH-2020-011", + "NamespaceName": "amzn:2", + "Severity": "High" + } + } +] \ No newline at end of file diff --git a/pkg/process/v4/transformers/os/transform.go b/pkg/process/v4/transformers/os/transform.go new file mode 100644 index 00000000..f85c95aa --- /dev/null +++ b/pkg/process/v4/transformers/os/transform.go @@ -0,0 +1,204 @@ +package os + +import ( + "fmt" + "strings" + + "github.com/anchore/grype/grype/db/v4/namespace" + "github.com/anchore/grype/grype/distro" + + "github.com/anchore/grype-db/pkg/data" + "github.com/anchore/grype-db/pkg/process/common" + "github.com/anchore/grype-db/pkg/process/v4/transformers" + "github.com/anchore/grype-db/pkg/provider/unmarshal" + grypeDB "github.com/anchore/grype/grype/db/v4" +) + +const ( + // TODO: tech debt from a previous design + feed = "vulnerabilities" +) + +func Transform(vulnerability unmarshal.OSVulnerability) ([]data.Entry, error) { + group := vulnerability.Vulnerability.NamespaceName + + var allVulns []grypeDB.Vulnerability + + recordSource := fmt.Sprintf("%s:%s", feed, group) + grypeNamespace, err := buildGrypeNamespace(feed, group) + if err != nil { + return nil, err + } + + entryNamespace := grypeNamespace.String() + vulnerability.Vulnerability.FixedIn = vulnerability.Vulnerability.FixedIn.FilterToHighestModularity() + + for idx, fixedInEntry := range vulnerability.Vulnerability.FixedIn { + constraint, err := enforceConstraint(fixedInEntry.Version, fixedInEntry.VersionFormat) + if err != nil { + return nil, err + } + + // create vulnerability entry + allVulns = append(allVulns, grypeDB.Vulnerability{ + ID: vulnerability.Vulnerability.Name, + VersionConstraint: constraint, + VersionFormat: fixedInEntry.VersionFormat, + PackageName: grypeNamespace.Resolver().Normalize(fixedInEntry.Name), + Namespace: entryNamespace, + RelatedVulnerabilities: getRelatedVulnerabilities(vulnerability), + Fix: getFix(vulnerability, idx), + Advisories: getAdvisories(vulnerability, idx), + }) + } + + // create vulnerability metadata entry (a single entry keyed off of the vulnerability ID) + metadata := grypeDB.VulnerabilityMetadata{ + ID: vulnerability.Vulnerability.Name, + Namespace: entryNamespace, + DataSource: vulnerability.Vulnerability.Link, + RecordSource: recordSource, + Severity: vulnerability.Vulnerability.Severity, + URLs: getLinks(vulnerability), + Description: vulnerability.Vulnerability.Description, + Cvss: getCvss(vulnerability), + } + + return transformers.NewEntries(allVulns, metadata), nil +} + +func buildGrypeNamespace(feed, group string) (namespace.Namespace, error) { + if feed != "vulnerabilities" { + return nil, fmt.Errorf("unable to determine grype namespace for enterprise feed=%s, group=%s", feed, group) + } + + feedGroupComponents := strings.Split(group, ":") + + if len(feedGroupComponents) < 2 { + return nil, fmt.Errorf("unable to determine grype namespace for enterprise feed=%s, group=%s", feed, group) + } + + // Currently known enterprise feed groups are expected to be of the form {distroID}:{version} + feedGroupDistroID := feedGroupComponents[0] + d, ok := distro.IDMapping[feedGroupDistroID] + if !ok { + return nil, fmt.Errorf("unable to determine grype namespace for enterprise feed=%s, group=%s", feed, group) + } + + providerName := d.String() + + switch d { + case distro.OracleLinux: + providerName = "oracle" + case distro.AmazonLinux: + providerName = "amazon" + } + + ns, err := namespace.FromString(fmt.Sprintf("%s:distro:%s:%s", providerName, d.String(), feedGroupComponents[1])) + + if err != nil { + return nil, err + } + + return ns, nil +} + +func getLinks(entry unmarshal.OSVulnerability) []string { + // find all URLs related to the vulnerability + links := []string{entry.Vulnerability.Link} + if entry.Vulnerability.Metadata.CVE != nil { + for _, cve := range entry.Vulnerability.Metadata.CVE { + if cve.Link != "" { + links = append(links, cve.Link) + } + } + } + return links +} + +func getCvss(entry unmarshal.OSVulnerability) (cvss []grypeDB.Cvss) { + for _, vendorCvss := range entry.Vulnerability.CVSS { + cvss = append(cvss, grypeDB.Cvss{ + Version: vendorCvss.Version, + Vector: vendorCvss.VectorString, + Metrics: grypeDB.NewCvssMetrics( + vendorCvss.BaseMetrics.BaseScore, + vendorCvss.BaseMetrics.ExploitabilityScore, + vendorCvss.BaseMetrics.ImpactScore, + ), + VendorMetadata: transformers.VendorBaseMetrics{ + BaseSeverity: vendorCvss.BaseMetrics.BaseSeverity, + Status: vendorCvss.Status, + }, + }) + } + return cvss +} + +func getAdvisories(entry unmarshal.OSVulnerability, idx int) (advisories []grypeDB.Advisory) { + fixedInEntry := entry.Vulnerability.FixedIn[idx] + + for _, advisory := range fixedInEntry.VendorAdvisory.AdvisorySummary { + advisories = append(advisories, grypeDB.Advisory{ + ID: advisory.ID, + Link: advisory.Link, + }) + } + return advisories +} + +func getFix(entry unmarshal.OSVulnerability, idx int) grypeDB.Fix { + fixedInEntry := entry.Vulnerability.FixedIn[idx] + + var fixedInVersions []string + fixedInVersion := common.CleanFixedInVersion(fixedInEntry.Version) + if fixedInVersion != "" { + fixedInVersions = append(fixedInVersions, fixedInVersion) + } + + fixState := grypeDB.NotFixedState + if len(fixedInVersions) > 0 { + fixState = grypeDB.FixedState + } else if fixedInEntry.VendorAdvisory.NoAdvisory { + fixState = grypeDB.WontFixState + } + + return grypeDB.Fix{ + Versions: fixedInVersions, + State: fixState, + } +} + +func getRelatedVulnerabilities(entry unmarshal.OSVulnerability) (vulns []grypeDB.VulnerabilityReference) { + // associate related vulnerabilities from the NVD namespace + if strings.HasPrefix(entry.Vulnerability.Name, "CVE") { + vulns = append(vulns, grypeDB.VulnerabilityReference{ + ID: entry.Vulnerability.Name, + Namespace: "nvd:cpe", + }) + } + + // note: an example of multiple CVEs for a record is centos:5 RHSA-2007:0055 which maps to CVE-2007-0002 and CVE-2007-1466 + for _, ref := range entry.Vulnerability.Metadata.CVE { + vulns = append(vulns, grypeDB.VulnerabilityReference{ + ID: ref.Name, + Namespace: "nvd:cpe", + }) + } + return vulns +} + +func enforceConstraint(constraint, format string) (string, error) { + constraint = common.CleanConstraint(constraint) + if len(constraint) == 0 { + return "", nil + } + switch strings.ToLower(format) { + case "dpkg", "rpm", "apk": + // the passed constraint is a fixed version + return fmt.Sprintf("< %s", constraint), nil + case "semver": + return common.EnforceSemVerConstraint(constraint), nil + } + return "", fmt.Errorf("unable to enforce constraint='%s' format='%s'", constraint, format) +} diff --git a/pkg/process/v4/transformers/os/transform_test.go b/pkg/process/v4/transformers/os/transform_test.go new file mode 100644 index 00000000..bcfbaea6 --- /dev/null +++ b/pkg/process/v4/transformers/os/transform_test.go @@ -0,0 +1,615 @@ +package os + +import ( + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/require" + "os" + "testing" + + testUtils "github.com/anchore/grype-db/pkg/process/tests" + "github.com/anchore/grype-db/pkg/process/v4/transformers" + "github.com/anchore/grype-db/pkg/provider/unmarshal" + grypeDB "github.com/anchore/grype/grype/db/v4" + "github.com/stretchr/testify/assert" +) + +func TestUnmarshalOSVulnerabilitiesEntries(t *testing.T) { + f, err := os.Open("test-fixtures/unmarshal-test.json") + require.NoError(t, err) + defer testUtils.CloseFile(f) + + entries, err := unmarshal.OSVulnerabilityEntries(f) + require.NoError(t, err) + + assert.Len(t, entries, 3) +} + +func TestParseVulnerabilitiesEntry(t *testing.T) { + tests := []struct { + name string + numEntries int + fixture string + vulns []grypeDB.Vulnerability + metadata grypeDB.VulnerabilityMetadata + feed, group string + }{ + { + name: "Amazon", + numEntries: 1, + fixture: "test-fixtures/amzn.json", + feed: "vulnerabilities", + group: "amzn:2", + vulns: []grypeDB.Vulnerability{ + { + ID: "ALAS-2018-1106", + VersionConstraint: "< 1.3.8.4-15.amzn2.0.1", + VersionFormat: "rpm", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2018-14648", + Namespace: "nvd:cpe", + }, + }, + PackageName: "389-ds-base", + Namespace: "amazon:distro:amazonlinux:2", + Fix: grypeDB.Fix{ + Versions: []string{"1.3.8.4-15.amzn2.0.1"}, + State: grypeDB.FixedState, + }, + }, + { + ID: "ALAS-2018-1106", + VersionConstraint: "< 1.3.8.4-15.amzn2.0.1", + VersionFormat: "rpm", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2018-14648", + Namespace: "nvd:cpe", + }, + }, + PackageName: "389-ds-base-debuginfo", + Namespace: "amazon:distro:amazonlinux:2", + Fix: grypeDB.Fix{ + Versions: []string{"1.3.8.4-15.amzn2.0.1"}, + State: grypeDB.FixedState, + }, + }, + { + ID: "ALAS-2018-1106", + VersionConstraint: "< 1.3.8.4-15.amzn2.0.1", + VersionFormat: "rpm", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2018-14648", + Namespace: "nvd:cpe", + }, + }, + PackageName: "389-ds-base-devel", + Namespace: "amazon:distro:amazonlinux:2", + Fix: grypeDB.Fix{ + Versions: []string{"1.3.8.4-15.amzn2.0.1"}, + State: grypeDB.FixedState, + }, + }, + { + ID: "ALAS-2018-1106", + VersionConstraint: "< 1.3.8.4-15.amzn2.0.1", + VersionFormat: "rpm", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2018-14648", + Namespace: "nvd:cpe", + }, + }, + PackageName: "389-ds-base-libs", + Namespace: "amazon:distro:amazonlinux:2", + Fix: grypeDB.Fix{ + Versions: []string{"1.3.8.4-15.amzn2.0.1"}, + State: grypeDB.FixedState, + }, + }, + { + ID: "ALAS-2018-1106", + VersionConstraint: "< 1.3.8.4-15.amzn2.0.1", + VersionFormat: "rpm", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2018-14648", + Namespace: "nvd:cpe", + }, + }, + PackageName: "389-ds-base-snmp", + Namespace: "amazon:distro:amazonlinux:2", + Fix: grypeDB.Fix{ + Versions: []string{"1.3.8.4-15.amzn2.0.1"}, + State: grypeDB.FixedState, + }, + }, + }, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "ALAS-2018-1106", + Namespace: "amazon:distro:amazonlinux:2", + DataSource: "https://alas.aws.amazon.com/AL2/ALAS-2018-1106.html", + RecordSource: "vulnerabilities:amzn:2", + Severity: "Medium", + URLs: []string{"https://alas.aws.amazon.com/AL2/ALAS-2018-1106.html"}, + }, + }, + { + name: "Debian", + numEntries: 1, + fixture: "test-fixtures/debian-8.json", + feed: "vulnerabilities", + group: "debian:8", + vulns: []grypeDB.Vulnerability{ + { + ID: "CVE-2008-7220", + PackageName: "asterisk", + VersionConstraint: "< 1:1.6.2.0~rc3-1", + VersionFormat: "dpkg", + Namespace: "debian:distro:debian:8", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2008-7220", + Namespace: "nvd:cpe", + }, + }, + Fix: grypeDB.Fix{ + Versions: []string{"1:1.6.2.0~rc3-1"}, + State: grypeDB.FixedState, + }, + }, + { + ID: "CVE-2008-7220", + PackageName: "auth2db", + VersionConstraint: "< 0.2.5-2+dfsg-1", + VersionFormat: "dpkg", + Namespace: "debian:distro:debian:8", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2008-7220", + Namespace: "nvd:cpe", + }, + }, + Fix: grypeDB.Fix{ + Versions: []string{"0.2.5-2+dfsg-1"}, + State: grypeDB.FixedState, + }, + }, + { + ID: "CVE-2008-7220", + PackageName: "exaile", + VersionConstraint: "< 0.2.14+debian-2.2", + VersionFormat: "dpkg", + Namespace: "debian:distro:debian:8", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2008-7220", + Namespace: "nvd:cpe", + }, + }, + Fix: grypeDB.Fix{ + Versions: []string{"0.2.14+debian-2.2"}, + State: grypeDB.FixedState, + }, + }, + { + ID: "CVE-2008-7220", + PackageName: "wordpress", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2008-7220", + Namespace: "nvd:cpe", + }, + }, + Fix: grypeDB.Fix{ + State: grypeDB.NotFixedState, + }, + VersionConstraint: "", + VersionFormat: "dpkg", + Namespace: "debian:distro:debian:8", + }, + }, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "CVE-2008-7220", + Namespace: "debian:distro:debian:8", + DataSource: "https://security-tracker.debian.org/tracker/CVE-2008-7220", + RecordSource: "vulnerabilities:debian:8", + Severity: "High", + URLs: []string{"https://security-tracker.debian.org/tracker/CVE-2008-7220"}, + Description: "", + }, + }, + { + name: "RHEL", + numEntries: 1, + fixture: "test-fixtures/rhel-8.json", + feed: "vulnerabilities", + group: "rhel:8", + vulns: []grypeDB.Vulnerability{ + { + ID: "CVE-2020-6819", + PackageName: "firefox", + VersionConstraint: "< 0:68.6.1-1.el8_1", + VersionFormat: "rpm", + Namespace: "redhat:distro:redhat:8", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2020-6819", + Namespace: "nvd:cpe", + }, + }, + Fix: grypeDB.Fix{ + Versions: []string{"0:68.6.1-1.el8_1"}, + State: grypeDB.FixedState, + }, + Advisories: []grypeDB.Advisory{ + { + ID: "RHSA-2020:1341", + Link: "https://access.redhat.com/errata/RHSA-2020:1341", + }, + }, + }, + { + ID: "CVE-2020-6819", + PackageName: "thunderbird", + VersionConstraint: "< 0:68.7.0-1.el8_1", + VersionFormat: "rpm", + Namespace: "redhat:distro:redhat:8", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2020-6819", + Namespace: "nvd:cpe", + }, + }, + Fix: grypeDB.Fix{ + Versions: []string{"0:68.7.0-1.el8_1"}, + State: grypeDB.FixedState, + }, + Advisories: []grypeDB.Advisory{ + { + ID: "RHSA-2020:1495", + Link: "https://access.redhat.com/errata/RHSA-2020:1495", + }, + }, + }, + }, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "CVE-2020-6819", + DataSource: "https://access.redhat.com/security/cve/CVE-2020-6819", + Namespace: "redhat:distro:redhat:8", + RecordSource: "vulnerabilities:rhel:8", + Severity: "Critical", + URLs: []string{"https://access.redhat.com/security/cve/CVE-2020-6819"}, + Description: "A flaw was found in Mozilla Firefox. A race condition can occur while running the nsDocShell destructor causing a use-after-free memory issue. The highest threat from this vulnerability is to data confidentiality and integrity as well as system availability.", + Cvss: []grypeDB.Cvss{ + { + Version: "3.1", + Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H", + Metrics: grypeDB.NewCvssMetrics( + 8.8, + 2.8, + 5.9, + ), + VendorMetadata: transformers.VendorBaseMetrics{ + Status: "verified", + BaseSeverity: "High", + }, + }, + }, + }, + }, + { + name: "RHEL with modularity", + numEntries: 1, + fixture: "test-fixtures/rhel-8-modules.json", + feed: "vulnerabilities", + group: "rhel:8", + vulns: []grypeDB.Vulnerability{ + { + ID: "CVE-2020-14350", + PackageName: "postgresql", + VersionConstraint: "< 0:12.5-1.module+el8.3.0+9042+664538f4", + VersionFormat: "rpm", + Namespace: "redhat:distro:redhat:8", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2020-14350", + Namespace: "nvd:cpe", + }, + }, + Fix: grypeDB.Fix{ + Versions: []string{"0:12.5-1.module+el8.3.0+9042+664538f4"}, + State: grypeDB.FixedState, + }, + Advisories: []grypeDB.Advisory{ + { + ID: "RHSA-2020:5620", + Link: "https://access.redhat.com/errata/RHSA-2020:5620", + }, + }, + }, + }, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "CVE-2020-14350", + DataSource: "https://access.redhat.com/security/cve/CVE-2020-14350", + Namespace: "redhat:distro:redhat:8", + RecordSource: "vulnerabilities:rhel:8", + Severity: "Medium", + URLs: []string{"https://access.redhat.com/security/cve/CVE-2020-14350"}, + Description: "A flaw was found in PostgreSQL, where some PostgreSQL extensions did not use the search_path safely in their installation script. This flaw allows an attacker with sufficient privileges to trick an administrator into executing a specially crafted script during the extension's installation or update. The highest threat from this vulnerability is to confidentiality, integrity, as well as system availability.", + Cvss: []grypeDB.Cvss{ + { + Version: "3.1", + Vector: "CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:U/C:H/I:H/A:H", + Metrics: grypeDB.NewCvssMetrics( + 7.1, + 1.2, + 5.9, + ), + VendorMetadata: transformers.VendorBaseMetrics{ + Status: "verified", + BaseSeverity: "High", + }, + }, + }, + }, + }, + { + name: "Alpine", + numEntries: 1, + fixture: "test-fixtures/alpine-3.9.json", + feed: "vulnerabilities", + group: "alpine:3.9", + vulns: []grypeDB.Vulnerability{ + { + ID: "CVE-2018-19967", + PackageName: "xen", + VersionConstraint: "< 4.11.1-r0", + VersionFormat: "apk", + Namespace: "alpine:distro:alpine:3.9", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2018-19967", + Namespace: "nvd:cpe", + }, + }, + Fix: grypeDB.Fix{ + Versions: []string{"4.11.1-r0"}, + State: grypeDB.FixedState, + }, + }, + }, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "CVE-2018-19967", + DataSource: "http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-19967", + Namespace: "alpine:distro:alpine:3.9", + RecordSource: "vulnerabilities:alpine:3.9", + Severity: "Medium", + URLs: []string{"http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-19967"}, + Description: "", + }, + }, + { + name: "Oracle", + numEntries: 1, + fixture: "test-fixtures/ol-8.json", + vulns: []grypeDB.Vulnerability{ + { + ID: "ELSA-2020-2550", + PackageName: "libexif", + VersionConstraint: "< 0:0.6.21-17.el8_2", + VersionFormat: "rpm", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2020-13112", + Namespace: "nvd:cpe", + }, + }, + Namespace: "oracle:distro:oraclelinux:8", + Fix: grypeDB.Fix{ + Versions: []string{"0:0.6.21-17.el8_2"}, + State: grypeDB.FixedState, + }, + }, + { + ID: "ELSA-2020-2550", + PackageName: "libexif-devel", + VersionConstraint: "< 0:0.6.21-17.el8_2", + VersionFormat: "rpm", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2020-13112", + Namespace: "nvd:cpe", + }, + }, + Namespace: "oracle:distro:oraclelinux:8", + Fix: grypeDB.Fix{ + Versions: []string{"0:0.6.21-17.el8_2"}, + State: grypeDB.FixedState, + }, + }, + { + ID: "ELSA-2020-2550", + PackageName: "libexif-dummy", + VersionConstraint: "", + VersionFormat: "rpm", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2020-13112", + Namespace: "nvd:cpe", + }, + }, + Namespace: "oracle:distro:oraclelinux:8", + Fix: grypeDB.Fix{ + Versions: nil, + State: grypeDB.NotFixedState, + }, + }, + }, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "ELSA-2020-2550", + DataSource: "http://linux.oracle.com/errata/ELSA-2020-2550.html", + Namespace: "oracle:distro:oraclelinux:8", + RecordSource: "vulnerabilities:ol:8", + Severity: "Medium", + URLs: []string{"http://linux.oracle.com/errata/ELSA-2020-2550.html", "http://linux.oracle.com/cve/CVE-2020-13112.html"}, + }, + }, + { + name: "Oracle Linux 8 with modularity", + numEntries: 1, + fixture: "test-fixtures/ol-8-modules.json", + vulns: []grypeDB.Vulnerability{ + { + ID: "CVE-2020-14350", + PackageName: "postgresql", + VersionConstraint: "< 0:12.5-1.module+el8.3.0+9042+664538f4", + VersionFormat: "rpm", + Namespace: "oracle:distro:oraclelinux:8", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2020-14350", + Namespace: "nvd:cpe", + }, + }, + Fix: grypeDB.Fix{ + Versions: []string{"0:12.5-1.module+el8.3.0+9042+664538f4"}, + State: grypeDB.FixedState, + }, + }, + }, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "CVE-2020-14350", + DataSource: "https://access.redhat.com/security/cve/CVE-2020-14350", + Namespace: "oracle:distro:oraclelinux:8", + RecordSource: "vulnerabilities:ol:8", + Severity: "Medium", + URLs: []string{"https://access.redhat.com/security/cve/CVE-2020-14350"}, + Description: "A flaw was found in PostgreSQL, where some PostgreSQL extensions did not use the search_path safely in their installation script. This flaw allows an attacker with sufficient privileges to trick an administrator into executing a specially crafted script during the extension's installation or update. The highest threat from this vulnerability is to confidentiality, integrity, as well as system availability.", + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + f, err := os.Open(test.fixture) + require.NoError(t, err) + t.Cleanup(func() { + assert.NoError(t, f.Close()) + }) + + entries, err := unmarshal.OSVulnerabilityEntries(f) + assert.NoError(t, err) + assert.Len(t, entries, 1) + + entry := entries[0] + + dataEntries, err := Transform(entry) + assert.NoError(t, err) + + var vulns []grypeDB.Vulnerability + for _, entry := range dataEntries { + switch vuln := entry.Data.(type) { + case grypeDB.Vulnerability: + vulns = append(vulns, vuln) + case grypeDB.VulnerabilityMetadata: + assert.Equal(t, test.metadata, vuln) + default: + t.Fatalf("unexpected condition: data entry does not have a vulnerability or a metadata: %+v", vuln) + } + } + + if diff := cmp.Diff(test.vulns, vulns); diff != "" { + t.Errorf("vulnerabilities do not match (-want +got):\n%s", diff) + } + + }) + } + +} + +func TestParseVulnerabilitiesAllEntries(t *testing.T) { + tests := []struct { + name string + numEntries int + fixture string + vulns []grypeDB.Vulnerability + }{ + { + name: "Debian", + numEntries: 2, + fixture: "test-fixtures/debian-8-multiple-entries-for-same-package.json", + vulns: []grypeDB.Vulnerability{ + { + ID: "CVE-2011-4623", + PackageName: "rsyslog", + VersionConstraint: "< 5.7.4-1", + VersionFormat: "dpkg", + Namespace: "debian:distro:debian:8", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2011-4623", + Namespace: "nvd:cpe", + }, + }, + Fix: grypeDB.Fix{ + Versions: []string{"5.7.4-1"}, + State: grypeDB.FixedState, + }, + }, + { + ID: "CVE-2008-5618", + PackageName: "rsyslog", + VersionConstraint: "< 3.18.6-1", + VersionFormat: "dpkg", + Namespace: "debian:distro:debian:8", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2008-5618", + Namespace: "nvd:cpe", + }, + }, + Fix: grypeDB.Fix{ + Versions: []string{"3.18.6-1"}, + State: grypeDB.FixedState, + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + f, err := os.Open(test.fixture) + require.NoError(t, err) + t.Cleanup(func() { + assert.NoError(t, f.Close()) + }) + + entries, err := unmarshal.OSVulnerabilityEntries(f) + assert.NoError(t, err) + assert.Len(t, entries, len(test.vulns)) + + var vulns []grypeDB.Vulnerability + for _, entry := range entries { + dataEntries, err := Transform(entry) + assert.NoError(t, err) + + for _, entry := range dataEntries { + switch vuln := entry.Data.(type) { + case grypeDB.Vulnerability: + vulns = append(vulns, vuln) + case grypeDB.VulnerabilityMetadata: + default: + t.Fatalf("unexpected condition: data entry does not have a vulnerability or a metadata: %+v", vuln) + } + } + } + + if diff := cmp.Diff(test.vulns, vulns); diff != "" { + t.Errorf("vulnerabilities do not match (-want +got):\n%s", diff) + } + }) + } +} diff --git a/pkg/process/v4/transformers/vulnerability_metadata.go b/pkg/process/v4/transformers/vulnerability_metadata.go new file mode 100644 index 00000000..c40e9f6b --- /dev/null +++ b/pkg/process/v4/transformers/vulnerability_metadata.go @@ -0,0 +1,8 @@ +package transformers + +// VendorBaseMetrics captures extra metrics that do not fit into a common CVSS +// struct, like Status and BaseSeverity +type VendorBaseMetrics struct { + BaseSeverity string `json:"base_severity"` + Status string `json:"status"` +} diff --git a/pkg/process/v4/writer.go b/pkg/process/v4/writer.go new file mode 100644 index 00000000..8129bc83 --- /dev/null +++ b/pkg/process/v4/writer.go @@ -0,0 +1,132 @@ +package v4 + +import ( + "crypto/sha256" + "fmt" + "path" + "path/filepath" + "strings" + "time" + + "github.com/anchore/grype-db/internal/log" + + "github.com/anchore/grype-db/internal/file" + "github.com/anchore/grype-db/pkg/data" + "github.com/anchore/grype/grype/db" + grypeDB "github.com/anchore/grype/grype/db/v4" + grypeDBStore "github.com/anchore/grype/grype/db/v4/store" + "github.com/spf13/afero" +) + +// TODO: add NVDNamespace const to grype.db package? +const nvdNamespace = "nvd:cpe" + +var _ data.Writer = (*writer)(nil) + +type writer struct { + dbPath string + store grypeDB.Store +} + +func NewWriter(directory string, dataAge time.Time) (data.Writer, error) { + dbPath := path.Join(directory, grypeDB.VulnerabilityStoreFileName) + theStore, err := grypeDBStore.New(dbPath, true) + if err != nil { + return nil, fmt.Errorf("unable to create writer: %w", err) + } + + if err := theStore.SetID(grypeDB.NewID(dataAge)); err != nil { + return nil, fmt.Errorf("unable to set DB ID: %w", err) + } + + return &writer{ + dbPath: dbPath, + store: theStore, + }, nil +} + +func (w writer) Write(entries ...data.Entry) error { + for _, entry := range entries { + if entry.DBSchemaVersion != grypeDB.SchemaVersion { + return fmt.Errorf("wrong schema version: want %+v got %+v", grypeDB.SchemaVersion, entry.DBSchemaVersion) + } + + switch row := entry.Data.(type) { + case grypeDB.Vulnerability: + if err := w.store.AddVulnerability(row); err != nil { + return fmt.Errorf("unable to write vulnerability to store: %w", err) + } + case grypeDB.VulnerabilityMetadata: + normalizeSeverity(&row, w.store) + if err := w.store.AddVulnerabilityMetadata(row); err != nil { + return fmt.Errorf("unable to write vulnerability metadata to store: %w", err) + } + case grypeDB.VulnerabilityMatchExclusion: + if err := w.store.AddVulnerabilityMatchExclusion(row); err != nil { + return fmt.Errorf("unable to write vulnerability match exclusion to store: %w", err) + } + default: + return fmt.Errorf("data entry is not of type vulnerability, vulnerability metadata, or exclusion: %T", row) + } + } + + return nil +} + +func (w writer) metadata() (*db.Metadata, error) { + hashStr, err := file.ContentDigest(afero.NewOsFs(), w.dbPath, sha256.New()) + if err != nil { + return nil, fmt.Errorf("failed to hash database file (%s): %w", w.dbPath, err) + } + + storeID, err := w.store.GetID() + if err != nil { + return nil, fmt.Errorf("failed to fetch store ID: %w", err) + } + + metadata := db.Metadata{ + Built: storeID.BuildTimestamp, + Version: storeID.SchemaVersion, + Checksum: "sha256:" + hashStr, + } + return &metadata, nil +} + +func (w writer) Close() error { + w.store.Close() + metadata, err := w.metadata() + if err != nil { + return err + } + + metadataPath := path.Join(filepath.Dir(w.dbPath), db.MetadataFileName) + + return metadata.Write(metadataPath) +} + +func normalizeSeverity(metadata *grypeDB.VulnerabilityMetadata, reader grypeDB.VulnerabilityMetadataStoreReader) { + if metadata.Severity != "" && strings.ToLower(metadata.Severity) != "unknown" { + return + } + if !strings.HasPrefix(strings.ToLower(metadata.ID), "cve") { + return + } + if strings.HasPrefix(metadata.Namespace, nvdNamespace) { + return + } + m, err := reader.GetVulnerabilityMetadata(metadata.ID, nvdNamespace) + if err != nil { + log.WithFields("id", metadata.ID, "error", err).Warn("error fetching vulnerability metadata from NVD namespace") + return + } + if m == nil { + log.WithFields("id", metadata.ID).Debug("unable to find vulnerability metadata from NVD namespace") + return + } + + newSeverity := string(data.ParseSeverity(m.Severity)) + + log.WithFields("id", metadata.ID, "namespace", metadata.Namespace, "from", metadata.Severity, "to", newSeverity).Trace("overriding irrelevant severity with data from NVD record") + + metadata.Severity = newSeverity +} diff --git a/pkg/process/v4/writer_test.go b/pkg/process/v4/writer_test.go new file mode 100644 index 00000000..f0d58996 --- /dev/null +++ b/pkg/process/v4/writer_test.go @@ -0,0 +1,116 @@ +package v4 + +import ( + "errors" + "github.com/anchore/grype-db/pkg/data" + grypeDB "github.com/anchore/grype/grype/db/v4" + "github.com/stretchr/testify/assert" + "testing" +) + +var _ grypeDB.VulnerabilityMetadataStoreReader = (*mockReader)(nil) + +type mockReader struct { + metadata *grypeDB.VulnerabilityMetadata + err error +} + +func newMockReader(sev string) *mockReader { + return &mockReader{ + metadata: &grypeDB.VulnerabilityMetadata{ + Severity: sev, + Namespace: "nvd", + }, + } +} + +func newDeadMockReader() *mockReader { + return &mockReader{ + err: errors.New("dead"), + } +} + +func (m mockReader) GetVulnerabilityMetadata(_, _ string) (*grypeDB.VulnerabilityMetadata, error) { + return m.metadata, m.err +} + +func (m mockReader) GetAllVulnerabilityMetadata() (*[]grypeDB.VulnerabilityMetadata, error) { + panic("implement me") +} + +func Test_normalizeSeverity(t *testing.T) { + + tests := []struct { + name string + initialSeverity string + namespace string + cveID string + reader grypeDB.VulnerabilityMetadataStoreReader + expected data.Severity + }{ + { + name: "skip missing metadata", + initialSeverity: "", + namespace: "test", + reader: &mockReader{}, + expected: "", + }, + { + name: "skip non-cve records metadata", + cveID: "GHSA-1234-1234-1234", + initialSeverity: "", + namespace: "test", + reader: newDeadMockReader(), // should not be used + expected: "", + }, + { + name: "override empty severity", + initialSeverity: "", + namespace: "test", + reader: newMockReader("low"), + expected: data.SeverityLow, + }, + { + name: "override unknown severity", + initialSeverity: "unknown", + namespace: "test", + reader: newMockReader("low"), + expected: data.SeverityLow, + }, + { + name: "ignore record with severity already set", + initialSeverity: "Low", + namespace: "test", + reader: newMockReader("critical"), // should not be used + expected: data.SeverityLow, + }, + { + name: "ignore nvd records", + initialSeverity: "Low", + namespace: "nvd:cpe", + reader: newDeadMockReader(), // should not be used + expected: data.SeverityLow, + }, + { + name: "db errors should not fail or modify the record", + initialSeverity: "", + namespace: "test", + reader: newDeadMockReader(), + expected: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + record := &grypeDB.VulnerabilityMetadata{ + ID: "cve-2020-0000", + Severity: tt.initialSeverity, + Namespace: tt.namespace, + } + if tt.cveID != "" { + record.ID = tt.cveID + } + normalizeSeverity(record, tt.reader) + assert.Equal(t, string(tt.expected), record.Severity) + }) + } +} diff --git a/pkg/process/v5/processors.go b/pkg/process/v5/processors.go new file mode 100644 index 00000000..cdc5b982 --- /dev/null +++ b/pkg/process/v5/processors.go @@ -0,0 +1,21 @@ +package v5 + +import ( + "github.com/anchore/grype-db/pkg/data" + "github.com/anchore/grype-db/pkg/process/processors" + "github.com/anchore/grype-db/pkg/process/v5/transformers/github" + "github.com/anchore/grype-db/pkg/process/v5/transformers/matchexclusions" + "github.com/anchore/grype-db/pkg/process/v5/transformers/msrc" + "github.com/anchore/grype-db/pkg/process/v5/transformers/nvd" + "github.com/anchore/grype-db/pkg/process/v5/transformers/os" +) + +func Processors() []data.Processor { + return []data.Processor{ + processors.NewGitHubProcessor(github.Transform), + processors.NewMSRCProcessor(msrc.Transform), + processors.NewNVDProcessor(nvd.Transform), + processors.NewOSProcessor(os.Transform), + processors.NewMatchExclusionProcessor(matchexclusions.Transform), + } +} diff --git a/pkg/process/v5/transformers/entry.go b/pkg/process/v5/transformers/entry.go new file mode 100644 index 00000000..842509da --- /dev/null +++ b/pkg/process/v5/transformers/entry.go @@ -0,0 +1,22 @@ +package transformers + +import ( + "github.com/anchore/grype-db/pkg/data" + grypeDB "github.com/anchore/grype/grype/db/v5" +) + +func NewEntries(vs []grypeDB.Vulnerability, metadata grypeDB.VulnerabilityMetadata) []data.Entry { + entries := []data.Entry{ + { + DBSchemaVersion: grypeDB.SchemaVersion, + Data: metadata, + }, + } + for _, vuln := range vs { + entries = append(entries, data.Entry{ + DBSchemaVersion: grypeDB.SchemaVersion, + Data: vuln, + }) + } + return entries +} diff --git a/pkg/process/v5/transformers/github/test-fixtures/github-github-npm-0.json b/pkg/process/v5/transformers/github/test-fixtures/github-github-npm-0.json new file mode 100644 index 00000000..b0a7d1e9 --- /dev/null +++ b/pkg/process/v5/transformers/github/test-fixtures/github-github-npm-0.json @@ -0,0 +1,31 @@ + +{ + "Advisory": { + "CVE": [ + "CVE-2020-14000" + ], + "FixedIn": [ + { + "ecosystem": "npm", + "identifier": "0.2.0-prerelease.20200714185213", + "name": "scratch-vm", + "namespace": "github:npm", + "range": "<= 0.2.0-prerelease.20200709173451" + } + ], + "Metadata": { + "CVE": [ + "CVE-2020-14000" + ] + }, + "Severity": "High", + "Summary": "Remote Code Execution in scratch-vm", + "ghsaId": "GHSA-vc9j-fhvv-8vrf", + "namespace": "github:npm", + "url": "https://github.com/advisories/GHSA-vc9j-fhvv-8vrf", + "withdrawn": null + }, + "Vulnerability": {} +} + + diff --git a/pkg/process/v5/transformers/github/test-fixtures/github-github-python-0.json b/pkg/process/v5/transformers/github/test-fixtures/github-github-python-0.json new file mode 100644 index 00000000..ad14aa60 --- /dev/null +++ b/pkg/process/v5/transformers/github/test-fixtures/github-github-python-0.json @@ -0,0 +1,58 @@ +[ + { + "Advisory": { + "CVE": [ + "CVE-2018-8768" + ], + "FixedIn": [ + { + "ecosystem": "python", + "identifier": "5.4.1", + "name": "notebook", + "namespace": "github:python", + "range": "< 5.4.1" + } + ], + "Metadata": { + "CVE": [ + "CVE-2018-8768" + ] + }, + "Severity": "Low", + "Summary": "Low severity vulnerability that affects notebook", + "ghsaId": "GHSA-6cwv-x26c-w2q4", + "namespace": "github:python", + "url": "https://github.com/advisories/GHSA-6cwv-x26c-w2q4", + "withdrawn": null + }, + "Vulnerability": {} + }, + { + "Advisory": { + "CVE": [ + "CVE-2017-5524" + ], + "FixedIn": [ + { + "ecosystem": "python", + "identifier": "4.3.12", + "name": "Plone", + "namespace": "github:python", + "range": ">= 4.0 < 4.3.12" + } + ], + "Metadata": { + "CVE": [ + "CVE-2017-5524" + ] + }, + "Severity": "Medium", + "Summary": "Moderate severity vulnerability that affects Plone", + "ghsaId": "GHSA-p5wr-vp8g-q5p4", + "namespace": "github:python", + "url": "https://github.com/advisories/GHSA-p5wr-vp8g-q5p4", + "withdrawn": null + }, + "Vulnerability": {} + } +] \ No newline at end of file diff --git a/pkg/process/v5/transformers/github/test-fixtures/github-github-python-1.json b/pkg/process/v5/transformers/github/test-fixtures/github-github-python-1.json new file mode 100644 index 00000000..bfa84922 --- /dev/null +++ b/pkg/process/v5/transformers/github/test-fixtures/github-github-python-1.json @@ -0,0 +1,43 @@ + +{ + "Advisory": { + "CVE": [ + "CVE-2017-5524" + ], + "FixedIn": [ + { + "ecosystem": "python", + "identifier": "4.3.12", + "name": "Plone", + "namespace": "github:python", + "range": ">= 4.0 < 4.3.12" + }, + { + "ecosystem": "python", + "identifier": "5.1b1", + "name": "Plone", + "namespace": "github:python", + "range": ">= 5.1a1 < 5.1b1" + }, + { + "ecosystem": "python", + "identifier": "5.0.7", + "name": "Plone", + "namespace": "github:python", + "range": ">= 5.0rc1 < 5.0.7" + } + ], + "Metadata": { + "CVE": [ + "CVE-2017-5524" + ] + }, + "Severity": "Medium", + "Summary": "Moderate severity vulnerability that affects Plone", + "ghsaId": "GHSA-p5wr-vp8g-q5p4", + "namespace": "github:python", + "url": "https://github.com/advisories/GHSA-p5wr-vp8g-q5p4", + "withdrawn": null + }, + "Vulnerability": {} +} diff --git a/pkg/process/v5/transformers/github/test-fixtures/github-withdrawn.json b/pkg/process/v5/transformers/github/test-fixtures/github-withdrawn.json new file mode 100644 index 00000000..04995e38 --- /dev/null +++ b/pkg/process/v5/transformers/github/test-fixtures/github-withdrawn.json @@ -0,0 +1,29 @@ + +{ + "Advisory": { + "CVE": [ + "CVE-2018-8768" + ], + "FixedIn": [ + { + "ecosystem": "python", + "identifier": "5.4.1", + "name": "notebook", + "namespace": "github:python", + "range": "< 5.4.1" + } + ], + "Metadata": { + "CVE": [ + "CVE-2018-8768" + ] + }, + "Severity": "Low", + "Summary": "Low severity vulnerability that affects notebook", + "ghsaId": "GHSA-6cwv-x26c-w2q4", + "namespace": "github:python", + "url": "https://github.com/advisories/GHSA-6cwv-x26c-w2q4", + "withdrawn": "2022-01-31T14:32:09Z" + }, + "Vulnerability": {} +} diff --git a/pkg/process/v5/transformers/github/test-fixtures/multiple-fixed-in-names.json b/pkg/process/v5/transformers/github/test-fixtures/multiple-fixed-in-names.json new file mode 100644 index 00000000..ac1ef982 --- /dev/null +++ b/pkg/process/v5/transformers/github/test-fixtures/multiple-fixed-in-names.json @@ -0,0 +1,43 @@ + +{ + "Advisory": { + "CVE": [ + "CVE-2017-5524" + ], + "FixedIn": [ + { + "ecosystem": "python", + "identifier": "4.3.12", + "name": "Plone", + "namespace": "github:python", + "range": ">= 4.0 < 4.3.12" + }, + { + "ecosystem": "python", + "identifier": "5.1b1", + "name": "Plone", + "namespace": "github:python", + "range": ">= 5.1a1 < 5.1b1" + }, + { + "ecosystem": "python", + "identifier": "5.0.7", + "name": "Plone-debug", + "namespace": "github:python", + "range": ">= 5.0rc1 < 5.0.7" + } + ], + "Metadata": { + "CVE": [ + "CVE-2017-5524" + ] + }, + "Severity": "Medium", + "Summary": "Moderate severity vulnerability that affects Plone", + "ghsaId": "GHSA-p5wr-vp8g-q5p4", + "namespace": "github:python", + "url": "https://github.com/advisories/GHSA-p5wr-vp8g-q5p4", + "withdrawn": null + }, + "Vulnerability": {} +} diff --git a/pkg/process/v5/transformers/github/transform.go b/pkg/process/v5/transformers/github/transform.go new file mode 100644 index 00000000..6f09ce87 --- /dev/null +++ b/pkg/process/v5/transformers/github/transform.go @@ -0,0 +1,132 @@ +package github + +import ( + "fmt" + "strings" + + "github.com/anchore/grype/grype/db/v5/namespace" + + "github.com/anchore/grype-db/pkg/data" + "github.com/anchore/grype-db/pkg/process/common" + "github.com/anchore/grype-db/pkg/process/v5/transformers" + "github.com/anchore/grype-db/pkg/provider/unmarshal" + grypeDB "github.com/anchore/grype/grype/db/v5" + syftPkg "github.com/anchore/syft/syft/pkg" +) + +func buildGrypeNamespace(group string) (namespace.Namespace, error) { + feedGroupComponents := strings.Split(group, ":") + + if len(feedGroupComponents) < 2 { + return nil, fmt.Errorf("unable to determine grype namespace for enterprise namespace=%s", group) + } + + feedGroupLang := feedGroupComponents[1] + syftLanguage := syftPkg.LanguageByName(feedGroupLang) + + if syftLanguage == syftPkg.UnknownLanguage { + // For now map nuget to dotnet as the language. + if feedGroupLang == "nuget" { + syftLanguage = syftPkg.Dotnet + } else { + return nil, fmt.Errorf("unable to determine grype namespace for enterprise namespace=%s", group) + } + } + + ns, err := namespace.FromString(fmt.Sprintf("github:language:%s", string(syftLanguage))) + + if err != nil { + return nil, err + } + + return ns, nil +} + +func Transform(vulnerability unmarshal.GitHubAdvisory) ([]data.Entry, error) { + var allVulns []grypeDB.Vulnerability + + // Exclude entries marked as withdrawn + if vulnerability.Advisory.Withdrawn != nil { + return nil, nil + } + + // TODO: stop capturing record source in the vulnerability metadata record (now that feed groups are not real) + recordSource := fmt.Sprintf("github:%s", vulnerability.Advisory.Namespace) + + grypeNamespace, err := buildGrypeNamespace(vulnerability.Advisory.Namespace) + if err != nil { + return nil, err + } + + entryNamespace := grypeNamespace.String() + + // there may be multiple packages indicated within the FixedIn field, we should make + // separate vulnerability entries (one for each name|namespaces combo) while merging + // constraint ranges as they are found. + for idx, fixedInEntry := range vulnerability.Advisory.FixedIn { + constraint := common.EnforceSemVerConstraint(fixedInEntry.Range) + + var versionFormat string + switch entryNamespace { + case "github:language:python": + versionFormat = "python" + default: + versionFormat = "unknown" + } + + // create vulnerability entry + allVulns = append(allVulns, grypeDB.Vulnerability{ + ID: vulnerability.Advisory.GhsaID, + VersionConstraint: constraint, + VersionFormat: versionFormat, + RelatedVulnerabilities: getRelatedVulnerabilities(vulnerability), + PackageName: grypeNamespace.Resolver().Normalize(fixedInEntry.Name), + Namespace: entryNamespace, + Fix: getFix(vulnerability, idx), + }) + } + + // create vulnerability metadata entry (a single entry keyed off of the vulnerability ID) + metadata := grypeDB.VulnerabilityMetadata{ + ID: vulnerability.Advisory.GhsaID, + DataSource: vulnerability.Advisory.URL, + Namespace: entryNamespace, + RecordSource: recordSource, + Severity: vulnerability.Advisory.Severity, + URLs: []string{vulnerability.Advisory.URL}, + Description: vulnerability.Advisory.Summary, + } + + return transformers.NewEntries(allVulns, metadata), nil +} + +func getFix(entry unmarshal.GitHubAdvisory, idx int) grypeDB.Fix { + fixedInEntry := entry.Advisory.FixedIn[idx] + + var fixedInVersions []string + fixedInVersion := common.CleanFixedInVersion(fixedInEntry.Identifier) + if fixedInVersion != "" { + fixedInVersions = append(fixedInVersions, fixedInVersion) + } + + fixState := grypeDB.NotFixedState + if len(fixedInVersions) > 0 { + fixState = grypeDB.FixedState + } + + return grypeDB.Fix{ + Versions: fixedInVersions, + State: fixState, + } +} + +func getRelatedVulnerabilities(entry unmarshal.GitHubAdvisory) []grypeDB.VulnerabilityReference { + vulns := make([]grypeDB.VulnerabilityReference, len(entry.Advisory.CVE)) + for idx, cve := range entry.Advisory.CVE { + vulns[idx] = grypeDB.VulnerabilityReference{ + ID: cve, + Namespace: "nvd:cpe", + } + } + return vulns +} diff --git a/pkg/process/v5/transformers/github/transform_test.go b/pkg/process/v5/transformers/github/transform_test.go new file mode 100644 index 00000000..86a64445 --- /dev/null +++ b/pkg/process/v5/transformers/github/transform_test.go @@ -0,0 +1,245 @@ +package github + +import ( + "github.com/anchore/grype/grype/db/v5/namespace" + "github.com/anchore/grype/grype/db/v5/namespace/language" + syftPkg "github.com/anchore/syft/syft/pkg" + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/require" + "os" + "testing" + + testUtils "github.com/anchore/grype-db/pkg/process/tests" + "github.com/anchore/grype-db/pkg/provider/unmarshal" + grypeDB "github.com/anchore/grype/grype/db/v5" + "github.com/stretchr/testify/assert" +) + +func TestBuildGrypeNamespace(t *testing.T) { + tests := []struct { + group string + namespace namespace.Namespace + }{ + { + group: "github:python", + namespace: language.NewNamespace("github", syftPkg.Python, ""), + }, + { + group: "github:composer", + namespace: language.NewNamespace("github", syftPkg.PHP, ""), + }, + { + group: "github:gem", + namespace: language.NewNamespace("github", syftPkg.Ruby, ""), + }, + { + group: "github:npm", + namespace: language.NewNamespace("github", syftPkg.JavaScript, ""), + }, + { + group: "github:go", + namespace: language.NewNamespace("github", syftPkg.Go, ""), + }, + { + group: "github:nuget", + namespace: language.NewNamespace("github", syftPkg.Dotnet, ""), + }, + { + group: "github:rust", + namespace: language.NewNamespace("github", syftPkg.Rust, ""), + }, + } + + for _, test := range tests { + ns, err := buildGrypeNamespace(test.group) + + assert.NoError(t, err) + assert.Equal(t, test.namespace, ns) + } +} + +func TestUnmarshalGitHubEntries(t *testing.T) { + f, err := os.Open("test-fixtures/github-github-python-0.json") + require.NoError(t, err) + defer testUtils.CloseFile(f) + + entries, err := unmarshal.GitHubAdvisoryEntries(f) + require.NoError(t, err) + + assert.Len(t, entries, 2) + +} + +func TestParseGitHubEntry(t *testing.T) { + expectedVulns := []grypeDB.Vulnerability{ + { + ID: "GHSA-p5wr-vp8g-q5p4", + VersionConstraint: ">=4.0,<4.3.12", + VersionFormat: "python", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2017-5524", + Namespace: "nvd:cpe", + }, + }, + PackageName: "plone", + Namespace: "github:language:python", + Fix: grypeDB.Fix{ + State: grypeDB.FixedState, + Versions: []string{"4.3.12"}, + }, + }, + { + ID: "GHSA-p5wr-vp8g-q5p4", + VersionConstraint: ">=5.1a1,<5.1b1", + VersionFormat: "python", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2017-5524", + Namespace: "nvd:cpe", + }, + }, + PackageName: "plone", + Namespace: "github:language:python", + Fix: grypeDB.Fix{ + Versions: []string{"5.1b1"}, + State: grypeDB.FixedState, + }, + }, + { + ID: "GHSA-p5wr-vp8g-q5p4", + VersionConstraint: ">=5.0rc1,<5.0.7", + VersionFormat: "python", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2017-5524", + Namespace: "nvd:cpe", + }, + }, + PackageName: "plone", + Namespace: "github:language:python", + Fix: grypeDB.Fix{ + Versions: []string{"5.0.7"}, + State: grypeDB.FixedState, + }, + }, + } + + expectedMetadata := grypeDB.VulnerabilityMetadata{ + ID: "GHSA-p5wr-vp8g-q5p4", + Namespace: "github:language:python", + RecordSource: "github:github:python", + DataSource: "https://github.com/advisories/GHSA-p5wr-vp8g-q5p4", + Severity: "Medium", + URLs: []string{"https://github.com/advisories/GHSA-p5wr-vp8g-q5p4"}, + Description: "Moderate severity vulnerability that affects Plone", + } + + f, err := os.Open("test-fixtures/github-github-python-1.json") + require.NoError(t, err) + defer testUtils.CloseFile(f) + + entries, err := unmarshal.GitHubAdvisoryEntries(f) + require.NoError(t, err) + + require.Len(t, entries, 1) + + entry := entries[0] + + dataEntries, err := Transform(entry) + require.NoError(t, err) + + var vulns []grypeDB.Vulnerability + for _, entry := range dataEntries { + switch vuln := entry.Data.(type) { + case grypeDB.Vulnerability: + vulns = append(vulns, vuln) + case grypeDB.VulnerabilityMetadata: + assert.Equal(t, expectedMetadata, vuln) + default: + t.Fatalf("unexpected condition: data entry does not have a vulnerability or a metadata") + } + } + + // check vulnerability + assert.Len(t, vulns, len(expectedVulns)) + + if diff := cmp.Diff(expectedVulns, vulns); diff != "" { + t.Errorf("vulnerabilities do not match (-want +got):\n%s", diff) + } +} + +func TestDefaultVersionFormatNpmGitHubEntry(t *testing.T) { + expectedVuln := grypeDB.Vulnerability{ + ID: "GHSA-vc9j-fhvv-8vrf", + VersionConstraint: "<=0.2.0-prerelease.20200709173451", + VersionFormat: "unknown", // TODO: this should reference a format, yes? (not a string) + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2020-14000", + Namespace: "nvd:cpe", + }, + }, + PackageName: "scratch-vm", + Namespace: "github:language:javascript", + Fix: grypeDB.Fix{ + Versions: []string{"0.2.0-prerelease.20200714185213"}, + State: grypeDB.FixedState, + }, + } + + expectedMetadata := grypeDB.VulnerabilityMetadata{ + ID: "GHSA-vc9j-fhvv-8vrf", + Namespace: "github:language:javascript", + RecordSource: "github:github:npm", + DataSource: "https://github.com/advisories/GHSA-vc9j-fhvv-8vrf", + Severity: "High", + URLs: []string{"https://github.com/advisories/GHSA-vc9j-fhvv-8vrf"}, + Description: "Remote Code Execution in scratch-vm", + } + + f, err := os.Open("test-fixtures/github-github-npm-0.json") + require.NoError(t, err) + defer testUtils.CloseFile(f) + + entries, err := unmarshal.GitHubAdvisoryEntries(f) + require.NoError(t, err) + + require.Len(t, entries, 1) + + entry := entries[0] + + dataEntries, err := Transform(entry) + assert.NoError(t, err) + + for _, entry := range dataEntries { + switch vuln := entry.Data.(type) { + case grypeDB.Vulnerability: + assert.Equal(t, expectedVuln, vuln) + case grypeDB.VulnerabilityMetadata: + assert.Equal(t, expectedMetadata, vuln) + default: + t.Fatalf("unexpected condition: data entry does not have a vulnerability or a metadata") + } + } + + // check vulnerability + assert.Len(t, dataEntries, 2) +} + +func TestFilterWithdrawnEntries(t *testing.T) { + f, err := os.Open("test-fixtures/github-withdrawn.json") + require.NoError(t, err) + defer testUtils.CloseFile(f) + + entries, err := unmarshal.GitHubAdvisoryEntries(f) + require.NoError(t, err) + + require.Len(t, entries, 1) + + entry := entries[0] + + dataEntries, err := Transform(entry) + assert.NoError(t, err) + assert.Nil(t, dataEntries) +} diff --git a/pkg/process/v5/transformers/matchexclusions/transform.go b/pkg/process/v5/transformers/matchexclusions/transform.go new file mode 100644 index 00000000..6597bf7a --- /dev/null +++ b/pkg/process/v5/transformers/matchexclusions/transform.go @@ -0,0 +1,43 @@ +package matchexclusions + +import ( + "github.com/anchore/grype-db/pkg/data" + "github.com/anchore/grype-db/pkg/provider/unmarshal" + + grypeDB "github.com/anchore/grype/grype/db/v5" +) + +func Transform(matchExclusion unmarshal.MatchExclusion) ([]data.Entry, error) { + exclusion := grypeDB.VulnerabilityMatchExclusion{ + ID: matchExclusion.ID, + Constraints: nil, + Justification: matchExclusion.Justification, + } + + for _, c := range matchExclusion.Constraints { + constraint := &grypeDB.VulnerabilityMatchExclusionConstraint{ + Vulnerability: grypeDB.VulnerabilityExclusionConstraint{ + Namespace: c.Vulnerability.Namespace, + FixState: grypeDB.FixState(c.Vulnerability.FixState), + }, + Package: grypeDB.PackageExclusionConstraint{ + Name: c.Package.Name, + Language: c.Package.Language, + Type: c.Package.Type, + Version: c.Package.Version, + Location: c.Package.Location, + }, + } + + exclusion.Constraints = append(exclusion.Constraints, *constraint) + } + + entries := []data.Entry{ + { + DBSchemaVersion: grypeDB.SchemaVersion, + Data: exclusion, + }, + } + + return entries, nil +} diff --git a/pkg/process/v5/transformers/msrc/test-fixtures/microsoft-msrc-0.json b/pkg/process/v5/transformers/msrc/test-fixtures/microsoft-msrc-0.json new file mode 100644 index 00000000..474b23b2 --- /dev/null +++ b/pkg/process/v5/transformers/msrc/test-fixtures/microsoft-msrc-0.json @@ -0,0 +1,194 @@ +[ + { + "cvss": { + "base_score": 7.8, + "temporal_score": 7, + "vector": "CVSS:3.0/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H/E:P/RL:O/RC:C" + }, + "fixed_in": [ + { + "id": "4493470", + "is_first": true, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4493470", + "https://support.microsoft.com/help/4493470" + ] + }, + { + "id": "4494440", + "is_first": false, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4494440", + "https://support.microsoft.com/help/4494440" + ] + }, + { + "id": "4503267", + "is_first": false, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4503267", + "https://support.microsoft.com/en-us/help/4503267" + ] + }, + { + "id": "4507460", + "is_first": false, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4507460", + "https://support.microsoft.com/help/4507460" + ] + }, + { + "id": "4512517", + "is_first": false, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4512517", + "https://support.microsoft.com/help/4512517" + ] + }, + { + "id": "4516044", + "is_first": false, + "is_latest": true, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4516044", + "https://support.microsoft.com/help/4516044" + ] + } + ], + "id": "CVE-2019-0671", + "link": "https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2019-0671", + "product": { + "family": "Windows", + "id": "10852", + "name": "Windows 10 Version 1607 for 32-bit Systems" + }, + "severity": "High", + "summary": "Microsoft Office Access Connectivity Engine Remote Code Execution Vulnerability", + "vulnerable": [ + "4480961", + "4483229", + "4487026", + "4489882" + ] + }, +{ + "cvss": { + "base_score": 4.4, + "temporal_score": 4, + "vector": "CVSS:3.0/AV:L/AC:H/PR:L/UI:R/S:U/C:N/I:N/A:H/E:P/RL:O/RC:C" + }, + "fixed_in": [ + { + "id": "4093119", + "is_first": true, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4093119" + ] + }, + { + "id": "4103723", + "is_first": false, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4103723" + ] + }, + { + "id": "4284880", + "is_first": false, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4284880" + ] + }, + { + "id": "4338814", + "is_first": false, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4338814" + ] + }, + { + "id": "4343887", + "is_first": false, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4343887" + ] + }, + { + "id": "4345418", + "is_first": false, + "is_latest": true, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4345418" + ] + }, + { + "id": "4457131", + "is_first": false, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4457131" + ] + }, + { + "id": "4462917", + "is_first": false, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4462917" + ] + }, + { + "id": "4467691", + "is_first": false, + "is_latest": false, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4467691" + ] + }, + { + "id": "4471321", + "is_first": false, + "is_latest": true, + "links": [ + "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB4471321" + ] + } + ], + "id": "CVE-2018-8116", + "link": "https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2018-8116", + "product": { + "family": "Windows", + "id": "10852", + "name": "Windows 10 Version 1607 for 32-bit Systems" + }, + "severity": "Medium", + "summary": "Microsoft Graphics Component Denial of Service Vulnerability", + "vulnerable": [ + "3213986", + "4013429", + "4015217", + "4019472", + "4022715", + "4025339", + "4034658", + "4038782", + "4041691", + "4048953", + "4053579", + "4056890", + "4074590", + "4088787" + ] + } +] diff --git a/pkg/process/v5/transformers/msrc/transform.go b/pkg/process/v5/transformers/msrc/transform.go new file mode 100644 index 00000000..a35e17e1 --- /dev/null +++ b/pkg/process/v5/transformers/msrc/transform.go @@ -0,0 +1,90 @@ +package msrc + +import ( + "fmt" + + "github.com/anchore/grype-db/pkg/data" + "github.com/anchore/grype-db/pkg/process/common" + "github.com/anchore/grype-db/pkg/process/v5/transformers" + "github.com/anchore/grype-db/pkg/provider/unmarshal" + grypeDB "github.com/anchore/grype/grype/db/v5" + "github.com/anchore/grype/grype/db/v5/namespace" + "github.com/anchore/grype/grype/distro" +) + +// Transform gets called by the parser, which consumes entries from the JSON files previously pulled. Each VulnDBVulnerability represents +// a single unmarshalled entry from the feed service +func Transform(vulnerability unmarshal.MSRCVulnerability) ([]data.Entry, error) { + // TODO: stop capturing record source in the vulnerability metadata record (now that feed groups are not real) + recordSource := fmt.Sprintf("microsoft:msrc:%s", vulnerability.Product.ID) + + grypeNamespace, err := namespace.FromString(fmt.Sprintf("msrc:distro:%s:%s", distro.Windows, vulnerability.Product.ID)) + if err != nil { + return nil, err + } + + entryNamespace := grypeNamespace.String() + + // In anchore-enterprise windows analyzer, "base" represents unpatched windows images (images with no KBs). + // If a vulnerability exists for a Microsoft Product ID and the image has no KBs (which are patches), + // then the image must be vulnerable to the image. + //nolint:gocritic + versionConstraint := append(vulnerability.Vulnerable, "base") + + allVulns := []grypeDB.Vulnerability{ + { + ID: vulnerability.ID, + VersionConstraint: common.OrConstraints(versionConstraint...), + VersionFormat: "kb", + PackageName: grypeNamespace.Resolver().Normalize(vulnerability.Product.ID), + Namespace: entryNamespace, + Fix: getFix(vulnerability), + }, + } + + // create vulnerability metadata entry (a single entry keyed off of the vulnerability ID) + metadata := grypeDB.VulnerabilityMetadata{ + ID: vulnerability.ID, + DataSource: vulnerability.Link, + Namespace: entryNamespace, + RecordSource: recordSource, + Severity: vulnerability.Severity, + URLs: []string{vulnerability.Link}, + // There is no description for vulnerabilities from the feed service + // summary gives something like "windows information disclosure vulnerability" + //Description: vulnerability.Summary, + Cvss: []grypeDB.Cvss{ + { + Metrics: grypeDB.CvssMetrics{BaseScore: vulnerability.Cvss.BaseScore}, + Vector: vulnerability.Cvss.Vector, + }, + }, + } + + return transformers.NewEntries(allVulns, metadata), nil +} + +func getFix(entry unmarshal.MSRCVulnerability) grypeDB.Fix { + fixedInVersion := fixedInKB(entry) + fixState := grypeDB.FixedState + + if fixedInVersion == "" { + fixState = grypeDB.NotFixedState + } + + return grypeDB.Fix{ + Versions: []string{fixedInVersion}, + State: fixState, + } +} + +// fixedInKB finds the "latest" patch (KB id) amongst the available microsoft patches and returns it +// if the "latest" patch cannot be found, an error is returned +func fixedInKB(vulnerability unmarshal.MSRCVulnerability) string { + for _, fixedIn := range vulnerability.FixedIn { + if fixedIn.IsLatest { + return fixedIn.ID + } + } + return "" +} diff --git a/pkg/process/v5/transformers/msrc/transform_test.go b/pkg/process/v5/transformers/msrc/transform_test.go new file mode 100644 index 00000000..674e6023 --- /dev/null +++ b/pkg/process/v5/transformers/msrc/transform_test.go @@ -0,0 +1,119 @@ +package msrc + +import ( + "github.com/stretchr/testify/require" + "os" + "testing" + + testUtils "github.com/anchore/grype-db/pkg/process/tests" + "github.com/anchore/grype-db/pkg/provider/unmarshal" + grypeDB "github.com/anchore/grype/grype/db/v5" + "github.com/stretchr/testify/assert" +) + +func TestUnmarshalMsrcVulnerabilities(t *testing.T) { + f, err := os.Open("test-fixtures/microsoft-msrc-0.json") + require.NoError(t, err) + defer testUtils.CloseFile(f) + + entries, err := unmarshal.MSRCVulnerabilityEntries(f) + require.NoError(t, err) + + assert.Equal(t, len(entries), 2) +} + +func TestParseMSRCEntry(t *testing.T) { + expectedVulns := []struct { + vulnerability grypeDB.Vulnerability + metadata grypeDB.VulnerabilityMetadata + }{ + { + vulnerability: grypeDB.Vulnerability{ + ID: "CVE-2019-0671", + VersionConstraint: `4480961 || 4483229 || 4487026 || 4489882 || base`, + VersionFormat: "kb", + PackageName: "10852", + Namespace: "msrc:distro:windows:10852", + Fix: grypeDB.Fix{ + Versions: []string{"4516044"}, + State: grypeDB.FixedState, + }, + }, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "CVE-2019-0671", + Severity: "High", + DataSource: "https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2019-0671", + URLs: []string{"https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2019-0671"}, + Description: "", + RecordSource: "microsoft:msrc:10852", + Namespace: "msrc:distro:windows:10852", + Cvss: []grypeDB.Cvss{ + { + Metrics: grypeDB.CvssMetrics{ + BaseScore: 7.8, + ImpactScore: nil, + }, + Vector: "CVSS:3.0/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H/E:P/RL:O/RC:C", + }, + }, + }, + }, + { + vulnerability: grypeDB.Vulnerability{ + ID: "CVE-2018-8116", + VersionConstraint: `3213986 || 4013429 || 4015217 || 4019472 || 4022715 || 4025339 || 4034658 || 4038782 || 4041691 || 4048953 || 4053579 || 4056890 || 4074590 || 4088787 || base`, + VersionFormat: "kb", + PackageName: "10852", + Namespace: "msrc:distro:windows:10852", + Fix: grypeDB.Fix{ + Versions: []string{"4345418"}, + State: grypeDB.FixedState, + }, + }, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "CVE-2018-8116", + Namespace: "msrc:distro:windows:10852", + DataSource: "https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2018-8116", + RecordSource: "microsoft:msrc:10852", + Severity: "Medium", + URLs: []string{"https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2018-8116"}, + Description: "", + Cvss: []grypeDB.Cvss{ + { + Metrics: grypeDB.CvssMetrics{ + BaseScore: 4.4, + ImpactScore: nil, + }, + Vector: "CVSS:3.0/AV:L/AC:H/PR:L/UI:R/S:U/C:N/I:N/A:H/E:P/RL:O/RC:C", + }, + }, + }, + }, + } + + f, err := os.Open("test-fixtures/microsoft-msrc-0.json") + require.NoError(t, err) + defer testUtils.CloseFile(f) + + entries, err := unmarshal.MSRCVulnerabilityEntries(f) + require.NoError(t, err) + + assert.Equal(t, len(entries), 2) + + for idx, entry := range entries { + dataEntries, err := Transform(entry) + assert.NoError(t, err) + assert.Len(t, dataEntries, 2) + expected := expectedVulns[idx] + for _, entry := range dataEntries { + switch vuln := entry.Data.(type) { + case grypeDB.Vulnerability: + assert.Equal(t, expected.vulnerability, vuln) + case grypeDB.VulnerabilityMetadata: + assert.Equal(t, expected.metadata, vuln) + default: + t.Fatalf("unexpected condition: data entry does not have a vulnerability or a metadata") + } + } + } +} diff --git a/pkg/process/v5/transformers/nvd/test-fixtures/compound-pkg.json b/pkg/process/v5/transformers/nvd/test-fixtures/compound-pkg.json new file mode 100644 index 00000000..8e658dcd --- /dev/null +++ b/pkg/process/v5/transformers/nvd/test-fixtures/compound-pkg.json @@ -0,0 +1,115 @@ +{ + "cve": { + "id": "CVE-2018-10189", + "sourceIdentifier": "cve@mitre.org", + "published": "2018-04-17T20:29:00.410", + "lastModified": "2018-05-23T14:41:49.073", + "vulnStatus": "Analyzed", + "descriptions": [ + { + "lang": "en", + "value": "An issue was discovered in Mautic 1.x and 2.x before 2.13.0. It is possible to systematically emulate tracking cookies per contact due to tracking the contact by their auto-incremented ID. Thus, a third party can manipulate the cookie value with +1 to systematically assume being tracked as each contact in Mautic. It is then possible to retrieve information about the contact through forms that have progressive profiling enabled." + }, + { + "lang": "es", + "value": "Se ha descubierto un problema en Mautic, en versiones 1.x y 2.x anteriores a la 2.13.0. Es posible emular de forma sistemática el rastreo de cookies por contacto debido al rastreo de contacto por su ID autoincrementada. Por lo tanto, un tercero puede manipular el valor de la cookie con un +1 para asumir sistemáticamente que se está rastreando como cada contacto en Mautic. Así, sería posible recuperar información sobre el contacto a través de formularios que tengan habilitada la generación de perfiles progresiva." + } + ], + "metrics": { + "cvssMetricV30": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "cvssData": { + "version": "3.0", + "vectorString": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N", + "attackVector": "NETWORK", + "attackComplexity": "LOW", + "privilegesRequired": "NONE", + "userInteraction": "NONE", + "scope": "UNCHANGED", + "confidentialityImpact": "HIGH", + "integrityImpact": "NONE", + "availabilityImpact": "NONE", + "baseScore": 7.5, + "baseSeverity": "HIGH" + }, + "exploitabilityScore": 3.9, + "impactScore": 3.6 + } + ], + "cvssMetricV2": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "cvssData": { + "version": "2.0", + "vectorString": "AV:N/AC:L/Au:N/C:P/I:N/A:N", + "accessVector": "NETWORK", + "accessComplexity": "LOW", + "authentication": "NONE", + "confidentialityImpact": "PARTIAL", + "integrityImpact": "NONE", + "availabilityImpact": "NONE", + "baseScore": 5.0 + }, + "baseSeverity": "MEDIUM", + "exploitabilityScore": 10.0, + "impactScore": 2.9, + "acInsufInfo": false, + "obtainAllPrivilege": false, + "obtainUserPrivilege": false, + "obtainOtherPrivilege": false, + "userInteractionRequired": false + } + ] + }, + "weaknesses": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "description": [ + { + "lang": "en", + "value": "CWE-200" + } + ] + } + ], + "configurations": [ + { + "nodes": [ + { + "operator": "OR", + "negate": false, + "cpeMatch": [ + { + "vulnerable": true, + "criteria": "cpe:2.3:a:mautic:mautic:*:*:*:*:*:*:*:*", + "versionStartIncluding": "1.0.0", + "versionEndIncluding": "1.4.1", + "matchCriteriaId": "5779710D-099E-40EE-8DF3-55BD3179A50C" + }, + { + "vulnerable": true, + "criteria": "cpe:2.3:a:mautic:mautic:*:*:*:*:*:*:*:*", + "versionStartIncluding": "2.0.0", + "versionEndExcluding": "2.13.0", + "matchCriteriaId": "4EFAEE48-4AEF-4F8C-95E0-6E8D848D900F" + } + ] + } + ] + } + ], + "references": [ + { + "url": "https://github.com/mautic/mautic/releases/tag/2.13.0", + "source": "cve@mitre.org", + "tags": [ + "Third Party Advisory" + ] + } + ] + } +} diff --git a/pkg/process/v5/transformers/nvd/test-fixtures/invalid_cpe.json b/pkg/process/v5/transformers/nvd/test-fixtures/invalid_cpe.json new file mode 100644 index 00000000..eac2ebd4 --- /dev/null +++ b/pkg/process/v5/transformers/nvd/test-fixtures/invalid_cpe.json @@ -0,0 +1,111 @@ +{ + "cve": { + "id": "CVE-2015-8978", + "sourceIdentifier": "cve@mitre.org", + "published": "2016-11-22T17:59:00.180", + "lastModified": "2016-11-28T19:50:59.600", + "vulnStatus": "Modified", + "descriptions": [ + { + "lang": "en", + "value": "In Soap Lite (aka the SOAP::Lite extension for Perl) 1.14 and earlier, an example attack consists of defining 10 or more XML entities, each defined as consisting of 10 of the previous entity, with the document consisting of a single instance of the largest entity, which expands to one billion copies of the first entity. The amount of computer memory used for handling an external SOAP call would likely exceed that available to the process parsing the XML." + }, + { + "lang": "es", + "value": "En Soap Lite (también conocido como la extensión SOAP::Lite para Perl) 1.14 y versiones anteriores, un ejemplo de ataque consiste en definir 10 o más entidades XML, cada una definida como consistente de 10 de la entidad anterior, con el documento consistente de una única instancia de la entidad más grande, que se expande a mil millones de copias de la primera entidad. La suma de la memoria del ordenador utilizada para manejar una llamada SOAP externa probablemente superaría el disponible para el proceso de análisis del XML." + } + ], + "metrics": { + "cvssMetricV30": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "cvssData": { + "version": "3.0", + "vectorString": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", + "attackVector": "NETWORK", + "attackComplexity": "LOW", + "privilegesRequired": "NONE", + "userInteraction": "NONE", + "scope": "UNCHANGED", + "confidentialityImpact": "NONE", + "integrityImpact": "NONE", + "availabilityImpact": "HIGH", + "baseScore": 7.5, + "baseSeverity": "HIGH" + }, + "exploitabilityScore": 3.9, + "impactScore": 3.6 + } + ], + "cvssMetricV2": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "cvssData": { + "version": "2.0", + "vectorString": "AV:N/AC:L/Au:N/C:N/I:N/A:P", + "accessVector": "NETWORK", + "accessComplexity": "LOW", + "authentication": "NONE", + "confidentialityImpact": "NONE", + "integrityImpact": "NONE", + "availabilityImpact": "PARTIAL", + "baseScore": 5.0 + }, + "baseSeverity": "MEDIUM", + "exploitabilityScore": 10.0, + "impactScore": 2.9, + "acInsufInfo": false, + "obtainAllPrivilege": false, + "obtainUserPrivilege": false, + "obtainOtherPrivilege": false, + "userInteractionRequired": false + } + ] + }, + "weaknesses": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "description": [ + { + "lang": "en", + "value": "CWE-399" + } + ] + } + ], + "configurations": [ + { + "nodes": [ + { + "operator": "OR", + "negate": false, + "cpeMatch": [ + { + "vulnerable": true, + "criteria": "cpe:2.3:a:soap::lite_project:soap::lite:*:*:*:*:*:perl:*:*", + "versionEndIncluding": "1.14", + "matchCriteriaId": "FB4DACB9-2E9E-4CBE-825F-FC0303D8CC86" + } + ] + } + ] + } + ], + "references": [ + { + "url": "http://cpansearch.perl.org/src/PHRED/SOAP-Lite-1.20/Changes", + "source": "cve@mitre.org", + "tags": [ + "Vendor Advisory" + ] + }, + { + "url": "http://www.securityfocus.com/bid/94487", + "source": "cve@mitre.org" + } + ] + } +} diff --git a/pkg/process/v5/transformers/nvd/test-fixtures/single-package-multi-distro.json b/pkg/process/v5/transformers/nvd/test-fixtures/single-package-multi-distro.json new file mode 100644 index 00000000..ed108475 --- /dev/null +++ b/pkg/process/v5/transformers/nvd/test-fixtures/single-package-multi-distro.json @@ -0,0 +1,174 @@ +{ + "cve": { + "id": "CVE-2018-1000222", + "sourceIdentifier": "cve@mitre.org", + "published": "2018-08-20T20:29:01.347", + "lastModified": "2020-03-31T02:15:12.667", + "vulnStatus": "Modified", + "descriptions": [ + { + "lang": "en", + "value": "Libgd version 2.2.5 contains a Double Free Vulnerability vulnerability in gdImageBmpPtr Function that can result in Remote Code Execution . This attack appear to be exploitable via Specially Crafted Jpeg Image can trigger double free. This vulnerability appears to have been fixed in after commit ac16bdf2d41724b5a65255d4c28fb0ec46bc42f5." + }, + { + "lang": "es", + "value": "Libgd 2.2.5 contiene una vulnerabilidad de doble liberación (double free) en la función gdImageBmpPtr que puede resultar en la ejecución remota de código. Este ataque parece ser explotable mediante una imagen JPEG especialmente manipulada que desencadene una doble liberación (double free). La vulnerabilidad parece haber sido solucionada tras el commit con ID ac16bdf2d41724b5a65255d4c28fb0ec46bc42f5." + } + ], + "metrics": { + "cvssMetricV30": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "cvssData": { + "version": "3.0", + "vectorString": "CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H", + "attackVector": "NETWORK", + "attackComplexity": "LOW", + "privilegesRequired": "NONE", + "userInteraction": "REQUIRED", + "scope": "UNCHANGED", + "confidentialityImpact": "HIGH", + "integrityImpact": "HIGH", + "availabilityImpact": "HIGH", + "baseScore": 8.8, + "baseSeverity": "HIGH" + }, + "exploitabilityScore": 2.8, + "impactScore": 5.9 + } + ], + "cvssMetricV2": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "cvssData": { + "version": "2.0", + "vectorString": "AV:N/AC:M/Au:N/C:P/I:P/A:P", + "accessVector": "NETWORK", + "accessComplexity": "MEDIUM", + "authentication": "NONE", + "confidentialityImpact": "PARTIAL", + "integrityImpact": "PARTIAL", + "availabilityImpact": "PARTIAL", + "baseScore": 6.8 + }, + "baseSeverity": "MEDIUM", + "exploitabilityScore": 8.6, + "impactScore": 6.4, + "acInsufInfo": false, + "obtainAllPrivilege": false, + "obtainUserPrivilege": false, + "obtainOtherPrivilege": false, + "userInteractionRequired": true + } + ] + }, + "weaknesses": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "description": [ + { + "lang": "en", + "value": "CWE-415" + } + ] + } + ], + "configurations": [ + { + "nodes": [ + { + "operator": "OR", + "negate": false, + "cpeMatch": [ + { + "vulnerable": true, + "criteria": "cpe:2.3:a:libgd:libgd:2.2.5:*:*:*:*:*:*:*", + "matchCriteriaId": "C257CC1C-BF6A-4125-AA61-9C2D09096084" + } + ] + } + ] + }, + { + "nodes": [ + { + "operator": "OR", + "negate": false, + "cpeMatch": [ + { + "vulnerable": true, + "criteria": "cpe:2.3:o:canonical:ubuntu_linux:14.04:*:*:*:lts:*:*:*", + "matchCriteriaId": "B5A6F2F3-4894-4392-8296-3B8DD2679084" + }, + { + "vulnerable": true, + "criteria": "cpe:2.3:o:canonical:ubuntu_linux:16.04:*:*:*:lts:*:*:*", + "matchCriteriaId": "F7016A2A-8365-4F1A-89A2-7A19F2BCAE5B" + }, + { + "vulnerable": true, + "criteria": "cpe:2.3:o:canonical:ubuntu_linux:18.04:*:*:*:lts:*:*:*", + "matchCriteriaId": "23A7C53F-B80F-4E6A-AFA9-58EEA84BE11D" + } + ] + } + ] + }, + { + "nodes": [ + { + "operator": "OR", + "negate": false, + "cpeMatch": [ + { + "vulnerable": true, + "criteria": "cpe:2.3:o:debian:debian_linux:8.0:*:*:*:*:*:*:*", + "matchCriteriaId": "C11E6FB0-C8C0-4527-9AA0-CB9B316F8F43" + } + ] + } + ] + } + ], + "references": [ + { + "url": "https://github.com/libgd/libgd/issues/447", + "source": "cve@mitre.org", + "tags": [ + "Issue Tracking", + "Third Party Advisory" + ] + }, + { + "url": "https://lists.debian.org/debian-lts-announce/2019/01/msg00028.html", + "source": "cve@mitre.org", + "tags": [ + "Mailing List", + "Third Party Advisory" + ] + }, + { + "url": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/3CZ2QADQTKRHTGB2AHD7J4QQNDLBEMM6/", + "source": "cve@mitre.org" + }, + { + "url": "https://security.gentoo.org/glsa/201903-18", + "source": "cve@mitre.org", + "tags": [ + "Third Party Advisory" + ] + }, + { + "url": "https://usn.ubuntu.com/3755-1/", + "source": "cve@mitre.org", + "tags": [ + "Mitigation", + "Third Party Advisory" + ] + } + ] + } +} diff --git a/pkg/process/v5/transformers/nvd/test-fixtures/unmarshal-test.json b/pkg/process/v5/transformers/nvd/test-fixtures/unmarshal-test.json new file mode 100644 index 00000000..2dc698fa --- /dev/null +++ b/pkg/process/v5/transformers/nvd/test-fixtures/unmarshal-test.json @@ -0,0 +1,109 @@ +{ + "cve": { + "id": "CVE-2003-0349", + "sourceIdentifier": "cve@mitre.org", + "published": "2003-07-24T04:00:00.000", + "lastModified": "2018-10-12T21:32:41.083", + "vulnStatus": "Modified", + "descriptions": [ + { + "lang": "en", + "value": "Buffer overflow in the streaming media component for logging multicast requests in the ISAPI for the logging capability of Microsoft Windows Media Services (nsiislog.dll), as installed in IIS 5.0, allows remote attackers to execute arbitrary code via a large POST request to nsiislog.dll." + }, + { + "lang": "es", + "value": "Desbordamiento de búfer en el componente de secuenciamiento (streaming) de medios para registrar peticiones de multidifusión en la librería ISAPI de la capacidad de registro (logging) de Microsoft Windows Media Services (nsiislog.dll), como el instalado en IIS 5.9, permite a atacantes remotos ejecutar código arbitrario mediante una petición POST larga a nsiislog.dll." + } + ], + "metrics": { + "cvssMetricV2": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "cvssData": { + "version": "2.0", + "vectorString": "AV:N/AC:L/Au:N/C:P/I:P/A:P", + "accessVector": "NETWORK", + "accessComplexity": "LOW", + "authentication": "NONE", + "confidentialityImpact": "PARTIAL", + "integrityImpact": "PARTIAL", + "availabilityImpact": "PARTIAL", + "baseScore": 7.5 + }, + "baseSeverity": "HIGH", + "exploitabilityScore": 10.0, + "impactScore": 6.4, + "acInsufInfo": false, + "obtainAllPrivilege": false, + "obtainUserPrivilege": true, + "obtainOtherPrivilege": false, + "userInteractionRequired": false + } + ] + }, + "weaknesses": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "description": [ + { + "lang": "en", + "value": "NVD-CWE-Other" + } + ] + } + ], + "configurations": [ + { + "nodes": [ + { + "operator": "OR", + "negate": false, + "cpeMatch": [ + { + "vulnerable": true, + "criteria": "cpe:2.3:o:microsoft:windows_2000:*:*:*:*:*:*:*:*", + "matchCriteriaId": "4E545C63-FE9C-4CA1-AF0F-D999D84D2AFD" + } + ] + } + ] + } + ], + "references": [ + { + "url": "http://marc.info/?l=bugtraq&m=105665030925504&w=2", + "source": "cve@mitre.org" + }, + { + "url": "http://securitytracker.com/id?1007059", + "source": "cve@mitre.org" + }, + { + "url": "http://www.kb.cert.org/vuls/id/113716", + "source": "cve@mitre.org", + "tags": [ + "US Government Resource" + ] + }, + { + "url": "http://www.ntbugtraq.com/default.asp?pid=36&sid=1&A2=ind0306&L=NTBUGTRAQ&P=R4563", + "source": "cve@mitre.org", + "tags": [ + "Exploit", + "Patch", + "Vendor Advisory" + ] + }, + { + "url": "https://docs.microsoft.com/en-us/security-updates/securitybulletins/2003/ms03-022", + "source": "cve@mitre.org" + }, + { + "url": "https://oval.cisecurity.org/repository/search/definition/oval%3Aorg.mitre.oval%3Adef%3A938", + "source": "cve@mitre.org" + } + ] + } +} diff --git a/pkg/process/v5/transformers/nvd/test-fixtures/version-range.json b/pkg/process/v5/transformers/nvd/test-fixtures/version-range.json new file mode 100644 index 00000000..3df5b86d --- /dev/null +++ b/pkg/process/v5/transformers/nvd/test-fixtures/version-range.json @@ -0,0 +1,121 @@ +{ + "cve": { + "id": "CVE-2018-5487", + "sourceIdentifier": "security-alert@netapp.com", + "published": "2018-05-24T14:29:00.390", + "lastModified": "2018-07-05T13:52:30.627", + "vulnStatus": "Analyzed", + "descriptions": [ + { + "lang": "en", + "value": "NetApp OnCommand Unified Manager for Linux versions 7.2 through 7.3 ship with the Java Management Extension Remote Method Invocation (JMX RMI) service bound to the network, and are susceptible to unauthenticated remote code execution." + }, + { + "lang": "es", + "value": "NetApp OnCommand Unified Manager for Linux, de la versión 7.2 hasta la 7.3, se distribuye con el servicio Java Management Extension Remote Method Invocation (JMX RMI) enlazado a la red y es susceptible a la ejecución remota de código sin autenticación." + } + ], + "metrics": { + "cvssMetricV30": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "cvssData": { + "version": "3.0", + "vectorString": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + "attackVector": "NETWORK", + "attackComplexity": "LOW", + "privilegesRequired": "NONE", + "userInteraction": "NONE", + "scope": "UNCHANGED", + "confidentialityImpact": "HIGH", + "integrityImpact": "HIGH", + "availabilityImpact": "HIGH", + "baseScore": 9.8, + "baseSeverity": "CRITICAL" + }, + "exploitabilityScore": 3.9, + "impactScore": 5.9 + } + ], + "cvssMetricV2": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "cvssData": { + "version": "2.0", + "vectorString": "AV:N/AC:L/Au:N/C:P/I:P/A:P", + "accessVector": "NETWORK", + "accessComplexity": "LOW", + "authentication": "NONE", + "confidentialityImpact": "PARTIAL", + "integrityImpact": "PARTIAL", + "availabilityImpact": "PARTIAL", + "baseScore": 7.5 + }, + "baseSeverity": "HIGH", + "exploitabilityScore": 10.0, + "impactScore": 6.4, + "acInsufInfo": true, + "obtainAllPrivilege": false, + "obtainUserPrivilege": false, + "obtainOtherPrivilege": false, + "userInteractionRequired": false + } + ] + }, + "weaknesses": [ + { + "source": "nvd@nist.gov", + "type": "Primary", + "description": [ + { + "lang": "en", + "value": "CWE-20" + } + ] + } + ], + "configurations": [ + { + "operator": "AND", + "nodes": [ + { + "operator": "OR", + "negate": false, + "cpeMatch": [ + { + "vulnerable": true, + "criteria": "cpe:2.3:a:netapp:oncommand_unified_manager:*:*:*:*:*:*:*:*", + "versionStartIncluding": "7.2", + "versionEndIncluding": "7.3", + "matchCriteriaId": "A5949307-3E9B-441F-B008-81A0E0228DC0" + } + ] + }, + { + "operator": "OR", + "negate": false, + "cpeMatch": [ + { + "vulnerable": false, + "criteria": "cpe:2.3:o:linux:linux_kernel:-:*:*:*:*:*:*:*", + "matchCriteriaId": "703AF700-7A70-47E2-BC3A-7FD03B3CA9C1" + } + ] + } + ] + } + ], + "references": [ + { + "url": "https://security.netapp.com/advisory/ntap-20180523-0001/", + "source": "security-alert@netapp.com", + "tags": [ + "Patch", + "Vendor Advisory" + ] + } + ] + } +} diff --git a/pkg/process/v5/transformers/nvd/transform.go b/pkg/process/v5/transformers/nvd/transform.go new file mode 100644 index 00000000..5c2ff94c --- /dev/null +++ b/pkg/process/v5/transformers/nvd/transform.go @@ -0,0 +1,88 @@ +package nvd + +import ( + "github.com/anchore/grype-db/internal" + "github.com/anchore/grype-db/pkg/data" + "github.com/anchore/grype-db/pkg/process/v5/transformers" + "github.com/anchore/grype-db/pkg/provider/unmarshal" + "github.com/anchore/grype-db/pkg/provider/unmarshal/nvd" + grypeDB "github.com/anchore/grype/grype/db/v5" + "github.com/anchore/grype/grype/db/v5/namespace" +) + +func Transform(vulnerability unmarshal.NVDVulnerability) ([]data.Entry, error) { + // TODO: stop capturing record source in the vulnerability metadata record (now that feed groups are not real) + recordSource := "nvdv2:nvdv2:cves" + + grypeNamespace, err := namespace.FromString("nvd:cpe") + if err != nil { + return nil, err + } + + entryNamespace := grypeNamespace.String() + + uniquePkgs := findUniquePkgs(vulnerability.Configurations...) + + // extract all links + var links []string + for _, externalRefs := range vulnerability.References { + // TODO: should we capture other information here? + if externalRefs.URL != "" { + links = append(links, externalRefs.URL) + } + } + + // duplicate the vulnerabilities based on the set of unique packages the vulnerability is for + var allVulns []grypeDB.Vulnerability + for _, p := range uniquePkgs.All() { + matches := uniquePkgs.Matches(p) + cpes := internal.NewStringSet() + for _, m := range matches { + cpes.Add(grypeNamespace.Resolver().Normalize(m.Criteria)) + } + + // create vulnerability entry + allVulns = append(allVulns, grypeDB.Vulnerability{ + ID: vulnerability.ID, + VersionConstraint: buildConstraints(uniquePkgs.Matches(p)), + VersionFormat: "unknown", + PackageName: grypeNamespace.Resolver().Normalize(p.Product), + Namespace: entryNamespace, + CPEs: cpes.ToSlice(), + Fix: grypeDB.Fix{ + State: grypeDB.UnknownFixState, + }, + }) + } + + // create vulnerability metadata entry (a single entry keyed off of the vulnerability ID) + allCVSS := vulnerability.CVSS() + metadata := grypeDB.VulnerabilityMetadata{ + ID: vulnerability.ID, + DataSource: "https://nvd.nist.gov/vuln/detail/" + vulnerability.ID, + Namespace: entryNamespace, + RecordSource: recordSource, + Severity: nvd.CvssSummaries(allCVSS).Sorted().Severity(), + URLs: links, + Description: vulnerability.Description(), + Cvss: getCvss(allCVSS...), + } + + return transformers.NewEntries(allVulns, metadata), nil +} + +func getCvss(cvss ...nvd.CvssSummary) []grypeDB.Cvss { + var results []grypeDB.Cvss + for _, c := range cvss { + results = append(results, grypeDB.Cvss{ + Version: c.Version, + Vector: c.Vector, + Metrics: grypeDB.CvssMetrics{ + BaseScore: c.BaseScore, + ExploitabilityScore: c.ExploitabilityScore, + ImpactScore: c.ImpactScore, + }, + }) + } + return results +} diff --git a/pkg/process/v5/transformers/nvd/transform_test.go b/pkg/process/v5/transformers/nvd/transform_test.go new file mode 100644 index 00000000..50bb4b42 --- /dev/null +++ b/pkg/process/v5/transformers/nvd/transform_test.go @@ -0,0 +1,255 @@ +package nvd + +import ( + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/require" + "os" + "testing" + + testUtils "github.com/anchore/grype-db/pkg/process/tests" + "github.com/anchore/grype-db/pkg/provider/unmarshal" + grypeDB "github.com/anchore/grype/grype/db/v5" + "github.com/go-test/deep" + "github.com/stretchr/testify/assert" +) + +func TestUnmarshalNVDVulnerabilitiesEntries(t *testing.T) { + f, err := os.Open("test-fixtures/unmarshal-test.json") + require.NoError(t, err) + defer testUtils.CloseFile(f) + + entries, err := unmarshal.NvdVulnerabilityEntries(f) + assert.NoError(t, err) + assert.Len(t, entries, 1) +} + +func TestParseAllNVDVulnerabilityEntries(t *testing.T) { + + tests := []struct { + name string + numEntries int + fixture string + vulns []grypeDB.Vulnerability + metadata grypeDB.VulnerabilityMetadata + }{ + { + name: "AppVersionRange", + numEntries: 1, + fixture: "test-fixtures/version-range.json", + vulns: []grypeDB.Vulnerability{ + { + ID: "CVE-2018-5487", + PackageName: "oncommand_unified_manager", + VersionConstraint: ">= 7.2, <= 7.3", + VersionFormat: "unknown", // TODO: this should reference a format, yes? (not a string) + Namespace: "nvd:cpe", + CPEs: []string{"cpe:2.3:a:netapp:oncommand_unified_manager:*:*:*:*:*:*:*:*"}, + Fix: grypeDB.Fix{ + State: "unknown", + }, + }, + }, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "CVE-2018-5487", + DataSource: "https://nvd.nist.gov/vuln/detail/CVE-2018-5487", + Namespace: "nvd:cpe", + RecordSource: "nvdv2:nvdv2:cves", + Severity: "Critical", + URLs: []string{"https://security.netapp.com/advisory/ntap-20180523-0001/"}, + Description: "NetApp OnCommand Unified Manager for Linux versions 7.2 through 7.3 ship with the Java Management Extension Remote Method Invocation (JMX RMI) service bound to the network, and are susceptible to unauthenticated remote code execution.", + Cvss: []grypeDB.Cvss{ + { + Metrics: grypeDB.NewCvssMetrics( + 7.5, + 10, + 6.4, + ), + Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P", + Version: "2.0", + }, + { + Metrics: grypeDB.NewCvssMetrics( + 9.8, + 3.9, + 5.9, + ), + Vector: "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + Version: "3.0", + }, + }, + }, + }, + { + name: "App+OS", + numEntries: 1, + fixture: "test-fixtures/single-package-multi-distro.json", + vulns: []grypeDB.Vulnerability{ + { + ID: "CVE-2018-1000222", + PackageName: "libgd", + VersionConstraint: "= 2.2.5", + VersionFormat: "unknown", // TODO: this should reference a format, yes? (not a string) + Namespace: "nvd:cpe", + CPEs: []string{"cpe:2.3:a:libgd:libgd:2.2.5:*:*:*:*:*:*:*"}, + Fix: grypeDB.Fix{ + State: "unknown", + }, + }, + // TODO: Question: should this match also the OS's? (as in the vulnerable_cpes list)... this seems wrong! + }, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "CVE-2018-1000222", + DataSource: "https://nvd.nist.gov/vuln/detail/CVE-2018-1000222", + Namespace: "nvd:cpe", + RecordSource: "nvdv2:nvdv2:cves", + Severity: "High", + URLs: []string{"https://github.com/libgd/libgd/issues/447", "https://lists.debian.org/debian-lts-announce/2019/01/msg00028.html", "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/3CZ2QADQTKRHTGB2AHD7J4QQNDLBEMM6/", "https://security.gentoo.org/glsa/201903-18", "https://usn.ubuntu.com/3755-1/"}, + Description: "Libgd version 2.2.5 contains a Double Free Vulnerability vulnerability in gdImageBmpPtr Function that can result in Remote Code Execution . This attack appear to be exploitable via Specially Crafted Jpeg Image can trigger double free. This vulnerability appears to have been fixed in after commit ac16bdf2d41724b5a65255d4c28fb0ec46bc42f5.", + Cvss: []grypeDB.Cvss{ + { + Metrics: grypeDB.NewCvssMetrics( + 6.8, + 8.6, + 6.4, + ), + Vector: "AV:N/AC:M/Au:N/C:P/I:P/A:P", + Version: "2.0", + }, + { + Metrics: grypeDB.NewCvssMetrics( + 8.8, + 2.8, + 5.9, + ), + Vector: "CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H", + Version: "3.0", + }, + }, + }, + }, + { + name: "AppCompoundVersionRange", + numEntries: 1, + fixture: "test-fixtures/compound-pkg.json", + vulns: []grypeDB.Vulnerability{ + { + ID: "CVE-2018-10189", + PackageName: "mautic", + VersionConstraint: ">= 1.0.0, <= 1.4.1 || >= 2.0.0, < 2.13.0", + VersionFormat: "unknown", + Namespace: "nvd:cpe", + CPEs: []string{"cpe:2.3:a:mautic:mautic:*:*:*:*:*:*:*:*"}, // note: entry was dedupicated + Fix: grypeDB.Fix{ + State: "unknown", + }, + }, + }, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "CVE-2018-10189", + DataSource: "https://nvd.nist.gov/vuln/detail/CVE-2018-10189", + Namespace: "nvd:cpe", + RecordSource: "nvdv2:nvdv2:cves", + Severity: "High", + URLs: []string{"https://github.com/mautic/mautic/releases/tag/2.13.0"}, + Description: "An issue was discovered in Mautic 1.x and 2.x before 2.13.0. It is possible to systematically emulate tracking cookies per contact due to tracking the contact by their auto-incremented ID. Thus, a third party can manipulate the cookie value with +1 to systematically assume being tracked as each contact in Mautic. It is then possible to retrieve information about the contact through forms that have progressive profiling enabled.", + Cvss: []grypeDB.Cvss{ + { + Metrics: grypeDB.NewCvssMetrics( + 5, + 10, + 2.9, + ), + Vector: "AV:N/AC:L/Au:N/C:P/I:N/A:N", + Version: "2.0", + }, + { + Metrics: grypeDB.NewCvssMetrics( + 7.5, + 3.9, + 3.6, + ), + Vector: "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N", + Version: "3.0", + }, + }, + }, + }, + { + // we always keep the metadata even though there are no vulnerability entries for it + name: "InvalidCPE", + numEntries: 1, + fixture: "test-fixtures/invalid_cpe.json", + vulns: nil, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "CVE-2015-8978", + Namespace: "nvd:cpe", + DataSource: "https://nvd.nist.gov/vuln/detail/CVE-2015-8978", + RecordSource: "nvdv2:nvdv2:cves", + Severity: "High", + URLs: []string{ + "http://cpansearch.perl.org/src/PHRED/SOAP-Lite-1.20/Changes", + "http://www.securityfocus.com/bid/94487", + }, + Description: "In Soap Lite (aka the SOAP::Lite extension for Perl) 1.14 and earlier, an example attack consists of defining 10 or more XML entities, each defined as consisting of 10 of the previous entity, with the document consisting of a single instance of the largest entity, which expands to one billion copies of the first entity. The amount of computer memory used for handling an external SOAP call would likely exceed that available to the process parsing the XML.", + Cvss: []grypeDB.Cvss{ + { + Metrics: grypeDB.NewCvssMetrics( + 5, + 10, + 2.9, + ), + Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P", + Version: "2.0", + }, + { + Metrics: grypeDB.NewCvssMetrics( + 7.5, + 3.9, + 3.6, + ), + Vector: "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", + Version: "3.0", + }, + }, + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + f, err := os.Open(test.fixture) + require.NoError(t, err) + t.Cleanup(func() { + assert.NoError(t, f.Close()) + }) + + entries, err := unmarshal.NvdVulnerabilityEntries(f) + require.NoError(t, err) + + var vulns []grypeDB.Vulnerability + for _, entry := range entries { + dataEntries, err := Transform(entry.Cve) + require.NoError(t, err) + + for _, entry := range dataEntries { + switch vuln := entry.Data.(type) { + case grypeDB.Vulnerability: + vulns = append(vulns, vuln) + case grypeDB.VulnerabilityMetadata: + // check metadata + if diff := deep.Equal(test.metadata, vuln); diff != nil { + for _, d := range diff { + t.Errorf("metadata diff: %+v", d) + } + } + default: + t.Fatalf("unexpected condition: data entry does not have a vulnerability or a metadata") + } + } + } + + if diff := cmp.Diff(test.vulns, vulns); diff != "" { + t.Errorf("vulnerabilities do not match (-want +got):\n%s", diff) + } + }) + } +} diff --git a/pkg/process/v5/transformers/nvd/unique_pkg.go b/pkg/process/v5/transformers/nvd/unique_pkg.go new file mode 100644 index 00000000..48791517 --- /dev/null +++ b/pkg/process/v5/transformers/nvd/unique_pkg.go @@ -0,0 +1,115 @@ +package nvd + +import ( + "fmt" + "strings" + + "github.com/anchore/grype-db/pkg/provider/unmarshal/nvd" + + "github.com/anchore/grype-db/internal/log" + "github.com/anchore/grype-db/pkg/process/common" + "github.com/umisama/go-cpe" +) + +const ( + ANY = "*" + NA = "-" +) + +type pkgCandidate struct { + Product string + Vendor string + TargetSoftware string +} + +func (p pkgCandidate) String() string { + return fmt.Sprintf("%s|%s|%s", p.Vendor, p.Product, p.TargetSoftware) +} + +func newPkgCandidate(match nvd.CpeMatch) (*pkgCandidate, error) { + // we are only interested in packages that are vulnerable (not related to secondary match conditioning) + if !match.Vulnerable { + return nil, nil + } + + c, err := cpe.NewItemFromFormattedString(match.Criteria) + if err != nil { + return nil, fmt.Errorf("unable to create uniquePkgEntry from '%s': %w", match.Criteria, err) + } + + // we are only interested in applications, not hardware or operating systems + if c.Part() != cpe.Application { + return nil, nil + } + + return &pkgCandidate{ + Product: c.Product().String(), + Vendor: c.Vendor().String(), + TargetSoftware: c.TargetSw().String(), + }, nil +} + +func findUniquePkgs(cfgs ...nvd.Configuration) uniquePkgTracker { + set := newUniquePkgTracker() + for _, c := range cfgs { + _findUniquePkgs(set, c.Nodes...) + } + return set +} + +func _findUniquePkgs(set uniquePkgTracker, ns ...nvd.Node) { + if len(ns) == 0 { + return + } + for _, node := range ns { + for _, match := range node.CpeMatch { + candidate, err := newPkgCandidate(match) + if err != nil { + // Do not halt all execution because of being unable to create + // a PkgCandidate. This can happen when a CPE is invalid which + // could avoid creating a database + log.Debugf("unable processing uniquePkg: %v", err) + continue + } + if candidate != nil { + set.Add(*candidate, match) + } + } + } +} + +func buildConstraints(matches []nvd.CpeMatch) string { + constraints := make([]string, 0) + for _, match := range matches { + constraints = append(constraints, buildConstraint(match)) + } + return common.OrConstraints(constraints...) +} + +func buildConstraint(match nvd.CpeMatch) string { + constraints := make([]string, 0) + if match.VersionStartIncluding != nil && *match.VersionStartIncluding != "" { + constraints = append(constraints, fmt.Sprintf(">= %s", *match.VersionStartIncluding)) + } else if match.VersionStartExcluding != nil && *match.VersionStartExcluding != "" { + constraints = append(constraints, fmt.Sprintf("> %s", *match.VersionStartExcluding)) + } + + if match.VersionEndIncluding != nil && *match.VersionEndIncluding != "" { + constraints = append(constraints, fmt.Sprintf("<= %s", *match.VersionEndIncluding)) + } else if match.VersionEndExcluding != nil && *match.VersionEndExcluding != "" { + constraints = append(constraints, fmt.Sprintf("< %s", *match.VersionEndExcluding)) + } + + if len(constraints) == 0 { + c, err := cpe.NewItemFromFormattedString(match.Criteria) + if err != nil { + return "" + } + version := c.Version().String() + if version != ANY && version != NA { + constraints = append(constraints, fmt.Sprintf("= %s", version)) + } + } + + return strings.Join(constraints, ", ") +} diff --git a/pkg/process/v5/transformers/nvd/unique_pkg_test.go b/pkg/process/v5/transformers/nvd/unique_pkg_test.go new file mode 100644 index 00000000..9a98731f --- /dev/null +++ b/pkg/process/v5/transformers/nvd/unique_pkg_test.go @@ -0,0 +1,352 @@ +package nvd + +import ( + "github.com/anchore/grype-db/pkg/provider/unmarshal/nvd" + "testing" + + "github.com/sergi/go-diff/diffmatchpatch" +) + +func newUniquePkgTrackerFromSlice(candidates []pkgCandidate) uniquePkgTracker { + set := newUniquePkgTracker() + for _, c := range candidates { + set[c] = nil + } + return set +} + +func TestFindUniquePkgs(t *testing.T) { + tests := []struct { + name string + nodes []nvd.Node + expected uniquePkgTracker + }{ + { + name: "simple-match", + nodes: []nvd.Node{ + { + CpeMatch: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendor:product:2.2.0:*:*:*:*:target:*:*", + Vulnerable: true, + }, + }, + }, + }, + expected: newUniquePkgTrackerFromSlice( + []pkgCandidate{ + { + Product: "product", + Vendor: "vendor", + TargetSoftware: "target", + }, + }), + }, + { + name: "skip-hw", + nodes: []nvd.Node{ + { + CpeMatch: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:h:vendor:product:2.2.0:*:*:*:*:target:*:*", + Vulnerable: true, + }, + }, + }, + }, + expected: newUniquePkgTrackerFromSlice([]pkgCandidate{}), + }, + { + name: "skip-os", + nodes: []nvd.Node{ + { + CpeMatch: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:o:vendor:product:2.2.0:*:*:*:*:target:*:*", + Vulnerable: true, + }, + }, + }, + }, + expected: newUniquePkgTrackerFromSlice([]pkgCandidate{}), + }, + { + name: "duplicate-by-product", + nodes: []nvd.Node{ + { + CpeMatch: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendor:productA:3.3.3:*:*:*:*:target:*:*", + Vulnerable: true, + }, + { + Criteria: "cpe:2.3:a:vendor:productB:2.2.0:*:*:*:*:target:*:*", + Vulnerable: true, + }, + }, + Operator: "OR", + }, + }, + expected: newUniquePkgTrackerFromSlice( + []pkgCandidate{ + { + Product: "productA", + Vendor: "vendor", + TargetSoftware: "target", + }, + { + Product: "productB", + Vendor: "vendor", + TargetSoftware: "target", + }, + }), + }, + { + name: "duplicate-by-target", + nodes: []nvd.Node{ + { + CpeMatch: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendor:product:3.3.3:*:*:*:*:targetA:*:*", + Vulnerable: true, + }, + { + Criteria: "cpe:2.3:a:vendor:product:2.2.0:*:*:*:*:targetB:*:*", + Vulnerable: true, + }, + }, + Operator: "OR", + }, + }, + expected: newUniquePkgTrackerFromSlice( + []pkgCandidate{ + { + Product: "product", + Vendor: "vendor", + TargetSoftware: "targetA", + }, + { + Product: "product", + Vendor: "vendor", + TargetSoftware: "targetB", + }, + }), + }, + { + name: "duplicate-by-vendor", + nodes: []nvd.Node{ + { + CpeMatch: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendorA:product:3.3.3:*:*:*:*:target:*:*", + Vulnerable: true, + }, + { + Criteria: "cpe:2.3:a:vendorB:product:2.2.0:*:*:*:*:target:*:*", + Vulnerable: true, + }, + }, + Operator: "OR", + }, + }, + expected: newUniquePkgTrackerFromSlice( + []pkgCandidate{ + { + Product: "product", + Vendor: "vendorA", + TargetSoftware: "target", + }, + { + Product: "product", + Vendor: "vendorB", + TargetSoftware: "target", + }, + }), + }, + { + name: "de-duplicate-case", + nodes: []nvd.Node{ + { + CpeMatch: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendor:product:3.3.3:A:B:C:D:target:E:F", + Vulnerable: true, + }, + { + Criteria: "cpe:2.3:a:vendor:product:2.2.0:Q:R:S:T:target:U:V", + Vulnerable: true, + }, + }, + Operator: "OR", + }, + }, + expected: newUniquePkgTrackerFromSlice( + []pkgCandidate{ + { + Product: "product", + Vendor: "vendor", + TargetSoftware: "target", + }, + }), + }, + { + name: "duplicate-from-nested-nodes", + nodes: []nvd.Node{ + { + CpeMatch: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendorB:product:2.2.0:*:*:*:*:target:*:*", + Vulnerable: true, + }, + }, + Operator: "OR", + }, + { + CpeMatch: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendorA:product:2.2.0:*:*:*:*:target:*:*", + Vulnerable: true, + }, + }, + Operator: "OR", + }, + }, + expected: newUniquePkgTrackerFromSlice( + []pkgCandidate{ + { + Product: "product", + Vendor: "vendorA", + TargetSoftware: "target", + }, + { + Product: "product", + Vendor: "vendorB", + TargetSoftware: "target", + }, + }), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actual := findUniquePkgs(nvd.Configuration{Nodes: test.nodes}) + missing, extra := test.expected.Diff(actual) + if len(missing) != 0 { + for _, c := range missing { + t.Errorf("missing candidate: %+v", c) + } + } + + if len(extra) != 0 { + for _, c := range extra { + t.Errorf("extra candidate: %+v", c) + } + } + }) + } + +} + +func strRef(s string) *string { + return &s +} + +func TestBuildConstraints(t *testing.T) { + tests := []struct { + name string + matches []nvd.CpeMatch + expected string + }{ + { + name: "Equals", + matches: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendor:product:2.2.0:*:*:*:*:target:*:*", + }, + }, + expected: "= 2.2.0", + }, + { + name: "VersionEndExcluding", + matches: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", + VersionEndExcluding: strRef("2.3.0"), + }, + }, + expected: "< 2.3.0", + }, + { + name: "VersionEndIncluding", + matches: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", + VersionEndIncluding: strRef("2.3.0"), + }, + }, + expected: "<= 2.3.0", + }, + { + name: "VersionStartExcluding", + matches: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", + VersionStartExcluding: strRef("2.3.0"), + }, + }, + expected: "> 2.3.0", + }, + { + name: "VersionStartIncluding", + matches: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", + VersionStartIncluding: strRef("2.3.0"), + }, + }, + expected: ">= 2.3.0", + }, + { + name: "Version Range", + matches: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", + VersionStartIncluding: strRef("2.3.0"), + VersionEndIncluding: strRef("2.5.0"), + }, + }, + expected: ">= 2.3.0, <= 2.5.0", + }, + { + name: "Multiple Version Ranges", + matches: []nvd.CpeMatch{ + { + Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", + VersionStartIncluding: strRef("2.3.0"), + VersionEndIncluding: strRef("2.5.0"), + }, + { + Criteria: "cpe:2.3:a:vendor:product:*:*:*:*:*:target:*:*", + VersionStartExcluding: strRef("3.3.0"), + VersionEndExcluding: strRef("3.5.0"), + }, + }, + expected: ">= 2.3.0, <= 2.5.0 || > 3.3.0, < 3.5.0", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actual := buildConstraints(test.matches) + + if actual != test.expected { + dmp := diffmatchpatch.New() + diffs := dmp.DiffMain(actual, test.expected, true) + t.Errorf("Expected: %q", test.expected) + t.Errorf("Got : %q", actual) + t.Errorf("Diff : %q", dmp.DiffPrettyText(diffs)) + } + }) + } + +} diff --git a/pkg/process/v5/transformers/nvd/unique_pkg_tracker.go b/pkg/process/v5/transformers/nvd/unique_pkg_tracker.go new file mode 100644 index 00000000..2b7e405d --- /dev/null +++ b/pkg/process/v5/transformers/nvd/unique_pkg_tracker.go @@ -0,0 +1,64 @@ +package nvd + +import ( + "sort" + + "github.com/anchore/grype-db/pkg/provider/unmarshal/nvd" +) + +type uniquePkgTracker map[pkgCandidate][]nvd.CpeMatch + +func newUniquePkgTracker() uniquePkgTracker { + return make(uniquePkgTracker) +} + +func (s uniquePkgTracker) Diff(other uniquePkgTracker) (missing []pkgCandidate, extra []pkgCandidate) { + for k := range s { + if !other.Contains(k) { + missing = append(missing, k) + } + } + + for k := range other { + if !s.Contains(k) { + extra = append(extra, k) + } + } + + return +} + +func (s uniquePkgTracker) Matches(i pkgCandidate) []nvd.CpeMatch { + return s[i] +} + +func (s uniquePkgTracker) Add(i pkgCandidate, match nvd.CpeMatch) { + if _, ok := s[i]; !ok { + s[i] = make([]nvd.CpeMatch, 0) + } + s[i] = append(s[i], match) +} + +func (s uniquePkgTracker) Remove(i pkgCandidate) { + delete(s, i) +} + +func (s uniquePkgTracker) Contains(i pkgCandidate) bool { + _, ok := s[i] + return ok +} + +func (s uniquePkgTracker) All() []pkgCandidate { + res := make([]pkgCandidate, len(s)) + idx := 0 + for k := range s { + res[idx] = k + idx++ + } + + sort.SliceStable(res, func(i, j int) bool { + return res[i].String() < res[j].String() + }) + + return res +} diff --git a/pkg/process/v5/transformers/os/test-fixtures/alpine-3.9.json b/pkg/process/v5/transformers/os/test-fixtures/alpine-3.9.json new file mode 100644 index 00000000..b9d84395 --- /dev/null +++ b/pkg/process/v5/transformers/os/test-fixtures/alpine-3.9.json @@ -0,0 +1,28 @@ +[ + { + "Vulnerability": { + "CVSS": [], + "Description": "", + "FixedIn": [ + { + "Name": "xen", + "NamespaceName": "alpine:3.9", + "Version": "4.11.1-r0", + "VersionFormat": "apk" + } + ], + "Link": "http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-19967", + "Metadata": { + "NVD": { + "CVSSv2": { + "Score": 4.9, + "Vectors": "AV:L/AC:L/Au:N/C:N/I:N/A:C" + } + } + }, + "Name": "CVE-2018-19967", + "NamespaceName": "alpine:3.9", + "Severity": "Medium" + } + } +] \ No newline at end of file diff --git a/pkg/process/v5/transformers/os/test-fixtures/amzn.json b/pkg/process/v5/transformers/os/test-fixtures/amzn.json new file mode 100644 index 00000000..9a7cd41e --- /dev/null +++ b/pkg/process/v5/transformers/os/test-fixtures/amzn.json @@ -0,0 +1,48 @@ +[ + { + "Vulnerability": { + "Description": "", + "FixedIn": [ + { + "Name": "389-ds-base", + "NamespaceName": "amzn:2", + "Version": "1.3.8.4-15.amzn2.0.1", + "VersionFormat": "rpm" + }, + { + "Name": "389-ds-base-debuginfo", + "NamespaceName": "amzn:2", + "Version": "1.3.8.4-15.amzn2.0.1", + "VersionFormat": "rpm" + }, + { + "Name": "389-ds-base-devel", + "NamespaceName": "amzn:2", + "Version": "1.3.8.4-15.amzn2.0.1", + "VersionFormat": "rpm" + }, + { + "Name": "389-ds-base-libs", + "NamespaceName": "amzn:2", + "Version": "1.3.8.4-15.amzn2.0.1", + "VersionFormat": "rpm" + }, + { + "Name": "389-ds-base-snmp", + "NamespaceName": "amzn:2", + "Version": "1.3.8.4-15.amzn2.0.1", + "VersionFormat": "rpm" + } + ], + "Link": "https://alas.aws.amazon.com/AL2/ALAS-2018-1106.html", + "Metadata": { + "CVE": [ + {"Name": "CVE-2018-14648"} + ] + }, + "Name": "ALAS-2018-1106", + "NamespaceName": "amzn:2", + "Severity": "Medium" + } + } +] \ No newline at end of file diff --git a/pkg/process/v5/transformers/os/test-fixtures/debian-8-multiple-entries-for-same-package.json b/pkg/process/v5/transformers/os/test-fixtures/debian-8-multiple-entries-for-same-package.json new file mode 100644 index 00000000..5025b56e --- /dev/null +++ b/pkg/process/v5/transformers/os/test-fixtures/debian-8-multiple-entries-for-same-package.json @@ -0,0 +1,62 @@ +[ + { + "Vulnerability": { + "CVSS": [], + "Description": "", + "FixedIn": [ + { + "Name": "rsyslog", + "NamespaceName": "debian:8", + "VendorAdvisory": { + "AdvisorySummary": [], + "NoAdvisory": false + }, + "Version": "5.7.4-1", + "VersionFormat": "dpkg" + } + ], + "Link": "https://security-tracker.debian.org/tracker/CVE-2011-4623", + "Metadata": { + "NVD": { + "CVSSv2": { + "Score": 2.1, + "Vectors": "AV:L/AC:L/Au:N/C:N/I:N/A:P" + } + } + }, + "Name": "CVE-2011-4623", + "NamespaceName": "debian:8", + "Severity": "Low" + } + }, + { + "Vulnerability": { + "CVSS": [], + "Description": "", + "FixedIn": [ + { + "Name": "rsyslog", + "NamespaceName": "debian:8", + "VendorAdvisory": { + "AdvisorySummary": [], + "NoAdvisory": false + }, + "Version": "3.18.6-1", + "VersionFormat": "dpkg" + } + ], + "Link": "https://security-tracker.debian.org/tracker/CVE-2008-5618", + "Metadata": { + "NVD": { + "CVSSv2": { + "Score": 5, + "Vectors": "AV:N/AC:L/Au:N/C:N/I:N/A:P" + } + } + }, + "Name": "CVE-2008-5618", + "NamespaceName": "debian:8", + "Severity": "Low" + } + } +] \ No newline at end of file diff --git a/pkg/process/v5/transformers/os/test-fixtures/debian-8.json b/pkg/process/v5/transformers/os/test-fixtures/debian-8.json new file mode 100644 index 00000000..a758f13c --- /dev/null +++ b/pkg/process/v5/transformers/os/test-fixtures/debian-8.json @@ -0,0 +1,62 @@ +[ + { + "Vulnerability": { + "CVSS": [], + "Description": "", + "FixedIn": [ + { + "Name": "asterisk", + "NamespaceName": "debian:8", + "VendorAdvisory": { + "AdvisorySummary": [], + "NoAdvisory": false + }, + "Version": "1:1.6.2.0~rc3-1", + "VersionFormat": "dpkg" + }, + { + "Name": "auth2db", + "NamespaceName": "debian:8", + "VendorAdvisory": { + "AdvisorySummary": [], + "NoAdvisory": false + }, + "Version": "0.2.5-2+dfsg-1", + "VersionFormat": "dpkg" + }, + { + "Name": "exaile", + "NamespaceName": "debian:8", + "VendorAdvisory": { + "AdvisorySummary": [], + "NoAdvisory": false + }, + "Version": "0.2.14+debian-2.2", + "VersionFormat": "dpkg" + }, + { + "Name": "wordpress", + "NamespaceName": "debian:8", + "VendorAdvisory": { + "AdvisorySummary": [], + "NoAdvisory": false + }, + "Version": "", + "VersionFormat": "dpkg" + } + ], + "Link": "https://security-tracker.debian.org/tracker/CVE-2008-7220", + "Metadata": { + "NVD": { + "CVSSv2": { + "Score": 7.5, + "Vectors": "AV:N/AC:L/Au:N/C:P/I:P/A:P" + } + } + }, + "Name": "CVE-2008-7220", + "NamespaceName": "debian:8", + "Severity": "High" + } + } +] \ No newline at end of file diff --git a/pkg/process/v5/transformers/os/test-fixtures/ol-8-modules.json b/pkg/process/v5/transformers/os/test-fixtures/ol-8-modules.json new file mode 100644 index 00000000..f1d7372b --- /dev/null +++ b/pkg/process/v5/transformers/os/test-fixtures/ol-8-modules.json @@ -0,0 +1,36 @@ +[ + { + "Vulnerability": { + "CVSS": [], + "Description": "A flaw was found in PostgreSQL, where some PostgreSQL extensions did not use the search_path safely in their installation script. This flaw allows an attacker with sufficient privileges to trick an administrator into executing a specially crafted script during the extension's installation or update. The highest threat from this vulnerability is to confidentiality, integrity, as well as system availability.", + "FixedIn": [ + { + "Module": "postgresql:10", + "Name": "postgresql", + "NamespaceName": "ol:8", + "Version": "0:10.14-1.module+el8.2.0+7801+be0fed80", + "VersionFormat": "rpm" + }, + { + "Module": "postgresql:12", + "Name": "postgresql", + "NamespaceName": "ol:8", + "Version": "0:12.5-1.module+el8.3.0+9042+664538f4", + "VersionFormat": "rpm" + }, + { + "Module": "postgresql:9.6", + "Name": "postgresql", + "NamespaceName": "ol:8", + "Version": "0:9.6.20-1.module+el8.3.0+8938+7f0e88b6", + "VersionFormat": "rpm" + } + ], + "Link": "https://access.redhat.com/security/cve/CVE-2020-14350", + "Metadata": {}, + "Name": "CVE-2020-14350", + "NamespaceName": "ol:8", + "Severity": "Medium" + } + } +] \ No newline at end of file diff --git a/pkg/process/v5/transformers/os/test-fixtures/ol-8.json b/pkg/process/v5/transformers/os/test-fixtures/ol-8.json new file mode 100644 index 00000000..09439ece --- /dev/null +++ b/pkg/process/v5/transformers/os/test-fixtures/ol-8.json @@ -0,0 +1,42 @@ +[ + { + "Vulnerability": { + "CVSS": [], + "Description": "", + "FixedIn": [ + { + "Name": "libexif", + "NamespaceName": "ol:8", + "Version": "0:0.6.21-17.el8_2", + "VersionFormat": "rpm" + }, + { + "Name": "libexif-devel", + "NamespaceName": "ol:8", + "Version": "0:0.6.21-17.el8_2", + "VersionFormat": "rpm" + }, + { + "Name": "libexif-dummy", + "NamespaceName": "ol:8", + "Version": "None", + "VersionFormat": "rpm" + } + ], + "Link": "http://linux.oracle.com/errata/ELSA-2020-2550.html", + "Metadata": { + "CVE": [ + { + "Link": "http://linux.oracle.com/cve/CVE-2020-13112.html", + "Name": "CVE-2020-13112" + } + ], + "Issued": "2020-06-15", + "RefId": "ELSA-2020-2550" + }, + "Name": "ELSA-2020-2550", + "NamespaceName": "ol:8", + "Severity": "Medium" + } + } +] \ No newline at end of file diff --git a/pkg/process/v5/transformers/os/test-fixtures/rhel-8-modules.json b/pkg/process/v5/transformers/os/test-fixtures/rhel-8-modules.json new file mode 100644 index 00000000..c0400ad5 --- /dev/null +++ b/pkg/process/v5/transformers/os/test-fixtures/rhel-8-modules.json @@ -0,0 +1,75 @@ +[ + { + "Vulnerability": { + "CVSS": [ + { + "base_metrics": { + "base_score": 7.1, + "base_severity": "High", + "exploitability_score": 1.2, + "impact_score": 5.9 + }, + "status": "verified", + "vector_string": "CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:U/C:H/I:H/A:H", + "version": "3.1" + } + ], + "Description": "A flaw was found in PostgreSQL, where some PostgreSQL extensions did not use the search_path safely in their installation script. This flaw allows an attacker with sufficient privileges to trick an administrator into executing a specially crafted script during the extension's installation or update. The highest threat from this vulnerability is to confidentiality, integrity, as well as system availability.", + "FixedIn": [ + { + "Module": "postgresql:10", + "Name": "postgresql", + "NamespaceName": "rhel:8", + "VendorAdvisory": { + "AdvisorySummary": [ + { + "ID": "RHSA-2020:3669", + "Link": "https://access.redhat.com/errata/RHSA-2020:3669" + } + ], + "NoAdvisory": false + }, + "Version": "0:10.14-1.module+el8.2.0+7801+be0fed80", + "VersionFormat": "rpm" + }, + { + "Module": "postgresql:12", + "Name": "postgresql", + "NamespaceName": "rhel:8", + "VendorAdvisory": { + "AdvisorySummary": [ + { + "ID": "RHSA-2020:5620", + "Link": "https://access.redhat.com/errata/RHSA-2020:5620" + } + ], + "NoAdvisory": false + }, + "Version": "0:12.5-1.module+el8.3.0+9042+664538f4", + "VersionFormat": "rpm" + }, + { + "Module": "postgresql:9.6", + "Name": "postgresql", + "NamespaceName": "rhel:8", + "VendorAdvisory": { + "AdvisorySummary": [ + { + "ID": "RHSA-2020:5619", + "Link": "https://access.redhat.com/errata/RHSA-2020:5619" + } + ], + "NoAdvisory": false + }, + "Version": "0:9.6.20-1.module+el8.3.0+8938+7f0e88b6", + "VersionFormat": "rpm" + } + ], + "Link": "https://access.redhat.com/security/cve/CVE-2020-14350", + "Metadata": {}, + "Name": "CVE-2020-14350", + "NamespaceName": "rhel:8", + "Severity": "Medium" + } + } +] \ No newline at end of file diff --git a/pkg/process/v5/transformers/os/test-fixtures/rhel-8.json b/pkg/process/v5/transformers/os/test-fixtures/rhel-8.json new file mode 100644 index 00000000..2779708c --- /dev/null +++ b/pkg/process/v5/transformers/os/test-fixtures/rhel-8.json @@ -0,0 +1,57 @@ +[ + { + "Vulnerability": { + "CVSS": [ + { + "base_metrics": { + "base_score": 8.8, + "base_severity": "High", + "exploitability_score": 2.8, + "impact_score": 5.9 + }, + "status": "verified", + "vector_string": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H", + "version": "3.1" + } + ], + "Description": "A flaw was found in Mozilla Firefox. A race condition can occur while running the nsDocShell destructor causing a use-after-free memory issue. The highest threat from this vulnerability is to data confidentiality and integrity as well as system availability.", + "FixedIn": [ + { + "Name": "firefox", + "NamespaceName": "rhel:8", + "VendorAdvisory": { + "AdvisorySummary": [ + { + "ID": "RHSA-2020:1341", + "Link": "https://access.redhat.com/errata/RHSA-2020:1341" + } + ], + "NoAdvisory": false + }, + "Version": "0:68.6.1-1.el8_1", + "VersionFormat": "rpm" + }, + { + "Name": "thunderbird", + "NamespaceName": "rhel:8", + "VendorAdvisory": { + "AdvisorySummary": [ + { + "ID": "RHSA-2020:1495", + "Link": "https://access.redhat.com/errata/RHSA-2020:1495" + } + ], + "NoAdvisory": false + }, + "Version": "0:68.7.0-1.el8_1", + "VersionFormat": "rpm" + } + ], + "Link": "https://access.redhat.com/security/cve/CVE-2020-6819", + "Metadata": {}, + "Name": "CVE-2020-6819", + "NamespaceName": "rhel:8", + "Severity": "Critical" + } + } +] \ No newline at end of file diff --git a/pkg/process/v5/transformers/os/test-fixtures/unmarshal-test.json b/pkg/process/v5/transformers/os/test-fixtures/unmarshal-test.json new file mode 100644 index 00000000..edc6d25b --- /dev/null +++ b/pkg/process/v5/transformers/os/test-fixtures/unmarshal-test.json @@ -0,0 +1,104 @@ +[ + { + "Vulnerability": { + "Description": "", + "FixedIn": [ + { + "Name": "389-ds-base", + "NamespaceName": "amzn:2", + "Version": "1.3.8.4-15.amzn2.0.1", + "VersionFormat": "rpm" + }, + { + "Name": "389-ds-base-debuginfo", + "NamespaceName": "amzn:2", + "Version": "1.3.8.4-15.amzn2.0.1", + "VersionFormat": "rpm" + }, + { + "Name": "389-ds-base-devel", + "NamespaceName": "amzn:2", + "Version": "1.3.8.4-15.amzn2.0.1", + "VersionFormat": "rpm" + }, + { + "Name": "389-ds-base-libs", + "NamespaceName": "amzn:2", + "Version": "1.3.8.4-15.amzn2.0.1", + "VersionFormat": "rpm" + }, + { + "Name": "389-ds-base-snmp", + "NamespaceName": "amzn:2", + "Version": "1.3.8.4-15.amzn2.0.1", + "VersionFormat": "rpm" + } + ], + "Link": "https://alas.aws.amazon.com/AL2/ALAS-2018-1106.html", + "Metadata": { + "CVE": [ + {"Name": "CVE-2018-14648"} + ] + }, + "Name": "ALAS-2018-1106", + "NamespaceName": "amzn:2", + "Severity": "Medium" + } + }, + { + "Vulnerability": { + "Description": "", + "FixedIn": [ + { + "Name": "kernel-livepatch-4.14.173-137.228", + "NamespaceName": "amzn:2", + "Version": "1.0-3.amzn2", + "VersionFormat": "rpm" + }, + { + "Name": "kernel-livepatch-4.14.173-137.228-debuginfo", + "NamespaceName": "amzn:2", + "Version": "1.0-3.amzn2", + "VersionFormat": "rpm" + } + ], + "Link": "https://alas.aws.amazon.com/AL2/ALASLIVEPATCH-2020-012.html", + "Metadata": { + "CVE": [ + {"Name": "CVE-2020-12657"} + ] + }, + "Name": "ALASLIVEPATCH-2020-012", + "NamespaceName": "amzn:2", + "Severity": "High" + } + }, + { + "Vulnerability": { + "Description": "", + "FixedIn": [ + { + "Name": "kernel-livepatch-4.14.171-136.231", + "NamespaceName": "amzn:2", + "Version": "1.0-5.amzn2", + "VersionFormat": "rpm" + }, + { + "Name": "kernel-livepatch-4.14.171-136.231-debuginfo", + "NamespaceName": "amzn:2", + "Version": "1.0-5.amzn2", + "VersionFormat": "rpm" + } + ], + "Link": "https://alas.aws.amazon.com/AL2/ALASLIVEPATCH-2020-011.html", + "Metadata": { + "CVE": [ + {"Name": "CVE-2020-12657"} + ] + }, + "Name": "ALASLIVEPATCH-2020-011", + "NamespaceName": "amzn:2", + "Severity": "High" + } + } +] \ No newline at end of file diff --git a/pkg/process/v5/transformers/os/transform.go b/pkg/process/v5/transformers/os/transform.go new file mode 100644 index 00000000..1c4c005c --- /dev/null +++ b/pkg/process/v5/transformers/os/transform.go @@ -0,0 +1,210 @@ +package os + +import ( + "fmt" + "strings" + + "github.com/anchore/grype/grype/db/v5/pkg/qualifier" + "github.com/anchore/grype/grype/db/v5/pkg/qualifier/rpmmodularity" + + "github.com/anchore/grype/grype/db/v5/namespace" + "github.com/anchore/grype/grype/distro" + + "github.com/anchore/grype-db/pkg/data" + "github.com/anchore/grype-db/pkg/process/common" + "github.com/anchore/grype-db/pkg/process/v5/transformers" + "github.com/anchore/grype-db/pkg/provider/unmarshal" + grypeDB "github.com/anchore/grype/grype/db/v5" +) + +func buildGrypeNamespace(group string) (namespace.Namespace, error) { + feedGroupComponents := strings.Split(group, ":") + + if len(feedGroupComponents) < 2 { + return nil, fmt.Errorf("unable to determine grype namespace for enterprise namespace=%s", group) + } + + // Currently known enterprise feed groups are expected to be of the form {distroID}:{version} + feedGroupDistroID := feedGroupComponents[0] + d, ok := distro.IDMapping[feedGroupDistroID] + if !ok { + return nil, fmt.Errorf("unable to determine grype namespace for enterprise namespace=%s", group) + } + + providerName := d.String() + + switch d { + case distro.OracleLinux: + providerName = "oracle" + case distro.AmazonLinux: + providerName = "amazon" + } + + ns, err := namespace.FromString(fmt.Sprintf("%s:distro:%s:%s", providerName, d.String(), feedGroupComponents[1])) + + if err != nil { + return nil, err + } + + return ns, nil +} + +func Transform(vulnerability unmarshal.OSVulnerability) ([]data.Entry, error) { + var allVulns []grypeDB.Vulnerability + + // TODO: stop capturing record source in the vulnerability metadata record (now that feed groups are not real) + recordSource := fmt.Sprintf("vulnerabilities:%s", vulnerability.Vulnerability.NamespaceName) + + grypeNamespace, err := buildGrypeNamespace(vulnerability.Vulnerability.NamespaceName) + if err != nil { + return nil, err + } + + entryNamespace := grypeNamespace.String() + + // there may be multiple packages indicated within the FixedIn field, we should make + // separate vulnerability entries (one for each name|namespace combo) while merging + // constraint ranges as they are found. + for idx, fixedInEntry := range vulnerability.Vulnerability.FixedIn { + constraint, err := enforceConstraint(fixedInEntry.Version, fixedInEntry.VersionFormat) + if err != nil { + return nil, err + } + + var qualifiers []qualifier.Qualifier + + if fixedInEntry.Module != nil { + qualifiers = []qualifier.Qualifier{rpmmodularity.Qualifier{ + Kind: "rpm-modularity", + Module: *fixedInEntry.Module, + }} + } + + // create vulnerability entry + allVulns = append(allVulns, grypeDB.Vulnerability{ + ID: vulnerability.Vulnerability.Name, + PackageQualifiers: qualifiers, + VersionConstraint: constraint, + VersionFormat: fixedInEntry.VersionFormat, + PackageName: grypeNamespace.Resolver().Normalize(fixedInEntry.Name), + Namespace: entryNamespace, + RelatedVulnerabilities: getRelatedVulnerabilities(vulnerability), + Fix: getFix(vulnerability, idx), + Advisories: getAdvisories(vulnerability, idx), + }) + } + + // create vulnerability metadata entry (a single entry keyed off of the vulnerability ID) + metadata := grypeDB.VulnerabilityMetadata{ + ID: vulnerability.Vulnerability.Name, + Namespace: entryNamespace, + DataSource: vulnerability.Vulnerability.Link, + RecordSource: recordSource, + Severity: vulnerability.Vulnerability.Severity, + URLs: getLinks(vulnerability), + Description: vulnerability.Vulnerability.Description, + Cvss: getCvss(vulnerability), + } + + return transformers.NewEntries(allVulns, metadata), nil +} + +func getLinks(entry unmarshal.OSVulnerability) []string { + // find all URLs related to the vulnerability + links := []string{entry.Vulnerability.Link} + if entry.Vulnerability.Metadata.CVE != nil { + for _, cve := range entry.Vulnerability.Metadata.CVE { + if cve.Link != "" { + links = append(links, cve.Link) + } + } + } + return links +} + +func getCvss(entry unmarshal.OSVulnerability) (cvss []grypeDB.Cvss) { + for _, vendorCvss := range entry.Vulnerability.CVSS { + cvss = append(cvss, grypeDB.Cvss{ + Version: vendorCvss.Version, + Vector: vendorCvss.VectorString, + Metrics: grypeDB.NewCvssMetrics( + vendorCvss.BaseMetrics.BaseScore, + vendorCvss.BaseMetrics.ExploitabilityScore, + vendorCvss.BaseMetrics.ImpactScore, + ), + VendorMetadata: transformers.VendorBaseMetrics{ + BaseSeverity: vendorCvss.BaseMetrics.BaseSeverity, + Status: vendorCvss.Status, + }, + }) + } + return cvss +} + +func getAdvisories(entry unmarshal.OSVulnerability, idx int) (advisories []grypeDB.Advisory) { + fixedInEntry := entry.Vulnerability.FixedIn[idx] + + for _, advisory := range fixedInEntry.VendorAdvisory.AdvisorySummary { + advisories = append(advisories, grypeDB.Advisory{ + ID: advisory.ID, + Link: advisory.Link, + }) + } + return advisories +} + +func getFix(entry unmarshal.OSVulnerability, idx int) grypeDB.Fix { + fixedInEntry := entry.Vulnerability.FixedIn[idx] + + var fixedInVersions []string + fixedInVersion := common.CleanFixedInVersion(fixedInEntry.Version) + if fixedInVersion != "" { + fixedInVersions = append(fixedInVersions, fixedInVersion) + } + + fixState := grypeDB.NotFixedState + if len(fixedInVersions) > 0 { + fixState = grypeDB.FixedState + } else if fixedInEntry.VendorAdvisory.NoAdvisory { + fixState = grypeDB.WontFixState + } + + return grypeDB.Fix{ + Versions: fixedInVersions, + State: fixState, + } +} + +func getRelatedVulnerabilities(entry unmarshal.OSVulnerability) (vulns []grypeDB.VulnerabilityReference) { + // associate related vulnerabilities from the NVD namespace + if strings.HasPrefix(entry.Vulnerability.Name, "CVE") { + vulns = append(vulns, grypeDB.VulnerabilityReference{ + ID: entry.Vulnerability.Name, + Namespace: "nvd:cpe", + }) + } + + // note: an example of multiple CVEs for a record is centos:5 RHSA-2007:0055 which maps to CVE-2007-0002 and CVE-2007-1466 + for _, ref := range entry.Vulnerability.Metadata.CVE { + vulns = append(vulns, grypeDB.VulnerabilityReference{ + ID: ref.Name, + Namespace: "nvd:cpe", + }) + } + return vulns +} + +func enforceConstraint(constraint, format string) (string, error) { + constraint = common.CleanConstraint(constraint) + if len(constraint) == 0 { + return "", nil + } + switch strings.ToLower(format) { + case "dpkg", "rpm", "apk": + // the passed constraint is a fixed version + return fmt.Sprintf("< %s", constraint), nil + case "semver": + return common.EnforceSemVerConstraint(constraint), nil + } + return "", fmt.Errorf("unable to enforce constraint='%s' format='%s'", constraint, format) +} diff --git a/pkg/process/v5/transformers/os/transform_test.go b/pkg/process/v5/transformers/os/transform_test.go new file mode 100644 index 00000000..caf3ae3d --- /dev/null +++ b/pkg/process/v5/transformers/os/transform_test.go @@ -0,0 +1,711 @@ +package os + +import ( + "github.com/anchore/grype/grype/db/v5/pkg/qualifier" + "github.com/anchore/grype/grype/db/v5/pkg/qualifier/rpmmodularity" + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/require" + "os" + "testing" + + testUtils "github.com/anchore/grype-db/pkg/process/tests" + "github.com/anchore/grype-db/pkg/process/v5/transformers" + "github.com/anchore/grype-db/pkg/provider/unmarshal" + grypeDB "github.com/anchore/grype/grype/db/v5" + "github.com/stretchr/testify/assert" +) + +func TestUnmarshalOSVulnerabilitiesEntries(t *testing.T) { + f, err := os.Open("test-fixtures/unmarshal-test.json") + require.NoError(t, err) + defer testUtils.CloseFile(f) + + entries, err := unmarshal.OSVulnerabilityEntries(f) + require.NoError(t, err) + + assert.Len(t, entries, 3) + +} + +func TestParseVulnerabilitiesEntry(t *testing.T) { + tests := []struct { + name string + numEntries int + fixture string + vulns []grypeDB.Vulnerability + metadata grypeDB.VulnerabilityMetadata + }{ + { + name: "Amazon", + numEntries: 1, + fixture: "test-fixtures/amzn.json", + vulns: []grypeDB.Vulnerability{ + { + ID: "ALAS-2018-1106", + VersionConstraint: "< 1.3.8.4-15.amzn2.0.1", + VersionFormat: "rpm", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2018-14648", + Namespace: "nvd:cpe", + }, + }, + PackageName: "389-ds-base", + Namespace: "amazon:distro:amazonlinux:2", + Fix: grypeDB.Fix{ + Versions: []string{"1.3.8.4-15.amzn2.0.1"}, + State: grypeDB.FixedState, + }, + }, + { + ID: "ALAS-2018-1106", + VersionConstraint: "< 1.3.8.4-15.amzn2.0.1", + VersionFormat: "rpm", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2018-14648", + Namespace: "nvd:cpe", + }, + }, + PackageName: "389-ds-base-debuginfo", + Namespace: "amazon:distro:amazonlinux:2", + Fix: grypeDB.Fix{ + Versions: []string{"1.3.8.4-15.amzn2.0.1"}, + State: grypeDB.FixedState, + }, + }, + { + ID: "ALAS-2018-1106", + VersionConstraint: "< 1.3.8.4-15.amzn2.0.1", + VersionFormat: "rpm", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2018-14648", + Namespace: "nvd:cpe", + }, + }, + PackageName: "389-ds-base-devel", + Namespace: "amazon:distro:amazonlinux:2", + Fix: grypeDB.Fix{ + Versions: []string{"1.3.8.4-15.amzn2.0.1"}, + State: grypeDB.FixedState, + }, + }, + { + ID: "ALAS-2018-1106", + VersionConstraint: "< 1.3.8.4-15.amzn2.0.1", + VersionFormat: "rpm", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2018-14648", + Namespace: "nvd:cpe", + }, + }, + PackageName: "389-ds-base-libs", + Namespace: "amazon:distro:amazonlinux:2", + Fix: grypeDB.Fix{ + Versions: []string{"1.3.8.4-15.amzn2.0.1"}, + State: grypeDB.FixedState, + }, + }, + { + ID: "ALAS-2018-1106", + VersionConstraint: "< 1.3.8.4-15.amzn2.0.1", + VersionFormat: "rpm", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2018-14648", + Namespace: "nvd:cpe", + }, + }, + PackageName: "389-ds-base-snmp", + Namespace: "amazon:distro:amazonlinux:2", + Fix: grypeDB.Fix{ + Versions: []string{"1.3.8.4-15.amzn2.0.1"}, + State: grypeDB.FixedState, + }, + }, + }, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "ALAS-2018-1106", + Namespace: "amazon:distro:amazonlinux:2", + DataSource: "https://alas.aws.amazon.com/AL2/ALAS-2018-1106.html", + RecordSource: "vulnerabilities:amzn:2", + Severity: "Medium", + URLs: []string{"https://alas.aws.amazon.com/AL2/ALAS-2018-1106.html"}, + }, + }, + { + name: "Debian", + numEntries: 1, + fixture: "test-fixtures/debian-8.json", + vulns: []grypeDB.Vulnerability{ + { + ID: "CVE-2008-7220", + PackageName: "asterisk", + VersionConstraint: "< 1:1.6.2.0~rc3-1", + VersionFormat: "dpkg", + Namespace: "debian:distro:debian:8", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2008-7220", + Namespace: "nvd:cpe", + }, + }, + Fix: grypeDB.Fix{ + Versions: []string{"1:1.6.2.0~rc3-1"}, + State: grypeDB.FixedState, + }, + }, + { + ID: "CVE-2008-7220", + PackageName: "auth2db", + VersionConstraint: "< 0.2.5-2+dfsg-1", + VersionFormat: "dpkg", + Namespace: "debian:distro:debian:8", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2008-7220", + Namespace: "nvd:cpe", + }, + }, + Fix: grypeDB.Fix{ + Versions: []string{"0.2.5-2+dfsg-1"}, + State: grypeDB.FixedState, + }, + }, + { + ID: "CVE-2008-7220", + PackageName: "exaile", + VersionConstraint: "< 0.2.14+debian-2.2", + VersionFormat: "dpkg", + Namespace: "debian:distro:debian:8", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2008-7220", + Namespace: "nvd:cpe", + }, + }, + Fix: grypeDB.Fix{ + Versions: []string{"0.2.14+debian-2.2"}, + State: grypeDB.FixedState, + }, + }, + { + ID: "CVE-2008-7220", + PackageName: "wordpress", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2008-7220", + Namespace: "nvd:cpe", + }, + }, + Fix: grypeDB.Fix{ + State: grypeDB.NotFixedState, + }, + VersionConstraint: "", + VersionFormat: "dpkg", + Namespace: "debian:distro:debian:8", + }, + }, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "CVE-2008-7220", + Namespace: "debian:distro:debian:8", + DataSource: "https://security-tracker.debian.org/tracker/CVE-2008-7220", + RecordSource: "vulnerabilities:debian:8", + Severity: "High", + URLs: []string{"https://security-tracker.debian.org/tracker/CVE-2008-7220"}, + Description: "", + }, + }, + { + name: "RHEL", + numEntries: 1, + fixture: "test-fixtures/rhel-8.json", + vulns: []grypeDB.Vulnerability{ + { + ID: "CVE-2020-6819", + PackageName: "firefox", + VersionConstraint: "< 0:68.6.1-1.el8_1", + VersionFormat: "rpm", + Namespace: "redhat:distro:redhat:8", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2020-6819", + Namespace: "nvd:cpe", + }, + }, + Fix: grypeDB.Fix{ + Versions: []string{"0:68.6.1-1.el8_1"}, + State: grypeDB.FixedState, + }, + Advisories: []grypeDB.Advisory{ + { + ID: "RHSA-2020:1341", + Link: "https://access.redhat.com/errata/RHSA-2020:1341", + }, + }, + }, + { + ID: "CVE-2020-6819", + PackageName: "thunderbird", + VersionConstraint: "< 0:68.7.0-1.el8_1", + VersionFormat: "rpm", + Namespace: "redhat:distro:redhat:8", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2020-6819", + Namespace: "nvd:cpe", + }, + }, + Fix: grypeDB.Fix{ + Versions: []string{"0:68.7.0-1.el8_1"}, + State: grypeDB.FixedState, + }, + Advisories: []grypeDB.Advisory{ + { + ID: "RHSA-2020:1495", + Link: "https://access.redhat.com/errata/RHSA-2020:1495", + }, + }, + }, + }, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "CVE-2020-6819", + DataSource: "https://access.redhat.com/security/cve/CVE-2020-6819", + Namespace: "redhat:distro:redhat:8", + RecordSource: "vulnerabilities:rhel:8", + Severity: "Critical", + URLs: []string{"https://access.redhat.com/security/cve/CVE-2020-6819"}, + Description: "A flaw was found in Mozilla Firefox. A race condition can occur while running the nsDocShell destructor causing a use-after-free memory issue. The highest threat from this vulnerability is to data confidentiality and integrity as well as system availability.", + Cvss: []grypeDB.Cvss{ + { + Version: "3.1", + Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H", + Metrics: grypeDB.NewCvssMetrics( + 8.8, + 2.8, + 5.9, + ), + VendorMetadata: transformers.VendorBaseMetrics{ + Status: "verified", + BaseSeverity: "High", + }, + }, + }, + }, + }, + { + name: "RHEL with modularity", + numEntries: 1, + fixture: "test-fixtures/rhel-8-modules.json", + vulns: []grypeDB.Vulnerability{ + { + ID: "CVE-2020-14350", + PackageName: "postgresql", + PackageQualifiers: []qualifier.Qualifier{rpmmodularity.Qualifier{ + Kind: "rpm-modularity", + Module: "postgresql:10", + }}, + VersionConstraint: "< 0:10.14-1.module+el8.2.0+7801+be0fed80", + VersionFormat: "rpm", + Namespace: "redhat:distro:redhat:8", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2020-14350", + Namespace: "nvd:cpe", + }, + }, + Fix: grypeDB.Fix{ + Versions: []string{"0:10.14-1.module+el8.2.0+7801+be0fed80"}, + State: grypeDB.FixedState, + }, + Advisories: []grypeDB.Advisory{ + { + ID: "RHSA-2020:3669", + Link: "https://access.redhat.com/errata/RHSA-2020:3669", + }, + }, + }, + { + ID: "CVE-2020-14350", + PackageName: "postgresql", + PackageQualifiers: []qualifier.Qualifier{rpmmodularity.Qualifier{ + Kind: "rpm-modularity", + Module: "postgresql:12", + }}, + VersionConstraint: "< 0:12.5-1.module+el8.3.0+9042+664538f4", + VersionFormat: "rpm", + Namespace: "redhat:distro:redhat:8", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2020-14350", + Namespace: "nvd:cpe", + }, + }, + Fix: grypeDB.Fix{ + Versions: []string{"0:12.5-1.module+el8.3.0+9042+664538f4"}, + State: grypeDB.FixedState, + }, + Advisories: []grypeDB.Advisory{ + { + ID: "RHSA-2020:5620", + Link: "https://access.redhat.com/errata/RHSA-2020:5620", + }, + }, + }, + { + ID: "CVE-2020-14350", + PackageName: "postgresql", + PackageQualifiers: []qualifier.Qualifier{rpmmodularity.Qualifier{ + Kind: "rpm-modularity", + Module: "postgresql:9.6", + }}, + VersionConstraint: "< 0:9.6.20-1.module+el8.3.0+8938+7f0e88b6", + VersionFormat: "rpm", + Namespace: "redhat:distro:redhat:8", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2020-14350", + Namespace: "nvd:cpe", + }, + }, + Fix: grypeDB.Fix{ + Versions: []string{"0:9.6.20-1.module+el8.3.0+8938+7f0e88b6"}, + State: grypeDB.FixedState, + }, + Advisories: []grypeDB.Advisory{ + { + ID: "RHSA-2020:5619", + Link: "https://access.redhat.com/errata/RHSA-2020:5619", + }, + }, + }, + }, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "CVE-2020-14350", + DataSource: "https://access.redhat.com/security/cve/CVE-2020-14350", + Namespace: "redhat:distro:redhat:8", + RecordSource: "vulnerabilities:rhel:8", + Severity: "Medium", + URLs: []string{"https://access.redhat.com/security/cve/CVE-2020-14350"}, + Description: "A flaw was found in PostgreSQL, where some PostgreSQL extensions did not use the search_path safely in their installation script. This flaw allows an attacker with sufficient privileges to trick an administrator into executing a specially crafted script during the extension's installation or update. The highest threat from this vulnerability is to confidentiality, integrity, as well as system availability.", + Cvss: []grypeDB.Cvss{ + { + Version: "3.1", + Vector: "CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:U/C:H/I:H/A:H", + Metrics: grypeDB.NewCvssMetrics( + 7.1, + 1.2, + 5.9, + ), + VendorMetadata: transformers.VendorBaseMetrics{ + Status: "verified", + BaseSeverity: "High", + }, + }, + }, + }, + }, + { + name: "Alpine", + numEntries: 1, + fixture: "test-fixtures/alpine-3.9.json", + vulns: []grypeDB.Vulnerability{ + { + ID: "CVE-2018-19967", + PackageName: "xen", + VersionConstraint: "< 4.11.1-r0", + VersionFormat: "apk", + Namespace: "alpine:distro:alpine:3.9", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2018-19967", + Namespace: "nvd:cpe", + }, + }, + Fix: grypeDB.Fix{ + Versions: []string{"4.11.1-r0"}, + State: grypeDB.FixedState, + }, + }, + }, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "CVE-2018-19967", + DataSource: "http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-19967", + Namespace: "alpine:distro:alpine:3.9", + RecordSource: "vulnerabilities:alpine:3.9", + Severity: "Medium", + URLs: []string{"http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-19967"}, + Description: "", + }, + }, + { + name: "Oracle", + numEntries: 1, + fixture: "test-fixtures/ol-8.json", + vulns: []grypeDB.Vulnerability{ + { + ID: "ELSA-2020-2550", + PackageName: "libexif", + VersionConstraint: "< 0:0.6.21-17.el8_2", + VersionFormat: "rpm", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2020-13112", + Namespace: "nvd:cpe", + }, + }, + Namespace: "oracle:distro:oraclelinux:8", + Fix: grypeDB.Fix{ + Versions: []string{"0:0.6.21-17.el8_2"}, + State: grypeDB.FixedState, + }, + }, + { + ID: "ELSA-2020-2550", + PackageName: "libexif-devel", + VersionConstraint: "< 0:0.6.21-17.el8_2", + VersionFormat: "rpm", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2020-13112", + Namespace: "nvd:cpe", + }, + }, + Namespace: "oracle:distro:oraclelinux:8", + Fix: grypeDB.Fix{ + Versions: []string{"0:0.6.21-17.el8_2"}, + State: grypeDB.FixedState, + }, + }, + { + ID: "ELSA-2020-2550", + PackageName: "libexif-dummy", + VersionConstraint: "", + VersionFormat: "rpm", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2020-13112", + Namespace: "nvd:cpe", + }, + }, + Namespace: "oracle:distro:oraclelinux:8", + Fix: grypeDB.Fix{ + Versions: nil, + State: grypeDB.NotFixedState, + }, + }, + }, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "ELSA-2020-2550", + DataSource: "http://linux.oracle.com/errata/ELSA-2020-2550.html", + Namespace: "oracle:distro:oraclelinux:8", + RecordSource: "vulnerabilities:ol:8", + Severity: "Medium", + URLs: []string{"http://linux.oracle.com/errata/ELSA-2020-2550.html", "http://linux.oracle.com/cve/CVE-2020-13112.html"}, + }, + }, + { + name: "Oracle Linux 8 with modularity", + numEntries: 1, + fixture: "test-fixtures/ol-8-modules.json", + vulns: []grypeDB.Vulnerability{ + { + ID: "CVE-2020-14350", + PackageName: "postgresql", + PackageQualifiers: []qualifier.Qualifier{rpmmodularity.Qualifier{ + Kind: "rpm-modularity", + Module: "postgresql:10", + }}, + VersionConstraint: "< 0:10.14-1.module+el8.2.0+7801+be0fed80", + VersionFormat: "rpm", + Namespace: "oracle:distro:oraclelinux:8", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2020-14350", + Namespace: "nvd:cpe", + }, + }, + Fix: grypeDB.Fix{ + Versions: []string{"0:10.14-1.module+el8.2.0+7801+be0fed80"}, + State: grypeDB.FixedState, + }, + }, + { + ID: "CVE-2020-14350", + PackageName: "postgresql", + PackageQualifiers: []qualifier.Qualifier{rpmmodularity.Qualifier{ + Kind: "rpm-modularity", + Module: "postgresql:12", + }}, + VersionConstraint: "< 0:12.5-1.module+el8.3.0+9042+664538f4", + VersionFormat: "rpm", + Namespace: "oracle:distro:oraclelinux:8", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2020-14350", + Namespace: "nvd:cpe", + }, + }, + Fix: grypeDB.Fix{ + Versions: []string{"0:12.5-1.module+el8.3.0+9042+664538f4"}, + State: grypeDB.FixedState, + }, + }, + { + ID: "CVE-2020-14350", + PackageName: "postgresql", + PackageQualifiers: []qualifier.Qualifier{rpmmodularity.Qualifier{ + Kind: "rpm-modularity", + Module: "postgresql:9.6", + }}, + VersionConstraint: "< 0:9.6.20-1.module+el8.3.0+8938+7f0e88b6", + VersionFormat: "rpm", + Namespace: "oracle:distro:oraclelinux:8", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2020-14350", + Namespace: "nvd:cpe", + }, + }, + Fix: grypeDB.Fix{ + Versions: []string{"0:9.6.20-1.module+el8.3.0+8938+7f0e88b6"}, + State: grypeDB.FixedState, + }, + }, + }, + metadata: grypeDB.VulnerabilityMetadata{ + ID: "CVE-2020-14350", + DataSource: "https://access.redhat.com/security/cve/CVE-2020-14350", + Namespace: "oracle:distro:oraclelinux:8", + RecordSource: "vulnerabilities:ol:8", + Severity: "Medium", + URLs: []string{"https://access.redhat.com/security/cve/CVE-2020-14350"}, + Description: "A flaw was found in PostgreSQL, where some PostgreSQL extensions did not use the search_path safely in their installation script. This flaw allows an attacker with sufficient privileges to trick an administrator into executing a specially crafted script during the extension's installation or update. The highest threat from this vulnerability is to confidentiality, integrity, as well as system availability.", + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + f, err := os.Open(test.fixture) + require.NoError(t, err) + t.Cleanup(func() { + assert.NoError(t, f.Close()) + }) + + entries, err := unmarshal.OSVulnerabilityEntries(f) + assert.NoError(t, err) + assert.Len(t, entries, 1) + + entry := entries[0] + + dataEntries, err := Transform(entry) + assert.NoError(t, err) + + var vulns []grypeDB.Vulnerability + for _, entry := range dataEntries { + switch vuln := entry.Data.(type) { + case grypeDB.Vulnerability: + vulns = append(vulns, vuln) + case grypeDB.VulnerabilityMetadata: + assert.Equal(t, test.metadata, vuln) + default: + t.Fatalf("unexpected condition: data entry does not have a vulnerability or a metadata: %+v", vuln) + } + } + + if diff := cmp.Diff(test.vulns, vulns); diff != "" { + t.Errorf("vulnerabilities do not match (-want +got):\n%s", diff) + } + + }) + } + +} + +func TestParseVulnerabilitiesAllEntries(t *testing.T) { + tests := []struct { + name string + numEntries int + fixture string + vulns []grypeDB.Vulnerability + }{ + { + name: "Debian", + numEntries: 2, + fixture: "test-fixtures/debian-8-multiple-entries-for-same-package.json", + vulns: []grypeDB.Vulnerability{ + { + ID: "CVE-2011-4623", + PackageName: "rsyslog", + VersionConstraint: "< 5.7.4-1", + VersionFormat: "dpkg", + Namespace: "debian:distro:debian:8", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2011-4623", + Namespace: "nvd:cpe", + }, + }, + Fix: grypeDB.Fix{ + Versions: []string{"5.7.4-1"}, + State: grypeDB.FixedState, + }, + }, + { + ID: "CVE-2008-5618", + PackageName: "rsyslog", + VersionConstraint: "< 3.18.6-1", + VersionFormat: "dpkg", + Namespace: "debian:distro:debian:8", + RelatedVulnerabilities: []grypeDB.VulnerabilityReference{ + { + ID: "CVE-2008-5618", + Namespace: "nvd:cpe", + }, + }, + Fix: grypeDB.Fix{ + Versions: []string{"3.18.6-1"}, + State: grypeDB.FixedState, + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + f, err := os.Open(test.fixture) + require.NoError(t, err) + t.Cleanup(func() { + assert.NoError(t, f.Close()) + }) + + entries, err := unmarshal.OSVulnerabilityEntries(f) + assert.NoError(t, err) + assert.Len(t, entries, len(test.vulns)) + + var vulns []grypeDB.Vulnerability + for _, entry := range entries { + dataEntries, err := Transform(entry) + assert.NoError(t, err) + + for _, entry := range dataEntries { + switch vuln := entry.Data.(type) { + case grypeDB.Vulnerability: + vulns = append(vulns, vuln) + case grypeDB.VulnerabilityMetadata: + default: + t.Fatalf("unexpected condition: data entry does not have a vulnerability or a metadata: %+v", vuln) + } + } + } + + if diff := cmp.Diff(test.vulns, vulns); diff != "" { + t.Errorf("vulnerabilities do not match (-want +got):\n%s", diff) + } + }) + } +} diff --git a/pkg/process/v5/transformers/vulnerability_metadata.go b/pkg/process/v5/transformers/vulnerability_metadata.go new file mode 100644 index 00000000..c40e9f6b --- /dev/null +++ b/pkg/process/v5/transformers/vulnerability_metadata.go @@ -0,0 +1,8 @@ +package transformers + +// VendorBaseMetrics captures extra metrics that do not fit into a common CVSS +// struct, like Status and BaseSeverity +type VendorBaseMetrics struct { + BaseSeverity string `json:"base_severity"` + Status string `json:"status"` +} diff --git a/pkg/process/v5/writer.go b/pkg/process/v5/writer.go new file mode 100644 index 00000000..aa2c6a8d --- /dev/null +++ b/pkg/process/v5/writer.go @@ -0,0 +1,133 @@ +package v5 + +import ( + "crypto/sha256" + "fmt" + "path" + "path/filepath" + "strings" + "time" + + "github.com/anchore/grype-db/internal/log" + + "github.com/anchore/grype-db/internal/file" + "github.com/anchore/grype-db/pkg/data" + "github.com/anchore/grype/grype/db" + grypeDB "github.com/anchore/grype/grype/db/v5" + grypeDBStore "github.com/anchore/grype/grype/db/v5/store" + "github.com/spf13/afero" +) + +// TODO: add NVDNamespace const to grype.db package? +const nvdNamespace = "nvd:cpe" + +var _ data.Writer = (*writer)(nil) + +type writer struct { + dbPath string + store grypeDB.Store +} + +func NewWriter(directory string, dataAge time.Time) (data.Writer, error) { + dbPath := path.Join(directory, grypeDB.VulnerabilityStoreFileName) + theStore, err := grypeDBStore.New(dbPath, true) + if err != nil { + return nil, fmt.Errorf("unable to create store: %w", err) + } + + if err := theStore.SetID(grypeDB.NewID(dataAge)); err != nil { + return nil, fmt.Errorf("unable to set DB ID: %w", err) + } + + return &writer{ + dbPath: dbPath, + store: theStore, + }, nil +} + +func (w writer) Write(entries ...data.Entry) error { + log.WithFields("records", len(entries)).Trace("writing records to DB") + for _, entry := range entries { + if entry.DBSchemaVersion != grypeDB.SchemaVersion { + return fmt.Errorf("wrong schema version: want %+v got %+v", grypeDB.SchemaVersion, entry.DBSchemaVersion) + } + + switch row := entry.Data.(type) { + case grypeDB.Vulnerability: + if err := w.store.AddVulnerability(row); err != nil { + return fmt.Errorf("unable to write vulnerability to store: %w", err) + } + case grypeDB.VulnerabilityMetadata: + normalizeSeverity(&row, w.store) + if err := w.store.AddVulnerabilityMetadata(row); err != nil { + return fmt.Errorf("unable to write vulnerability metadata to store: %w", err) + } + case grypeDB.VulnerabilityMatchExclusion: + if err := w.store.AddVulnerabilityMatchExclusion(row); err != nil { + return fmt.Errorf("unable to write vulnerability match exclusion to store: %w", err) + } + default: + return fmt.Errorf("data entry is not of type vulnerability, vulnerability metadata, or exclusion: %T", row) + } + } + + return nil +} + +func (w writer) metadata() (*db.Metadata, error) { + hashStr, err := file.ContentDigest(afero.NewOsFs(), w.dbPath, sha256.New()) + if err != nil { + return nil, fmt.Errorf("failed to hash database file (%s): %w", w.dbPath, err) + } + + storeID, err := w.store.GetID() + if err != nil { + return nil, fmt.Errorf("failed to fetch store ID: %w", err) + } + + metadata := db.Metadata{ + Built: storeID.BuildTimestamp, + Version: storeID.SchemaVersion, + Checksum: "sha256:" + hashStr, + } + return &metadata, nil +} + +func (w writer) Close() error { + w.store.Close() + metadata, err := w.metadata() + if err != nil { + return err + } + + metadataPath := path.Join(filepath.Dir(w.dbPath), db.MetadataFileName) + + return metadata.Write(metadataPath) +} + +func normalizeSeverity(metadata *grypeDB.VulnerabilityMetadata, reader grypeDB.VulnerabilityMetadataStoreReader) { + if metadata.Severity != "" && strings.ToLower(metadata.Severity) != "unknown" { + return + } + if !strings.HasPrefix(strings.ToLower(metadata.ID), "cve") { + return + } + if strings.HasPrefix(metadata.Namespace, nvdNamespace) { + return + } + m, err := reader.GetVulnerabilityMetadata(metadata.ID, nvdNamespace) + if err != nil { + log.WithFields("id", metadata.ID, "error", err).Warn("error fetching vulnerability metadata from NVD namespace") + return + } + if m == nil { + log.WithFields("id", metadata.ID).Trace("unable to find vulnerability metadata from NVD namespace") + return + } + + newSeverity := string(data.ParseSeverity(m.Severity)) + + log.WithFields("id", metadata.ID, "namespace", metadata.Namespace, "sev-from", metadata.Severity, "sev-to", newSeverity).Trace("overriding irrelevant severity with data from NVD record") + + metadata.Severity = newSeverity +} diff --git a/pkg/process/v5/writer_test.go b/pkg/process/v5/writer_test.go new file mode 100644 index 00000000..32b814d3 --- /dev/null +++ b/pkg/process/v5/writer_test.go @@ -0,0 +1,116 @@ +package v5 + +import ( + "errors" + "github.com/anchore/grype-db/pkg/data" + grypeDB "github.com/anchore/grype/grype/db/v5" + "github.com/stretchr/testify/assert" + "testing" +) + +var _ grypeDB.VulnerabilityMetadataStoreReader = (*mockReader)(nil) + +type mockReader struct { + metadata *grypeDB.VulnerabilityMetadata + err error +} + +func newMockReader(sev string) *mockReader { + return &mockReader{ + metadata: &grypeDB.VulnerabilityMetadata{ + Severity: sev, + Namespace: "nvd", + }, + } +} + +func newDeadMockReader() *mockReader { + return &mockReader{ + err: errors.New("dead"), + } +} + +func (m mockReader) GetVulnerabilityMetadata(_, _ string) (*grypeDB.VulnerabilityMetadata, error) { + return m.metadata, m.err +} + +func (m mockReader) GetAllVulnerabilityMetadata() (*[]grypeDB.VulnerabilityMetadata, error) { + panic("implement me") +} + +func Test_normalizeSeverity(t *testing.T) { + + tests := []struct { + name string + initialSeverity string + namespace string + cveID string + reader grypeDB.VulnerabilityMetadataStoreReader + expected data.Severity + }{ + { + name: "skip missing metadata", + initialSeverity: "", + namespace: "test", + reader: &mockReader{}, + expected: "", + }, + { + name: "skip non-cve records metadata", + cveID: "GHSA-1234-1234-1234", + initialSeverity: "", + namespace: "test", + reader: newDeadMockReader(), // should not be used + expected: "", + }, + { + name: "override empty severity", + initialSeverity: "", + namespace: "test", + reader: newMockReader("low"), + expected: data.SeverityLow, + }, + { + name: "override unknown severity", + initialSeverity: "unknown", + namespace: "test", + reader: newMockReader("low"), + expected: data.SeverityLow, + }, + { + name: "ignore record with severity already set", + initialSeverity: "Low", + namespace: "test", + reader: newMockReader("critical"), // should not be used + expected: data.SeverityLow, + }, + { + name: "ignore nvd records", + initialSeverity: "Low", + namespace: "nvd:cpe", + reader: newDeadMockReader(), // should not be used + expected: data.SeverityLow, + }, + { + name: "db errors should not fail or modify the record", + initialSeverity: "", + namespace: "test", + reader: newDeadMockReader(), + expected: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + record := &grypeDB.VulnerabilityMetadata{ + ID: "cve-2020-0000", + Severity: tt.initialSeverity, + Namespace: tt.namespace, + } + if tt.cveID != "" { + record.ID = tt.cveID + } + normalizeSeverity(record, tt.reader) + assert.Equal(t, string(tt.expected), record.Severity) + }) + } +} diff --git a/pkg/provider/config.go b/pkg/provider/config.go new file mode 100644 index 00000000..eae6050a --- /dev/null +++ b/pkg/provider/config.go @@ -0,0 +1,16 @@ +package provider + +type Collection struct { + Root string + Providers []Provider +} + +type Config struct { + Identifier `json:",inline" yaml:",inline" mapstructure:",squash"` + Config interface{} `yaml:"config,omitempty" json:"config" mapstructure:"config"` +} + +type Identifier struct { + Name string `yaml:"name" json:"name" mapstructure:"name"` + Kind Kind `yaml:"kind,omitempty" json:"kind" mapstructure:"kind"` +} diff --git a/pkg/provider/entry/file.go b/pkg/provider/entry/file.go new file mode 100644 index 00000000..530a5ce2 --- /dev/null +++ b/pkg/provider/entry/file.go @@ -0,0 +1,29 @@ +package entry + +import ( + "io" + "os" +) + +type fileOpener struct { + path string +} + +func fileOpeners(resultPaths []string) <-chan Opener { + openers := make(chan Opener) + go func() { + defer close(openers) + for _, p := range resultPaths { + openers <- fileOpener{path: p} + } + }() + return openers +} + +func (e fileOpener) Open() (io.ReadCloser, error) { + return os.Open(e.path) +} + +func (e fileOpener) String() string { + return e.path +} diff --git a/pkg/provider/entry/opener.go b/pkg/provider/entry/opener.go new file mode 100644 index 00000000..d16583e7 --- /dev/null +++ b/pkg/provider/entry/opener.go @@ -0,0 +1,31 @@ +package entry + +import ( + "fmt" + "io" +) + +type Opener interface { + Open() (io.ReadCloser, error) + fmt.Stringer +} + +func Openers(store string, resultPaths []string) (<-chan Opener, int64, error) { + switch store { + case "flat-file": + return fileOpeners(resultPaths), int64(len(resultPaths)), nil + case "sqlite": + return sqliteOpeners(resultPaths) + } + return nil, 0, fmt.Errorf("unknown store: %q", store) +} + +func Count(store string, resultPaths []string) (int64, error) { + switch store { + case "flat-file": + return int64(len(resultPaths)), nil + case "sqlite": + return sqliteEntryCount(resultPaths) + } + return 0, fmt.Errorf("unknown store: %q", store) +} diff --git a/pkg/provider/entry/sqlite.go b/pkg/provider/entry/sqlite.go new file mode 100644 index 00000000..93318316 --- /dev/null +++ b/pkg/provider/entry/sqlite.go @@ -0,0 +1,182 @@ +package entry + +import ( + "bytes" + "context" + "fmt" + "io" + "strings" + "time" + + "github.com/anchore/grype-db/internal/log" + "github.com/anchore/sqlite" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +var readOptions = []string{ + "immutable=1", + "cache=shared", + "mode=ro", +} + +// note: the name of the struct is tied to the table name +type results struct { + ID string `gorm:"column:id"` + Record []byte `gorm:"column:record"` +} + +type bytesOpener struct { + contents []byte + name string +} + +type errorOpener struct { + err error +} + +func sqliteEntryCount(resultPaths []string) (int64, error) { + var dbPath string + for _, p := range resultPaths { + if strings.HasSuffix(p, ".db") { + dbPath = p + break + } + } + + if dbPath == "" { + return 0, fmt.Errorf("unable to find DB result file") + } + + db, err := openDB(dbPath) + if err != nil { + return 0, err + } + + var count int64 + db.Model(&results{}).Count(&count) + + return count, nil +} + +func sqliteOpeners(resultPaths []string) (<-chan Opener, int64, error) { + var dbPath string + for _, p := range resultPaths { + if strings.HasSuffix(p, ".db") { + dbPath = p + break + } + } + + if dbPath == "" { + return nil, 0, fmt.Errorf("unable to find DB result file") + } + + db, err := openDB(dbPath) + if err != nil { + return nil, 0, err + } + + var count int64 + db.Model(&results{}).Count(&count) + + openers := make(chan Opener) + go func() { + defer close(openers) + + var models []results + + current := 0 + check := db.FindInBatches(&models, 100, func(tx *gorm.DB, batch int) error { + for _, result := range models { + openers <- bytesOpener{ + contents: result.Record, + name: result.ID, + } + } + + current += len(models) + + log.WithFields("count", current).Trace("records read from the provider cache DB") + + // note: returning an error will stop future batches + return nil + }) + + if check.Error != nil { + openers <- errorOpener{err: check.Error} + } + }() + return openers, count, nil +} + +func (e bytesOpener) Open() (io.ReadCloser, error) { + return io.NopCloser(bytes.NewReader(e.contents)), nil +} + +func (e bytesOpener) String() string { + return e.name +} + +func (e errorOpener) Open() (io.ReadCloser, error) { + return nil, e.err +} + +func (e errorOpener) String() string { + return e.err.Error() +} + +// Open a new connection to a sqlite3 database file +func openDB(path string) (*gorm.DB, error) { + connStr, err := connectionString(path) + if err != nil { + return nil, err + } + + // &immutable=1&cache=shared&mode=ro + for _, o := range readOptions { + connStr += fmt.Sprintf("&%s", o) + } + + dbObj, err := gorm.Open(sqlite.Open(connStr), &gorm.Config{Logger: newLogger()}) + if err != nil { + return nil, fmt.Errorf("unable to connect to DB: %w", err) + } + + return dbObj, nil +} + +// ConnectionString creates a connection string for sqlite3 +func connectionString(path string) (string, error) { + if path == "" { + return "", fmt.Errorf("no db filepath given") + } + return fmt.Sprintf("file:%s?cache=shared", path), nil +} + +type logAdapter struct { +} + +func newLogger() logger.Interface { + return logAdapter{} +} + +func (l logAdapter) LogMode(logger.LogLevel) logger.Interface { + return l +} + +func (l logAdapter) Info(_ context.Context, fmt string, v ...interface{}) { + // unimplemented +} + +func (l logAdapter) Warn(_ context.Context, fmt string, v ...interface{}) { + log.Warnf("gorm: "+fmt, v...) +} + +func (l logAdapter) Error(_ context.Context, fmt string, v ...interface{}) { + log.Errorf("gorm: "+fmt, v...) +} + +func (l logAdapter) Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error) { + // unimplemented +} diff --git a/pkg/provider/file.go b/pkg/provider/file.go new file mode 100644 index 00000000..10262b4d --- /dev/null +++ b/pkg/provider/file.go @@ -0,0 +1,68 @@ +package provider + +import ( + "os" + "path/filepath" + + "github.com/OneOfOne/xxhash" + "github.com/anchore/grype-db/internal/file" + "github.com/spf13/afero" +) + +type File struct { + Path string `json:"path"` + Digest string `json:"digest"` + Algorithm string `json:"algorithm"` +} + +type Files []File + +func NewFile(path string) (*File, error) { + digest, err := file.ContentDigest(afero.NewOsFs(), path, xxhash.New64()) + if err != nil { + return nil, err + } + + return &File{ + Path: path, + Digest: digest, + Algorithm: "xxh64", + }, nil +} + +func NewFiles(paths ...string) (Files, error) { + var files []File + for _, path := range paths { + input, err := NewFile(path) + if err != nil { + return nil, err + } + files = append(files, *input) + } + return files, nil +} + +func (i Files) Paths() []string { + var paths []string + for _, input := range i { + paths = append(paths, input.Path) + } + return paths +} + +func NewFilesFromDir(dir string) (Files, error) { + listing, err := os.ReadDir(dir) + if err != nil { + return nil, err + } + + var paths []string + for _, f := range listing { + if f.IsDir() { + continue + } + paths = append(paths, filepath.Join(dir, f.Name())) + } + + return NewFiles(paths...) +} diff --git a/pkg/provider/provider.go b/pkg/provider/provider.go new file mode 100644 index 00000000..eae59fc9 --- /dev/null +++ b/pkg/provider/provider.go @@ -0,0 +1,31 @@ +package provider + +import "context" + +type Kind string + +const ( + InternalKind Kind = "internal" // reserved, not implemented (golang vulnerability data providers in-repo) + ExternalKind Kind = "external" + VunnelKind Kind = "vunnel" // special case of external +) + +type Provider interface { + ID() Identifier + Update(context.Context) error + State() (*State, error) +} + +type Providers []Provider + +func (ps Providers) Filter(names ...string) Providers { + var filtered Providers + for _, p := range ps { + for _, name := range names { + if p.ID().Name == name { + filtered = append(filtered, p) + } + } + } + return filtered +} diff --git a/pkg/provider/providers/external/log_writer.go b/pkg/provider/providers/external/log_writer.go new file mode 100644 index 00000000..ce4e1e99 --- /dev/null +++ b/pkg/provider/providers/external/log_writer.go @@ -0,0 +1,29 @@ +package external + +import ( + "fmt" + "strings" + + "github.com/anchore/grype-db/internal/log" +) + +type logWriter struct { + name string +} + +func newLogWriter(name string) *logWriter { + return &logWriter{ + name: name, + } +} + +func (lw logWriter) Write(p []byte) (n int, err error) { + for _, line := range strings.Split(string(p), "\n") { + line = strings.TrimRight(line, "\n") + if line != "" { + log.Debug(fmt.Sprintf("[%s]", lw.name) + line) + } + } + + return len(p), nil +} diff --git a/pkg/provider/providers/external/provider.go b/pkg/provider/providers/external/provider.go new file mode 100644 index 00000000..9d17cf0a --- /dev/null +++ b/pkg/provider/providers/external/provider.go @@ -0,0 +1,101 @@ +package external + +import ( + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/anchore/grype-db/internal/log" + + "github.com/anchore/grype-db/pkg/provider" + "github.com/google/shlex" +) + +var _ provider.Provider = (*pvdr)(nil) + +type Config struct { + Cmd string `yaml:"cmd" json:"cmd" mapstructure:"cmd"` + ExecDir string `yaml:"dir,omitempty" json:"dir,omitempty" mapstructure:"dir"` + State string `yaml:"state" json:"state" mapstructure:"state"` + Env map[string]string `yaml:"env,omitempty" json:"env,omitempty" mapstructure:"env"` +} + +type pvdr struct { + id provider.Identifier + cfg Config + root string +} + +func NewProvider(root string, id provider.Identifier, cfg Config) provider.Provider { + return &pvdr{ + id: id, + cfg: cfg, + root: root, + } +} + +func (p pvdr) ID() provider.Identifier { + return p.id +} + +func (p pvdr) State() (*provider.State, error) { + return provider.ReadState(filepath.Join(p.root, p.cfg.State)) +} + +func (p pvdr) Update(ctx context.Context) error { + if err := run(ctx, p.cfg.Cmd, p.cfg.ExecDir, p.ID().Name, p.cfg.Env); err != nil { + return fmt.Errorf("failed to pull data from %q provider: %w", p.id.Name, err) + } + return nil +} + +func run(ctx context.Context, cmd, dir, name string, env map[string]string) error { + log.WithFields("provider", name, "dir", dir).Tracef("running external provider: %q", cmd) + + parsedArgs, err := shlex.Split(cmd) + if err != nil { + return fmt.Errorf("unable to parse shell arguments %q: %w", cmd, err) + } + + if len(parsedArgs) == 0 { + return fmt.Errorf("no command specified") + } + cmdStr := parsedArgs[0] + var args []string + if len(parsedArgs) > 1 { + args = parsedArgs[1:] + } + cmdObj := exec.CommandContext(ctx, cmdStr, args...) + cmdObj.Dir = dir + cmdObj.Env = append(cmdObj.Env, envMapToSlice(env)...) + + cmdObj.Stdout = newLogWriter(name) + cmdObj.Stderr = newLogWriter(name) + + if err := cmdObj.Run(); err != nil { + if exitError, ok := err.(*exec.ExitError); ok { //nolint: errorlint + return fmt.Errorf("command failed: %d", exitError.ExitCode()) + } + return err + } + + return nil +} + +func envMapToSlice(env map[string]string) (envList []string) { + for key, val := range env { + if key == "" { + continue + } + if strings.HasPrefix(val, "$") { + val = os.Getenv(val[1:]) + // for safety, assume that all values from environment variables are sensitive + log.Redact(val) + } + envList = append(envList, fmt.Sprintf("%s=%s", key, val)) + } + return +} diff --git a/pkg/provider/providers/providers.go b/pkg/provider/providers/providers.go new file mode 100644 index 00000000..a219f1de --- /dev/null +++ b/pkg/provider/providers/providers.go @@ -0,0 +1,49 @@ +package providers + +import ( + "fmt" + + "github.com/anchore/grype-db/pkg/provider/providers/vunnel" + + "github.com/anchore/grype-db/pkg/provider" + "github.com/anchore/grype-db/pkg/provider/providers/external" + "github.com/mitchellh/mapstructure" +) + +func New(root string, vCfg vunnel.Config, cfgs ...provider.Config) (provider.Providers, error) { + var providers []provider.Provider + for _, cfg := range cfgs { + p, err := newProvider(root, vCfg, cfg) + if err != nil { + return nil, err + } + if p.ID().Name == "nvd" { + // it is important that NVD is processed first since other providers depend on the severity information from these records + providers = append([]provider.Provider{p}, providers...) + } else { + providers = append(providers, p) + } + } + return providers, nil +} + +func newProvider(root string, vCfg vunnel.Config, cfg provider.Config) (provider.Provider, error) { + switch cfg.Kind { + case provider.VunnelKind, "": // note: this is the default + return vunnel.NewProvider(root, cfg.Identifier, vCfg), nil + case provider.ExternalKind: + var c external.Config + if err := mapstructure.Decode(cfg.Config, &c); err != nil { + return nil, fmt.Errorf("failed to decode external provider config: %w", err) + } + return external.NewProvider(root, cfg.Identifier, c), nil + case provider.InternalKind: + return newInternal(root, cfg) + default: + return nil, fmt.Errorf("unknown provider kind %q", cfg.Kind) + } +} + +func newInternal(_ string, _ provider.Config) (provider.Provider, error) { + return nil, fmt.Errorf("internal providers not yet implemented") +} diff --git a/pkg/provider/providers/vunnel/provider.go b/pkg/provider/providers/vunnel/provider.go new file mode 100644 index 00000000..12427ee5 --- /dev/null +++ b/pkg/provider/providers/vunnel/provider.go @@ -0,0 +1,77 @@ +package vunnel + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/anchore/grype-db/internal/log" + "github.com/anchore/grype-db/pkg/provider" + "github.com/anchore/grype-db/pkg/provider/providers/external" +) + +type Config struct { + Config string `yaml:"config" json:"config" mapstructure:"config"` + Executor string `yaml:"executor" json:"executor" mapstructure:"executor"` + DockerImage string `yaml:"dockerImage" json:"dockerImage" mapstructure:"dockerImage"` + DockerTag string `yaml:"dockerTag" json:"dockerTag" mapstructure:"dockerTag"` + Env map[string]string `yaml:"env,omitempty" json:"env,omitempty" mapstructure:"env"` +} + +func NewProvider(root string, id provider.Identifier, cfg Config) provider.Provider { + return external.NewProvider(root, id, + external.Config{ + Cmd: getCommand(root, id, cfg), + State: fmt.Sprintf("%s/metadata.json", id.Name), + Env: cfg.Env, + }, + ) +} + +func getCommand(root string, id provider.Identifier, cfg Config) string { + switch cfg.Executor { + case "docker", "podman": + dataRootCtr := root + if !strings.HasPrefix(root, "/") { + dataRootCtr = strings.TrimPrefix(root, "./") + } + + dataRootHost, err := filepath.Abs(root) + if err != nil { + log.WithFields("error", err).Warn("unable to get absolute path for provider root directory, using relative path") + dataRootHost = root + } + + var cfgVol string + if _, err := os.Stat(".vunnel.yaml"); !os.IsNotExist(err) { + cwd, err := os.Getwd() + if err != nil { + log.WithFields("error", err, "provider", id.Name).Warn("unable to get current working directory, ignoring vunnel config") + } else { + cfgVol = fmt.Sprintf("-v %s/.vunnel.yaml:/.vunnel.yaml", cwd) + } + } + + var envStr string + if cfg.Env != nil { + for k, v := range cfg.Env { + if strings.HasPrefix(v, "$") { + v = os.Getenv(v[1:]) + // for safety, assume that all values from environment variables are sensitive + log.Redact(v) + } + envStr += fmt.Sprintf("-e %s=%s ", k, v) + } + } + + return fmt.Sprintf("%s run --rm -t -v %s:/%s %s %s %s:%s run %s", cfg.Executor, dataRootHost, dataRootCtr, cfgVol, envStr, cfg.DockerImage, cfg.DockerTag, id.Name) + } + + var cfgSection string + if cfg.Config != "" { + cfgSection = fmt.Sprintf("-c %s", cfg.Config) + } + + return fmt.Sprintf("vunnel %s run %s", cfgSection, id.Name) +} diff --git a/pkg/provider/state.go b/pkg/provider/state.go new file mode 100644 index 00000000..79b3fe56 --- /dev/null +++ b/pkg/provider/state.go @@ -0,0 +1,131 @@ +package provider + +import ( + "bufio" + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + "time" + + "github.com/OneOfOne/xxhash" + "github.com/anchore/grype-db/internal/file" + "github.com/anchore/grype-db/internal/log" +) + +// data shape dictated by vunnel "provider workspace state" schema definition + +type State struct { + location string + root string + Provider string `json:"provider"` + Schema Schema `json:"schema"` + URLs []string `json:"urls"` + Timestamp time.Time `json:"timestamp"` + Listing *File `json:"listing"` + Store string `json:"store"` + resultFileStates []File +} + +type Schema struct { + Version string `json:"version"` + URL string `json:"url"` +} + +type States []State + +func ReadState(location string) (*State, error) { + by, err := os.ReadFile(location) + if err != nil { + return nil, fmt.Errorf("unable to read state file %q: %w", location, err) + } + + var sd State + if err := json.Unmarshal(by, &sd); err != nil { + return nil, fmt.Errorf("unable to parse state file %q: %w", location, err) + } + + root := filepath.Dir(location) + sd.root = root + sd.location = location + // we usually have a lot of records (depending on the source) + sd.resultFileStates = make([]File, 0, 300000) + + start := time.Now() + if sd.Listing != nil { + algorithm := "xxh64" // sane default for performance + + // get extension from listing file + extension := filepath.Ext(sd.Listing.Path) + if extension != "" { + algorithm = strings.TrimPrefix(extension, ".") + } + + listingPath := filepath.Join(root, sd.Listing.Path) + f, err := os.Open(listingPath) + if err != nil { + return nil, fmt.Errorf("unable to open listing file %q: %w", listingPath, err) + } + + // note: bufio scanner is **much** faster than Fscan + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := scanner.Text() + index := strings.Index(line, " ") // faster than strings.Split + if index != -1 { + sd.resultFileStates = append(sd.resultFileStates, + File{ + Path: line[index+2:], + Digest: line[:index], + Algorithm: algorithm, + }, + ) + } + } + } + + log.WithFields("duration", time.Since(start), "entries", len(sd.resultFileStates)).Trace("loaded result listing file") + + return &sd, nil +} + +func (sd State) ResultPath(filename string) string { + return filepath.Join(sd.root, filename) +} + +func (sd State) ResultPaths() []string { + var paths []string + for _, p := range sd.resultFileStates { + paths = append(paths, sd.ResultPath(p.Path)) + } + return paths +} + +func (sd State) Verify(workspaceRoots ...string) error { + if sd.root != "" { + workspaceRoots = append(workspaceRoots, sd.root) + } + for _, workspaceRoot := range workspaceRoots { + for _, resultConfig := range sd.resultFileStates { + workspace := NewWorkspaceFromExisting(workspaceRoot) + path := filepath.Join(workspace.Path(), resultConfig.Path) + + log.WithFields("path", resultConfig.Path, "provider", sd.Provider).Trace("validating result file") + + if err := file.ValidateDigest(path, resultConfig.Digest, xxhash.New64()); err != nil { + return fmt.Errorf("unable to validate result file %q: %w", path, err) + } + } + } + + return nil +} + +func (s States) Names() []string { + var names []string + for _, state := range s { + names = append(names, state.Provider) + } + return names +} diff --git a/pkg/provider/unmarshal/errors.go b/pkg/provider/unmarshal/errors.go new file mode 100644 index 00000000..2c44c105 --- /dev/null +++ b/pkg/provider/unmarshal/errors.go @@ -0,0 +1,15 @@ +package unmarshal + +import ( + "encoding/json" + "fmt" +) + +func handleJSONUnmarshalError(err error) error { + if ute, ok := err.(*json.UnmarshalTypeError); ok { //nolint: errorlint + return fmt.Errorf("unmarshal type error: expected=%v, got=%v, field=%v, offset=%v", ute.Type, ute.Value, ute.Field, ute.Offset) + } else if se, ok := err.(*json.SyntaxError); ok { //nolint: errorlint + return fmt.Errorf("syntax error: offset=%v, error=%w", se.Offset, se) + } + return err +} diff --git a/pkg/provider/unmarshal/github_advisory.go b/pkg/provider/unmarshal/github_advisory.go new file mode 100644 index 00000000..0040664c --- /dev/null +++ b/pkg/provider/unmarshal/github_advisory.go @@ -0,0 +1,35 @@ +package unmarshal + +import ( + "io" +) + +type GitHubAdvisory struct { + Advisory struct { + CVE []string `json:"CVE"` + FixedIn []struct { + Ecosystem string `json:"ecosystem"` + Identifier string `json:"identifier"` + Name string `json:"name"` + Namespace string `json:"namespace"` + Range string `json:"range"` + } `json:"FixedIn"` + Metadata struct { + CVE []string `json:"CVE"` + } `json:"Metadata"` + Severity string `json:"Severity"` + Summary string `json:"Summary"` + GhsaID string `json:"ghsaId"` + Namespace string `json:"namespace"` + URL string `json:"url"` + Withdrawn interface{} `json:"withdrawn"` + } `json:"Advisory"` +} + +func (g GitHubAdvisory) IsEmpty() bool { + return g.Advisory.GhsaID == "" +} + +func GitHubAdvisoryEntries(reader io.Reader) ([]GitHubAdvisory, error) { + return unmarshalSingleOrMulti[GitHubAdvisory](reader) +} diff --git a/pkg/provider/unmarshal/items_envelope.go b/pkg/provider/unmarshal/items_envelope.go new file mode 100644 index 00000000..8b4c5e22 --- /dev/null +++ b/pkg/provider/unmarshal/items_envelope.go @@ -0,0 +1,23 @@ +package unmarshal + +import ( + "encoding/json" + "fmt" + "io" +) + +type ItemsEnvelope struct { + Schema string `yaml:"schema" json:"schema" mapstructure:"schema"` + Identifier string `yaml:"identifier" json:"identifier" mapstructure:"identifier"` + Item json.RawMessage `yaml:"item" json:"item" mapstructure:"item"` +} + +func Envelope(reader io.Reader) (*ItemsEnvelope, error) { + var envelope ItemsEnvelope + dec := json.NewDecoder(reader) + err := dec.Decode(&envelope) + if err != nil { + return nil, fmt.Errorf("unable to open envelope: %w", err) + } + return &envelope, nil +} diff --git a/pkg/provider/unmarshal/match_exclusion.go b/pkg/provider/unmarshal/match_exclusion.go new file mode 100644 index 00000000..88fc37c0 --- /dev/null +++ b/pkg/provider/unmarshal/match_exclusion.go @@ -0,0 +1,31 @@ +package unmarshal + +import ( + "io" +) + +type MatchExclusion struct { + ID string `json:"id"` + Constraints []struct { + Vulnerability struct { + Namespace string `json:"namespace,omitempty"` + FixState string `json:"fix_state,omitempty"` + } `json:"vulnerability,omitempty"` + Package struct { + Language string `json:"language,omitempty"` + Type string `json:"type,omitempty"` + Name string `json:"name,omitempty"` + Version string `json:"version,omitempty"` + Location string `json:"location,omitempty"` + } `json:"package,omitempty"` + } `json:"constraints,omitempty"` + Justification string `json:"justification"` +} + +func (m MatchExclusion) IsEmpty() bool { + return m.ID == "" +} + +func MatchExclusions(reader io.Reader) ([]MatchExclusion, error) { + return unmarshalSingleOrMulti[MatchExclusion](reader) +} diff --git a/pkg/provider/unmarshal/msrc_vulnerability.go b/pkg/provider/unmarshal/msrc_vulnerability.go new file mode 100644 index 00000000..ed3ed06c --- /dev/null +++ b/pkg/provider/unmarshal/msrc_vulnerability.go @@ -0,0 +1,38 @@ +package unmarshal + +import ( + "io" +) + +// MSRCVulnerability represents a single Msrc entry with vulnerability metadata +type MSRCVulnerability struct { + Cvss struct { + BaseScore float64 `json:"base_score"` + TemporalScore float64 `json:"temporal_score"` + Vector string `json:"vector"` + } `json:"cvss"` + FixedIn []struct { + ID string `json:"id"` + IsFirst bool `json:"is_first"` + IsLatest bool `json:"is_latest"` + Links []string `json:"links"` + } `json:"fixed_in"` + ID string `json:"id"` + Link string `json:"link"` + Product struct { + Family string `json:"family"` + ID string `json:"id"` + Name string `json:"name"` + } `json:"product"` + Severity string `json:"severity"` + Summary string `json:"summary"` + Vulnerable []string `json:"vulnerable"` +} + +func (o MSRCVulnerability) IsEmpty() bool { + return o.ID == "" +} + +func MSRCVulnerabilityEntries(reader io.Reader) ([]MSRCVulnerability, error) { + return unmarshalSingleOrMulti[MSRCVulnerability](reader) +} diff --git a/pkg/provider/unmarshal/nvd/cve.go b/pkg/provider/unmarshal/nvd/cve.go new file mode 100644 index 00000000..b53024a9 --- /dev/null +++ b/pkg/provider/unmarshal/nvd/cve.go @@ -0,0 +1,288 @@ +package nvd + +import ( + "sort" + + "github.com/Masterminds/semver/v3" + "github.com/anchore/grype-db/pkg/provider/unmarshal/nvd/cvss20" + "github.com/anchore/grype-db/pkg/provider/unmarshal/nvd/cvss30" + "github.com/anchore/grype-db/pkg/provider/unmarshal/nvd/cvss31" + "github.com/jinzhu/copier" + "golang.org/x/text/cases" + "golang.org/x/text/language" +) + +// note: this was autogenerated with some manual tweaking (see schema/nvd/cve-api-json/README.md) + +type Operator string + +const ( + And Operator = "AND" + Or Operator = "OR" +) + +const englishLanguage = "en" + +// this is the struct to use when unmarshalling directly from the API (which grype-db is NOT doing) +// type APIResults struct { +// Format string `json:"format"` +// ResultsPerPage int64 `json:"resultsPerPage"` +// StartIndex int64 `json:"startIndex"` +// Timestamp string `json:"timestamp"` +// TotalResults int64 `json:"totalResults"` +// Version string `json:"version"` +// Vulnerabilities []Vulnerability `json:"vulnerabilities"` +//} + +type Vulnerability struct { + Cve CveItem `json:"cve"` +} + +type CveItem struct { + ID string `json:"id"` + // CisaActionDue *string `json:"cisaActionDue,omitempty"` + // CisaExploitAdd *string `json:"cisaExploitAdd,omitempty"` + // CisaRequiredAction *string `json:"cisaRequiredAction,omitempty"` + // CisaVulnerabilityName *string `json:"cisaVulnerabilityName,omitempty"` + Configurations []Configuration `json:"configurations,omitempty"` + Descriptions []LangString `json:"descriptions"` + // EvaluatorComment *string `json:"evaluatorComment,omitempty"` + // EvaluatorImpact *string `json:"evaluatorImpact,omitempty"` + // EvaluatorSolution *string `json:"evaluatorSolution,omitempty"` + // LastModified string `json:"lastModified"` + Metrics *Metrics `json:"metrics,omitempty"` + // Published string `json:"published"` + References []Reference `json:"references"` + // SourceIdentifier *string `json:"sourceIdentifier,omitempty"` + // VendorComments []VendorComment `json:"vendorComments,omitempty"` + // VulnStatus *string `json:"vulnStatus,omitempty"` + // Weaknesses []Weakness `json:"weaknesses,omitempty"` +} + +type Configuration struct { + Negate *bool `json:"negate,omitempty"` + Nodes []Node `json:"nodes"` + Operator *Operator `json:"operator,omitempty"` +} + +type Node struct { + CpeMatch []CpeMatch `json:"cpeMatch"` + Negate *bool `json:"negate,omitempty"` + Operator Operator `json:"operator"` +} + +type CpeMatch struct { + Criteria string `json:"criteria"` + MatchCriteriaID string `json:"matchCriteriaId"` + VersionEndExcluding *string `json:"versionEndExcluding,omitempty"` + VersionEndIncluding *string `json:"versionEndIncluding,omitempty"` + VersionStartExcluding *string `json:"versionStartExcluding,omitempty"` + VersionStartIncluding *string `json:"versionStartIncluding,omitempty"` + Vulnerable bool `json:"vulnerable"` +} + +type LangString struct { + Lang string `json:"lang"` + Value string `json:"value"` +} + +// Metric scores for a vulnerability as found on NVD. +type Metrics struct { + CvssMetricV2 []CvssV2 `json:"cvssMetricV2,omitempty"` // CVSS V2.0 score. + CvssMetricV30 []CvssV30 `json:"cvssMetricV30,omitempty"` // CVSS V3.0 score. + CvssMetricV31 []CvssV31 `json:"cvssMetricV31,omitempty"` // CVSS V3.1 score. +} + +type CvssV2 struct { + // ACInsufInfo *bool `json:"acInsufInfo,omitempty"` + BaseSeverity *string `json:"baseSeverity,omitempty"` + CvssData cvss20.Cvss20 `json:"cvssData"` + ExploitabilityScore *float64 `json:"exploitabilityScore,omitempty"` + ImpactScore *float64 `json:"impactScore,omitempty"` + // ObtainAllPrivilege *bool `json:"obtainAllPrivilege,omitempty"` + // ObtainOtherPrivilege *bool `json:"obtainOtherPrivilege,omitempty"` + // ObtainUserPrivilege *bool `json:"obtainUserPrivilege,omitempty"` + // Source string `json:"source"` + Type CvssType `json:"type"` + // UserInteractionRequired *bool `json:"userInteractionRequired,omitempty"` +} + +type CvssV30 struct { + CvssData cvss30.Cvss30 `json:"cvssData"` + ExploitabilityScore *float64 `json:"exploitabilityScore,omitempty"` + ImpactScore *float64 `json:"impactScore,omitempty"` + // Source string `json:"source"` + Type CvssType `json:"type"` +} + +type CvssV31 struct { + CvssData cvss31.Cvss31 `json:"cvssData"` + ExploitabilityScore *float64 `json:"exploitabilityScore,omitempty"` + ImpactScore *float64 `json:"impactScore,omitempty"` + // Source string `json:"source"` + Type CvssType `json:"type"` +} + +// "type identifies whether the organization is a primary or secondary source. Primary sources +// include the NVD and CNA who have reached the provider level in CVMAP. 10% of provider level +// submissions are audited by the NVD. If a submission has been audited the NVD will appear as +// the primary source and the provider level CNA will appear as the secondary source." +type CvssType string + +const ( + Primary CvssType = "Primary" + Secondary CvssType = "Secondary" +) + +type Reference struct { + Source *string `json:"source,omitempty"` + Tags []string `json:"tags,omitempty"` + URL string `json:"url"` +} + +// type VendorComment struct { +// Comment string `json:"comment"` +// LastModified string `json:"lastModified"` +// Organization string `json:"organization"` +//} +// +// type Weakness struct { +// Description []LangString `json:"description"` +// Source string `json:"source"` +// Type string `json:"type"` +//} + +func (o CveItem) Description() string { + for _, d := range o.Descriptions { + if d.Lang == englishLanguage { + return d.Value + } + } + return "" +} + +type CvssSummary struct { + Type CvssType + Version string + Vector string + BaseScore float64 + ExploitabilityScore *float64 + ImpactScore *float64 + baseSeverity *string +} + +func (o CvssSummary) Severity() string { + if o.baseSeverity != nil { + return cases.Title(language.English).String(*o.baseSeverity) + } + return "" +} + +func (o CvssSummary) version() *semver.Version { + v, err := semver.NewVersion(o.Version) + if err != nil { + return semver.MustParse("2.0") + } + return v +} + +type CvssSummaries []CvssSummary + +func (o CvssSummaries) Len() int { + return len(o) +} + +func (o CvssSummaries) Less(i, j int) bool { + iEntry := o[i] + jEntry := o[j] + iV := iEntry.version() + jV := jEntry.version() + if iV == jV { + if iEntry.Type == Primary && jEntry.Type == Secondary { + return false + } else if iEntry.Type == Secondary && jEntry.Type == Primary { + return true + } + return false + } + return iV.LessThan(jV) +} + +func (o CvssSummaries) Swap(i, j int) { + o[i], o[j] = o[j], o[i] +} + +func (o CvssSummaries) Severity() string { + for _, c := range o { + sev := c.Severity() + if sev != "" { + return sev + } + } + return "" +} + +func (o CvssSummaries) Sorted() CvssSummaries { + var n CvssSummaries + if err := copier.Copy(&n, &o); err != nil { + panic(err) + } + sort.Sort(sort.Reverse(n)) + return n +} + +func (o CveItem) CVSS() []CvssSummary { + if o.Metrics == nil { + return nil + } + + var results CvssSummaries + + for _, c := range o.Metrics.CvssMetricV2 { + results = append(results, + CvssSummary{ + Type: c.Type, + Version: c.CvssData.Version, + Vector: c.CvssData.VectorString, + BaseScore: c.CvssData.BaseScore, + ExploitabilityScore: c.ExploitabilityScore, + ImpactScore: c.ImpactScore, + baseSeverity: c.BaseSeverity, + }, + ) + } + for _, c := range o.Metrics.CvssMetricV30 { + sev := string(c.CvssData.BaseSeverity) + results = append(results, + CvssSummary{ + Type: c.Type, + Version: c.CvssData.Version, + Vector: c.CvssData.VectorString, + BaseScore: c.CvssData.BaseScore, + ExploitabilityScore: c.ExploitabilityScore, + ImpactScore: c.ImpactScore, + baseSeverity: &sev, + }, + ) + } + for _, c := range o.Metrics.CvssMetricV31 { + sev := string(c.CvssData.BaseSeverity) + results = append(results, + CvssSummary{ + Type: c.Type, + Version: c.CvssData.Version, + Vector: c.CvssData.VectorString, + BaseScore: c.CvssData.BaseScore, + ExploitabilityScore: c.ExploitabilityScore, + ImpactScore: c.ImpactScore, + baseSeverity: &sev, + }, + ) + } + + return results +} + +func (o Vulnerability) IsEmpty() bool { + return o.Cve.ID == "" +} diff --git a/pkg/provider/unmarshal/nvd/cvss20/cvss20.go b/pkg/provider/unmarshal/nvd/cvss20/cvss20.go new file mode 100644 index 00000000..2f8a17ac --- /dev/null +++ b/pkg/provider/unmarshal/nvd/cvss20/cvss20.go @@ -0,0 +1,116 @@ +package cvss20 + +// note: this was autogenerated with some manual tweaking + +type Cvss20 struct { + // AccessComplexity *AccessComplexityType `json:"accessComplexity,omitempty"` + // AccessVector *AccessVectorType `json:"accessVector,omitempty"` + // Authentication *AuthenticationType `json:"authentication,omitempty"` + // AvailabilityImpact *CiaType `json:"availabilityImpact,omitempty"` + // AvailabilityRequirement *CiaRequirementType `json:"availabilityRequirement,omitempty"` + BaseScore float64 `json:"baseScore"` + // CollateralDamagePotential *CollateralDamagePotentialType `json:"collateralDamagePotential,omitempty"` + // ConfidentialityImpact *CiaType `json:"confidentialityImpact,omitempty"` + // ConfidentialityRequirement *CiaRequirementType `json:"confidentialityRequirement,omitempty"` + // EnvironmentalScore *float64 `json:"environmentalScore,omitempty"` + // Exploitability *ExploitabilityType `json:"exploitability,omitempty"` + // IntegrityImpact *CiaType `json:"integrityImpact,omitempty"` + // IntegrityRequirement *CiaRequirementType `json:"integrityRequirement,omitempty"` + // RemediationLevel *RemediationLevelType `json:"remediationLevel,omitempty"` + // ReportConfidence *ReportConfidenceType `json:"reportConfidence,omitempty"` + // TargetDistribution *TargetDistributionType `json:"targetDistribution,omitempty"` + // TemporalScore *float64 `json:"temporalScore,omitempty"` + VectorString string `json:"vectorString"` + Version string `json:"version"` // CVSS Version +} + +// type AccessComplexityType string +// +// const ( +// AccessComplexityTypeHIGH AccessComplexityType = "HIGH" +// AccessComplexityTypeLOW AccessComplexityType = "LOW" +// AccessComplexityTypeMEDIUM AccessComplexityType = "MEDIUM" +//) +// +// type AccessVectorType string +// +// const ( +// AdjacentNetwork AccessVectorType = "ADJACENT_NETWORK" +// Local AccessVectorType = "LOCAL" +// Network AccessVectorType = "NETWORK" +//) +// +// type AuthenticationType string +// +// const ( +// AuthenticationTypeNONE AuthenticationType = "NONE" +// Multiple AuthenticationType = "MULTIPLE" +// Single AuthenticationType = "SINGLE" +//) +// +// type CiaType string +// +// const ( +// CiaTypeNONE CiaType = "NONE" +// Complete CiaType = "COMPLETE" +// Partial CiaType = "PARTIAL" +//) +// +// type CiaRequirementType string +// +// const ( +// CiaRequirementTypeHIGH CiaRequirementType = "HIGH" +// CiaRequirementTypeLOW CiaRequirementType = "LOW" +// CiaRequirementTypeMEDIUM CiaRequirementType = "MEDIUM" +// CiaRequirementTypeNOTDEFINED CiaRequirementType = "NOT_DEFINED" +//) +// +// type CollateralDamagePotentialType string +// +// const ( +// CollateralDamagePotentialTypeHIGH CollateralDamagePotentialType = "HIGH" +// CollateralDamagePotentialTypeLOW CollateralDamagePotentialType = "LOW" +// CollateralDamagePotentialTypeNONE CollateralDamagePotentialType = "NONE" +// CollateralDamagePotentialTypeNOTDEFINED CollateralDamagePotentialType = "NOT_DEFINED" +// LowMedium CollateralDamagePotentialType = "LOW_MEDIUM" +// MediumHigh CollateralDamagePotentialType = "MEDIUM_HIGH" +//) +// +// type ExploitabilityType string +// +// const ( +// ExploitabilityTypeHIGH ExploitabilityType = "HIGH" +// ExploitabilityTypeNOTDEFINED ExploitabilityType = "NOT_DEFINED" +// Functional ExploitabilityType = "FUNCTIONAL" +// ProofOfConcept ExploitabilityType = "PROOF_OF_CONCEPT" +// Unproven ExploitabilityType = "UNPROVEN" +//) +// +// type RemediationLevelType string +// +// const ( +// OfficialFix RemediationLevelType = "OFFICIAL_FIX" +// RemediationLevelTypeNOTDEFINED RemediationLevelType = "NOT_DEFINED" +// TemporaryFix RemediationLevelType = "TEMPORARY_FIX" +// Unavailable RemediationLevelType = "UNAVAILABLE" +// Workaround RemediationLevelType = "WORKAROUND" +//) +// +// type ReportConfidenceType string +// +// const ( +// Confirmed ReportConfidenceType = "CONFIRMED" +// ReportConfidenceTypeNOTDEFINED ReportConfidenceType = "NOT_DEFINED" +// Unconfirmed ReportConfidenceType = "UNCONFIRMED" +// Uncorroborated ReportConfidenceType = "UNCORROBORATED" +//) +// +// type TargetDistributionType string +// +// const ( +// TargetDistributionTypeHIGH TargetDistributionType = "HIGH" +// TargetDistributionTypeLOW TargetDistributionType = "LOW" +// TargetDistributionTypeMEDIUM TargetDistributionType = "MEDIUM" +// TargetDistributionTypeNONE TargetDistributionType = "NONE" +// TargetDistributionTypeNOTDEFINED TargetDistributionType = "NOT_DEFINED" +//) diff --git a/pkg/provider/unmarshal/nvd/cvss30/cvss30.go b/pkg/provider/unmarshal/nvd/cvss30/cvss30.go new file mode 100644 index 00000000..3d9cba1d --- /dev/null +++ b/pkg/provider/unmarshal/nvd/cvss30/cvss30.go @@ -0,0 +1,166 @@ +package cvss30 + +// note: this was autogenerated with some manual tweaking + +type Cvss30 struct { + // AttackComplexity *AttackComplexityType `json:"attackComplexity,omitempty"` + // AttackVector *AttackVectorType `json:"attackVector,omitempty"` + // AvailabilityImpact *Type `json:"availabilityImpact,omitempty"` + // AvailabilityRequirement *CiaRequirementType `json:"availabilityRequirement,omitempty"` + BaseScore float64 `json:"baseScore"` + BaseSeverity SeverityType `json:"baseSeverity"` + // ConfidentialityImpact *Type `json:"confidentialityImpact,omitempty"` + // ConfidentialityRequirement *CiaRequirementType `json:"confidentialityRequirement,omitempty"` + // EnvironmentalScore *float64 `json:"environmentalScore,omitempty"` + // EnvironmentalSeverity *SeverityType `json:"environmentalSeverity,omitempty"` + // ExploitCodeMaturity *ExploitCodeMaturityType `json:"exploitCodeMaturity,omitempty"` + // IntegrityImpact *Type `json:"integrityImpact,omitempty"` + // IntegrityRequirement *CiaRequirementType `json:"integrityRequirement,omitempty"` + // ModifiedAttackComplexity *ModifiedAttackComplexityType `json:"modifiedAttackComplexity,omitempty"` + // ModifiedAttackVector *ModifiedAttackVectorType `json:"modifiedAttackVector,omitempty"` + // ModifiedAvailabilityImpact *ModifiedType `json:"modifiedAvailabilityImpact,omitempty"` + // ModifiedConfidentialityImpact *ModifiedType `json:"modifiedConfidentialityImpact,omitempty"` + // ModifiedIntegrityImpact *ModifiedType `json:"modifiedIntegrityImpact,omitempty"` + // ModifiedPrivilegesRequired *ModifiedType `json:"modifiedPrivilegesRequired,omitempty"` + // ModifiedScope *ModifiedScopeType `json:"modifiedScope,omitempty"` + // ModifiedUserInteraction *ModifiedUserInteractionType `json:"modifiedUserInteraction,omitempty"` + // PrivilegesRequired *Type `json:"privilegesRequired,omitempty"` + // RemediationLevel *RemediationLevelType `json:"remediationLevel,omitempty"` + // ReportConfidence *ConfidenceType `json:"reportConfidence,omitempty"` + // Scope *ScopeType `json:"scope,omitempty"` + // TemporalScore *float64 `json:"temporalScore,omitempty"` + // TemporalSeverity *SeverityType `json:"temporalSeverity,omitempty"` + // UserInteraction *UserInteractionType `json:"userInteraction,omitempty"` + VectorString string `json:"vectorString"` + Version string `json:"version"` // CVSS Version +} + +// type AttackComplexityType string +// +// const ( +// AttackComplexityTypeHIGH AttackComplexityType = "HIGH" +// AttackComplexityTypeLOW AttackComplexityType = "LOW" +//) +// +// type AttackVectorType string +// +// const ( +// AttackVectorTypeADJACENTNETWORK AttackVectorType = "ADJACENT_NETWORK" +// AttackVectorTypeLOCAL AttackVectorType = "LOCAL" +// AttackVectorTypeNETWORK AttackVectorType = "NETWORK" +// AttackVectorTypePHYSICAL AttackVectorType = "PHYSICAL" +//) +// +// type Type string +// +// const ( +// TypeHIGH Type = "HIGH" +// TypeLOW Type = "LOW" +// TypeNONE Type = "NONE" +//) +// +// type CiaRequirementType string +// +// const ( +// CiaRequirementTypeHIGH CiaRequirementType = "HIGH" +// CiaRequirementTypeLOW CiaRequirementType = "LOW" +// CiaRequirementTypeMEDIUM CiaRequirementType = "MEDIUM" +// CiaRequirementTypeNOTDEFINED CiaRequirementType = "NOT_DEFINED" +//) + +type SeverityType string + +const ( + Critical SeverityType = "CRITICAL" + SeverityTypeHIGH SeverityType = "HIGH" + SeverityTypeLOW SeverityType = "LOW" + SeverityTypeMEDIUM SeverityType = "MEDIUM" + SeverityTypeNONE SeverityType = "NONE" +) + +// +// type ExploitCodeMaturityType string +// +// const ( +// ExploitCodeMaturityTypeHIGH ExploitCodeMaturityType = "HIGH" +// ExploitCodeMaturityTypeNOTDEFINED ExploitCodeMaturityType = "NOT_DEFINED" +// Functional ExploitCodeMaturityType = "FUNCTIONAL" +// ProofOfConcept ExploitCodeMaturityType = "PROOF_OF_CONCEPT" +// Unproven ExploitCodeMaturityType = "UNPROVEN" +//) +// +// type ModifiedAttackComplexityType string +// +// const ( +// ModifiedAttackComplexityTypeHIGH ModifiedAttackComplexityType = "HIGH" +// ModifiedAttackComplexityTypeLOW ModifiedAttackComplexityType = "LOW" +// ModifiedAttackComplexityTypeNOTDEFINED ModifiedAttackComplexityType = "NOT_DEFINED" +//) +// +// type ModifiedAttackVectorType string +// +// const ( +// ModifiedAttackVectorTypeADJACENTNETWORK ModifiedAttackVectorType = "ADJACENT_NETWORK" +// ModifiedAttackVectorTypeLOCAL ModifiedAttackVectorType = "LOCAL" +// ModifiedAttackVectorTypeNETWORK ModifiedAttackVectorType = "NETWORK" +// ModifiedAttackVectorTypeNOTDEFINED ModifiedAttackVectorType = "NOT_DEFINED" +// ModifiedAttackVectorTypePHYSICAL ModifiedAttackVectorType = "PHYSICAL" +//) +// +// type ModifiedType string +// +// const ( +// ModifiedTypeHIGH ModifiedType = "HIGH" +// ModifiedTypeLOW ModifiedType = "LOW" +// ModifiedTypeNONE ModifiedType = "NONE" +// ModifiedTypeNOTDEFINED ModifiedType = "NOT_DEFINED" +//) +// +// type ModifiedScopeType string +// +// const ( +// ModifiedScopeTypeCHANGED ModifiedScopeType = "CHANGED" +// ModifiedScopeTypeNOTDEFINED ModifiedScopeType = "NOT_DEFINED" +// ModifiedScopeTypeUNCHANGED ModifiedScopeType = "UNCHANGED" +//) +// +// type ModifiedUserInteractionType string +// +// const ( +// ModifiedUserInteractionTypeNONE ModifiedUserInteractionType = "NONE" +// ModifiedUserInteractionTypeNOTDEFINED ModifiedUserInteractionType = "NOT_DEFINED" +// ModifiedUserInteractionTypeREQUIRED ModifiedUserInteractionType = "REQUIRED" +//) +// +// type RemediationLevelType string +// +// const ( +// OfficialFix RemediationLevelType = "OFFICIAL_FIX" +// RemediationLevelTypeNOTDEFINED RemediationLevelType = "NOT_DEFINED" +// TemporaryFix RemediationLevelType = "TEMPORARY_FIX" +// Unavailable RemediationLevelType = "UNAVAILABLE" +// Workaround RemediationLevelType = "WORKAROUND" +//) +// +// type ConfidenceType string +// +// const ( +// ConfidenceTypeNOTDEFINED ConfidenceType = "NOT_DEFINED" +// Confirmed ConfidenceType = "CONFIRMED" +// Reasonable ConfidenceType = "REASONABLE" +// Unknown ConfidenceType = "UNKNOWN" +//) +// +// type ScopeType string +// +// const ( +// ScopeTypeCHANGED ScopeType = "CHANGED" +// ScopeTypeUNCHANGED ScopeType = "UNCHANGED" +//) +// +// type UserInteractionType string +// +// const ( +// UserInteractionTypeNONE UserInteractionType = "NONE" +// UserInteractionTypeREQUIRED UserInteractionType = "REQUIRED" +//) diff --git a/pkg/provider/unmarshal/nvd/cvss31/cvss31.go b/pkg/provider/unmarshal/nvd/cvss31/cvss31.go new file mode 100644 index 00000000..f346aeb2 --- /dev/null +++ b/pkg/provider/unmarshal/nvd/cvss31/cvss31.go @@ -0,0 +1,165 @@ +package cvss31 + +// note: this was autogenerated with some manual tweaking + +type Cvss31 struct { + // AttackComplexity *AttackComplexityType `json:"attackComplexity,omitempty"` + // AttackVector *AttackVectorType `json:"attackVector,omitempty"` + // AvailabilityImpact *Type `json:"availabilityImpact,omitempty"` + // AvailabilityRequirement *CiaRequirementType `json:"availabilityRequirement,omitempty"` + BaseScore float64 `json:"baseScore"` + BaseSeverity SeverityType `json:"baseSeverity"` + // ConfidentialityImpact *Type `json:"confidentialityImpact,omitempty"` + // ConfidentialityRequirement *CiaRequirementType `json:"confidentialityRequirement,omitempty"` + // EnvironmentalScore *float64 `json:"environmentalScore,omitempty"` + // EnvironmentalSeverity *SeverityType `json:"environmentalSeverity,omitempty"` + // ExploitCodeMaturity *ExploitCodeMaturityType `json:"exploitCodeMaturity,omitempty"` + // IntegrityImpact *Type `json:"integrityImpact,omitempty"` + // IntegrityRequirement *CiaRequirementType `json:"integrityRequirement,omitempty"` + // ModifiedAttackComplexity *ModifiedAttackComplexityType `json:"modifiedAttackComplexity,omitempty"` + // ModifiedAttackVector *ModifiedAttackVectorType `json:"modifiedAttackVector,omitempty"` + // ModifiedAvailabilityImpact *ModifiedType `json:"modifiedAvailabilityImpact,omitempty"` + // ModifiedConfidentialityImpact *ModifiedType `json:"modifiedConfidentialityImpact,omitempty"` + // ModifiedIntegrityImpact *ModifiedType `json:"modifiedIntegrityImpact,omitempty"` + // ModifiedPrivilegesRequired *ModifiedType `json:"modifiedPrivilegesRequired,omitempty"` + // ModifiedScope *ModifiedScopeType `json:"modifiedScope,omitempty"` + // ModifiedUserInteraction *ModifiedUserInteractionType `json:"modifiedUserInteraction,omitempty"` + // PrivilegesRequired *Type `json:"privilegesRequired,omitempty"` + // RemediationLevel *RemediationLevelType `json:"remediationLevel,omitempty"` + // ReportConfidence *ConfidenceType `json:"reportConfidence,omitempty"` + // Scope *ScopeType `json:"scope,omitempty"` + // TemporalScore *float64 `json:"temporalScore,omitempty"` + // TemporalSeverity *SeverityType `json:"temporalSeverity,omitempty"` + // UserInteraction *UserInteractionType `json:"userInteraction,omitempty"` + VectorString string `json:"vectorString"` + Version string `json:"version"` // CVSS Version +} + +// type AttackComplexityType string +// +// const ( +// AttackComplexityTypeHIGH AttackComplexityType = "HIGH" +// AttackComplexityTypeLOW AttackComplexityType = "LOW" +//) +// +// type AttackVectorType string +// +// const ( +// AttackVectorTypeADJACENTNETWORK AttackVectorType = "ADJACENT_NETWORK" +// AttackVectorTypeLOCAL AttackVectorType = "LOCAL" +// AttackVectorTypeNETWORK AttackVectorType = "NETWORK" +// AttackVectorTypePHYSICAL AttackVectorType = "PHYSICAL" +//) +// +// type Type string +// +// const ( +// TypeHIGH Type = "HIGH" +// TypeLOW Type = "LOW" +// TypeNONE Type = "NONE" +//) +// +// type CiaRequirementType string +// +// const ( +// CiaRequirementTypeHIGH CiaRequirementType = "HIGH" +// CiaRequirementTypeLOW CiaRequirementType = "LOW" +// CiaRequirementTypeMEDIUM CiaRequirementType = "MEDIUM" +// CiaRequirementTypeNOTDEFINED CiaRequirementType = "NOT_DEFINED" +//) + +type SeverityType string + +const ( + Critical SeverityType = "CRITICAL" + SeverityTypeHIGH SeverityType = "HIGH" + SeverityTypeLOW SeverityType = "LOW" + SeverityTypeMEDIUM SeverityType = "MEDIUM" + SeverityTypeNONE SeverityType = "NONE" +) + +// type ExploitCodeMaturityType string +// +// const ( +// ExploitCodeMaturityTypeHIGH ExploitCodeMaturityType = "HIGH" +// ExploitCodeMaturityTypeNOTDEFINED ExploitCodeMaturityType = "NOT_DEFINED" +// Functional ExploitCodeMaturityType = "FUNCTIONAL" +// ProofOfConcept ExploitCodeMaturityType = "PROOF_OF_CONCEPT" +// Unproven ExploitCodeMaturityType = "UNPROVEN" +//) +// +// type ModifiedAttackComplexityType string +// +// const ( +// ModifiedAttackComplexityTypeHIGH ModifiedAttackComplexityType = "HIGH" +// ModifiedAttackComplexityTypeLOW ModifiedAttackComplexityType = "LOW" +// ModifiedAttackComplexityTypeNOTDEFINED ModifiedAttackComplexityType = "NOT_DEFINED" +//) +// +// type ModifiedAttackVectorType string +// +// const ( +// ModifiedAttackVectorTypeADJACENTNETWORK ModifiedAttackVectorType = "ADJACENT_NETWORK" +// ModifiedAttackVectorTypeLOCAL ModifiedAttackVectorType = "LOCAL" +// ModifiedAttackVectorTypeNETWORK ModifiedAttackVectorType = "NETWORK" +// ModifiedAttackVectorTypeNOTDEFINED ModifiedAttackVectorType = "NOT_DEFINED" +// ModifiedAttackVectorTypePHYSICAL ModifiedAttackVectorType = "PHYSICAL" +//) +// +// type ModifiedType string +// +// const ( +// ModifiedTypeHIGH ModifiedType = "HIGH" +// ModifiedTypeLOW ModifiedType = "LOW" +// ModifiedTypeNONE ModifiedType = "NONE" +// ModifiedTypeNOTDEFINED ModifiedType = "NOT_DEFINED" +//) +// +// type ModifiedScopeType string +// +// const ( +// ModifiedScopeTypeCHANGED ModifiedScopeType = "CHANGED" +// ModifiedScopeTypeNOTDEFINED ModifiedScopeType = "NOT_DEFINED" +// ModifiedScopeTypeUNCHANGED ModifiedScopeType = "UNCHANGED" +//) +// +// type ModifiedUserInteractionType string +// +// const ( +// ModifiedUserInteractionTypeNONE ModifiedUserInteractionType = "NONE" +// ModifiedUserInteractionTypeNOTDEFINED ModifiedUserInteractionType = "NOT_DEFINED" +// ModifiedUserInteractionTypeREQUIRED ModifiedUserInteractionType = "REQUIRED" +//) +// +// type RemediationLevelType string +// +// const ( +// OfficialFix RemediationLevelType = "OFFICIAL_FIX" +// RemediationLevelTypeNOTDEFINED RemediationLevelType = "NOT_DEFINED" +// TemporaryFix RemediationLevelType = "TEMPORARY_FIX" +// Unavailable RemediationLevelType = "UNAVAILABLE" +// Workaround RemediationLevelType = "WORKAROUND" +//) +// +// type ConfidenceType string +// +// const ( +// ConfidenceTypeNOTDEFINED ConfidenceType = "NOT_DEFINED" +// Confirmed ConfidenceType = "CONFIRMED" +// Reasonable ConfidenceType = "REASONABLE" +// Unknown ConfidenceType = "UNKNOWN" +//) +// +// type ScopeType string +// +// const ( +// ScopeTypeCHANGED ScopeType = "CHANGED" +// ScopeTypeUNCHANGED ScopeType = "UNCHANGED" +//) +// +// type UserInteractionType string +// +// const ( +// UserInteractionTypeNONE UserInteractionType = "NONE" +// UserInteractionTypeREQUIRED UserInteractionType = "REQUIRED" +//) diff --git a/pkg/provider/unmarshal/nvd_vulnerability.go b/pkg/provider/unmarshal/nvd_vulnerability.go new file mode 100644 index 00000000..0a739528 --- /dev/null +++ b/pkg/provider/unmarshal/nvd_vulnerability.go @@ -0,0 +1,15 @@ +package unmarshal + +import ( + "io" + + "github.com/anchore/grype-db/pkg/provider/unmarshal/nvd" +) + +type ( + NVDVulnerability = nvd.CveItem +) + +func NvdVulnerabilityEntries(reader io.Reader) ([]nvd.Vulnerability, error) { + return unmarshalSingleOrMulti[nvd.Vulnerability](reader) +} diff --git a/pkg/provider/unmarshal/os_vulnerability.go b/pkg/provider/unmarshal/os_vulnerability.go new file mode 100644 index 00000000..8dec9f52 --- /dev/null +++ b/pkg/provider/unmarshal/os_vulnerability.go @@ -0,0 +1,163 @@ +package unmarshal + +import ( + "fmt" + "io" + "sort" + "strings" + "unicode" + + "github.com/anchore/grype/grype/version" +) + +type OSFixedIn struct { + Module *string `json:"Module,omitempty"` + Name string `json:"Name"` + NamespaceName string `json:"NamespaceName"` + VendorAdvisory struct { + AdvisorySummary []struct { + ID string `json:"ID"` + Link string `json:"Link"` + } `json:"AdvisorySummary"` + NoAdvisory bool `json:"NoAdvisory"` + } `json:"VendorAdvisory"` + Version string `json:"Version"` + VersionFormat string `json:"VersionFormat"` +} + +type OSFixedIns []OSFixedIn + +type OSVulnerability struct { + Vulnerability struct { + CVSS []struct { + BaseMetrics struct { + BaseScore float64 `json:"base_score"` + BaseSeverity string `json:"base_severity"` + ExploitabilityScore float64 `json:"exploitability_score"` + ImpactScore float64 `json:"impact_score"` + } `json:"base_metrics"` + Status string `json:"status"` + VectorString string `json:"vector_string"` + Version string `json:"version"` + } `json:"CVSS"` + Description string `json:"Description"` + FixedIn OSFixedIns + Link string `json:"Link"` + Metadata struct { + Issued string `json:"Issued"` + Updated string `json:"Updated"` + RefID string `json:"RefId"` + CVE []struct { + Name string `json:"Name"` + Link string `json:"Link"` + } `json:"CVE"` + NVD struct { + CVSSv2 struct { + Score float64 `json:"Score"` + Vectors string `json:"Vectors"` + } `json:"CVSSv2"` + } `json:"NVD"` + } `json:"Metadata"` + Name string `json:"Name"` + NamespaceName string `json:"NamespaceName"` + Severity string `json:"Severity"` + } `json:"Vulnerability"` +} + +func (o OSVulnerability) IsEmpty() bool { + return o.Vulnerability.Name == "" +} + +func OSVulnerabilityEntries(reader io.Reader) ([]OSVulnerability, error) { + return unmarshalSingleOrMulti[OSVulnerability](reader) +} + +// FilterToHighestModularity returns a new distinct set of fixes, keeping only the highest version module fix. +// In cases where there is no modularity the fix is kept. +// + +func (fixes OSFixedIns) FilterToHighestModularity() OSFixedIns { + if len(fixes) < 2 { + return fixes + } + + type moduleFix struct { + constraint version.Constraint + fix OSFixedIn + } + + var keep []OSFixedIn + moduleHighestFixes := make(map[string]moduleFix) + + for _, f := range fixes { + validModule, moduleName, v, c := moduleNameAndVersion(f.Module) + if !validModule { + keep = append(keep, f) + continue + } + + k := fmt.Sprintf("%s|%s|%s", f.Name, f.NamespaceName, moduleName) + + if m, exists := moduleHighestFixes[k]; exists { + satisfied, err := m.constraint.Satisfied(v) + if err != nil { + keep = append(keep, f) + continue + } + if !satisfied { + continue + } + } + moduleHighestFixes[k] = moduleFix{ + constraint: *c, + fix: f, + } + } + + // To ensure stable output ordering for tests + var orderedKeys []string + for k := range moduleHighestFixes { + orderedKeys = append(orderedKeys, k) + } + sort.Strings(orderedKeys) + + for _, k := range orderedKeys { + keep = append(keep, moduleHighestFixes[k].fix) + } + + return keep +} + +func moduleNameAndVersion(module *string) (bool, string, *version.Version, *version.Constraint) { + if module == nil || *module == "" { + return false, "", nil, nil + } + + moduleComponents := strings.Split(*module, ":") + + if len(moduleComponents) < 2 { + return false, "", nil, nil + } + + moduleName := strings.Join(moduleComponents[0:len(moduleComponents)-1], ":") + moduleVersion := moduleComponents[len(moduleComponents)-1] + isPotentiallyVersionedModule := len(moduleVersion) > 0 && unicode.IsDigit(rune(moduleVersion[0])) + + v, c := moduleVersionConstraint(moduleVersion) + if v == nil || c == nil { + return false, "", nil, nil + } + + return isPotentiallyVersionedModule, moduleName, v, c +} + +func moduleVersionConstraint(moduleVersion string) (*version.Version, *version.Constraint) { + v, err := version.NewVersion(moduleVersion, version.UnknownFormat) + + if v == nil || err != nil { + return nil, nil + } + + c := version.MustGetConstraint(fmt.Sprintf("> %s", moduleVersion), version.UnknownFormat) + return v, &c +} diff --git a/pkg/provider/unmarshal/os_vulnerability_test.go b/pkg/provider/unmarshal/os_vulnerability_test.go new file mode 100644 index 00000000..c3d465a2 --- /dev/null +++ b/pkg/provider/unmarshal/os_vulnerability_test.go @@ -0,0 +1,265 @@ +package unmarshal + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_OSFixedIns_FilterToHighestModularity(t *testing.T) { + + keepAll := []OSFixedIn{ + { + Module: nil, + Name: "name", + NamespaceName: "namespace", + Version: "v1.0.2", + VersionFormat: "semver", + }, + { + Module: func() *string { + x := "" + return &x + }(), + Name: "name", + NamespaceName: "namespace", + Version: "v1.0.2", + VersionFormat: "semver", + }, + } + + table := []struct { + name string + start OSFixedIns + expect OSFixedIns + }{ + { + name: "go case: no filtering", + start: keepAll, + expect: keepAll, + }, + { + name: "keep the highest version of a module", + start: []OSFixedIn{ + { + Module: func() *string { + x := "name:1.0.2" + return &x + }(), + Name: "name", + NamespaceName: "namespace", + Version: "v1.0.2", + VersionFormat: "semver", + }, + { + Module: func() *string { + x := "name:1.0.3" + return &x + }(), + Name: "name", + NamespaceName: "namespace", + Version: "v1.0.3", + VersionFormat: "semver", + }, + }, + expect: []OSFixedIn{ + { + Module: func() *string { + x := "name:1.0.3" + return &x + }(), + Name: "name", + NamespaceName: "namespace", + Version: "v1.0.3", + VersionFormat: "semver", + }, + }, + }, + { + name: "keep the highest version of a module (version processing flipped)", + start: []OSFixedIn{ + + { + Module: func() *string { + x := "name:1.0.3" + return &x + }(), + Name: "name", + NamespaceName: "namespace", + Version: "v1.0.3", + VersionFormat: "semver", + }, + { + Module: func() *string { + x := "name:1.0.2" + return &x + }(), + Name: "name", + NamespaceName: "namespace", + Version: "v1.0.2", + VersionFormat: "semver", + }, + }, + expect: []OSFixedIn{ + { + Module: func() *string { + x := "name:1.0.3" + return &x + }(), + Name: "name", + NamespaceName: "namespace", + Version: "v1.0.3", + VersionFormat: "semver", + }, + }, + }, + { + name: "keep distinct module names (even though the package info is the same)", + start: []OSFixedIn{ + { + Module: func() *string { + x := "name-1:1.0.2" // <-- important + return &x + }(), + Name: "name", + NamespaceName: "namespace", + Version: "v1.0.2", + VersionFormat: "semver", + }, + { + Module: func() *string { + x := "name:1.0.3" + return &x + }(), + Name: "name", + NamespaceName: "namespace", + Version: "v1.0.3", + VersionFormat: "semver", + }, + }, + expect: []OSFixedIn{ + { + Module: func() *string { + x := "name:1.0.3" + return &x + }(), + Name: "name", + NamespaceName: "namespace", + Version: "v1.0.3", + VersionFormat: "semver", + }, + { + Module: func() *string { + x := "name-1:1.0.2" + return &x + }(), + Name: "name", + NamespaceName: "namespace", + Version: "v1.0.2", + VersionFormat: "semver", + }, + }, + }, + { + name: "keep distinct namespaces", + start: []OSFixedIn{ + { + Module: func() *string { + x := "name:1.0.2" + return &x + }(), + Name: "name", + NamespaceName: "namespace-1", // <-- important + Version: "v1.0.2", + VersionFormat: "semver", + }, + { + Module: func() *string { + x := "name:1.0.3" + return &x + }(), + Name: "name", + NamespaceName: "namespace", + Version: "v1.0.3", + VersionFormat: "semver", + }, + }, + expect: []OSFixedIn{ + { + Module: func() *string { + x := "name:1.0.2" + return &x + }(), + Name: "name", + NamespaceName: "namespace-1", + Version: "v1.0.2", + VersionFormat: "semver", + }, + { + Module: func() *string { + x := "name:1.0.3" + return &x + }(), + Name: "name", + NamespaceName: "namespace", + Version: "v1.0.3", + VersionFormat: "semver", + }, + }, + }, + { + name: "keep module with no numeric versions", + start: []OSFixedIn{ + { + Module: func() *string { + x := "name:prefix1.0.2" // <-- important + return &x + }(), + Name: "name", + NamespaceName: "namespace", + Version: "v1.0.2", + VersionFormat: "semver", + }, + { + Module: func() *string { + x := "name:1.0.3" + return &x + }(), + Name: "name", + NamespaceName: "namespace", + Version: "v1.0.3", + VersionFormat: "semver", + }, + }, + expect: []OSFixedIn{ + { + Module: func() *string { + x := "name:prefix1.0.2" + return &x + }(), + Name: "name", + NamespaceName: "namespace", + Version: "v1.0.2", + VersionFormat: "semver", + }, + { + Module: func() *string { + x := "name:1.0.3" + return &x + }(), + Name: "name", + NamespaceName: "namespace", + Version: "v1.0.3", + VersionFormat: "semver", + }, + }, + }, + } + + for _, tt := range table { + t.Run(tt.name, func(t *testing.T) { + got := tt.start.FilterToHighestModularity() + assert.Equal(t, tt.expect, got) + }) + } +} diff --git a/pkg/provider/unmarshal/single_or_multi.go b/pkg/provider/unmarshal/single_or_multi.go new file mode 100644 index 00000000..dcd5f2df --- /dev/null +++ b/pkg/provider/unmarshal/single_or_multi.go @@ -0,0 +1,31 @@ +package unmarshal + +import ( + "bytes" + "encoding/json" + "fmt" + "io" +) + +func unmarshalSingleOrMulti[T interface{}](reader io.Reader) ([]T, error) { + var entry T + + var buf bytes.Buffer + r := io.TeeReader(reader, &buf) + + dec := json.NewDecoder(r) + err := dec.Decode(&entry) + if err == nil { + return []T{entry}, nil + } + + // TODO: enhance the error handling to return the original error if the item is found to not be an array of items + + var entries []T + dec = json.NewDecoder(io.MultiReader(&buf, reader)) + + if err = dec.Decode(&entries); err != nil { + return nil, fmt.Errorf("unable to decode vulnerability: %w", handleJSONUnmarshalError(err)) + } + return entries, nil +} diff --git a/pkg/provider/workspace.go b/pkg/provider/workspace.go new file mode 100644 index 00000000..55e036f0 --- /dev/null +++ b/pkg/provider/workspace.go @@ -0,0 +1,44 @@ +package provider + +import ( + "path/filepath" +) + +type Workspace struct { + Root string + Name string +} + +func NewWorkspace(root, name string) Workspace { + return Workspace{ + Root: root, + Name: name, + } +} + +func NewWorkspaceFromExisting(workspacePath string) Workspace { + return Workspace{ + Root: filepath.Dir(workspacePath), + Name: filepath.Base(workspacePath), + } +} + +func (w Workspace) Path() string { + return filepath.Join(w.Root, w.Name) +} + +func (w Workspace) StatePath() string { + return filepath.Join(w.Path(), "metadata.json") +} + +func (w Workspace) InputPath() string { + return filepath.Join(w.Path(), "input") +} + +func (w Workspace) ResultsPath() string { + return filepath.Join(w.Path(), "results") +} + +func (w Workspace) ReadState() (*State, error) { + return ReadState(w.StatePath()) +} diff --git a/publish/.gitignore b/publish/.gitignore new file mode 100644 index 00000000..922d2228 --- /dev/null +++ b/publish/.gitignore @@ -0,0 +1,146 @@ +build/ +# ignore cache DIRECTORIES, not files (or symlinks) +cache/ +stage/ + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ +© 2021 GitHub, Inc. +Terms +Privacy +Security +Status +Docs +Contact GitHub +Pricing +API +Training +Blog +About diff --git a/publish/poetry.lock b/publish/poetry.lock new file mode 100644 index 00000000..0879db9b --- /dev/null +++ b/publish/poetry.lock @@ -0,0 +1,1044 @@ +[[package]] +name = "atomicwrites" +version = "1.4.1" +description = "Atomic file writes." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "attrs" +version = "22.1.0" +description = "Classes Without Boilerplate" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.extras] +dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"] +docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] +tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"] +tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] + +[[package]] +name = "boto3" +version = "1.24.83" +description = "The AWS SDK for Python" +category = "main" +optional = false +python-versions = ">= 3.7" + +[package.dependencies] +botocore = ">=1.27.83,<1.28.0" +jmespath = ">=0.7.1,<2.0.0" +s3transfer = ">=0.6.0,<0.7.0" + +[package.extras] +crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] + +[[package]] +name = "botocore" +version = "1.27.83" +description = "Low-level, data-driven core of boto 3." +category = "main" +optional = false +python-versions = ">= 3.7" + +[package.dependencies] +jmespath = ">=0.7.1,<2.0.0" +python-dateutil = ">=2.1,<3.0.0" +urllib3 = ">=1.25.4,<1.27" + +[package.extras] +crt = ["awscrt (==0.14.0)"] + +[[package]] +name = "certifi" +version = "2022.9.24" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "cffi" +version = "1.15.1" +description = "Foreign Function Interface for Python calling C code." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "charset-normalizer" +version = "2.1.1" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" +optional = false +python-versions = ">=3.6.0" + +[package.extras] +unicode_backport = ["unicodedata2"] + +[[package]] +name = "click" +version = "8.1.3" +description = "Composable command line interface toolkit" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.5" +description = "Cross-platform colored terminal text." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "Colr" +version = "0.9.1" +description = "Easy terminal colors, with chainable methods.\n" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "commonmark" +version = "0.9.1" +description = "Python parser for the CommonMark Markdown spec" +category = "dev" +optional = false +python-versions = "*" + +[package.extras] +test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] + +[[package]] +name = "dataclasses-json" +version = "0.5.7" +description = "Easily serialize dataclasses to and from JSON" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +marshmallow = ">=3.3.0,<4.0.0" +marshmallow-enum = ">=1.5.1,<2.0.0" +typing-inspect = ">=0.4.0" + +[package.extras] +dev = ["flake8", "hypothesis", "ipython", "mypy (>=0.710)", "portray", "pytest (>=6.2.3)", "simplejson", "types-dataclasses"] + +[[package]] +name = "distlib" +version = "0.3.6" +description = "Distribution utilities" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "filelock" +version = "3.8.0" +description = "A platform independent file lock." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["furo (>=2022.6.21)", "sphinx (>=5.1.1)", "sphinx-autodoc-typehints (>=1.19.1)"] +testing = ["covdefaults (>=2.2)", "coverage (>=6.4.2)", "pytest (>=7.1.2)", "pytest-cov (>=3)", "pytest-timeout (>=2.1)"] + +[[package]] +name = "gitdb" +version = "4.0.10" +description = "Git Object Database" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +smmap = ">=3.0.1,<6" + +[[package]] +name = "GitPython" +version = "3.1.30" +description = "GitPython is a python library used to interact with Git repositories" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +gitdb = ">=4.0.1,<5" + +[[package]] +name = "idna" +version = "3.4" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "iso8601" +version = "0.1.16" +description = "Simple module to parse ISO 8601 dates" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "jmespath" +version = "1.0.1" +description = "JSON Matching Expressions" +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "marshmallow" +version = "3.18.0" +description = "A lightweight library for converting complex datatypes to and from native Python datatypes." +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +packaging = ">=17.0" + +[package.extras] +dev = ["flake8 (==5.0.4)", "flake8-bugbear (==22.9.11)", "mypy (==0.971)", "pre-commit (>=2.4,<3.0)", "pytest", "pytz", "simplejson", "tox"] +docs = ["alabaster (==0.7.12)", "autodocsumm (==0.2.9)", "sphinx (==5.1.1)", "sphinx-issues (==3.0.1)", "sphinx-version-warning (==1.1.2)"] +lint = ["flake8 (==5.0.4)", "flake8-bugbear (==22.9.11)", "mypy (==0.971)", "pre-commit (>=2.4,<3.0)"] +tests = ["pytest", "pytz", "simplejson"] + +[[package]] +name = "marshmallow-enum" +version = "1.5.1" +description = "Enum field for Marshmallow" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +marshmallow = ">=2.0.0" + +[[package]] +name = "mashumaro" +version = "3.3" +description = "Fast serialization framework on top of dataclasses" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +pyyaml = {version = ">=3.13", optional = true, markers = "extra == \"yaml\""} +typing-extensions = ">=4.1.0" + +[package.extras] +msgpack = ["msgpack (>=0.5.6)"] +orjson = ["orjson"] +toml = ["tomli (>=1.1.0)", "tomli-w (>=1.0)"] +yaml = ["pyyaml (>=3.13)"] + +[[package]] +name = "mypy-extensions" +version = "0.4.3" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "omitempty" +version = "0.1.1" +description = "enums for Python" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "packaging" +version = "21.3" +description = "Core utilities for Python packages" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" + +[[package]] +name = "platformdirs" +version = "2.5.2" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx (>=4)", "sphinx-autodoc-typehints (>=1.12)"] +test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] + +[[package]] +name = "pluggy" +version = "1.0.0" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pprintpp" +version = "0.4.0" +description = "A drop-in replacement for pprint that's actually pretty" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "prompt-toolkit" +version = "3.0.36" +description = "Library for building powerful interactive command lines in Python" +category = "main" +optional = false +python-versions = ">=3.6.2" + +[package.dependencies] +wcwidth = "*" + +[[package]] +name = "py" +version = "1.11.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "pycparser" +version = "2.21" +description = "C parser in Python" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "Pygments" +version = "2.13.0" +description = "Pygments is a syntax highlighting package written in Python." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +plugins = ["importlib-metadata"] + +[[package]] +name = "pyparsing" +version = "3.0.9" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +category = "main" +optional = false +python-versions = ">=3.6.8" + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + +[[package]] +name = "pytest" +version = "6.2.5" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +py = ">=1.8.2" +toml = "*" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] + +[[package]] +name = "pytest-clarity" +version = "1.0.1" +description = "A plugin providing an alternative, colourful diff output for failing assertions." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +pprintpp = ">=0.4.0" +pytest = ">=3.5.0" +rich = ">=8.0.0" + +[[package]] +name = "pytest-sugar" +version = "0.9.6" +description = "pytest-sugar is a plugin for pytest that changes the default look and feel of pytest (e.g. progressbar, show tests that fail instantly)." +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +packaging = ">=14.1" +pytest = ">=2.9" +termcolor = ">=1.1.0" + +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "PyYAML" +version = "6.0" +description = "YAML parser and emitter for Python" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "requests" +version = "2.28.1" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=3.7, <4" + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<3" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "rfc3339" +version = "6.2" +description = "Format dates according to the RFC 3339." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "rich" +version = "13.0.0" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +category = "dev" +optional = false +python-versions = ">=3.7.0" + +[package.dependencies] +commonmark = ">=0.9.0,<0.10.0" +pygments = ">=2.6.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"] + +[[package]] +name = "s3transfer" +version = "0.6.0" +description = "An Amazon S3 Transfer Manager" +category = "main" +optional = false +python-versions = ">= 3.7" + +[package.dependencies] +botocore = ">=1.12.36,<2.0a.0" + +[package.extras] +crt = ["botocore[crt] (>=1.20.29,<2.0a.0)"] + +[[package]] +name = "semver" +version = "2.13.0" +description = "Python helper for Semantic Versioning (http://semver.org/)" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "smmap" +version = "5.0.0" +description = "A pure Python implementation of a sliding window memory map manager" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "tabulate" +version = "0.8.10" +description = "Pretty-print tabular data" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.extras] +widechars = ["wcwidth"] + +[[package]] +name = "termcolor" +version = "2.1.1" +description = "ANSI color formatting for output in terminal" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +tests = ["pytest", "pytest-cov"] + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "tox" +version = "3.26.0" +description = "tox is a generic virtualenv management and test command line tool" +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[package.dependencies] +colorama = {version = ">=0.4.1", markers = "platform_system == \"Windows\""} +filelock = ">=3.0.0" +packaging = ">=14" +pluggy = ">=0.12.0" +py = ">=1.4.17" +six = ">=1.14.0" +tomli = {version = ">=2.0.1", markers = "python_version >= \"3.7\" and python_version < \"3.11\""} +virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2,<20.0.3 || >20.0.3,<20.0.4 || >20.0.4,<20.0.5 || >20.0.5,<20.0.6 || >20.0.6,<20.0.7 || >20.0.7" + +[package.extras] +docs = ["pygments-github-lexers (>=0.0.5)", "sphinx (>=2.0.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "towncrier (>=18.5.0)"] +testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pathlib2 (>=2.3.3)", "psutil (>=5.6.1)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)"] + +[[package]] +name = "typing-extensions" +version = "4.3.0" +description = "Backported and Experimental Type Hints for Python 3.7+" +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "typing-inspect" +version = "0.8.0" +description = "Runtime inspection utilities for typing module." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +mypy-extensions = ">=0.3.0" +typing-extensions = ">=3.7.4" + +[[package]] +name = "urllib3" +version = "1.26.12" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[[package]] +name = "virtualenv" +version = "20.16.5" +description = "Virtual Python Environment builder" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +distlib = ">=0.3.5,<1" +filelock = ">=3.4.1,<4" +platformdirs = ">=2.4,<3" + +[package.extras] +docs = ["proselint (>=0.13)", "sphinx (>=5.1.1)", "sphinx-argparse (>=0.3.1)", "sphinx-rtd-theme (>=1)", "towncrier (>=21.9)"] +testing = ["coverage (>=6.2)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=21.3)", "pytest (>=7.0.1)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.6.1)", "pytest-randomly (>=3.10.3)", "pytest-timeout (>=2.1)"] + +[[package]] +name = "wcwidth" +version = "0.2.5" +description = "Measures the displayed width of unicode strings in a terminal" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "yardstick" +version = "0.1.0" +description = "Tool for comparing the results from vulnerability scanners" +category = "main" +optional = false +python-versions = "^3.7" +develop = false + +[package.dependencies] +click = "^8" +Colr = "^0.9.1" +dataclasses-json = "^0.5.2" +GitPython = "^3.1.15" +mashumaro = {version = "^3.0.4", extras = ["yaml"]} +omitempty = "^0.1.1" +prompt-toolkit = "^3.0.18" +Pygments = "^2.8.1" +requests = "^2.25.1" +rfc3339 = "^6.2" +tabulate = "^0.8.9" + +[package.source] +type = "git" +url = "https://github.com/anchore/yardstick.git" +reference = "028b7723cd1133dd649ac5f5db90ea743767f2b8" +resolved_reference = "028b7723cd1133dd649ac5f5db90ea743767f2b8" + +[[package]] +name = "zstandard" +version = "0.18.0" +description = "Zstandard bindings for Python" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +cffi = {version = ">=1.11", markers = "platform_python_implementation == \"PyPy\""} + +[package.extras] +cffi = ["cffi (>=1.11)"] + +[metadata] +lock-version = "1.1" +python-versions = "^3.10" +content-hash = "2ca47336bd25b0ba938c09b74fcc3fb92b951b0b08c8f5a1f6e1114c395cf5bf" + +[metadata.files] +atomicwrites = [ + {file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"}, +] +attrs = [ + {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"}, + {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, +] +boto3 = [ + {file = "boto3-1.24.83-py3-none-any.whl", hash = "sha256:a29214908224da132ecc025f4da266f4124c26d22205002bfd02aed5743a2e47"}, + {file = "boto3-1.24.83.tar.gz", hash = "sha256:b10d9ecaba3f0ed844192828d2c2b26bfa1dfd2b40ccccc25507575f28097e32"}, +] +botocore = [ + {file = "botocore-1.27.83-py3-none-any.whl", hash = "sha256:9a1fb823c68aef1227f2d63af856a485e09f43792f257a821b53ea86481c66bd"}, + {file = "botocore-1.27.83.tar.gz", hash = "sha256:c57f74322cf672405f0ec7ee8fda7d80d323a9c664a90926c11313ca3bfbca91"}, +] +certifi = [ + {file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"}, + {file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"}, +] +cffi = [ + {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, + {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"}, + {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"}, + {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"}, + {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"}, + {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"}, + {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"}, + {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"}, + {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"}, + {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"}, + {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"}, + {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"}, + {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"}, + {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"}, + {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"}, + {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"}, + {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"}, + {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"}, + {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"}, + {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"}, + {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"}, + {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"}, + {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"}, + {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"}, + {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"}, + {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"}, + {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"}, + {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"}, + {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"}, + {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"}, + {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"}, + {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"}, + {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"}, + {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, + {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, +] +charset-normalizer = [ + {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, + {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, +] +click = [ + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, +] +colorama = [ + {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, + {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, +] +Colr = [ + {file = "Colr-0.9.1.tar.gz", hash = "sha256:8c15437eeb2ec8821c6df24b62946dfc6b79f69a1d84c1a6c131945a5ff4623c"}, +] +commonmark = [ + {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, + {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, +] +dataclasses-json = [ + {file = "dataclasses-json-0.5.7.tar.gz", hash = "sha256:c2c11bc8214fbf709ffc369d11446ff6945254a7f09128154a7620613d8fda90"}, + {file = "dataclasses_json-0.5.7-py3-none-any.whl", hash = "sha256:bc285b5f892094c3a53d558858a88553dd6a61a11ab1a8128a0e554385dcc5dd"}, +] +distlib = [ + {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, + {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"}, +] +filelock = [ + {file = "filelock-3.8.0-py3-none-any.whl", hash = "sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4"}, + {file = "filelock-3.8.0.tar.gz", hash = "sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc"}, +] +gitdb = [ + {file = "gitdb-4.0.10-py3-none-any.whl", hash = "sha256:c286cf298426064079ed96a9e4a9d39e7f3e9bf15ba60701e95f5492f28415c7"}, + {file = "gitdb-4.0.10.tar.gz", hash = "sha256:6eb990b69df4e15bad899ea868dc46572c3f75339735663b81de79b06f17eb9a"}, +] +GitPython = [ + {file = "GitPython-3.1.30-py3-none-any.whl", hash = "sha256:cd455b0000615c60e286208ba540271af9fe531fa6a87cc590a7298785ab2882"}, + {file = "GitPython-3.1.30.tar.gz", hash = "sha256:769c2d83e13f5d938b7688479da374c4e3d49f71549aaf462b646db9602ea6f8"}, +] +idna = [ + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, +] +iniconfig = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] +iso8601 = [ + {file = "iso8601-0.1.16-py2.py3-none-any.whl", hash = "sha256:906714829fedbc89955d52806c903f2332e3948ed94e31e85037f9e0226b8376"}, + {file = "iso8601-0.1.16.tar.gz", hash = "sha256:36532f77cc800594e8f16641edae7f1baf7932f05d8e508545b95fc53c6dc85b"}, +] +jmespath = [ + {file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"}, + {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, +] +marshmallow = [ + {file = "marshmallow-3.18.0-py3-none-any.whl", hash = "sha256:35e02a3a06899c9119b785c12a22f4cda361745d66a71ab691fd7610202ae104"}, + {file = "marshmallow-3.18.0.tar.gz", hash = "sha256:6804c16114f7fce1f5b4dadc31f4674af23317fcc7f075da21e35c1a35d781f7"}, +] +marshmallow-enum = [ + {file = "marshmallow-enum-1.5.1.tar.gz", hash = "sha256:38e697e11f45a8e64b4a1e664000897c659b60aa57bfa18d44e226a9920b6e58"}, + {file = "marshmallow_enum-1.5.1-py2.py3-none-any.whl", hash = "sha256:57161ab3dbfde4f57adeb12090f39592e992b9c86d206d02f6bd03ebec60f072"}, +] +mashumaro = [ + {file = "mashumaro-3.3-py3-none-any.whl", hash = "sha256:14d223d8479e5cbddbfd6980f380b14301f48498e458dc3cb1b8b96838c8f713"}, + {file = "mashumaro-3.3.tar.gz", hash = "sha256:7ef12e2e81ad0ccb4560435ded1bec06ba38b941ce1470e82a13157c6b201661"}, +] +mypy-extensions = [ + {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, + {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, +] +omitempty = [ + {file = "omitempty-0.1.1.tar.gz", hash = "sha256:761fea43d0edb7a31e3322158f73c97d77e939e57c1e62754be23e081ab853d8"}, +] +packaging = [ + {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, + {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, +] +platformdirs = [ + {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, + {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, +] +pluggy = [ + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, +] +pprintpp = [ + {file = "pprintpp-0.4.0-py2.py3-none-any.whl", hash = "sha256:b6b4dcdd0c0c0d75e4d7b2f21a9e933e5b2ce62b26e1a54537f9651ae5a5c01d"}, + {file = "pprintpp-0.4.0.tar.gz", hash = "sha256:ea826108e2c7f49dc6d66c752973c3fc9749142a798d6b254e1e301cfdbc6403"}, +] +prompt-toolkit = [ + {file = "prompt_toolkit-3.0.36-py3-none-any.whl", hash = "sha256:aa64ad242a462c5ff0363a7b9cfe696c20d55d9fc60c11fd8e632d064804d305"}, + {file = "prompt_toolkit-3.0.36.tar.gz", hash = "sha256:3e163f254bef5a03b146397d7c1963bd3e2812f0964bb9a24e6ec761fd28db63"}, +] +py = [ + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, +] +pycparser = [ + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, +] +Pygments = [ + {file = "Pygments-2.13.0-py3-none-any.whl", hash = "sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42"}, + {file = "Pygments-2.13.0.tar.gz", hash = "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1"}, +] +pyparsing = [ + {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, + {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, +] +pytest = [ + {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, + {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, +] +pytest-clarity = [ + {file = "pytest-clarity-1.0.1.tar.gz", hash = "sha256:505fe345fad4fe11c6a4187fe683f2c7c52c077caa1e135f3e483fe112db7772"}, +] +pytest-sugar = [ + {file = "pytest-sugar-0.9.6.tar.gz", hash = "sha256:c4793495f3c32e114f0f5416290946c316eb96ad5a3684dcdadda9267e59b2b8"}, + {file = "pytest_sugar-0.9.6-py2.py3-none-any.whl", hash = "sha256:30e5225ed2b3cc988a8a672f8bda0fc37bcd92d62e9273937f061112b3f2186d"}, +] +python-dateutil = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] +PyYAML = [ + {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, + {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, + {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, + {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, + {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, + {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, + {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, + {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, + {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, + {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, + {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, + {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, + {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, + {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, + {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, + {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, + {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, + {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, +] +requests = [ + {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, + {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, +] +rfc3339 = [ + {file = "rfc3339-6.2-py3-none-any.whl", hash = "sha256:f44316b21b21db90a625cde04ebb0d46268f153e6093021fa5893e92a96f58a3"}, + {file = "rfc3339-6.2.tar.gz", hash = "sha256:d53c3b5eefaef892b7240ba2a91fef012e86faa4d0a0ca782359c490e00ad4d0"}, +] +rich = [ + {file = "rich-13.0.0-py3-none-any.whl", hash = "sha256:12b1d77ee7edf251b741531323f0d990f5f570a4e7c054d0bfb59fb7981ad977"}, + {file = "rich-13.0.0.tar.gz", hash = "sha256:3aa9eba7219b8c575c6494446a59f702552efe1aa261e7eeb95548fa586e1950"}, +] +s3transfer = [ + {file = "s3transfer-0.6.0-py3-none-any.whl", hash = "sha256:06176b74f3a15f61f1b4f25a1fc29a4429040b7647133a463da8fa5bd28d5ecd"}, + {file = "s3transfer-0.6.0.tar.gz", hash = "sha256:2ed07d3866f523cc561bf4a00fc5535827981b117dd7876f036b0c1aca42c947"}, +] +semver = [ + {file = "semver-2.13.0-py2.py3-none-any.whl", hash = "sha256:ced8b23dceb22134307c1b8abfa523da14198793d9787ac838e70e29e77458d4"}, + {file = "semver-2.13.0.tar.gz", hash = "sha256:fa0fe2722ee1c3f57eac478820c3a5ae2f624af8264cbdf9000c980ff7f75e3f"}, +] +six = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] +smmap = [ + {file = "smmap-5.0.0-py3-none-any.whl", hash = "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94"}, + {file = "smmap-5.0.0.tar.gz", hash = "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"}, +] +tabulate = [ + {file = "tabulate-0.8.10-py3-none-any.whl", hash = "sha256:0ba055423dbaa164b9e456abe7920c5e8ed33fcc16f6d1b2f2d152c8e1e8b4fc"}, + {file = "tabulate-0.8.10.tar.gz", hash = "sha256:6c57f3f3dd7ac2782770155f3adb2db0b1a269637e42f27599925e64b114f519"}, +] +termcolor = [ + {file = "termcolor-2.1.1-py3-none-any.whl", hash = "sha256:fa852e957f97252205e105dd55bbc23b419a70fec0085708fc0515e399f304fd"}, + {file = "termcolor-2.1.1.tar.gz", hash = "sha256:67cee2009adc6449c650f6bcf3bdeed00c8ba53a8cda5362733c53e0a39fb70b"}, +] +toml = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] +tomli = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] +tox = [ + {file = "tox-3.26.0-py2.py3-none-any.whl", hash = "sha256:bf037662d7c740d15c9924ba23bb3e587df20598697bb985ac2b49bdc2d847f6"}, + {file = "tox-3.26.0.tar.gz", hash = "sha256:44f3c347c68c2c68799d7d44f1808f9d396fc8a1a500cbc624253375c7ae107e"}, +] +typing-extensions = [ + {file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"}, + {file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"}, +] +typing-inspect = [ + {file = "typing_inspect-0.8.0-py3-none-any.whl", hash = "sha256:5fbf9c1e65d4fa01e701fe12a5bca6c6e08a4ffd5bc60bfac028253a447c5188"}, + {file = "typing_inspect-0.8.0.tar.gz", hash = "sha256:8b1ff0c400943b6145df8119c41c244ca8207f1f10c9c057aeed1560e4806e3d"}, +] +urllib3 = [ + {file = "urllib3-1.26.12-py2.py3-none-any.whl", hash = "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"}, + {file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"}, +] +virtualenv = [ + {file = "virtualenv-20.16.5-py3-none-any.whl", hash = "sha256:d07dfc5df5e4e0dbc92862350ad87a36ed505b978f6c39609dc489eadd5b0d27"}, + {file = "virtualenv-20.16.5.tar.gz", hash = "sha256:227ea1b9994fdc5ea31977ba3383ef296d7472ea85be9d6732e42a91c04e80da"}, +] +wcwidth = [ + {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, + {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, +] +yardstick = [] +zstandard = [ + {file = "zstandard-0.18.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef7e8a200e4c8ac9102ed3c90ed2aa379f6b880f63032200909c1be21951f556"}, + {file = "zstandard-0.18.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2dc466207016564805e56d28375f4f533b525ff50d6776946980dff5465566ac"}, + {file = "zstandard-0.18.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a2ee1d4f98447f3e5183ecfce5626f983504a4a0c005fbe92e60fa8e5d547ec"}, + {file = "zstandard-0.18.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d956e2f03c7200d7e61345e0880c292783ec26618d0d921dcad470cb195bbce2"}, + {file = "zstandard-0.18.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ce6f59cba9854fd14da5bfe34217a1501143057313966637b7291d1b0267bd1e"}, + {file = "zstandard-0.18.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7fa67cba473623848b6e88acf8d799b1906178fd883fb3a1da24561c779593b"}, + {file = "zstandard-0.18.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cdb44d7284c8c5dd1b66dfb86dda7f4560fa94bfbbc1d2da749ba44831335e32"}, + {file = "zstandard-0.18.0-cp310-cp310-win32.whl", hash = "sha256:63694a376cde0aa8b1971d06ca28e8f8b5f492779cb6ee1cc46bbc3f019a42a5"}, + {file = "zstandard-0.18.0-cp310-cp310-win_amd64.whl", hash = "sha256:702a8324cd90c74d9c8780d02bf55e79da3193c870c9665ad3a11647e3ad1435"}, + {file = "zstandard-0.18.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:46f679bc5dfd938db4fb058218d9dc4db1336ffaf1ea774ff152ecadabd40805"}, + {file = "zstandard-0.18.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc2a4de9f363b3247d472362a65041fe4c0f59e01a2846b15d13046be866a885"}, + {file = "zstandard-0.18.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd3220d7627fd4d26397211cb3b560ec7cc4a94b75cfce89e847e8ce7fabe32d"}, + {file = "zstandard-0.18.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:39e98cf4773234bd9cebf9f9db730e451dfcfe435e220f8921242afda8321887"}, + {file = "zstandard-0.18.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5228e596eb1554598c872a337bbe4e5afe41cd1f8b1b15f2e35b50d061e35244"}, + {file = "zstandard-0.18.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d4a8fd45746a6c31e729f35196e80b8f1e9987c59f5ccb8859d7c6a6fbeb9c63"}, + {file = "zstandard-0.18.0-cp36-cp36m-win32.whl", hash = "sha256:4cbb85f29a990c2fdbf7bc63246567061a362ddca886d7fae6f780267c0a9e67"}, + {file = "zstandard-0.18.0-cp36-cp36m-win_amd64.whl", hash = "sha256:bfa6c8549fa18e6497a738b7033c49f94a8e2e30c5fbe2d14d0b5aa8bbc1695d"}, + {file = "zstandard-0.18.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e02043297c1832f2666cd2204f381bef43b10d56929e13c42c10c732c6e3b4ed"}, + {file = "zstandard-0.18.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7231543d38d2b7e02ef7cc78ef7ffd86419437e1114ff08709fe25a160e24bd6"}, + {file = "zstandard-0.18.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c86befac87445927488f5c8f205d11566f64c11519db223e9d282b945fa60dab"}, + {file = "zstandard-0.18.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:999a4e1768f219826ba3fa2064fab1c86dd72fdd47a42536235478c3bb3ca3e2"}, + {file = "zstandard-0.18.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9df59cd1cf3c62075ee2a4da767089d19d874ac3ad42b04a71a167e91b384722"}, + {file = "zstandard-0.18.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1be31e9e3f7607ee0cdd60915410a5968b205d3e7aa83b7fcf3dd76dbbdb39e0"}, + {file = "zstandard-0.18.0-cp37-cp37m-win32.whl", hash = "sha256:490d11b705b8ae9dc845431bacc8dd1cef2408aede176620a5cd0cd411027936"}, + {file = "zstandard-0.18.0-cp37-cp37m-win_amd64.whl", hash = "sha256:266aba27fa9cc5e9091d3d325ebab1fa260f64e83e42516d5e73947c70216a5b"}, + {file = "zstandard-0.18.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8b2260c4e07dd0723eadb586de7718b61acca4083a490dda69c5719d79bc715c"}, + {file = "zstandard-0.18.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3af8c2383d02feb6650e9255491ec7d0824f6e6dd2bbe3e521c469c985f31fb1"}, + {file = "zstandard-0.18.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28723a1d2e4df778573b76b321ebe9f3469ac98988104c2af116dd344802c3f8"}, + {file = "zstandard-0.18.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19cac7108ff2c342317fad6dc97604b47a41f403c8f19d0bfc396dfadc3638b8"}, + {file = "zstandard-0.18.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:76725d1ee83a8915100a310bbad5d9c1fc6397410259c94033b8318d548d9990"}, + {file = "zstandard-0.18.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d716a7694ce1fa60b20bc10f35c4a22be446ef7f514c8dbc8f858b61976de2fb"}, + {file = "zstandard-0.18.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:49685bf9a55d1ab34bd8423ea22db836ba43a181ac6b045ac4272093d5cb874e"}, + {file = "zstandard-0.18.0-cp38-cp38-win32.whl", hash = "sha256:1af1268a7dc870eb27515fb8db1f3e6c5a555d2b7bcc476fc3bab8886c7265ab"}, + {file = "zstandard-0.18.0-cp38-cp38-win_amd64.whl", hash = "sha256:1dc2d3809e763055a1a6c1a73f2b677320cc9a5aa1a7c6cfb35aee59bddc42d9"}, + {file = "zstandard-0.18.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eea18c1e7442f2aa9aff1bb84550dbb6a1f711faf6e48e7319de8f2b2e923c2a"}, + {file = "zstandard-0.18.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8677ffc6a6096cccbd892e558471c901fd821aba12b7fbc63833c7346f549224"}, + {file = "zstandard-0.18.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:083dc08abf03807af9beeb2b6a91c23ad78add2499f828176a3c7b742c44df02"}, + {file = "zstandard-0.18.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c990063664c08169c84474acecc9251ee035871589025cac47c060ff4ec4bc1a"}, + {file = "zstandard-0.18.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:533db8a6fac6248b2cb2c935e7b92f994efbdeb72e1ffa0b354432e087bb5a3e"}, + {file = "zstandard-0.18.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbb3cb8a082d62b8a73af42291569d266b05605e017a3d8a06a0e5c30b5f10f0"}, + {file = "zstandard-0.18.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d6c85ca5162049ede475b7ec98e87f9390501d44a3d6776ddd504e872464ec25"}, + {file = "zstandard-0.18.0-cp39-cp39-win32.whl", hash = "sha256:75479e7c2b3eebf402c59fbe57d21bc400cefa145ca356ee053b0a08908c5784"}, + {file = "zstandard-0.18.0-cp39-cp39-win_amd64.whl", hash = "sha256:d85bfabad444812133a92fc6fbe463e1d07581dba72f041f07a360e63808b23c"}, + {file = "zstandard-0.18.0.tar.gz", hash = "sha256:0ac0357a0d985b4ff31a854744040d7b5754385d1f98f7145c30e02c6865cb6f"}, +] diff --git a/publish/pyproject.toml b/publish/pyproject.toml new file mode 100644 index 00000000..fcc01488 --- /dev/null +++ b/publish/pyproject.toml @@ -0,0 +1,32 @@ +[tool.poetry.scripts] +publisher = "publisher.console:cli" + +[tool.poetry.group.dev.dependencies] +pytest-sugar = "^0.9.6" +pytest-clarity = "^1.0.1" + +[tool.poetry] +name = "publisher" +version = "0.1.0" +description = "Collection of scripts used to generate and publish supported grype DBs" +authors = ["Alex Goodman "] +license = "Apache 2.0" +exclude = [ + "tests/**/*" +] + +[tool.poetry.dependencies] +python = "^3.10" +#yardstick = {path = "../../../yardstick", develop = true} +click = "^8" +boto3 = "^1.18.0" +requests = "^2.26.0" +semver = "^2.13.0" +dataclasses-json = "^0.5.4" +iso8601 = "^0.1.14" +zstandard = "^0.18.0" +yardstick = {git = "https://github.com/anchore/yardstick.git", rev = "028b7723cd1133dd649ac5f5db90ea743767f2b8"} + +[tool.poetry.dev-dependencies] +pytest = "^6.2.2" +tox = "^3.23.0" diff --git a/publish/src/publisher/__init__.py b/publish/src/publisher/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/publish/src/publisher/console.py b/publish/src/publisher/console.py new file mode 100644 index 00000000..be16873c --- /dev/null +++ b/publish/src/publisher/console.py @@ -0,0 +1,282 @@ +#!/usr/bin/env python3 +import os +import hashlib +import tempfile +import logging.config +from datetime import datetime, timezone +from urllib.parse import urlparse, urlunparse +from typing import Dict, Set, Generator, List, Optional + +import iso8601 # type: ignore +import click + +import publisher.utils.grype as grype +import publisher.utils.builder as builder +import publisher.utils.test as test +import publisher.utils.listing as listing +import publisher.utils.metadata as metadata +import publisher.utils.s3utils as s3utils +from publisher.utils.constants import ( + DB_DIR, + DB_SUFFIXES, + LEGACY_DB_SUFFIXES, + NEW_DB_SUFFIXES, + GOLDEN_REPORT_LOCATION, + TEST_IMAGE, + STAGE_DIR, +) + +MAX_DB_AGE = 120 # ~4 months in days +MINIMUM_DB_COUNT = MAX_DB_AGE # number of db entries per schema +GRYPE_TEST_SCHEMA = os.environ.get("GRYPE_TEST_SCHEMA", None) +GRYPE_TEST_RELEASE = os.environ.get("GRYPE_TEST_BRANCH", None) + + +@click.group(help="Tooling to support generating the publishing supported versions of grype DB.") +def cli(): + # pylint: disable=redefined-outer-name, import-outside-toplevel + import logging.config + + logging.config.dictConfig( + { + 'version': 1, + 'formatters': { + 'standard': { + 'format': '%(asctime)s [%(levelname)s] [%(module)s.%(funcName)s] %(message)s', + 'datefmt': '', + }, + }, + 'handlers': { + 'default': { + 'level': 'DEBUG', + 'formatter': 'standard', + 'class': 'logging.StreamHandler', + 'stream': 'ext://sys.stdout', # (default is stderr) + }, + }, + 'loggers': { + '': { # root logger + 'handlers': ['default'], + 'level': 'DEBUG', + }, + } + } + ) + + +@cli.command() +@click.option("--schema-version", required=True, help="the DB schema version to build and package") +def generate(schema_version: int): + golden_image_report = os.path.join( + GOLDEN_REPORT_LOCATION, "%s.json" % TEST_IMAGE.replace(":", "-") + ) + + runner = test.Runner(user_input=TEST_IMAGE, golden_report_path=golden_image_report) + + # TODO: use a released version of grype-db + logging.info(f"using grype-db from main branch") + + db_builder = builder.GrypeDbBuilder() + grype_obj = grype.Grype(schema_version=schema_version, release=GRYPE_TEST_RELEASE) + + # build set of test cases derived from the grype version and its configured schema versions + runner.add_case( + case=test.Case( + grype=grype_obj, schema_version=schema_version, builder=db_builder + ) + ) + + # build a database for the schema needed + db_builder.build_and_package( + db_dir=os.path.join(DB_DIR, str(schema_version)), + schema_version=schema_version, + stage_dir=STAGE_DIR, + ) + + # run the test runner (an exception will be raised on failure) + runner.run() + + logging.info("all DBs generated & passed acceptance testing") + logging.info(f"staged DB artifacts @ {STAGE_DIR}") + + + +@cli.command() +@click.option("--s3-bucket", required=True, help="S3 bucket to upload the DBs") +@click.option( + "--s3-path", + required=True, + help="base path in the S3 bucket where assets should be uploaded to (DBs and listing files)", +) +@click.option( + "--dry-run", + default=False, + is_flag=True, + help="do everything except upload the listing file", +) +def upload_listing(s3_bucket: str, s3_path: str, dry_run: bool): + if not dry_run: + ensure_running_in_CI() + else: + logging.warning(f"DRY-RUN! nothing in S3 will be added or changed") + + # get existing listing file... if does not exist, create new empty listing file + the_listing = listing.fetch(bucket=s3_bucket, path=s3_path) + + # look for existing DBs in S3 + existing_paths_by_basename = existing_dbs_in_s3( + s3_bucket=s3_bucket, s3_path=s3_path, suffixes=LEGACY_DB_SUFFIXES, + ) + + # determine what basenames are new relative to the listing file and the current S3 state + new_basenames, missing_basenames = the_listing.basename_difference( + set(existing_paths_by_basename.keys()), + ) + + if missing_basenames: + logging.warning(f"missing {len(missing_basenames)} databases in S3 which were in the existing listing file (removing entries in the next listing file)") + for basename in missing_basenames: + logging.warning(f" {basename}") + + the_listing.remove_by_basename(missing_basenames) + + # add DBs that were discovered in S3 but not already in the listing file + logging.info(f"discovered {len(new_basenames)} new databases to add to the listing") + for entry in listing_entries_dbs_in_s3( + basenames=new_basenames, + paths_by_basename=existing_paths_by_basename, + s3_bucket=s3_bucket, + s3_path=s3_path, + suffixes=LEGACY_DB_SUFFIXES, + max_age=MAX_DB_AGE, + ): + the_listing.add(entry) + + # prune the listing to the top X many, by schema, sorted by build date + # note: we do not delete the entries from S3 in case they need to be referenced again + the_listing.prune(max_age_days=MAX_DB_AGE, minimum_elements=MINIMUM_DB_COUNT) + + # TODO: test out the new listing file with all supported versions of grype + logging.info("acceptance testing the new listing") + override_schema_release = None + if GRYPE_TEST_SCHEMA and GRYPE_TEST_RELEASE: + override_schema_release = (GRYPE_TEST_SCHEMA, GRYPE_TEST_RELEASE) + listing.acceptance_test(test_listing=the_listing, image=TEST_IMAGE, override_schema_release=override_schema_release) + + the_listing.log() + + if dry_run: + logging.warning(f"DRY-RUN! skipping upload...") + return + + # upload the listing + s3utils.upload(bucket=s3_bucket, + key=the_listing.url(s3_path), + contents=the_listing.to_json(), + CacheControl="public,max-age=2700") # type: ignore + + # TODO: for a future PR: add deletion of pruned objects... + # delete all objects in the bucket that were pruned from the listing + # for entry in extra: + # db_url = urlparse(entry.url, allow_fragments=False) + # path = "/".join([path, os.path.basename(db_url.path)]) + # if not any([path.endswith(s) for s in DB_SUFFIXES]): + # raise RuntimeError(f"attempted to delete non-archive: {s3_path}") + # + # s3utils.delete(bucket=s3_bucket, key=s3_path) + + +def listing_entries_dbs_in_s3( + basenames: Set[str], paths_by_basename: Dict[str, str], s3_bucket: str, s3_path: str, suffixes: set[str], max_age: int +) -> Generator[listing.Entry, None, None]: + # generate metadata from each downloaded archive and add to the listing file + for basename in basenames: + if not any([basename.endswith(s) for s in suffixes]): + logging.info(f" skipping db (unsupported extension) {basename}") + continue + + age = age_from_basename(basename) + if age is None or age > max_age: + logging.info(f" skipping db (too old -- {age} days) {basename}") + continue + + s3_existing_path = paths_by_basename[basename] + logging.info(f" new db {s3_existing_path}") + + # we don't want to keep around files between processing of each db file, so purge on each iteration + with tempfile.TemporaryDirectory(prefix="grype-downloaded-db") as tempdir: + local_path = os.path.join(tempdir, basename) + s3utils.download_to_file( + bucket=s3_bucket, key=s3_existing_path, path=local_path + ) + + # derive the checksum from the sha256 of the archive + checksum = hash_file(path=local_path) + + # extract the metadata from the archive + meta = metadata.from_archive(path=local_path) + + # create a new listing entry and add it to the listing + url = "https://{}".format("/".join([s3_bucket, s3_path, basename])) + url = urlunparse(urlparse(url)) # normalize the url + + yield listing.Entry( + built=meta.built, version=meta.version, url=url, checksum=checksum + ) + + +def existing_dbs_in_s3(s3_bucket: str, s3_path: str, suffixes: set[str]) -> Dict[str, str]: + # list objects in the db bucket path, download all objects not in the listing to a temp dir + existing_databases = [] + + for suffix in suffixes: + found = list( + s3utils.get_matching_s3_keys(bucket=s3_bucket, prefix=s3_path, suffix=suffix) + ) + logging.info( + f"{len(found)} existing databases in bucket={s3_bucket} path={s3_path} suffix={suffix}" + ) + existing_databases.extend(found) + + return get_paths_by_basename(existing_databases) + + +def get_paths_by_basename(paths: List[str]) -> Dict[str, str]: + paths_by_basename: Dict[str, str] = {} + for path in paths: + basename = os.path.basename(path) + if basename not in paths_by_basename: + logging.info(f" existing db {path}") + + paths_by_basename[basename] = path + else: + raise RuntimeError( + f"duplicate basenames found (this should not happen): {basename}" + ) + return paths_by_basename + +def age_from_basename(basename: str) -> Optional[int]: + fields = basename.split("_") + if len(fields) < 3: + return None + try: + return (datetime.now(timezone.utc)-iso8601.parse_date(fields[2])).days + except: + logging.error(f"unable to parse age from basename {basename}") + +def hash_file(path: str) -> str: + hasher = hashlib.sha256() + + with open(path, "rb") as f: + while True: + data = f.read(65536) + if not data: + break + hasher.update(data) + + return "sha256:%s" % hasher.hexdigest() + +def ensure_running_in_CI(): + # make certain we are in CI (see https://docs.github.com/en/actions/reference/environment-variables#default-environment-variables) + if not os.environ.get("CI"): + raise RuntimeError("This is only intended to run within CI, not in a local development workflow.") diff --git a/publish/src/publisher/utils/__init__.py b/publish/src/publisher/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/publish/src/publisher/utils/builder.py b/publish/src/publisher/utils/builder.py new file mode 100644 index 00000000..3199f768 --- /dev/null +++ b/publish/src/publisher/utils/builder.py @@ -0,0 +1,91 @@ +import os +import logging +import glob +import subprocess +import shutil +from typing import Dict + + +from publisher.utils.repo_root import repo_root +from publisher.utils.constants import CACHE_DIR + + +class GrypeDbBuilder: + def __init__(self): + self.dbs: Dict[int, str] = {} + + def build_and_package(self, db_dir: str, schema_version: int, stage_dir: str): + logging.info(f"building DB (schema={schema_version})") + + # create the staging dir and ensure it is empty + os.makedirs(stage_dir, exist_ok=True) + if list(os.listdir(stage_dir)): + raise RuntimeError("staging directory must be empty") + + db_pattern = os.path.join( + db_dir, f"*_v{schema_version}_*.tar.*" + ) + matches = glob.glob(db_pattern) + if len(matches): + raise RuntimeError(f"there are already existing DB archives: {matches}") + + # generate a new DB archive + self.build_db(build_dir=db_dir, schema_version=schema_version) + self.package_db(build_dir=db_dir) + + matches = glob.glob(db_pattern) + if len(matches) != 1: + logging.error(f"no db file matches found: {matches}") + raise RuntimeError("failed to build db") + logging.info(f"db archive created: {matches[0]}") + + # move the build db archive to the staging dir + dest = os.path.join(stage_dir, os.path.basename(matches[0])) + logging.info(f"copying db archive to {dest}") + shutil.copy(matches[0], dest) + + self.dbs[schema_version] = dest + + def db_path(self, scheme_version: int): + return self.dbs[scheme_version] + + @classmethod + def run(cls, *args, cache_dir=CACHE_DIR, caller=subprocess.check_call): + cmd = " ".join(["go run ./cmd/grype-db/main.go", *args]) + print(f"running {cmd!r}") + + env = dict( + **os.environ.copy(), + **{ + "GRYPE_DB_PROVIDER_ROOT": cache_dir, + }, + ) + + return caller(cmd, cwd=repo_root(), env=env, shell=True) + + @classmethod + def build_db(cls, build_dir, schema_version: int, cache_dir=CACHE_DIR): + cls.run("build", "-v", "--schema", str(schema_version), "--dir", build_dir, + cache_dir=cache_dir, + ) + + @classmethod + def package_db(cls, build_dir, cache_dir=CACHE_DIR): + cls.run("package", "-v", "--dir", build_dir, + cache_dir=cache_dir, + ) + + @classmethod + def pull(cls): + if not cls.cache_exists(): + cls.run("pull", "-v") + + @classmethod + def cache_exists(cls): + try: + return "Last Pull" in cls.run( + "cache", + caller=subprocess.check_output, + ).decode("utf-8") + except subprocess.CalledProcessError: + return False diff --git a/publish/src/publisher/utils/constants.py b/publish/src/publisher/utils/constants.py new file mode 100644 index 00000000..22a952b1 --- /dev/null +++ b/publish/src/publisher/utils/constants.py @@ -0,0 +1,20 @@ +import os + +from publisher.utils.repo_root import repo_root + + +TEST_IMAGE = "centos:8.2.2004" +# note: this is additionally set as a constant in upload_dbs.sh +STAGE_DIR = os.path.join(repo_root(), "publish", "stage") +BUILD_DIR = os.path.join(repo_root(), "publish", "build") +CACHE_DIR = os.path.join(repo_root(), "publish", "cache") +ASSET_DIR = os.path.join(BUILD_DIR, "assets") +DB_DIR = os.path.join(BUILD_DIR, "dbs") +ERROR_DIR = os.path.join(BUILD_DIR, "errors") + +# BUCKET = os.environ["AWS_BUCKET"] # e.g. "toolbox-data.anchore.io" +# DBS_PATH = os.environ["AWS_BUCKET_PATH"] # e.g. "grype/databases" +LEGACY_DB_SUFFIXES = {".tar.gz"} +NEW_DB_SUFFIXES = {".tar.zst"} +DB_SUFFIXES = {*LEGACY_DB_SUFFIXES, *NEW_DB_SUFFIXES} +GOLDEN_REPORT_LOCATION = os.path.join(repo_root(), "publish", "test-fixtures") diff --git a/publish/src/publisher/utils/grype.py b/publish/src/publisher/utils/grype.py new file mode 100644 index 00000000..b4b71d50 --- /dev/null +++ b/publish/src/publisher/utils/grype.py @@ -0,0 +1,223 @@ +import os +import re +import json +import logging +import collections +from typing import Tuple, Set, Optional + +from yardstick.tool import grype + +from publisher.utils.constants import ERROR_DIR, ASSET_DIR +from publisher.utils.repo_root import repo_root + + +# +/- ratio for matching packages and vulnerabilities +# we know that there could be slight differences in grype output between versions +# and if new vulnerability data is used. +TOLERANCE = 0.1 + + +class Grype: + + BIN = "grype" + + def __init__(self, schema_version: int, update_url: str = "", release: Optional[str] = None): + self.schema_version = schema_version + if release: + logging.warning(f"overriding grype release for schema={schema_version!r} with release={release!r}") + self.release = release + else: + self.release = self.release_version_for_schema_version(schema_version) + logging.debug(f"using grype release={self.release!r} for schema={schema_version!r}") + + env = {} + if update_url: + env["GRYPE_DB_UPDATE_URL"] = update_url + self.tool = grype.Grype.install(version=self.release, path=os.path.join(ASSET_DIR, self.release), env=env) + + @staticmethod + def release_version_for_schema_version(schema_version: int): + path = os.path.join(repo_root(), "grype-schema-version-mapping.json") + with open(path) as fh: + obj = json.load(fh) + return obj[str(schema_version)] + + @staticmethod + def supported_schema_versions(): + path = os.path.join(repo_root(), "grype-schema-version-mapping.json") + with open(path) as fh: + obj = json.load(fh) + return obj.keys() + + def update_db(self): + self.tool.run("db", "update", "-vv") + + # ensure the db cache is not empty for the current schema + check_db_cache_dir(self.schema_version, os.path.join(self.tool.path, "db")) + + def import_db(self, db_path: str): + self.tool.run("db", "import", db_path) + + # ensure the db cache is not empty for the current schema + check_db_cache_dir(self.schema_version, os.path.join(self.tool.path, "db")) + + def run(self, user_input: str) -> str: + return self.tool.run("-o", "json", "-v", user_input) + + +Package = collections.namedtuple("Package", "name type version") +Vulnerability = collections.namedtuple("Vulnerability", "id") + + +class Report: + def __init__(self, report_contents): + self.report_contents = report_contents + + def _enumerate(self, section): + try: + data = json.loads(self.report_contents) + except Exception as exc: + os.makedirs(ERROR_DIR, exist_ok=True) + report_path = os.path.join(ERROR_DIR, "grype-error.json") + with open(report_path, "w") as f: + f.write(self.report_contents) + logging.error( + f"json decode failed for written to: {report_path}", exc_info=exc + ) + raise + + if section == "matches" and isinstance(data, list): + # < v0.1.0-beta.10 there was no mapping at the root of the document (so could only support matches info) + for entry in data: + yield entry + else: + # try the new approach has section names (supported >= v0.1.0-beta.10) + for entry in data[section]: + yield entry + + def parse(self) -> Tuple[Set["Package"], Set["Vulnerability"]]: + packages = set() + vulnerabilities = set() + for entry in self._enumerate(section="matches"): + # not all versions of grype included epoch in the version, so for comparison it is vital that + # we do not consider this field of the version at all. + version = entry["artifact"]["version"] + if re.match(r'^\d+:', version): + version = ":".join(version.split(":")[1:]) + + package = Package( + name=entry["artifact"]["name"], + type=entry["artifact"]["type"], + version=version, + ) + vulnerability = Vulnerability(id=entry["vulnerability"]["id"]) + + packages.add(package) + vulnerabilities.add(vulnerability) + return packages, vulnerabilities + + def compare(self, other: "Report"): + my_packages, my_vulnerabilities = self.parse() + their_packages, their_vulnerabilities = other.parse() + + # this is valid, but suspicious. We should never use a test image with no results. Assume the worst and fail. + if not their_packages and not my_packages: + raise RuntimeError("nobody found any packages") + + if len(my_vulnerabilities) == 0 and len(their_vulnerabilities) == 0: + raise RuntimeError("nobody found any vulnerabilities") + + # find differences in packages + same_packages = their_packages & my_packages + percent_overlap_packages = ( + float(len(same_packages)) / float(len(my_packages)) + ) * 100.0 + + extra_packages = their_packages - my_packages + missing_packages = my_packages - their_packages + + # find differences in vulnerabilities + same_vulnerabilities = their_vulnerabilities & my_vulnerabilities + percent_overlap_vulnerabilities = ( + float(len(same_vulnerabilities)) / float(len(my_vulnerabilities)) + ) * 100.0 + + extra_vulnerabilities = their_vulnerabilities - my_vulnerabilities + missing_vulnerabilities = my_vulnerabilities - their_vulnerabilities + + if extra_packages: + logging.error("extra packages: %s" % repr(sorted(list(extra_packages)))) + + if len(missing_packages) > 0: + logging.error("missing packages: %s" % repr(sorted(list(missing_packages)))) + + if len(extra_vulnerabilities) > 0: + logging.error("extra vulnerabilities: %d" % len(extra_vulnerabilities)) + for v in sorted(list(extra_vulnerabilities)): + print(" ", v) + + if len(missing_vulnerabilities) > 0: + logging.error("missing vulnerabilities: %d" % len(missing_vulnerabilities)) + for v in sorted(list(missing_vulnerabilities)): + print(" ", v) + + logging.info(f"baseline packages: {len(my_packages)}") + logging.info(f"new packages: {len(their_packages)}") + + logging.info( + "baseline packages matched: %.2f %% (%d/%d packages)" + % (percent_overlap_packages, len(same_packages), len(my_packages)) + ) + logging.info( + "baseline vulnerabilities matched: %.2f %% (%d/%d vulnerabilities)" + % ( + percent_overlap_vulnerabilities, + len(same_vulnerabilities), + len(my_vulnerabilities), + ) + ) + + if not within_tolerance(len(my_packages), len(their_packages)): + raise RuntimeError( + "failed quality gate: packages not within tolerance (%d vs %d)" + % (len(my_packages), len(their_packages)) + ) + + if not within_tolerance(len(my_vulnerabilities), len(their_vulnerabilities)): + raise RuntimeError( + "failed quality gate: vulnerabilities not within tolerance (%d vs %d)" + % (len(my_vulnerabilities), len(their_vulnerabilities)) + ) + + +def within_tolerance(under_test, golden, tolerance=TOLERANCE): + return golden * (1 - tolerance) <= under_test <= golden * (1 + tolerance) + + +def check_db_cache_dir(schema_version, db_runtime_dir): + """ + Ensure that there is a `metadata.json` file for the cache directory, which signals that there + are files related to a database pull + """ + # ensure the db cache is not empty for the current schema + if schema_version == "1": + # older grype versions do not support schema-based cache directories + db_metadata_file = os.path.join(db_runtime_dir, "metadata.json") + else: + db_metadata_file = os.path.join(db_runtime_dir, schema_version, "metadata.json") + + if os.path.exists(db_metadata_file): + # the metadata.json file exists and grype will be able to work with it + return + + logging.error(f"db_runtime_dir: {db_runtime_dir}") + logging.error( + f"db import appears to have failed, was expecting path: {db_metadata_file}" + ) + logging.error("db runtime directory has these files: ") + for _f in os.listdir(db_runtime_dir): + logging.error(f"{_f}") + + raise RuntimeError( + "db import appears to have failed, was expecting path: %s" % db_metadata_file + ) diff --git a/publish/src/publisher/utils/listing.py b/publish/src/publisher/utils/listing.py new file mode 100644 index 00000000..cd337bfb --- /dev/null +++ b/publish/src/publisher/utils/listing.py @@ -0,0 +1,235 @@ +import os +import json +import logging +import tempfile +import threading +import contextlib +from http.server import HTTPServer, SimpleHTTPRequestHandler +from urllib.parse import urlparse, urlunparse +from typing import List, Dict, Set, Optional, Tuple +from dataclasses import dataclass +from datetime import datetime, timezone +from publisher.utils.constants import ( + DB_SUFFIXES, + LEGACY_DB_SUFFIXES, +) + +import iso8601 # type: ignore +from dataclasses_json import dataclass_json + +import publisher.utils.s3utils as s3utils +import publisher.utils.grype as grype + +LISTING_FILENAME = "listing.json" + + +@dataclass_json +@dataclass +class Entry: + built: str + version: int + url: str + checksum: str + + def basename(self): + basename = os.path.basename(urlparse(self.url, allow_fragments=False).path) + if not has_suffix(basename, suffixes=DB_SUFFIXES): + raise RuntimeError(f"entry url is not a db archive: {basename}") + + return basename + + def age(self, now=None): + if not now: + now = datetime.now(timezone.utc) + return (now-iso8601.parse_date(self.built)).days + + +@dataclass_json +@dataclass +class Listing: + available: Dict[int, List[Entry]] + + def prune(self, max_age_days, minimum_elements, now=None): + for schema_version, entries in self.available.items(): + kept = [] + pruned = [] + + if len(entries) <= minimum_elements: + logging.info(f"too few entries to prune for schema version {schema_version} ({len(entries)} entries < {minimum_elements})") + continue + + for entry in entries: + if entry.age(now) > max_age_days: + pruned.append(entry) + else: + kept.append(entry) + + # latest elements are in the back + pruned.sort( + key=lambda x: iso8601.parse_date(x.built) + ) + + while len(kept) < minimum_elements and len(pruned) > 0: + kept.append(pruned.pop()) + + # latest elements are in the front + kept.sort( + key=lambda x: iso8601.parse_date(x.built), + reverse=True + ) + + if not pruned: + logging.info(f"no entries to prune from schema version {schema_version}") + continue + + logging.info(f"pruning {len(pruned)} entries from schema version {schema_version}, {len(kept)} entries remain") + self.available[schema_version] = kept + + def add(self, entry: Entry, quiet: bool = False): + if not quiet: + logging.info(f"adding new listing entry: {entry}") + + if not self.available.get(entry.version): + self.available[entry.version] = [] + + self.available[entry.version].append(entry) + + # keep listing entries sorted by date (rfc3339 formatted entries, which iso8601 is a superset of) + self.available[entry.version].sort( + key=lambda x: iso8601.parse_date(x.built), + reverse=True + ) + + def remove_by_basename(self, basenames: set[str], quiet: bool = False): + if not quiet: + logging.info(f"removing {len(basenames)} from existing listing") + + for version, entries in self.available.items(): + remove = [] + for entry in entries: + if entry.basename() in basenames: + remove.append(entry) + for entry in remove: + entries.remove(entry) + + def log(self): + logging.info(f"listing contents:") + for schema, entries in self.available.items(): + logging.info(f" schema: {schema}") + for entry in entries: + logging.info(f" entry: {entry}") + + @staticmethod + def url(path: str): + url = os.path.normpath("/".join([path, LISTING_FILENAME]).lstrip("/")) + return urlunparse(urlparse(url)) # normalize the url + + def basenames(self) -> Set[str]: + names = set() + for _, entries in self.available.items(): + for entry in entries: + names.add(entry.basename()) + return names + + def basename_difference(self, other: set[str]) -> (set[str], set[str]): + basenames = self.basenames() + new_basenames = other - basenames + missing_basenames = basenames - other + return new_basenames, missing_basenames + + def latest(self, schema_version: int) -> Entry: + return self.available[schema_version][0] + + +def has_suffix(el: str, suffixes: Optional[set[str]]): + if not suffixes: + return True + for s in suffixes: + if el.endswith(s): + return True + return False + +def empty_listing() -> Listing: + return Listing(available={}) + + +def fetch(bucket: str, path: str): + logging.info(f"fetching existing listing") + listing_path = Listing.url(path) + try: + listing_contents = s3utils.get_s3_object_contents( + bucket=bucket, key=listing_path + ) + if listing_contents: + logging.info( + f"discovered existing listing entry bucket={bucket} key={listing_path}" + ) + return Listing.from_json(listing_contents) # type: ignore + + # TODO: this is not a safe assumption in the long run... remove this after the first run? + logging.warning("could not find existing listing, assuming empty") + return empty_listing() + except json.decoder.JSONDecodeError: + logging.error("listing exists, but json parse failed") + raise + + +@contextlib.contextmanager +def http_server(directory: str): + server_address = ("127.0.0.1", 5555) + url = f"http://{server_address[0]}:{server_address[1]}" + listing_url = f"{url}/{LISTING_FILENAME}" + + def serve(): + os.chdir(directory) + httpd = HTTPServer(server_address, SimpleHTTPRequestHandler) + logging.info(f"starting test server at {url}") + httpd.serve_forever() + + thread = threading.Thread(target=serve) + thread.daemon = True + thread.start() + try: + yield listing_url + finally: + pass + + +def acceptance_test(test_listing: Listing, image: str, override_schema_release: Optional[Tuple[str, str]] = None): + # write the listing to a temp dir that is served up locally on an HTTP server. This is used by grype to locally + # download the listing file and check that it works against S3 (since the listing entries have DB urls that + # reside in S3). + with tempfile.TemporaryDirectory(prefix="grype-db-acceptance") as tempdir: + listing_contents = test_listing.to_json() + # way too verbose! + # logging.info(listing_contents) + with open(os.path.join(tempdir, LISTING_FILENAME), "w") as f: + f.write(listing_contents) # type: ignore + + # ensure grype can perform a db update for all supported schema versions. Note: we are only testing the + # listing entry for the DB is usable (the download succeeds and grype and the update process, which does + # checksum verifications, passes). This test does NOT check the integrity of the DB since that has already + # been tested in the build steps. + with http_server(directory=tempdir) as listing_url: + if override_schema_release: + override_schema, override_release = override_schema_release + logging.warning(f"overriding schema={override_schema!r} with release={override_release!r}") + logging.info(f"testing grype schema-version={override_schema!r}") + tool_obj = grype.Grype( + schema_version=override_schema, + update_url=listing_url, + release=override_release + ) + else: + for schema_version in grype.Grype.supported_schema_versions(): + logging.info(f"testing grype schema-version={schema_version!r}") + tool_obj = grype.Grype( + schema_version=schema_version, + update_url=listing_url, + ) + + output = tool_obj.run(user_input=image) + packages, vulnerabilities = grype.Report(report_contents=output).parse() + logging.info(f"scan result with downloaded DB: packages={len(packages)} vulnerabilities={len(vulnerabilities)}") + if not packages or not vulnerabilities: + raise RuntimeError("validation failed: missing packages and/or vulnerabilities") diff --git a/publish/src/publisher/utils/metadata.py b/publish/src/publisher/utils/metadata.py new file mode 100644 index 00000000..bdfc3450 --- /dev/null +++ b/publish/src/publisher/utils/metadata.py @@ -0,0 +1,47 @@ +import tarfile +import tempfile +from pathlib import Path +from dataclasses import dataclass + +import zstandard +from dataclasses_json import dataclass_json + +FILE = "metadata.json" + + +@dataclass_json +@dataclass +class Metadata: + built: str + version: int + # note: the checksum is not included here since that is for the contained db file, not the checksum of the archive itself + + +def from_archive(path: str) -> Metadata: + if path.endswith(".tar.gz"): + return from_tar_gz(path) + elif path.endswith(".tar.zst"): + return from_tar_zst(path) + raise RuntimeError(f"unsupported archive type: {path}") + +def from_tar(tar_obj) -> Metadata: + f = tar_obj.extractfile(tar_obj.getmember(FILE)) + if not f: + raise RuntimeError(f"failed to find {FILE}") + return Metadata.from_json(f.read().decode()) # type: ignore + + +def from_tar_gz(path: str) -> Metadata: + with tarfile.open(path, "r") as a: + return from_tar(a) + +def from_tar_zst(path: str) -> Metadata: + archive = Path(path).expanduser() + dctx = zstandard.ZstdDecompressor(max_window_size=2147483648) + + with tempfile.TemporaryFile(suffix=".tar") as ofh: + with archive.open("rb") as ifh: + dctx.copy_stream(ifh, ofh) + ofh.seek(0) + with tarfile.open(fileobj=ofh) as z: + return from_tar(z) diff --git a/publish/src/publisher/utils/repo_root.py b/publish/src/publisher/utils/repo_root.py new file mode 100644 index 00000000..b1d3f9b0 --- /dev/null +++ b/publish/src/publisher/utils/repo_root.py @@ -0,0 +1,12 @@ +import subprocess +import functools + + +@functools.lru_cache(maxsize=1) +def repo_root() -> str: + """returns the absolute path of the repository root""" + try: + base = subprocess.check_output("git rev-parse --show-toplevel", shell=True) + except subprocess.CalledProcessError: + raise IOError("Current working directory is not a git repository") + return base.decode("utf-8").strip() diff --git a/publish/src/publisher/utils/s3utils.py b/publish/src/publisher/utils/s3utils.py new file mode 100644 index 00000000..f217227d --- /dev/null +++ b/publish/src/publisher/utils/s3utils.py @@ -0,0 +1,82 @@ +import logging + +import boto3 # type: ignore + + +class LoggingContext(object): + def __init__(self, logger=None, level=None): + self.logger = logger or logging.root + self.level = level + + def __enter__(self): + if self.level is not None: + self.old_level = self.logger.level + self.logger.setLevel(self.level) + + def __exit__(self, et, ev, tb): + if self.level is not None: + self.logger.setLevel(self.old_level) + +def download_to_file(bucket: str, key: str, path: str): + logging.info(f"downloading file from s3 bucket={bucket} key={key} to local={path}") + + s3 = boto3.client("s3") + og_level = logging.root.level + + # boto is a little too verbose... let's tone that down just for a bit + with LoggingContext(level=logging.WARNING): + s3.download_file(Bucket=bucket, Key=key.lstrip("/"), Filename=path) + + +def upload(bucket: str, key: str, contents: str, **kwargs): + logging.info(f"uploading to s3 bucket={bucket} key={key}") + + # boto is a little too verbose... let's tone that down just for a bit + with LoggingContext(level=logging.WARNING): + s3 = boto3.client("s3") + s3.put_object(Body=contents, Bucket=bucket, Key=key.lstrip("/"), **kwargs) + + +def get_s3_object_contents(bucket: str, key: str): + logging.info(f"get s3 contents bucket={bucket} key={key}") + + # boto is a little too verbose... let's tone that down just for a bit + with LoggingContext(level=logging.WARNING): + s3 = boto3.client("s3") + try: + obj = s3.get_object(Bucket=bucket, Key=key) + return obj["Body"].read().decode("utf-8") + except s3.exceptions.NoSuchKey: + return + + +def get_matching_s3_objects(bucket: str, prefix: str = "", suffix: str = ""): + s3 = boto3.client("s3") + paginator = s3.get_paginator("list_objects_v2") + + kwargs = {"Bucket": bucket} + + # we can pass the prefix directly to the S3 API. If the user has passed + # a tuple or list of prefixes, we go through them one by one. + if isinstance(prefix, str): + prefixes = (prefix,) + else: + prefixes = prefix + + for key_prefix in prefixes: + kwargs["Prefix"] = key_prefix.lstrip("/") + + for page in paginator.paginate(**kwargs): + try: + contents = page["Contents"] + except KeyError: + break + + for obj in contents: + if obj["Key"].endswith(suffix): + yield obj + + +def get_matching_s3_keys(bucket: str, prefix: str = "", suffix: str = ""): + for obj in get_matching_s3_objects(bucket, prefix, suffix): + yield obj["Key"] diff --git a/publish/src/publisher/utils/test.py b/publish/src/publisher/utils/test.py new file mode 100644 index 00000000..b7d609bb --- /dev/null +++ b/publish/src/publisher/utils/test.py @@ -0,0 +1,67 @@ +import logging +from dataclasses import dataclass +from typing import Dict, List + +import publisher.utils.grype as grype +import publisher.utils.builder as builder + + +@dataclass +class Case: + grype: "grype.Grype" + builder: "builder.GrypeDbBuilder" + schema_version: int + + +class Runner: + def __init__(self, user_input: str, golden_report_path: str): + self.cases: Dict[int, Case] = {} + self.user_input = user_input + self.golden_report_path = golden_report_path + + def add_case(self, case: Case): + existing_case = self.cases.get(case.schema_version) + if not existing_case: + self.cases[case.schema_version] = case + return + + # only keep the latest version for schema versions already covered + if existing_case.grype.release.version < case.grype.release.version: + self.cases[case.schema_version] = case + + def schema_versions(self) -> List[int]: + return list(self.cases.keys()) + + def run(self): + if len(self.cases) == 0: + logging.error("no test cases found!") + return False + + for schema_version, case in self.cases.items(): + # note: failing tests should raise exceptions + self.run_test_case(case=case) + + def run_test_case(self, case: Case): + logging.info( + f"running acceptance test for schema={case.schema_version} grype={case.grype.release}" + ) + with open(self.golden_report_path, "r") as f: + golden_report_content = f.read() + + try: + # import the DB to a preconfigured destination + db_path = case.builder.db_path(scheme_version=case.schema_version) + case.grype.import_db(db_path=db_path) + + output = case.grype.run(user_input=self.user_input) + + # run the test and ensure the output is "good enough" + current_run = grype.Report(report_contents=output) + golden_run = grype.Report(report_contents=golden_report_content) + + # either raises an error or returns nothing based on the comparison + golden_run.compare(other=current_run) + + except Exception as exc: + logging.error(f"failed test case={case.grype.release}: {str(exc)}") + raise diff --git a/publish/test-fixtures/centos-8.2.2004.json b/publish/test-fixtures/centos-8.2.2004.json new file mode 100644 index 00000000..baf0e2fb --- /dev/null +++ b/publish/test-fixtures/centos-8.2.2004.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f06ba2c966030bcd8a0dc8f2e6cf30b895e2794d032fe659d95239ca717448ed +size 2476916 diff --git a/publish/tests/__init__.py b/publish/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/publish/tests/test_update_listing.py b/publish/tests/test_update_listing.py new file mode 100644 index 00000000..1fdefa35 --- /dev/null +++ b/publish/tests/test_update_listing.py @@ -0,0 +1,41 @@ +import pytest + +from publisher.console import get_paths_by_basename + + +def test_get_paths_by_basename(): + paths = [ + "somewhere/a/place/thing-1.tar.gz", + "/b/place/thing-2.tar.gz", + "somewhere/thing-3.tar.gz", + "somewhere/a/place/thing-1.tar.zst", + "/b/place/thing-2.tar.zst", + "somewhere/thing-3.tar.zst", + ] + + expected = { + "thing-1.tar.gz": "somewhere/a/place/thing-1.tar.gz", + "thing-2.tar.gz": "/b/place/thing-2.tar.gz", + "thing-3.tar.gz": "somewhere/thing-3.tar.gz", + "thing-1.tar.zst": "somewhere/a/place/thing-1.tar.zst", + "thing-2.tar.zst": "/b/place/thing-2.tar.zst", + "thing-3.tar.zst": "somewhere/thing-3.tar.zst", + } + + assert expected == get_paths_by_basename(paths) + + +def test_get_paths_by_basename_raises_duplicates(): + paths = [ + "somewhere/a/place/thing-1.tar.gz", + "so/thing-1.tar.gz", + "/b/place/thing-2.tar.gz", + "somewhere/thing-3.tar.gz", + "somewhere/a/place/thing-1.tar.zst", + "so/thing-1.tar.zst", + "/b/place/thing-2.tar.zst", + "somewhere/thing-3.tar.zst", + ] + + with pytest.raises(RuntimeError): + get_paths_by_basename(paths) diff --git a/publish/tests/utils/__init__.py b/publish/tests/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/publish/tests/utils/test_listing.py b/publish/tests/utils/test_listing.py new file mode 100644 index 00000000..b116bfd3 --- /dev/null +++ b/publish/tests/utils/test_listing.py @@ -0,0 +1,604 @@ +import datetime +import json + +import pytest + +from publisher.utils import listing +from publisher.utils.constants import ( + LEGACY_DB_SUFFIXES, +) + + +def test_listing_add_sorts_by_date(): + subject = listing.empty_listing() + + subject.add( + listing.Entry( + built=datetime.datetime(2017, 11, 28, 23, 55, 59, 342380).strftime( + "%Y-%m-%dT%H:%M:%S.%f%z" + ), + version=3, + url="https://b-place.com/something.tar.gz", + checksum="123456789", + ) + ) + + subject.add( + listing.Entry( + built=datetime.datetime(2016, 11, 28, 23, 55, 59, 342380).strftime( + "%Y-%m-%dT%H:%M:%S.%f%z" + ), + version=3, + url="https://a-place.com/something.tar.gz", + checksum="123456789", + ) + ) + + subject.add( + listing.Entry( + built=datetime.datetime(2019, 11, 28, 23, 55, 59, 342380).strftime( + "%Y-%m-%dT%H:%M:%S.%f%z" + ), + version=3, + url="https://c-place.com/something.tar.gz", + checksum="123456789", + ) + ) + + subject.add( + listing.Entry( + built=datetime.datetime(2017, 11, 28, 23, 55, 59, 342380).strftime( + "%Y-%m-%dT%H:%M:%S.%f%z" + ), + version=4, + url="https://b-place.com/something.tar.zst", + checksum="123456789", + ) + ) + + subject.add( + listing.Entry( + built=datetime.datetime(2016, 11, 28, 23, 55, 59, 342380).strftime( + "%Y-%m-%dT%H:%M:%S.%f%z" + ), + version=4, + url="https://a-place.com/something.tar.zst", + checksum="123456789", + ) + ) + + subject.add( + listing.Entry( + built=datetime.datetime(2019, 11, 28, 23, 55, 59, 342380).strftime( + "%Y-%m-%dT%H:%M:%S.%f%z" + ), + version=4, + url="https://c-place.com/something.tar.zst", + checksum="123456789", + ) + ) + + bytes = subject.to_json() + + obj = json.loads(bytes) + + # we're expecting the most recent entry first + assert obj["available"]["3"][0]["url"] == "https://c-place.com/something.tar.gz" + assert obj["available"]["3"][1]["url"] == "https://b-place.com/something.tar.gz" + assert obj["available"]["3"][2]["url"] == "https://a-place.com/something.tar.gz" + assert obj["available"]["4"][0]["url"] == "https://c-place.com/something.tar.zst" + assert obj["available"]["4"][1]["url"] == "https://b-place.com/something.tar.zst" + assert obj["available"]["4"][2]["url"] == "https://a-place.com/something.tar.zst" + + +@pytest.mark.parametrize( + "s3_path, expected", + ( + ("somewhere/in/the/bucket", "somewhere/in/the/bucket/listing.json"), + ("somewhere/in/the/bucket///", "somewhere/in/the/bucket/listing.json"), + ("//somewhere/in/the/bucket/", "somewhere/in/the/bucket/listing.json"), + ), +) +def test_listing_url(s3_path, expected): + assert expected == listing.Listing.url(s3_path) + + +def test_listing_basenames(): + subject = listing.empty_listing() + + subject.add( + listing.Entry( + built=datetime.datetime(2017, 11, 28, 23, 55, 59, 342380).strftime( + "%Y-%m-%dT%H:%M:%S.%f%z" + ), + version=3, + url="https://b-place.com/something-1.tar.gz", + checksum="123456789", + ) + ) + + subject.add( + listing.Entry( + built=datetime.datetime(2016, 11, 28, 23, 55, 59, 342380).strftime( + "%Y-%m-%dT%H:%M:%S.%f%z" + ), + version=3, + url="https://a-place.com/something.tar.gz", + checksum="123456789", + ) + ) + + subject.add( + listing.Entry( + built=datetime.datetime(2019, 11, 28, 23, 55, 59, 342380).strftime( + "%Y-%m-%dT%H:%M:%S.%f%z" + ), + version=3, + url="https://c-place.com/something.tar.gz", + checksum="123456789", + ) + ) + + subject.add( + listing.Entry( + built=datetime.datetime(2017, 11, 28, 23, 55, 59, 342380).strftime( + "%Y-%m-%dT%H:%M:%S.%f%z" + ), + version=4, + url="https://b-place.com/something-1.tar.zst", + checksum="123456789", + ) + ) + + subject.add( + listing.Entry( + built=datetime.datetime(2016, 11, 28, 23, 55, 59, 342380).strftime( + "%Y-%m-%dT%H:%M:%S.%f%z" + ), + version=4, + url="https://a-place.com/something.tar.zst", + checksum="123456789", + ) + ) + + subject.add( + listing.Entry( + built=datetime.datetime(2019, 11, 28, 23, 55, 59, 342380).strftime( + "%Y-%m-%dT%H:%M:%S.%f%z" + ), + version=4, + url="https://c-place.com/something.tar.zst", + checksum="123456789", + ) + ) + + assert {"something.tar.gz", "something-1.tar.gz", "something.tar.zst", "something-1.tar.zst"} == subject.basenames() + + +def test_listing_latest(): + subject = listing.empty_listing() + + subject.add( + listing.Entry( + built=datetime.datetime(2017, 11, 28, 23, 55, 59, 342380).strftime( + "%Y-%m-%dT%H:%M:%S.%f%z" + ), + version=3, + url="https://b-place.com/something-1.tar.gz", + checksum="123456789", + ) + ) + + subject.add( + listing.Entry( + built=datetime.datetime(2016, 11, 28, 23, 55, 59, 342380).strftime( + "%Y-%m-%dT%H:%M:%S.%f%z" + ), + version=3, + url="https://a-place.com/something.tar.gz", + checksum="123456789", + ) + ) + + subject.add( + listing.Entry( + built=datetime.datetime(2019, 11, 28, 23, 55, 59, 342380).strftime( + "%Y-%m-%dT%H:%M:%S.%f%z" + ), + version=3, + url="https://c-place.com/something.tar.gz", + checksum="123456789", + ) + ) + + subject.add( + listing.Entry( + built=datetime.datetime(2017, 11, 28, 23, 55, 59, 342380).strftime( + "%Y-%m-%dT%H:%M:%S.%f%z" + ), + version=4, + url="https://b-place.com/something-1.tar.zst", + checksum="123456789", + ) + ) + + subject.add( + listing.Entry( + built=datetime.datetime(2016, 11, 28, 23, 55, 59, 342380).strftime( + "%Y-%m-%dT%H:%M:%S.%f%z" + ), + version=4, + url="https://a-place.com/something.tar.zst", + checksum="123456789", + ) + ) + + subject.add( + listing.Entry( + built=datetime.datetime(2019, 11, 28, 23, 55, 59, 342380).strftime( + "%Y-%m-%dT%H:%M:%S.%f%z" + ), + version=4, + url="https://c-place.com/something.tar.zst", + checksum="123456789", + ) + ) + + assert "https://c-place.com/something.tar.gz" == subject.latest(3).url + assert "https://c-place.com/something.tar.zst" == subject.latest(4).url + + +def test_listing_basename_difference(): + subject = listing.empty_listing() + + subject.add( + listing.Entry( + built=datetime.datetime(2017, 11, 28, 23, 55, 59, 342380).strftime( + "%Y-%m-%dT%H:%M:%S.%f%z" + ), + version=3, + url="https://b-place.com/something-1.tar.gz", + checksum="123456789", + ) + ) + + subject.add( + listing.Entry( + built=datetime.datetime(2016, 11, 28, 23, 55, 59, 342380).strftime( + "%Y-%m-%dT%H:%M:%S.%f%z" + ), + version=3, + url="https://a-place.com/something-2.tar.gz", + checksum="123456789", + ) + ) + + subject.add( + listing.Entry( + built=datetime.datetime(2019, 11, 28, 23, 55, 59, 342380).strftime( + "%Y-%m-%dT%H:%M:%S.%f%z" + ), + version=3, + url="https://c-place.com/something-3.tar.gz", + checksum="123456789", + ) + ) + + basenames = {"something-3.tar.gz", "something-4.tar.gz"} + expected_missing = {"something-1.tar.gz", "something-2.tar.gz"} + expected_new = {"something-4.tar.gz"} + + actual_new, actual_missing = subject.basename_difference(basenames) + + assert expected_new == actual_new + assert expected_missing == actual_missing + + +def test_filtering_listing_basename_difference(): + subject = listing.empty_listing() + + something1 = listing.Entry( + built=datetime.datetime(2017, 11, 28, 23, 55, 59, 342380).strftime( + "%Y-%m-%dT%H:%M:%S.%f%z" + ), + version=3, + url="https://b-place.com/something-1.tar.zst", # note: this gets filtered out! + checksum="123456789", + ) + + something2 = listing.Entry( + built=datetime.datetime(2016, 11, 28, 23, 55, 59, 342380).strftime( + "%Y-%m-%dT%H:%M:%S.%f%z" + ), + version=3, + url="https://a-place.com/something-2.tar.gz", # note: this gets filtered out! + checksum="123456789", + ) + + something3 = listing.Entry( + built=datetime.datetime(2019, 11, 28, 23, 55, 59, 342380).strftime( + "%Y-%m-%dT%H:%M:%S.%f%z" + ), + version=3, + url="https://c-place.com/something-3.tar.gz", + checksum="123456789", + ) + + subject.add(something1) + subject.add(something2) + subject.add(something3) + + expected = listing.empty_listing() + expected.add(something3) + + basenames_from_s3 = {"something-3.tar.gz", "something-4.tar.gz"} + expected_missing = {"something-1.tar.zst", "something-2.tar.gz"} + expected_new = {"something-4.tar.gz"} + + actual_new, actual_missing = subject.basename_difference(basenames_from_s3) + + assert expected_new == actual_new + assert expected_missing == actual_missing + + subject.remove_by_basename(actual_missing) + + assert subject == expected + + +def listing_over_years(): + subject = listing.empty_listing() + + subject.add( + listing.Entry( + built=datetime.datetime(2017, 11, 28, 23, 55, 59, 342380, tzinfo=datetime.timezone.utc).strftime( + "%Y-%m-%dT%H:%M:%S.%f%z" + ), + version=3, + url="https://b-place.com/something.tar.gz", + checksum="123456789", + ) + ) + + subject.add( + listing.Entry( + built=datetime.datetime(2016, 11, 28, 23, 55, 59, 342380, tzinfo=datetime.timezone.utc).strftime( + "%Y-%m-%dT%H:%M:%S.%f%z" + ), + version=3, + url="https://a-place.com/something.tar.gz", + checksum="123456789", + ) + ) + + subject.add( + listing.Entry( + built=datetime.datetime(2019, 11, 28, 23, 55, 59, 342380, tzinfo=datetime.timezone.utc).strftime( + "%Y-%m-%dT%H:%M:%S.%f%z" + ), + version=3, + url="https://c-place.com/something.tar.gz", + checksum="123456789", + ) + ) + + subject.add( + listing.Entry( + built=datetime.datetime(2017, 11, 28, 23, 55, 59, 342380, tzinfo=datetime.timezone.utc).strftime( + "%Y-%m-%dT%H:%M:%S.%f%z" + ), + version=4, + url="https://b-place.com/something.tar.zst", + checksum="123456789", + ) + ) + + subject.add( + listing.Entry( + built=datetime.datetime(2016, 11, 28, 23, 55, 59, 342380, tzinfo=datetime.timezone.utc).strftime( + "%Y-%m-%dT%H:%M:%S.%f%z" + ), + version=4, + url="https://a-place.com/something.tar.zst", + checksum="123456789", + ) + ) + + subject.add( + listing.Entry( + built=datetime.datetime(2019, 11, 28, 23, 55, 59, 342380, tzinfo=datetime.timezone.utc).strftime( + "%Y-%m-%dT%H:%M:%S.%f%z" + ), + version=4, + url="https://c-place.com/something.tar.zst", + checksum="123456789", + ) + ) + + return subject + + +def listing_day_by_day(): + subject = listing.empty_listing() + + subject.add( + listing.Entry( + built=datetime.datetime(2019, 11, 26, 23, 55, 59, 342380, tzinfo=datetime.timezone.utc).strftime( + "%Y-%m-%dT%H:%M:%S.%f%z" + ), + version=3, + url="https://b-place.com/something.tar.gz", + checksum="123456789", + ) + ) + + subject.add( + listing.Entry( + built=datetime.datetime(2019, 11, 27, 23, 55, 59, 342380, tzinfo=datetime.timezone.utc).strftime( + "%Y-%m-%dT%H:%M:%S.%f%z" + ), + version=3, + url="https://a-place.com/something.tar.gz", + checksum="123456789", + ) + ) + + subject.add( + listing.Entry( + built=datetime.datetime(2019, 11, 28, 23, 55, 59, 342380, tzinfo=datetime.timezone.utc).strftime( + "%Y-%m-%dT%H:%M:%S.%f%z" + ), + version=3, + url="https://c-place.com/something.tar.gz", + checksum="123456789", + ) + ) + + subject.add( + listing.Entry( + built=datetime.datetime(2019, 11, 26, 23, 55, 59, 342380, tzinfo=datetime.timezone.utc).strftime( + "%Y-%m-%dT%H:%M:%S.%f%z" + ), + version=4, + url="https://b-place.com/something.tar.zst", + checksum="123456789", + ) + ) + + subject.add( + listing.Entry( + built=datetime.datetime(2019, 11, 27, 23, 55, 59, 342380, tzinfo=datetime.timezone.utc).strftime( + "%Y-%m-%dT%H:%M:%S.%f%z" + ), + version=4, + url="https://a-place.com/something.tar.zst", + checksum="123456789", + ) + ) + + subject.add( + listing.Entry( + built=datetime.datetime(2019, 11, 28, 23, 55, 59, 342380, tzinfo=datetime.timezone.utc).strftime( + "%Y-%m-%dT%H:%M:%S.%f%z" + ), + version=4, + url="https://c-place.com/something.tar.zst", + checksum="123456789", + ) + ) + + return subject + +@pytest.mark.parametrize("subject,now,max_age,min_elements,urls", + [ + ( + # dont prune anything... + listing_over_years(), + datetime.datetime(2019, 11, 28, 23, 55, 59, 342380, tzinfo=datetime.timezone.utc), + 9000, + 3, + { + "3": [ + "https://c-place.com/something.tar.gz", + "https://b-place.com/something.tar.gz", + "https://a-place.com/something.tar.gz", + ], + "4": [ + "https://c-place.com/something.tar.zst", + "https://b-place.com/something.tar.zst", + "https://a-place.com/something.tar.zst", + ] + } + ), + ( + # we prune based on the age... + listing_over_years(), + datetime.datetime(2019, 11, 28, 23, 55, 59, 342380, tzinfo=datetime.timezone.utc), + 7, + 1, + { + "3": [ + "https://c-place.com/something.tar.gz", + ], + "4": [ + "https://c-place.com/something.tar.zst", + ] + } + ), + ( + # we prune based on the age... older elements are kept to ensure minimum elements + listing_over_years(), + datetime.datetime(2019, 11, 28, 23, 55, 59, 342380, tzinfo=datetime.timezone.utc), + 7, + 2, + { + "3": [ + "https://c-place.com/something.tar.gz", + "https://b-place.com/something.tar.gz", + ], + "4": [ + "https://c-place.com/something.tar.zst", + "https://b-place.com/something.tar.zst", + ] + } + ), + ( + # we prune based on the age... minimum elements is ignored + listing_day_by_day(), + datetime.datetime(2019, 11, 28, 23, 55, 59, 342380, tzinfo=datetime.timezone.utc), + 1, + 1, + { + "3": [ + "https://c-place.com/something.tar.gz", + "https://a-place.com/something.tar.gz", + ], + "4": [ + "https://c-place.com/something.tar.zst", + "https://a-place.com/something.tar.zst", + ] + } + ), + ( + # we prune based on the age... minimum elements is ignored (+ 1 day in the future) + listing_day_by_day(), + datetime.datetime(2019, 11, 29, 23, 55, 59, 342380, tzinfo=datetime.timezone.utc), + 2, + 1, + { + "3": [ + "https://c-place.com/something.tar.gz", + "https://a-place.com/something.tar.gz", + ], + "4": [ + "https://c-place.com/something.tar.zst", + "https://a-place.com/something.tar.zst", + ] + } + ), + ( + # we prune based on the age... minimum elements is ignored (+ 1 year in the future) + listing_day_by_day(), + datetime.datetime(2020, 11, 29, 23, 55, 59, 342380, tzinfo=datetime.timezone.utc), + 2, + 1, + { + "3": [ + "https://c-place.com/something.tar.gz", + ], + "4": [ + "https://c-place.com/something.tar.zst", + ] + } + ), + ] + ) +def test_prune(subject, now, max_age, min_elements, urls): + subject.prune(max_age_days=max_age, minimum_elements=min_elements, now=now) + + obj = json.loads(subject.to_json()) + + actual = {} + for schema_version, elements in obj["available"].items(): + actual[schema_version] = [e["url"] for e in elements] + + assert urls == actual diff --git a/publish/upload-dbs.sh b/publish/upload-dbs.sh new file mode 100755 index 00000000..f9dfe281 --- /dev/null +++ b/publish/upload-dbs.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +set -e + +# make certain we are in CI (see https://docs.github.com/en/actions/reference/environment-variables#default-environment-variables) +if test -z "${CI}"; then + echo "This is only intended to run within CI, not in a local development workflow." + exit 1 +fi + +set -u + +AWS_BUCKET=$1 +AWS_BUCKET_PATH=$2 +# note: this is additionally set as a constant in utils/constants.py +STAGE_DIR=$(git rev-parse --show-toplevel)/publish/stage + +docker run --rm \ + -i \ + -e AWS_DEFAULT_REGION=us-west-2 \ + -e AWS_ACCESS_KEY_ID \ + -e AWS_SECRET_ACCESS_KEY \ + -v "${STAGE_DIR}/:/stagemount" \ + amazon/aws-cli \ + s3 cp /stagemount/ "s3://${AWS_BUCKET}/${AWS_BUCKET_PATH}/" \ + --recursive \ + --exclude '*' \ + --include '*.tar.gz' \ + --include '*.tar.zst' \ + --cache-control 'public,max-age=31536000' diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..cc4ca0e2 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,15 @@ +[tool.poetry] +name = "grype-db" +version = "0.1.0" +description = "" +authors = ["Alex Goodman "] +readme = "README.md" +packages = [{include = "grype_db"}] + +[tool.poetry.dependencies] +python = "^3.10" + + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/test/acceptance/.gitignore b/test/acceptance/.gitignore new file mode 100644 index 00000000..6c367568 --- /dev/null +++ b/test/acceptance/.gitignore @@ -0,0 +1,4 @@ +# ignore cache DIRECTORIES, not files (or symlinks) +cache/ +build/ +.yardstick \ No newline at end of file diff --git a/test/acceptance/cache b/test/acceptance/cache new file mode 120000 index 00000000..e67b4559 --- /dev/null +++ b/test/acceptance/cache @@ -0,0 +1 @@ +../../data \ No newline at end of file diff --git a/test/acceptance/grype-ingest.py b/test/acceptance/grype-ingest.py new file mode 100755 index 00000000..fcb40ed7 --- /dev/null +++ b/test/acceptance/grype-ingest.py @@ -0,0 +1,271 @@ +#!/usr/bin/env python3 -u + +import os +import sys +import json +import subprocess +import functools +import glob +from typing import Dict, List, Any + +import click +from tabulate import tabulate + +import yardstick +from publisher.utils.builder import GrypeDbBuilder + +# filter results to only consider years before and at this year (inclusive) +MAX_YEAR = 2021 +MAX_ALLOWED_DROPPED_UNIQUE_MATCHES = 10 +MAX_ALLOWED_UNIQUE_MATCHES = 15 +RESULT_SET_NAME = "acceptance-test" + +TEST_IMAGES = [ + "anchore/test_images:vulnerabilities-alpine-3.11-d5be50d@sha256:01c78cee3fe398bf1f77566177770b07f1d2af01753c2434cb0735bd43a078b6", + "anchore/test_images:vulnerabilities-alpine-3.12-d5be50d@sha256:55c9ba4e24e15c0467a071d93fead0990b8f04bb60b359b4056a997598aa56a1", + "anchore/test_images:vulnerabilities-alpine-3.13-d5be50d@sha256:6749b1509fc4dd3f2b4e8688325fc5d447751bc9ae3be10c0f1fb92ec062b798", + "anchore/test_images:vulnerabilities-alpine-3.14-d5be50d@sha256:fe242a3a63699425317fba0a749253bceb700fb3d63e7a0f6497f53a587e38c5", + "anchore/test_images:vulnerabilities-alpine-3.15-d5be50d@sha256:7790691e5efae8bfe9cf4a4447312318d8daaf05ffd5f265ae913edf660f4653", + "anchore/test_images:vulnerabilities-alpine-3.6-d5be50d@sha256:58637f273108e3e9eb4df4d73f7b6b1da303cbbf64f65e65fb7762482f2de63d", + "anchore/test_images:vulnerabilities-alpine-3.8-d5be50d@sha256:a287a0ff98ac343aa710f4f4258d7198e240e9d416d5c7274663564202f832fb", + "anchore/test_images:vulnerabilities-amazonlinux-2-5c26ce9@sha256:cf742eca189b02902a0a7926ac3fbb423e799937bf4358b0d2acc6cc36ab82aa", + "anchore/test_images:vulnerabilities-centos@sha256:746d31247006cc06434ce91ccf3523b2c230ff6c378ffed7ca1c60bbb48ea86f", + "anchore/test_images:vulnerabilities-debian@sha256:88e3684e2284fd61531cafd61a5fe3ce1258bcad2b7d4038bc0116abe59cb358", + "anchore/test_images:vulnerabilities-no-distro-6bde59e@sha256:347fba6fbfa15d4e11217f9d49bf70a5a6eef35c6c642dc8c5db89115912d0c1", + "anchore/test_images:vulnerabilities-oraclelinux-7-5c26ce9@sha256:4b28f61016b9d4ad0c0198343e4cc2bd51029f4a1733ed2c4bcc3e2d0dd71bbc", + "anchore/test_images:vulnerabilities-ubuntu-16.04-d5be50d@sha256:05a70ba6d55e6d59b06ce8329bdd9540813e3d155ee7f41fe6044117caf81991", + "anchore/test_images:vulnerabilities-ubuntu-18.04-5c26ce9@sha256:835483c1a36f6cf50bbf84dcef135b4640ea7d8eb9cf15b9edc4f1734f8335d4", + "anchore/test_images:vulnerabilities-package-name-normalization-984794b@sha256:92f1981518e92bf3712ff95cf342f7f4d5fc83fb93a30a36d7d1204e64342199", + "anchore/test_images:vulnerabilities-centos-stream9-ebc653b@sha256:3fa6909fa6f9a8ca8b7f9ba783af8cf84773c14084154073f1f331058ab646cb", + "anchore/test_images:appstreams-centos-stream-8-1a287dd@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9", + "anchore/test_images:appstreams-oraclelinux-8-1a287dd@sha256:c8d664b0e728d52f57eeb98ed1899c16d3b265f02ddfb41303d7a16c31e0b0f1", + "anchore/test_images:appstreams-rhel-8-1a287dd@sha256:524ff8a75f21fd886ec7ed82387766df386671e8b77e898d05786118d5b7880b", +] + + +@functools.lru_cache(maxsize=1) +def repo_root() -> str: + """ returns the absolute path of the repository root """ + try: + base = subprocess.check_output('git rev-parse --show-toplevel', shell=True) + except subprocess.CalledProcessError: + raise IOError('Current working directory is not a git repository') + return base.decode('utf-8').strip() + + +def grype_schema_version_mappings() -> Dict[str, str]: + """ returns the mapping of schema versions to grype version that supports that schema version """ + with open(os.path.join(repo_root(), "grype-schema-version-mapping.json")) as fh: + return json.load(fh) + + +@click.group() +def cli(): + yardstick.store.config.set_values(store_root=os.path.join(repo_root(), "test", "acceptance", "test-fixtures")) + + +@cli.command() +@click.pass_context +def test_all(ctx): + print(f"testing all schema version: {list(grype_schema_version_mappings().keys())}") + for schema_version in grype_schema_version_mappings().keys(): + ctx.invoke(generate, schema_version=schema_version) + ctx.invoke(test, schema_version=schema_version) + + +@cli.command() +@click.option('--schema-version', '-s', required=True, help='The DB schema version to generate') +def generate(schema_version: str): + build_dir = db_dir(schema_version=schema_version) + cache_dir = os.path.join(repo_root(), "test", "acceptance", "cache") + + GrypeDbBuilder.build_db(build_dir=build_dir, schema_version=schema_version, cache_dir=cache_dir) + GrypeDbBuilder.package_db(build_dir=build_dir, cache_dir=cache_dir) + + +@cli.command() +@click.option('--schema-version', '-s', required=True, help='The DB schema version to test') +@click.option('--images', '-i', default=None, help='The images to use as a test subject') +def test(schema_version: str, images: str): + if not images: + images = TEST_IMAGES + + print(f"testing schema-version={schema_version!r} against test-fixtures for images={images!r}") + + grype_version = grype_schema_version_mappings()[schema_version] + db_archive_path = db_archive(schema_version) + tool = get_tool(grype_version, db_import_path=db_archive_path) + tool_request_name_version = f"grype@{grype_version}" + + # for label CVE lookups + yardstick.utils.grype_db.raise_on_failure(False) + yardstick.utils.grype_db.use(tool.db_root) + + result_set = yardstick.store.result_set.load(name=RESULT_SET_NAME) + + for image in images: + print(f"testing image {image!r} schema-version={schema_version!r}") + # get test fixture results relative to the request of the result set + + state = result_set.get(tool=tool_request_name_version, image=image) + if not state: + raise RuntimeError(f"could not find result set state for tool={tool_request_name_version} image={image}") + fixture_config = state.config + fixture_results = yardstick.store.scan_result.load(fixture_config, year_max_limit=MAX_YEAR) + + # get results with test DB + test_tool_version = f"{grype_version}-test-DB" + test_tool = f"grype@{test_tool_version}" + test_config = yardstick.artifact.ScanConfiguration.new(image=image, tool=test_tool) + actual_results, _ = yardstick.capture.run_scan(test_config, tool=tool) + test_config.tool_version = test_tool_version + + actual_results = yardstick.store.scan_result.filter_by_year([actual_results], year_max_limit=MAX_YEAR)[0] + + # compare + comparison = yardstick.comparison.ByPreservedMatch([actual_results, fixture_results]) + show_comparison(comparison) + + yesterdays_unique_matches = [] + todays_unique_matches = [] + for result_id, matches in comparison.unique.items(): + if result_id == fixture_results.ID: + yesterdays_unique_matches = matches + elif result_id == actual_results.ID: + todays_unique_matches = matches + else: + raise RuntimeError(f"unknown results found: {result_id}") + + failed = False + + print(f"Quality Gates:") + if len(yesterdays_unique_matches) > (len(todays_unique_matches) + MAX_ALLOWED_DROPPED_UNIQUE_MATCHES): + print(f" - unique matches quality gate: FAILED! Number of expected matches dropped relative to test fixture: test-fixture={len(yesterdays_unique_matches)} with-test-db={len(todays_unique_matches)}") + failed = True + else: + print(f" - unique matches quality gate: Passed") + + if len(todays_unique_matches) > MAX_ALLOWED_UNIQUE_MATCHES: + print(f" - max allowable matches quality gate: FAILED. Max allowable unique matches with test DB reached: limit={MAX_ALLOWED_UNIQUE_MATCHES} with-test-db={len(todays_unique_matches)}") + failed = True + else: + print(f" - max allowable matches quality gate: Passed") + + if failed: + sys.exit(1) + print() + print("="*80) + + print() + print(f"comparison passed!") + + +@cli.command() +@click.option('--schema-version', '-s', 'schema_versions', default=None, multiple=True, help='The DB schema versions to test') +@click.option('--image', '-i', 'images', default=None, multiple=True, help='The images to use as a test subject') +def capture_test_fixtures(schema_versions: List[str], images: List[str]): + + result_set = yardstick.artifact.ResultSet(name=RESULT_SET_NAME) + + if not images and not schema_versions: + yardstick.store.scan_result.clear() + + if not schema_versions: + schema_versions = grype_schema_version_mappings().keys() + + if not images: + images = TEST_IMAGES + + for schema_version in schema_versions: + print(f"capturing test fixture for schema-version={schema_version} images={images!r}") + grype_version = grype_schema_version_mappings()[schema_version] + + for image in images: + scan_config = yardstick.artifact.ScanConfiguration.new(image=image, tool=f"grype@{grype_version}") + results, raw_json = yardstick.capture.run_scan(scan_config) + yardstick.store.scan_result.save(raw_json, results) + + request = yardstick.artifact.ScanRequest(image=image, tool=f"grype@{grype_version}") + result_set.add(request=request, scan_config=scan_config) + + yardstick.store.result_set.save(result_set) + + +def show_comparison(comparison): + all_rows: List[List[Any]] = [] + for result in comparison.results: + for unique_match in comparison.unique[result.ID]: + all_rows.append( + [ + f"{result.config.tool_name}@{result.config.tool_version}-only", + unique_match.package.name, + unique_match.package.version, + unique_match.vulnerability.id, + ] + ) + + all_rows = sorted(all_rows) + print(tabulate(all_rows, tablefmt="plain")) + print() + print(comparison.summary) + print() + + +def get_tool(version: str, **kwargs): + config = yardstick.artifact.ScanConfiguration(image_repo="", + image_digest="", + image_tag="", + tool_name="grype", + tool_version=version) + install_path = yardstick.store.tool.install_path(config=config) + return yardstick.tool.Grype.install(version=version, path=install_path, **kwargs) + + +def db_dir(schema_version: str) -> str: + build_dir = os.path.join(repo_root(), "test", "acceptance", "build") + return os.path.join(build_dir, "v" + schema_version) + + +def db_archive(schema_version: str) -> str: + pattern = db_dir(schema_version=schema_version) + f"/*_v{schema_version}_*.tar.*" + matching = glob.glob(pattern) + print(f"discovered DB archives: {matching}") + return matching[0] + + +def setup_logging(): + import logging.config + + log_level = "INFO" + + logging.config.dictConfig( + { + "version": 1, + "formatters": { + "standard": { + # [%(module)s.%(funcName)s] + "format": "%(asctime)s [%(levelname)s] %(message)s", + "datefmt": "", + }, + }, + "handlers": { + "default": { + "level": log_level, + "formatter": "standard", + "class": "logging.StreamHandler", + "stream": "ext://sys.stderr", + }, + }, + "loggers": { + "": { # root logger + "handlers": ["default"], + "level": log_level, + }, + }, + } + ) + + +if __name__ == '__main__': + setup_logging() + cli() diff --git a/test/acceptance/poetry.lock b/test/acceptance/poetry.lock new file mode 100644 index 00000000..6a070226 --- /dev/null +++ b/test/acceptance/poetry.lock @@ -0,0 +1,974 @@ +[[package]] +name = "atomicwrites" +version = "1.4.1" +description = "Atomic file writes." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "attrs" +version = "22.1.0" +description = "Classes Without Boilerplate" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.extras] +dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"] +docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] +tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"] +tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] + +[[package]] +name = "boto3" +version = "1.26.45" +description = "The AWS SDK for Python" +category = "main" +optional = false +python-versions = ">= 3.7" + +[package.dependencies] +botocore = ">=1.29.45,<1.30.0" +jmespath = ">=0.7.1,<2.0.0" +s3transfer = ">=0.6.0,<0.7.0" + +[package.extras] +crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] + +[[package]] +name = "botocore" +version = "1.29.45" +description = "Low-level, data-driven core of boto 3." +category = "main" +optional = false +python-versions = ">= 3.7" + +[package.dependencies] +jmespath = ">=0.7.1,<2.0.0" +python-dateutil = ">=2.1,<3.0.0" +urllib3 = ">=1.25.4,<1.27" + +[package.extras] +crt = ["awscrt (==0.15.3)"] + +[[package]] +name = "certifi" +version = "2022.12.7" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "cffi" +version = "1.15.1" +description = "Foreign Function Interface for Python calling C code." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "charset-normalizer" +version = "2.1.1" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" +optional = false +python-versions = ">=3.6.0" + +[package.extras] +unicode_backport = ["unicodedata2"] + +[[package]] +name = "click" +version = "8.1.3" +description = "Composable command line interface toolkit" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.5" +description = "Cross-platform colored terminal text." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "Colr" +version = "0.9.1" +description = "Easy terminal colors, with chainable methods.\n" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "dataclasses-json" +version = "0.5.7" +description = "Easily serialize dataclasses to and from JSON" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +marshmallow = ">=3.3.0,<4.0.0" +marshmallow-enum = ">=1.5.1,<2.0.0" +typing-inspect = ">=0.4.0" + +[package.extras] +dev = ["flake8", "hypothesis", "ipython", "mypy (>=0.710)", "portray", "pytest (>=6.2.3)", "simplejson", "types-dataclasses"] + +[[package]] +name = "distlib" +version = "0.3.6" +description = "Distribution utilities" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "filelock" +version = "3.8.0" +description = "A platform independent file lock." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["furo (>=2022.6.21)", "sphinx (>=5.1.1)", "sphinx-autodoc-typehints (>=1.19.1)"] +testing = ["covdefaults (>=2.2)", "coverage (>=6.4.2)", "pytest (>=7.1.2)", "pytest-cov (>=3)", "pytest-timeout (>=2.1)"] + +[[package]] +name = "gitdb" +version = "4.0.10" +description = "Git Object Database" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +smmap = ">=3.0.1,<6" + +[[package]] +name = "GitPython" +version = "3.1.30" +description = "GitPython is a python library used to interact with Git repositories" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +gitdb = ">=4.0.1,<5" + +[[package]] +name = "idna" +version = "3.4" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "iso8601" +version = "0.1.16" +description = "Simple module to parse ISO 8601 dates" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "jmespath" +version = "1.0.1" +description = "JSON Matching Expressions" +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "marshmallow" +version = "3.19.0" +description = "A lightweight library for converting complex datatypes to and from native Python datatypes." +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +packaging = ">=17.0" + +[package.extras] +dev = ["flake8 (==5.0.4)", "flake8-bugbear (==22.10.25)", "mypy (==0.990)", "pre-commit (>=2.4,<3.0)", "pytest", "pytz", "simplejson", "tox"] +docs = ["alabaster (==0.7.12)", "autodocsumm (==0.2.9)", "sphinx (==5.3.0)", "sphinx-issues (==3.0.1)", "sphinx-version-warning (==1.1.2)"] +lint = ["flake8 (==5.0.4)", "flake8-bugbear (==22.10.25)", "mypy (==0.990)", "pre-commit (>=2.4,<3.0)"] +tests = ["pytest", "pytz", "simplejson"] + +[[package]] +name = "marshmallow-enum" +version = "1.5.1" +description = "Enum field for Marshmallow" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +marshmallow = ">=2.0.0" + +[[package]] +name = "mashumaro" +version = "3.3" +description = "Fast serialization framework on top of dataclasses" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +pyyaml = {version = ">=3.13", optional = true, markers = "extra == \"yaml\""} +typing-extensions = ">=4.1.0" + +[package.extras] +msgpack = ["msgpack (>=0.5.6)"] +orjson = ["orjson"] +toml = ["tomli (>=1.1.0)", "tomli-w (>=1.0)"] +yaml = ["pyyaml (>=3.13)"] + +[[package]] +name = "mypy-extensions" +version = "0.4.3" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "omitempty" +version = "0.1.1" +description = "enums for Python" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "packaging" +version = "21.3" +description = "Core utilities for Python packages" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" + +[[package]] +name = "platformdirs" +version = "2.5.2" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx (>=4)", "sphinx-autodoc-typehints (>=1.12)"] +test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] + +[[package]] +name = "pluggy" +version = "1.0.0" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "prompt-toolkit" +version = "3.0.36" +description = "Library for building powerful interactive command lines in Python" +category = "main" +optional = false +python-versions = ">=3.6.2" + +[package.dependencies] +wcwidth = "*" + +[[package]] +name = "publisher" +version = "0.1.0" +description = "Collection of scripts used to generate and publish supported grype DBs" +category = "main" +optional = false +python-versions = "^3.10" +develop = false + +[package.dependencies] +boto3 = "^1.18.0" +click = "^8" +dataclasses-json = "^0.5.4" +iso8601 = "^0.1.14" +requests = "^2.26.0" +semver = "^2.13.0" +yardstick = {git = "https://github.com/anchore/yardstick.git", rev = "028b7723cd1133dd649ac5f5db90ea743767f2b8"} +zstandard = "^0.18.0" + +[package.source] +type = "directory" +url = "../../publish" + +[[package]] +name = "py" +version = "1.11.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "pycparser" +version = "2.21" +description = "C parser in Python" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "Pygments" +version = "2.14.0" +description = "Pygments is a syntax highlighting package written in Python." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +plugins = ["importlib-metadata"] + +[[package]] +name = "pyparsing" +version = "3.0.9" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +category = "main" +optional = false +python-versions = ">=3.6.8" + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + +[[package]] +name = "pytest" +version = "6.2.5" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +py = ">=1.8.2" +toml = "*" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] + +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "PyYAML" +version = "6.0" +description = "YAML parser and emitter for Python" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "requests" +version = "2.28.1" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=3.7, <4" + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<3" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "rfc3339" +version = "6.2" +description = "Format dates according to the RFC 3339." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "s3transfer" +version = "0.6.0" +description = "An Amazon S3 Transfer Manager" +category = "main" +optional = false +python-versions = ">= 3.7" + +[package.dependencies] +botocore = ">=1.12.36,<2.0a.0" + +[package.extras] +crt = ["botocore[crt] (>=1.20.29,<2.0a.0)"] + +[[package]] +name = "semver" +version = "2.13.0" +description = "Python helper for Semantic Versioning (http://semver.org/)" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "smmap" +version = "5.0.0" +description = "A pure Python implementation of a sliding window memory map manager" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "tabulate" +version = "0.8.10" +description = "Pretty-print tabular data" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.extras] +widechars = ["wcwidth"] + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "tox" +version = "3.26.0" +description = "tox is a generic virtualenv management and test command line tool" +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[package.dependencies] +colorama = {version = ">=0.4.1", markers = "platform_system == \"Windows\""} +filelock = ">=3.0.0" +packaging = ">=14" +pluggy = ">=0.12.0" +py = ">=1.4.17" +six = ">=1.14.0" +tomli = {version = ">=2.0.1", markers = "python_version >= \"3.7\" and python_version < \"3.11\""} +virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2,<20.0.3 || >20.0.3,<20.0.4 || >20.0.4,<20.0.5 || >20.0.5,<20.0.6 || >20.0.6,<20.0.7 || >20.0.7" + +[package.extras] +docs = ["pygments-github-lexers (>=0.0.5)", "sphinx (>=2.0.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "towncrier (>=18.5.0)"] +testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pathlib2 (>=2.3.3)", "psutil (>=5.6.1)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)"] + +[[package]] +name = "typing-extensions" +version = "4.4.0" +description = "Backported and Experimental Type Hints for Python 3.7+" +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "typing-inspect" +version = "0.8.0" +description = "Runtime inspection utilities for typing module." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +mypy-extensions = ">=0.3.0" +typing-extensions = ">=3.7.4" + +[[package]] +name = "urllib3" +version = "1.26.13" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[[package]] +name = "virtualenv" +version = "20.16.5" +description = "Virtual Python Environment builder" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +distlib = ">=0.3.5,<1" +filelock = ">=3.4.1,<4" +platformdirs = ">=2.4,<3" + +[package.extras] +docs = ["proselint (>=0.13)", "sphinx (>=5.1.1)", "sphinx-argparse (>=0.3.1)", "sphinx-rtd-theme (>=1)", "towncrier (>=21.9)"] +testing = ["coverage (>=6.2)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=21.3)", "pytest (>=7.0.1)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.6.1)", "pytest-randomly (>=3.10.3)", "pytest-timeout (>=2.1)"] + +[[package]] +name = "wcwidth" +version = "0.2.5" +description = "Measures the displayed width of unicode strings in a terminal" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "yardstick" +version = "0.1.0" +description = "Tool for comparing the results from vulnerability scanners" +category = "main" +optional = false +python-versions = "^3.7" +develop = false + +[package.dependencies] +click = "^8" +Colr = "^0.9.1" +dataclasses-json = "^0.5.2" +GitPython = "^3.1.15" +mashumaro = {version = "^3.0.4", extras = ["yaml"]} +omitempty = "^0.1.1" +prompt-toolkit = "^3.0.18" +Pygments = "^2.8.1" +requests = "^2.25.1" +rfc3339 = "^6.2" +tabulate = "^0.8.9" + +[package.source] +type = "git" +url = "https://github.com/anchore/yardstick.git" +reference = "028b7723cd1133dd649ac5f5db90ea743767f2b8" +resolved_reference = "028b7723cd1133dd649ac5f5db90ea743767f2b8" + +[[package]] +name = "zstandard" +version = "0.18.0" +description = "Zstandard bindings for Python" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +cffi = {version = ">=1.11", markers = "platform_python_implementation == \"PyPy\""} + +[package.extras] +cffi = ["cffi (>=1.11)"] + +[metadata] +lock-version = "1.1" +python-versions = "^3.10" +content-hash = "6cc240cfff1d40795f9f0ba82cceccdcbbbad39ad45820f547554be1b1998ec0" + +[metadata.files] +atomicwrites = [ + {file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"}, +] +attrs = [ + {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"}, + {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, +] +boto3 = [ + {file = "boto3-1.26.45-py3-none-any.whl", hash = "sha256:b1bc7db503dc49bdccf5dada080077056a32af9982afdde84578a109cd741d05"}, + {file = "boto3-1.26.45.tar.gz", hash = "sha256:cc7f652df93e1ce818413fd82ffd645d4f92a64fec67c72946212d3750eaa80f"}, +] +botocore = [ + {file = "botocore-1.29.45-py3-none-any.whl", hash = "sha256:a5c0e13f266ee9a74335a1e5d3e377f2baae27226ae23d78f023bae0d18f3161"}, + {file = "botocore-1.29.45.tar.gz", hash = "sha256:62ae03e591ff25555854aa338da35190ffe18c0b1be2ebf5cfb277164233691f"}, +] +certifi = [ + {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, + {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, +] +cffi = [ + {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, + {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"}, + {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"}, + {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"}, + {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"}, + {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"}, + {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"}, + {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"}, + {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"}, + {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"}, + {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"}, + {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"}, + {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"}, + {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"}, + {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"}, + {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"}, + {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"}, + {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"}, + {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"}, + {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"}, + {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"}, + {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"}, + {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"}, + {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"}, + {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"}, + {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"}, + {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"}, + {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"}, + {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"}, + {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"}, + {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"}, + {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"}, + {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"}, + {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, + {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, +] +charset-normalizer = [ + {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, + {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, +] +click = [ + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, +] +colorama = [ + {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, + {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, +] +Colr = [ + {file = "Colr-0.9.1.tar.gz", hash = "sha256:8c15437eeb2ec8821c6df24b62946dfc6b79f69a1d84c1a6c131945a5ff4623c"}, +] +dataclasses-json = [ + {file = "dataclasses-json-0.5.7.tar.gz", hash = "sha256:c2c11bc8214fbf709ffc369d11446ff6945254a7f09128154a7620613d8fda90"}, + {file = "dataclasses_json-0.5.7-py3-none-any.whl", hash = "sha256:bc285b5f892094c3a53d558858a88553dd6a61a11ab1a8128a0e554385dcc5dd"}, +] +distlib = [ + {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, + {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"}, +] +filelock = [ + {file = "filelock-3.8.0-py3-none-any.whl", hash = "sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4"}, + {file = "filelock-3.8.0.tar.gz", hash = "sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc"}, +] +gitdb = [ + {file = "gitdb-4.0.10-py3-none-any.whl", hash = "sha256:c286cf298426064079ed96a9e4a9d39e7f3e9bf15ba60701e95f5492f28415c7"}, + {file = "gitdb-4.0.10.tar.gz", hash = "sha256:6eb990b69df4e15bad899ea868dc46572c3f75339735663b81de79b06f17eb9a"}, +] +GitPython = [ + {file = "GitPython-3.1.30-py3-none-any.whl", hash = "sha256:cd455b0000615c60e286208ba540271af9fe531fa6a87cc590a7298785ab2882"}, + {file = "GitPython-3.1.30.tar.gz", hash = "sha256:769c2d83e13f5d938b7688479da374c4e3d49f71549aaf462b646db9602ea6f8"}, +] +idna = [ + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, +] +iniconfig = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] +iso8601 = [ + {file = "iso8601-0.1.16-py2.py3-none-any.whl", hash = "sha256:906714829fedbc89955d52806c903f2332e3948ed94e31e85037f9e0226b8376"}, + {file = "iso8601-0.1.16.tar.gz", hash = "sha256:36532f77cc800594e8f16641edae7f1baf7932f05d8e508545b95fc53c6dc85b"}, +] +jmespath = [ + {file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"}, + {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, +] +marshmallow = [ + {file = "marshmallow-3.19.0-py3-none-any.whl", hash = "sha256:93f0958568da045b0021ec6aeb7ac37c81bfcccbb9a0e7ed8559885070b3a19b"}, + {file = "marshmallow-3.19.0.tar.gz", hash = "sha256:90032c0fd650ce94b6ec6dc8dfeb0e3ff50c144586462c389b81a07205bedb78"}, +] +marshmallow-enum = [ + {file = "marshmallow-enum-1.5.1.tar.gz", hash = "sha256:38e697e11f45a8e64b4a1e664000897c659b60aa57bfa18d44e226a9920b6e58"}, + {file = "marshmallow_enum-1.5.1-py2.py3-none-any.whl", hash = "sha256:57161ab3dbfde4f57adeb12090f39592e992b9c86d206d02f6bd03ebec60f072"}, +] +mashumaro = [ + {file = "mashumaro-3.3-py3-none-any.whl", hash = "sha256:14d223d8479e5cbddbfd6980f380b14301f48498e458dc3cb1b8b96838c8f713"}, + {file = "mashumaro-3.3.tar.gz", hash = "sha256:7ef12e2e81ad0ccb4560435ded1bec06ba38b941ce1470e82a13157c6b201661"}, +] +mypy-extensions = [ + {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, + {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, +] +omitempty = [ + {file = "omitempty-0.1.1.tar.gz", hash = "sha256:761fea43d0edb7a31e3322158f73c97d77e939e57c1e62754be23e081ab853d8"}, +] +packaging = [ + {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, + {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, +] +platformdirs = [ + {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, + {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, +] +pluggy = [ + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, +] +prompt-toolkit = [ + {file = "prompt_toolkit-3.0.36-py3-none-any.whl", hash = "sha256:aa64ad242a462c5ff0363a7b9cfe696c20d55d9fc60c11fd8e632d064804d305"}, + {file = "prompt_toolkit-3.0.36.tar.gz", hash = "sha256:3e163f254bef5a03b146397d7c1963bd3e2812f0964bb9a24e6ec761fd28db63"}, +] +publisher = [] +py = [ + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, +] +pycparser = [ + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, +] +Pygments = [ + {file = "Pygments-2.14.0-py3-none-any.whl", hash = "sha256:fa7bd7bd2771287c0de303af8bfdfc731f51bd2c6a47ab69d117138893b82717"}, + {file = "Pygments-2.14.0.tar.gz", hash = "sha256:b3ed06a9e8ac9a9aae5a6f5dbe78a8a58655d17b43b93c078f094ddc476ae297"}, +] +pyparsing = [ + {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, + {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, +] +pytest = [ + {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, + {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, +] +python-dateutil = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] +PyYAML = [ + {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, + {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, + {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, + {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, + {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, + {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, + {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, + {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, + {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, + {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, + {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, + {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, + {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, + {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, + {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, + {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, + {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, + {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, +] +requests = [ + {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, + {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, +] +rfc3339 = [ + {file = "rfc3339-6.2-py3-none-any.whl", hash = "sha256:f44316b21b21db90a625cde04ebb0d46268f153e6093021fa5893e92a96f58a3"}, + {file = "rfc3339-6.2.tar.gz", hash = "sha256:d53c3b5eefaef892b7240ba2a91fef012e86faa4d0a0ca782359c490e00ad4d0"}, +] +s3transfer = [ + {file = "s3transfer-0.6.0-py3-none-any.whl", hash = "sha256:06176b74f3a15f61f1b4f25a1fc29a4429040b7647133a463da8fa5bd28d5ecd"}, + {file = "s3transfer-0.6.0.tar.gz", hash = "sha256:2ed07d3866f523cc561bf4a00fc5535827981b117dd7876f036b0c1aca42c947"}, +] +semver = [ + {file = "semver-2.13.0-py2.py3-none-any.whl", hash = "sha256:ced8b23dceb22134307c1b8abfa523da14198793d9787ac838e70e29e77458d4"}, + {file = "semver-2.13.0.tar.gz", hash = "sha256:fa0fe2722ee1c3f57eac478820c3a5ae2f624af8264cbdf9000c980ff7f75e3f"}, +] +six = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] +smmap = [ + {file = "smmap-5.0.0-py3-none-any.whl", hash = "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94"}, + {file = "smmap-5.0.0.tar.gz", hash = "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"}, +] +tabulate = [ + {file = "tabulate-0.8.10-py3-none-any.whl", hash = "sha256:0ba055423dbaa164b9e456abe7920c5e8ed33fcc16f6d1b2f2d152c8e1e8b4fc"}, + {file = "tabulate-0.8.10.tar.gz", hash = "sha256:6c57f3f3dd7ac2782770155f3adb2db0b1a269637e42f27599925e64b114f519"}, +] +toml = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] +tomli = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] +tox = [ + {file = "tox-3.26.0-py2.py3-none-any.whl", hash = "sha256:bf037662d7c740d15c9924ba23bb3e587df20598697bb985ac2b49bdc2d847f6"}, + {file = "tox-3.26.0.tar.gz", hash = "sha256:44f3c347c68c2c68799d7d44f1808f9d396fc8a1a500cbc624253375c7ae107e"}, +] +typing-extensions = [ + {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, + {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, +] +typing-inspect = [ + {file = "typing_inspect-0.8.0-py3-none-any.whl", hash = "sha256:5fbf9c1e65d4fa01e701fe12a5bca6c6e08a4ffd5bc60bfac028253a447c5188"}, + {file = "typing_inspect-0.8.0.tar.gz", hash = "sha256:8b1ff0c400943b6145df8119c41c244ca8207f1f10c9c057aeed1560e4806e3d"}, +] +urllib3 = [ + {file = "urllib3-1.26.13-py2.py3-none-any.whl", hash = "sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc"}, + {file = "urllib3-1.26.13.tar.gz", hash = "sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8"}, +] +virtualenv = [ + {file = "virtualenv-20.16.5-py3-none-any.whl", hash = "sha256:d07dfc5df5e4e0dbc92862350ad87a36ed505b978f6c39609dc489eadd5b0d27"}, + {file = "virtualenv-20.16.5.tar.gz", hash = "sha256:227ea1b9994fdc5ea31977ba3383ef296d7472ea85be9d6732e42a91c04e80da"}, +] +wcwidth = [ + {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, + {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, +] +yardstick = [] +zstandard = [ + {file = "zstandard-0.18.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef7e8a200e4c8ac9102ed3c90ed2aa379f6b880f63032200909c1be21951f556"}, + {file = "zstandard-0.18.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2dc466207016564805e56d28375f4f533b525ff50d6776946980dff5465566ac"}, + {file = "zstandard-0.18.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a2ee1d4f98447f3e5183ecfce5626f983504a4a0c005fbe92e60fa8e5d547ec"}, + {file = "zstandard-0.18.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d956e2f03c7200d7e61345e0880c292783ec26618d0d921dcad470cb195bbce2"}, + {file = "zstandard-0.18.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ce6f59cba9854fd14da5bfe34217a1501143057313966637b7291d1b0267bd1e"}, + {file = "zstandard-0.18.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7fa67cba473623848b6e88acf8d799b1906178fd883fb3a1da24561c779593b"}, + {file = "zstandard-0.18.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cdb44d7284c8c5dd1b66dfb86dda7f4560fa94bfbbc1d2da749ba44831335e32"}, + {file = "zstandard-0.18.0-cp310-cp310-win32.whl", hash = "sha256:63694a376cde0aa8b1971d06ca28e8f8b5f492779cb6ee1cc46bbc3f019a42a5"}, + {file = "zstandard-0.18.0-cp310-cp310-win_amd64.whl", hash = "sha256:702a8324cd90c74d9c8780d02bf55e79da3193c870c9665ad3a11647e3ad1435"}, + {file = "zstandard-0.18.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:46f679bc5dfd938db4fb058218d9dc4db1336ffaf1ea774ff152ecadabd40805"}, + {file = "zstandard-0.18.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc2a4de9f363b3247d472362a65041fe4c0f59e01a2846b15d13046be866a885"}, + {file = "zstandard-0.18.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd3220d7627fd4d26397211cb3b560ec7cc4a94b75cfce89e847e8ce7fabe32d"}, + {file = "zstandard-0.18.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:39e98cf4773234bd9cebf9f9db730e451dfcfe435e220f8921242afda8321887"}, + {file = "zstandard-0.18.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5228e596eb1554598c872a337bbe4e5afe41cd1f8b1b15f2e35b50d061e35244"}, + {file = "zstandard-0.18.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d4a8fd45746a6c31e729f35196e80b8f1e9987c59f5ccb8859d7c6a6fbeb9c63"}, + {file = "zstandard-0.18.0-cp36-cp36m-win32.whl", hash = "sha256:4cbb85f29a990c2fdbf7bc63246567061a362ddca886d7fae6f780267c0a9e67"}, + {file = "zstandard-0.18.0-cp36-cp36m-win_amd64.whl", hash = "sha256:bfa6c8549fa18e6497a738b7033c49f94a8e2e30c5fbe2d14d0b5aa8bbc1695d"}, + {file = "zstandard-0.18.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e02043297c1832f2666cd2204f381bef43b10d56929e13c42c10c732c6e3b4ed"}, + {file = "zstandard-0.18.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7231543d38d2b7e02ef7cc78ef7ffd86419437e1114ff08709fe25a160e24bd6"}, + {file = "zstandard-0.18.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c86befac87445927488f5c8f205d11566f64c11519db223e9d282b945fa60dab"}, + {file = "zstandard-0.18.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:999a4e1768f219826ba3fa2064fab1c86dd72fdd47a42536235478c3bb3ca3e2"}, + {file = "zstandard-0.18.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9df59cd1cf3c62075ee2a4da767089d19d874ac3ad42b04a71a167e91b384722"}, + {file = "zstandard-0.18.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1be31e9e3f7607ee0cdd60915410a5968b205d3e7aa83b7fcf3dd76dbbdb39e0"}, + {file = "zstandard-0.18.0-cp37-cp37m-win32.whl", hash = "sha256:490d11b705b8ae9dc845431bacc8dd1cef2408aede176620a5cd0cd411027936"}, + {file = "zstandard-0.18.0-cp37-cp37m-win_amd64.whl", hash = "sha256:266aba27fa9cc5e9091d3d325ebab1fa260f64e83e42516d5e73947c70216a5b"}, + {file = "zstandard-0.18.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8b2260c4e07dd0723eadb586de7718b61acca4083a490dda69c5719d79bc715c"}, + {file = "zstandard-0.18.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3af8c2383d02feb6650e9255491ec7d0824f6e6dd2bbe3e521c469c985f31fb1"}, + {file = "zstandard-0.18.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28723a1d2e4df778573b76b321ebe9f3469ac98988104c2af116dd344802c3f8"}, + {file = "zstandard-0.18.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19cac7108ff2c342317fad6dc97604b47a41f403c8f19d0bfc396dfadc3638b8"}, + {file = "zstandard-0.18.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:76725d1ee83a8915100a310bbad5d9c1fc6397410259c94033b8318d548d9990"}, + {file = "zstandard-0.18.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d716a7694ce1fa60b20bc10f35c4a22be446ef7f514c8dbc8f858b61976de2fb"}, + {file = "zstandard-0.18.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:49685bf9a55d1ab34bd8423ea22db836ba43a181ac6b045ac4272093d5cb874e"}, + {file = "zstandard-0.18.0-cp38-cp38-win32.whl", hash = "sha256:1af1268a7dc870eb27515fb8db1f3e6c5a555d2b7bcc476fc3bab8886c7265ab"}, + {file = "zstandard-0.18.0-cp38-cp38-win_amd64.whl", hash = "sha256:1dc2d3809e763055a1a6c1a73f2b677320cc9a5aa1a7c6cfb35aee59bddc42d9"}, + {file = "zstandard-0.18.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eea18c1e7442f2aa9aff1bb84550dbb6a1f711faf6e48e7319de8f2b2e923c2a"}, + {file = "zstandard-0.18.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8677ffc6a6096cccbd892e558471c901fd821aba12b7fbc63833c7346f549224"}, + {file = "zstandard-0.18.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:083dc08abf03807af9beeb2b6a91c23ad78add2499f828176a3c7b742c44df02"}, + {file = "zstandard-0.18.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c990063664c08169c84474acecc9251ee035871589025cac47c060ff4ec4bc1a"}, + {file = "zstandard-0.18.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:533db8a6fac6248b2cb2c935e7b92f994efbdeb72e1ffa0b354432e087bb5a3e"}, + {file = "zstandard-0.18.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbb3cb8a082d62b8a73af42291569d266b05605e017a3d8a06a0e5c30b5f10f0"}, + {file = "zstandard-0.18.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d6c85ca5162049ede475b7ec98e87f9390501d44a3d6776ddd504e872464ec25"}, + {file = "zstandard-0.18.0-cp39-cp39-win32.whl", hash = "sha256:75479e7c2b3eebf402c59fbe57d21bc400cefa145ca356ee053b0a08908c5784"}, + {file = "zstandard-0.18.0-cp39-cp39-win_amd64.whl", hash = "sha256:d85bfabad444812133a92fc6fbe463e1d07581dba72f041f07a360e63808b23c"}, + {file = "zstandard-0.18.0.tar.gz", hash = "sha256:0ac0357a0d985b4ff31a854744040d7b5754385d1f98f7145c30e02c6865cb6f"}, +] diff --git a/test/acceptance/pyproject.toml b/test/acceptance/pyproject.toml new file mode 100644 index 00000000..cfac5320 --- /dev/null +++ b/test/acceptance/pyproject.toml @@ -0,0 +1,20 @@ +[tool.poetry] +name = "publish" +version = "0.1.0" +description = "Acceptance test generated DBs against grype" +authors = ["Alex Goodman "] +license = "Apache 2.0" +exclude = [ + "test/**/*" +] + +[tool.poetry.dependencies] +python = "^3.10" +#yardstick = {path = "../../../yardstick", develop = true} +tabulate = "^0.8.9" +publisher = {path = "../../publish"} + + +[tool.poetry.dev-dependencies] +pytest = "^6.2.2" +tox = "^3.23.0" diff --git a/test/acceptance/test-fixtures/.gitignore b/test/acceptance/test-fixtures/.gitignore new file mode 100644 index 00000000..8dd772da --- /dev/null +++ b/test/acceptance/test-fixtures/.gitignore @@ -0,0 +1,2 @@ +tool/ +tools/ \ No newline at end of file diff --git a/test/acceptance/test-fixtures/result/sets/acceptance-test.json b/test/acceptance/test-fixtures/result/sets/acceptance-test.json new file mode 100644 index 00000000..8798de8b --- /dev/null +++ b/test/acceptance/test-fixtures/result/sets/acceptance-test.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c2a42eb3563ba931ec4cf271c3d286f8175eb87a7c1aa353dc00cc3e84d8841e +size 110295 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:01c78cee3fe398bf1f77566177770b07f1d2af01753c2434cb0735bd43a078b6/grype@v0.12.1/2023-02-16T20:15:38+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:01c78cee3fe398bf1f77566177770b07f1d2af01753c2434cb0735bd43a078b6/grype@v0.12.1/2023-02-16T20:15:38+00:00/data.json new file mode 100644 index 00000000..ac9bef95 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:01c78cee3fe398bf1f77566177770b07f1d2af01753c2434cb0735bd43a078b6/grype@v0.12.1/2023-02-16T20:15:38+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:baae953122793a4e05b5259935b9d1fd3bd4c27ca6698b05fa19deaf2ac7f95e +size 51550 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:01c78cee3fe398bf1f77566177770b07f1d2af01753c2434cb0735bd43a078b6/grype@v0.12.1/2023-02-16T20:15:38+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:01c78cee3fe398bf1f77566177770b07f1d2af01753c2434cb0735bd43a078b6/grype@v0.12.1/2023-02-16T20:15:38+00:00/metadata.json new file mode 100644 index 00000000..917fc0b3 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:01c78cee3fe398bf1f77566177770b07f1d2af01753c2434cb0735bd43a078b6/grype@v0.12.1/2023-02-16T20:15:38+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:37fa9ebcd1d052fef8a615ee81bd4bd402f9034150a1abdfd9c251a1e5f11393 +size 881 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:01c78cee3fe398bf1f77566177770b07f1d2af01753c2434cb0735bd43a078b6/grype@v0.40.1/2023-02-16T20:18:42+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:01c78cee3fe398bf1f77566177770b07f1d2af01753c2434cb0735bd43a078b6/grype@v0.40.1/2023-02-16T20:18:42+00:00/data.json new file mode 100644 index 00000000..c0848e25 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:01c78cee3fe398bf1f77566177770b07f1d2af01753c2434cb0735bd43a078b6/grype@v0.40.1/2023-02-16T20:18:42+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c812d098dc5b2603b406a2430b3f87dd993ad299ebbde07942bc1c5b80f5273c +size 103644 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:01c78cee3fe398bf1f77566177770b07f1d2af01753c2434cb0735bd43a078b6/grype@v0.40.1/2023-02-16T20:18:42+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:01c78cee3fe398bf1f77566177770b07f1d2af01753c2434cb0735bd43a078b6/grype@v0.40.1/2023-02-16T20:18:42+00:00/metadata.json new file mode 100644 index 00000000..30facb8a --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:01c78cee3fe398bf1f77566177770b07f1d2af01753c2434cb0735bd43a078b6/grype@v0.40.1/2023-02-16T20:18:42+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f27d772de3c579aa12567f78ccf19fe76ea4718fe2db9cd16d515771d86e91f3 +size 881 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:01c78cee3fe398bf1f77566177770b07f1d2af01753c2434cb0735bd43a078b6/grype@v0.50.2/2023-02-16T20:21:51+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:01c78cee3fe398bf1f77566177770b07f1d2af01753c2434cb0735bd43a078b6/grype@v0.50.2/2023-02-16T20:21:51+00:00/data.json new file mode 100644 index 00000000..13f8e34a --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:01c78cee3fe398bf1f77566177770b07f1d2af01753c2434cb0735bd43a078b6/grype@v0.50.2/2023-02-16T20:21:51+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a76f44b178c62ec8a9cf014c8bbcaf9ae3c3664f40e1ee937022f5555a07fa22 +size 104570 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:01c78cee3fe398bf1f77566177770b07f1d2af01753c2434cb0735bd43a078b6/grype@v0.50.2/2023-02-16T20:21:51+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:01c78cee3fe398bf1f77566177770b07f1d2af01753c2434cb0735bd43a078b6/grype@v0.50.2/2023-02-16T20:21:51+00:00/metadata.json new file mode 100644 index 00000000..8a27f615 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:01c78cee3fe398bf1f77566177770b07f1d2af01753c2434cb0735bd43a078b6/grype@v0.50.2/2023-02-16T20:21:51+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d0d369bea0dfc7ff9d64203a7da6efc0104f700fca52555e33212a13eaabb95 +size 881 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:01c78cee3fe398bf1f77566177770b07f1d2af01753c2434cb0735bd43a078b6/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:03+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:01c78cee3fe398bf1f77566177770b07f1d2af01753c2434cb0735bd43a078b6/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:03+00:00/data.json new file mode 100644 index 00000000..cbd79bfb --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:01c78cee3fe398bf1f77566177770b07f1d2af01753c2434cb0735bd43a078b6/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:03+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c76719f748255356b41a1b45c3e356f1e5b5507103c191cc13d69f6aaba01771 +size 111883 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:01c78cee3fe398bf1f77566177770b07f1d2af01753c2434cb0735bd43a078b6/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:03+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:01c78cee3fe398bf1f77566177770b07f1d2af01753c2434cb0735bd43a078b6/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:03+00:00/metadata.json new file mode 100644 index 00000000..0ec49198 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:01c78cee3fe398bf1f77566177770b07f1d2af01753c2434cb0735bd43a078b6/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:03+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:82159d5a0d1f396afe31e4f71849a672c5f70f5ef7dca008facaad88f6ed7741 +size 959 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:01c78cee3fe398bf1f77566177770b07f1d2af01753c2434cb0735bd43a078b6/grype@v0.7.0/2023-02-16T20:09:51+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:01c78cee3fe398bf1f77566177770b07f1d2af01753c2434cb0735bd43a078b6/grype@v0.7.0/2023-02-16T20:09:51+00:00/data.json new file mode 100644 index 00000000..5de492be --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:01c78cee3fe398bf1f77566177770b07f1d2af01753c2434cb0735bd43a078b6/grype@v0.7.0/2023-02-16T20:09:51+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4110b76d0d303386126020a7e9180604fbf88272daa43c1186da5ed090c3a851 +size 34061 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:01c78cee3fe398bf1f77566177770b07f1d2af01753c2434cb0735bd43a078b6/grype@v0.7.0/2023-02-16T20:09:51+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:01c78cee3fe398bf1f77566177770b07f1d2af01753c2434cb0735bd43a078b6/grype@v0.7.0/2023-02-16T20:09:51+00:00/metadata.json new file mode 100644 index 00000000..cf9aaf80 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:01c78cee3fe398bf1f77566177770b07f1d2af01753c2434cb0735bd43a078b6/grype@v0.7.0/2023-02-16T20:09:51+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:75e1ecb86fe88c2d5b9b7024e051bda06a708909cebf7b19761bbe6522b1eeaf +size 626 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:05a70ba6d55e6d59b06ce8329bdd9540813e3d155ee7f41fe6044117caf81991/grype@v0.12.1/2023-02-16T20:16:48+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:05a70ba6d55e6d59b06ce8329bdd9540813e3d155ee7f41fe6044117caf81991/grype@v0.12.1/2023-02-16T20:16:48+00:00/data.json new file mode 100644 index 00000000..b7ce8ce9 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:05a70ba6d55e6d59b06ce8329bdd9540813e3d155ee7f41fe6044117caf81991/grype@v0.12.1/2023-02-16T20:16:48+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6cac82ac1084ff916451c2c369bac6c2aa0eb57a9425f3ad4002c712be44a230 +size 62922 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:05a70ba6d55e6d59b06ce8329bdd9540813e3d155ee7f41fe6044117caf81991/grype@v0.12.1/2023-02-16T20:16:48+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:05a70ba6d55e6d59b06ce8329bdd9540813e3d155ee7f41fe6044117caf81991/grype@v0.12.1/2023-02-16T20:16:48+00:00/metadata.json new file mode 100644 index 00000000..37aa9c38 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:05a70ba6d55e6d59b06ce8329bdd9540813e3d155ee7f41fe6044117caf81991/grype@v0.12.1/2023-02-16T20:16:48+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6e459b863caf336a16faa9e8a8d148cd8f9fd489e4f6b64f6dbeec8a9acae13f +size 882 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:05a70ba6d55e6d59b06ce8329bdd9540813e3d155ee7f41fe6044117caf81991/grype@v0.40.1/2023-02-16T20:19:54+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:05a70ba6d55e6d59b06ce8329bdd9540813e3d155ee7f41fe6044117caf81991/grype@v0.40.1/2023-02-16T20:19:54+00:00/data.json new file mode 100644 index 00000000..db873e47 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:05a70ba6d55e6d59b06ce8329bdd9540813e3d155ee7f41fe6044117caf81991/grype@v0.40.1/2023-02-16T20:19:54+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aeb01d846f15f9556ddc89cb1809d18fdece7c7c079befe5a08b886cf8f5a11c +size 150142 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:05a70ba6d55e6d59b06ce8329bdd9540813e3d155ee7f41fe6044117caf81991/grype@v0.40.1/2023-02-16T20:19:54+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:05a70ba6d55e6d59b06ce8329bdd9540813e3d155ee7f41fe6044117caf81991/grype@v0.40.1/2023-02-16T20:19:54+00:00/metadata.json new file mode 100644 index 00000000..6d33f657 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:05a70ba6d55e6d59b06ce8329bdd9540813e3d155ee7f41fe6044117caf81991/grype@v0.40.1/2023-02-16T20:19:54+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:87505878203bdc4813de95b35cc9b99d22e37ff1286710a4d1c7f35bb8e842d2 +size 882 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:05a70ba6d55e6d59b06ce8329bdd9540813e3d155ee7f41fe6044117caf81991/grype@v0.50.2/2023-02-16T20:23:04+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:05a70ba6d55e6d59b06ce8329bdd9540813e3d155ee7f41fe6044117caf81991/grype@v0.50.2/2023-02-16T20:23:04+00:00/data.json new file mode 100644 index 00000000..79eed698 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:05a70ba6d55e6d59b06ce8329bdd9540813e3d155ee7f41fe6044117caf81991/grype@v0.50.2/2023-02-16T20:23:04+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b265567f81e05e09e6bd99c1c1e7ba067ded0e6ad33ff784fbd5f884057a843c +size 151476 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:05a70ba6d55e6d59b06ce8329bdd9540813e3d155ee7f41fe6044117caf81991/grype@v0.50.2/2023-02-16T20:23:04+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:05a70ba6d55e6d59b06ce8329bdd9540813e3d155ee7f41fe6044117caf81991/grype@v0.50.2/2023-02-16T20:23:04+00:00/metadata.json new file mode 100644 index 00000000..d35d3d1b --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:05a70ba6d55e6d59b06ce8329bdd9540813e3d155ee7f41fe6044117caf81991/grype@v0.50.2/2023-02-16T20:23:04+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a8ab2e893df07494ecdde0b90e0500ab9bb3b28d59063fb35071f73d1ac5cb76 +size 882 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:05a70ba6d55e6d59b06ce8329bdd9540813e3d155ee7f41fe6044117caf81991/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:26:37+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:05a70ba6d55e6d59b06ce8329bdd9540813e3d155ee7f41fe6044117caf81991/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:26:37+00:00/data.json new file mode 100644 index 00000000..b6700f7c --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:05a70ba6d55e6d59b06ce8329bdd9540813e3d155ee7f41fe6044117caf81991/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:26:37+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bfd29ad6e6243967a686195ebf00dc13d447077b3a4a5a71bfa9d2f062f52705 +size 152888 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:05a70ba6d55e6d59b06ce8329bdd9540813e3d155ee7f41fe6044117caf81991/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:26:37+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:05a70ba6d55e6d59b06ce8329bdd9540813e3d155ee7f41fe6044117caf81991/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:26:37+00:00/metadata.json new file mode 100644 index 00000000..548e609f --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:05a70ba6d55e6d59b06ce8329bdd9540813e3d155ee7f41fe6044117caf81991/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:26:37+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a646a5f8e1d18ed14da91c5bc3ce544ac4e26aea2912f411469f9bf58e8280f5 +size 961 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:05a70ba6d55e6d59b06ce8329bdd9540813e3d155ee7f41fe6044117caf81991/grype@v0.7.0/2023-02-16T20:12:03+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:05a70ba6d55e6d59b06ce8329bdd9540813e3d155ee7f41fe6044117caf81991/grype@v0.7.0/2023-02-16T20:12:03+00:00/data.json new file mode 100644 index 00000000..1720ea68 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:05a70ba6d55e6d59b06ce8329bdd9540813e3d155ee7f41fe6044117caf81991/grype@v0.7.0/2023-02-16T20:12:03+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d49e5c6558cb6e7d6616d2bb2d5677a6a475f90bb8d08849bdcdb8609d553763 +size 64477 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:05a70ba6d55e6d59b06ce8329bdd9540813e3d155ee7f41fe6044117caf81991/grype@v0.7.0/2023-02-16T20:12:03+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:05a70ba6d55e6d59b06ce8329bdd9540813e3d155ee7f41fe6044117caf81991/grype@v0.7.0/2023-02-16T20:12:03+00:00/metadata.json new file mode 100644 index 00000000..562595e0 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:05a70ba6d55e6d59b06ce8329bdd9540813e3d155ee7f41fe6044117caf81991/grype@v0.7.0/2023-02-16T20:12:03+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e7fc68b5e608844881f2ceae70b1a38ea456051df466e150f6da772b61472fd0 +size 627 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:347fba6fbfa15d4e11217f9d49bf70a5a6eef35c6c642dc8c5db89115912d0c1/grype@v0.12.1/2023-02-16T20:16:24+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:347fba6fbfa15d4e11217f9d49bf70a5a6eef35c6c642dc8c5db89115912d0c1/grype@v0.12.1/2023-02-16T20:16:24+00:00/data.json new file mode 100644 index 00000000..4c545f50 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:347fba6fbfa15d4e11217f9d49bf70a5a6eef35c6c642dc8c5db89115912d0c1/grype@v0.12.1/2023-02-16T20:16:24+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c869d32d78008bb80acd960116992dfc08aee43913dc1a650d45dc923844b5a8 +size 96114 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:347fba6fbfa15d4e11217f9d49bf70a5a6eef35c6c642dc8c5db89115912d0c1/grype@v0.12.1/2023-02-16T20:16:24+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:347fba6fbfa15d4e11217f9d49bf70a5a6eef35c6c642dc8c5db89115912d0c1/grype@v0.12.1/2023-02-16T20:16:24+00:00/metadata.json new file mode 100644 index 00000000..7dcd4bed --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:347fba6fbfa15d4e11217f9d49bf70a5a6eef35c6c642dc8c5db89115912d0c1/grype@v0.12.1/2023-02-16T20:16:24+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:25735bef89c46abfb1b9bf1cf9b96360eee8363935e5f8dc0dc0172835844c17 +size 879 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:347fba6fbfa15d4e11217f9d49bf70a5a6eef35c6c642dc8c5db89115912d0c1/grype@v0.40.1/2023-02-16T20:19:27+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:347fba6fbfa15d4e11217f9d49bf70a5a6eef35c6c642dc8c5db89115912d0c1/grype@v0.40.1/2023-02-16T20:19:27+00:00/data.json new file mode 100644 index 00000000..457ab080 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:347fba6fbfa15d4e11217f9d49bf70a5a6eef35c6c642dc8c5db89115912d0c1/grype@v0.40.1/2023-02-16T20:19:27+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0b1db3639af2fdc7dd0656d29d3a3bddeade89b0237239f49d96800e0db02763 +size 102599 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:347fba6fbfa15d4e11217f9d49bf70a5a6eef35c6c642dc8c5db89115912d0c1/grype@v0.40.1/2023-02-16T20:19:27+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:347fba6fbfa15d4e11217f9d49bf70a5a6eef35c6c642dc8c5db89115912d0c1/grype@v0.40.1/2023-02-16T20:19:27+00:00/metadata.json new file mode 100644 index 00000000..08aef270 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:347fba6fbfa15d4e11217f9d49bf70a5a6eef35c6c642dc8c5db89115912d0c1/grype@v0.40.1/2023-02-16T20:19:27+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1dee09c3cad58927bed8b2b6d691bcab3d23a1f9e43eebfd16889c3977444871 +size 879 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:347fba6fbfa15d4e11217f9d49bf70a5a6eef35c6c642dc8c5db89115912d0c1/grype@v0.50.2/2023-02-16T20:22:36+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:347fba6fbfa15d4e11217f9d49bf70a5a6eef35c6c642dc8c5db89115912d0c1/grype@v0.50.2/2023-02-16T20:22:36+00:00/data.json new file mode 100644 index 00000000..82209b21 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:347fba6fbfa15d4e11217f9d49bf70a5a6eef35c6c642dc8c5db89115912d0c1/grype@v0.50.2/2023-02-16T20:22:36+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dbdec4ec6daa06568c2201f2af2081d6d4b4f4ba2c6572cb6d732c5bb150e021 +size 92840 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:347fba6fbfa15d4e11217f9d49bf70a5a6eef35c6c642dc8c5db89115912d0c1/grype@v0.50.2/2023-02-16T20:22:36+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:347fba6fbfa15d4e11217f9d49bf70a5a6eef35c6c642dc8c5db89115912d0c1/grype@v0.50.2/2023-02-16T20:22:36+00:00/metadata.json new file mode 100644 index 00000000..d0b50a66 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:347fba6fbfa15d4e11217f9d49bf70a5a6eef35c6c642dc8c5db89115912d0c1/grype@v0.50.2/2023-02-16T20:22:36+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4f42f19a88f956e77a7c825e43a000ce3ea0496eceb7ea0da6912eefa5bfa209 +size 879 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:347fba6fbfa15d4e11217f9d49bf70a5a6eef35c6c642dc8c5db89115912d0c1/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:26:00+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:347fba6fbfa15d4e11217f9d49bf70a5a6eef35c6c642dc8c5db89115912d0c1/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:26:00+00:00/data.json new file mode 100644 index 00000000..666b112f --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:347fba6fbfa15d4e11217f9d49bf70a5a6eef35c6c642dc8c5db89115912d0c1/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:26:00+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a6f920518cf8477a929949b7b7caaaf8399acda2a4592edc4707c158d84f2dbb +size 93624 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:347fba6fbfa15d4e11217f9d49bf70a5a6eef35c6c642dc8c5db89115912d0c1/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:26:00+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:347fba6fbfa15d4e11217f9d49bf70a5a6eef35c6c642dc8c5db89115912d0c1/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:26:00+00:00/metadata.json new file mode 100644 index 00000000..de28caca --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:347fba6fbfa15d4e11217f9d49bf70a5a6eef35c6c642dc8c5db89115912d0c1/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:26:00+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3f349765356b063f13f73b874103730e3f98a2e0a938585c06b1640562447155 +size 958 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:347fba6fbfa15d4e11217f9d49bf70a5a6eef35c6c642dc8c5db89115912d0c1/grype@v0.7.0/2023-02-16T20:11:13+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:347fba6fbfa15d4e11217f9d49bf70a5a6eef35c6c642dc8c5db89115912d0c1/grype@v0.7.0/2023-02-16T20:11:13+00:00/data.json new file mode 100644 index 00000000..ec303265 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:347fba6fbfa15d4e11217f9d49bf70a5a6eef35c6c642dc8c5db89115912d0c1/grype@v0.7.0/2023-02-16T20:11:13+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:45245d56e70725d807557c0188d22b0282c0b298c3cc39807a19a92aa48f6bc8 +size 144953 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:347fba6fbfa15d4e11217f9d49bf70a5a6eef35c6c642dc8c5db89115912d0c1/grype@v0.7.0/2023-02-16T20:11:13+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:347fba6fbfa15d4e11217f9d49bf70a5a6eef35c6c642dc8c5db89115912d0c1/grype@v0.7.0/2023-02-16T20:11:13+00:00/metadata.json new file mode 100644 index 00000000..8bb099ef --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:347fba6fbfa15d4e11217f9d49bf70a5a6eef35c6c642dc8c5db89115912d0c1/grype@v0.7.0/2023-02-16T20:11:13+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:43fcef2abb4b7fcc22fc2a989b69b4e2337a27d7da015a1f3f8ec672232e1886 +size 624 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:3fa6909fa6f9a8ca8b7f9ba783af8cf84773c14084154073f1f331058ab646cb/grype@v0.12.1/2023-02-16T20:16:59+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:3fa6909fa6f9a8ca8b7f9ba783af8cf84773c14084154073f1f331058ab646cb/grype@v0.12.1/2023-02-16T20:16:59+00:00/data.json new file mode 100644 index 00000000..30ddf984 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:3fa6909fa6f9a8ca8b7f9ba783af8cf84773c14084154073f1f331058ab646cb/grype@v0.12.1/2023-02-16T20:16:59+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5c44c1ae4754d5b06c580d0e18b6edfc27418498f25cecb8a656c5ba2e0adc64 +size 17399 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:3fa6909fa6f9a8ca8b7f9ba783af8cf84773c14084154073f1f331058ab646cb/grype@v0.12.1/2023-02-16T20:16:59+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:3fa6909fa6f9a8ca8b7f9ba783af8cf84773c14084154073f1f331058ab646cb/grype@v0.12.1/2023-02-16T20:16:59+00:00/metadata.json new file mode 100644 index 00000000..2c382213 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:3fa6909fa6f9a8ca8b7f9ba783af8cf84773c14084154073f1f331058ab646cb/grype@v0.12.1/2023-02-16T20:16:59+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7968344d96118e5a16e2159310b1b02e9e5fc277806fca9f987f4fdcd5a68eb4 +size 884 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:3fa6909fa6f9a8ca8b7f9ba783af8cf84773c14084154073f1f331058ab646cb/grype@v0.40.1/2023-02-16T20:20:06+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:3fa6909fa6f9a8ca8b7f9ba783af8cf84773c14084154073f1f331058ab646cb/grype@v0.40.1/2023-02-16T20:20:06+00:00/data.json new file mode 100644 index 00000000..310dc346 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:3fa6909fa6f9a8ca8b7f9ba783af8cf84773c14084154073f1f331058ab646cb/grype@v0.40.1/2023-02-16T20:20:06+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:821ec6f68fdafa2732231641f21f0faafd0d6e48dec646ff54a9dd6c895844b3 +size 737450 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:3fa6909fa6f9a8ca8b7f9ba783af8cf84773c14084154073f1f331058ab646cb/grype@v0.40.1/2023-02-16T20:20:06+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:3fa6909fa6f9a8ca8b7f9ba783af8cf84773c14084154073f1f331058ab646cb/grype@v0.40.1/2023-02-16T20:20:06+00:00/metadata.json new file mode 100644 index 00000000..be461585 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:3fa6909fa6f9a8ca8b7f9ba783af8cf84773c14084154073f1f331058ab646cb/grype@v0.40.1/2023-02-16T20:20:06+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:39d2638f1dca0c9069c5b66da82ad34388e7bc8d5e6a03216bfe98ad47e9d998 +size 884 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:3fa6909fa6f9a8ca8b7f9ba783af8cf84773c14084154073f1f331058ab646cb/grype@v0.50.2/2023-02-16T20:23:17+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:3fa6909fa6f9a8ca8b7f9ba783af8cf84773c14084154073f1f331058ab646cb/grype@v0.50.2/2023-02-16T20:23:17+00:00/data.json new file mode 100644 index 00000000..850a8b6f --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:3fa6909fa6f9a8ca8b7f9ba783af8cf84773c14084154073f1f331058ab646cb/grype@v0.50.2/2023-02-16T20:23:17+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c61b7a5be1cdbbaace7a69db8376c539eb13b35a7da8e4f5c0cd474a09b3636d +size 748869 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:3fa6909fa6f9a8ca8b7f9ba783af8cf84773c14084154073f1f331058ab646cb/grype@v0.50.2/2023-02-16T20:23:17+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:3fa6909fa6f9a8ca8b7f9ba783af8cf84773c14084154073f1f331058ab646cb/grype@v0.50.2/2023-02-16T20:23:17+00:00/metadata.json new file mode 100644 index 00000000..77e56814 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:3fa6909fa6f9a8ca8b7f9ba783af8cf84773c14084154073f1f331058ab646cb/grype@v0.50.2/2023-02-16T20:23:17+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5a7cb70306fa164f1e12fae6ddb22b77675b5fe007fb5120711309f2a075b356 +size 884 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:3fa6909fa6f9a8ca8b7f9ba783af8cf84773c14084154073f1f331058ab646cb/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:26:53+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:3fa6909fa6f9a8ca8b7f9ba783af8cf84773c14084154073f1f331058ab646cb/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:26:53+00:00/data.json new file mode 100644 index 00000000..34c1f687 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:3fa6909fa6f9a8ca8b7f9ba783af8cf84773c14084154073f1f331058ab646cb/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:26:53+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:66415b8f42272db490389de8f8e13d1c48bd22229388e2d9cb782855f890a6cf +size 767620 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:3fa6909fa6f9a8ca8b7f9ba783af8cf84773c14084154073f1f331058ab646cb/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:26:53+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:3fa6909fa6f9a8ca8b7f9ba783af8cf84773c14084154073f1f331058ab646cb/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:26:53+00:00/metadata.json new file mode 100644 index 00000000..92bffe5b --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:3fa6909fa6f9a8ca8b7f9ba783af8cf84773c14084154073f1f331058ab646cb/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:26:53+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6d777d4cdac48be30e15154bc71b90d7b828ac11e6e839b25240fba167e2952f +size 963 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:3fa6909fa6f9a8ca8b7f9ba783af8cf84773c14084154073f1f331058ab646cb/grype@v0.7.0/2023-02-16T20:12:29+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:3fa6909fa6f9a8ca8b7f9ba783af8cf84773c14084154073f1f331058ab646cb/grype@v0.7.0/2023-02-16T20:12:29+00:00/data.json new file mode 100644 index 00000000..db06f45e --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:3fa6909fa6f9a8ca8b7f9ba783af8cf84773c14084154073f1f331058ab646cb/grype@v0.7.0/2023-02-16T20:12:29+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dcc41cffe50bdaf2864764422573ba1223e46d78e46712e06667c540308dfe8e +size 16674 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:3fa6909fa6f9a8ca8b7f9ba783af8cf84773c14084154073f1f331058ab646cb/grype@v0.7.0/2023-02-16T20:12:29+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:3fa6909fa6f9a8ca8b7f9ba783af8cf84773c14084154073f1f331058ab646cb/grype@v0.7.0/2023-02-16T20:12:29+00:00/metadata.json new file mode 100644 index 00000000..81c40958 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:3fa6909fa6f9a8ca8b7f9ba783af8cf84773c14084154073f1f331058ab646cb/grype@v0.7.0/2023-02-16T20:12:29+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:875f4e80eb2459e7030f9d87900a391a48dc727420093c94b040db85c8cf806a +size 629 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:4b28f61016b9d4ad0c0198343e4cc2bd51029f4a1733ed2c4bcc3e2d0dd71bbc/grype@v0.12.1/2023-02-16T20:16:28+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:4b28f61016b9d4ad0c0198343e4cc2bd51029f4a1733ed2c4bcc3e2d0dd71bbc/grype@v0.12.1/2023-02-16T20:16:28+00:00/data.json new file mode 100644 index 00000000..fea38958 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:4b28f61016b9d4ad0c0198343e4cc2bd51029f4a1733ed2c4bcc3e2d0dd71bbc/grype@v0.12.1/2023-02-16T20:16:28+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f1e85f5bb1403c06d107ab4b4d691989c54e61cce7cc61b90641471fe683c8c4 +size 284676 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:4b28f61016b9d4ad0c0198343e4cc2bd51029f4a1733ed2c4bcc3e2d0dd71bbc/grype@v0.12.1/2023-02-16T20:16:28+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:4b28f61016b9d4ad0c0198343e4cc2bd51029f4a1733ed2c4bcc3e2d0dd71bbc/grype@v0.12.1/2023-02-16T20:16:28+00:00/metadata.json new file mode 100644 index 00000000..a31b4d3a --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:4b28f61016b9d4ad0c0198343e4cc2bd51029f4a1733ed2c4bcc3e2d0dd71bbc/grype@v0.12.1/2023-02-16T20:16:28+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2af9ce59040d2a72d152429daf03ec7449c0588dfb5f6006f3d563cf797e0d32 +size 883 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:4b28f61016b9d4ad0c0198343e4cc2bd51029f4a1733ed2c4bcc3e2d0dd71bbc/grype@v0.40.1/2023-02-16T20:19:31+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:4b28f61016b9d4ad0c0198343e4cc2bd51029f4a1733ed2c4bcc3e2d0dd71bbc/grype@v0.40.1/2023-02-16T20:19:31+00:00/data.json new file mode 100644 index 00000000..cbb27acd --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:4b28f61016b9d4ad0c0198343e4cc2bd51029f4a1733ed2c4bcc3e2d0dd71bbc/grype@v0.40.1/2023-02-16T20:19:31+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9f09f401f9e59f2e16b6c511244cb6b8641f11e325777a78c0b27348364643fe +size 394728 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:4b28f61016b9d4ad0c0198343e4cc2bd51029f4a1733ed2c4bcc3e2d0dd71bbc/grype@v0.40.1/2023-02-16T20:19:31+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:4b28f61016b9d4ad0c0198343e4cc2bd51029f4a1733ed2c4bcc3e2d0dd71bbc/grype@v0.40.1/2023-02-16T20:19:31+00:00/metadata.json new file mode 100644 index 00000000..e76e8033 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:4b28f61016b9d4ad0c0198343e4cc2bd51029f4a1733ed2c4bcc3e2d0dd71bbc/grype@v0.40.1/2023-02-16T20:19:31+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c8810d77b02d899300fb3c89036470acdf0684c6b54c0eafc8196fb4183d83db +size 883 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:4b28f61016b9d4ad0c0198343e4cc2bd51029f4a1733ed2c4bcc3e2d0dd71bbc/grype@v0.50.2/2023-02-16T20:22:40+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:4b28f61016b9d4ad0c0198343e4cc2bd51029f4a1733ed2c4bcc3e2d0dd71bbc/grype@v0.50.2/2023-02-16T20:22:40+00:00/data.json new file mode 100644 index 00000000..8c5e139e --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:4b28f61016b9d4ad0c0198343e4cc2bd51029f4a1733ed2c4bcc3e2d0dd71bbc/grype@v0.50.2/2023-02-16T20:22:40+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1d9de1bb5f399dcf9d6609d32dffac67f83e0abcd613b27750185b8b636744c6 +size 389736 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:4b28f61016b9d4ad0c0198343e4cc2bd51029f4a1733ed2c4bcc3e2d0dd71bbc/grype@v0.50.2/2023-02-16T20:22:40+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:4b28f61016b9d4ad0c0198343e4cc2bd51029f4a1733ed2c4bcc3e2d0dd71bbc/grype@v0.50.2/2023-02-16T20:22:40+00:00/metadata.json new file mode 100644 index 00000000..807566fd --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:4b28f61016b9d4ad0c0198343e4cc2bd51029f4a1733ed2c4bcc3e2d0dd71bbc/grype@v0.50.2/2023-02-16T20:22:40+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b350cf6af321ef1c3ef4bf6016559096daf47b6eec824efc15e24cc330583984 +size 883 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:4b28f61016b9d4ad0c0198343e4cc2bd51029f4a1733ed2c4bcc3e2d0dd71bbc/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:26:05+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:4b28f61016b9d4ad0c0198343e4cc2bd51029f4a1733ed2c4bcc3e2d0dd71bbc/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:26:05+00:00/data.json new file mode 100644 index 00000000..997e5025 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:4b28f61016b9d4ad0c0198343e4cc2bd51029f4a1733ed2c4bcc3e2d0dd71bbc/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:26:05+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:42111f379057ef06c215ef23285a45b4f59c9c601d0e1ac3c51a86bb707ab448 +size 406614 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:4b28f61016b9d4ad0c0198343e4cc2bd51029f4a1733ed2c4bcc3e2d0dd71bbc/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:26:05+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:4b28f61016b9d4ad0c0198343e4cc2bd51029f4a1733ed2c4bcc3e2d0dd71bbc/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:26:05+00:00/metadata.json new file mode 100644 index 00000000..ee26f3de --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:4b28f61016b9d4ad0c0198343e4cc2bd51029f4a1733ed2c4bcc3e2d0dd71bbc/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:26:05+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6574e53080653ff8f6afaea2c15ac173460ce5975eea9a823ef51c18243c1cc8 +size 962 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:4b28f61016b9d4ad0c0198343e4cc2bd51029f4a1733ed2c4bcc3e2d0dd71bbc/grype@v0.7.0/2023-02-16T20:11:19+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:4b28f61016b9d4ad0c0198343e4cc2bd51029f4a1733ed2c4bcc3e2d0dd71bbc/grype@v0.7.0/2023-02-16T20:11:19+00:00/data.json new file mode 100644 index 00000000..542365fd --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:4b28f61016b9d4ad0c0198343e4cc2bd51029f4a1733ed2c4bcc3e2d0dd71bbc/grype@v0.7.0/2023-02-16T20:11:19+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2a26c380b1ee42582acb929f20c9708a2a5a711d3c0e0b4e4ec0880a391d2150 +size 349780 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:4b28f61016b9d4ad0c0198343e4cc2bd51029f4a1733ed2c4bcc3e2d0dd71bbc/grype@v0.7.0/2023-02-16T20:11:19+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:4b28f61016b9d4ad0c0198343e4cc2bd51029f4a1733ed2c4bcc3e2d0dd71bbc/grype@v0.7.0/2023-02-16T20:11:19+00:00/metadata.json new file mode 100644 index 00000000..3558c83a --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:4b28f61016b9d4ad0c0198343e4cc2bd51029f4a1733ed2c4bcc3e2d0dd71bbc/grype@v0.7.0/2023-02-16T20:11:19+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a7edd64273e09e58e7fb207873f80ae1309f1a815bf381063e00864c33b655d5 +size 628 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:524ff8a75f21fd886ec7ed82387766df386671e8b77e898d05786118d5b7880b/grype@v0.12.1/2023-02-16T20:17:50+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:524ff8a75f21fd886ec7ed82387766df386671e8b77e898d05786118d5b7880b/grype@v0.12.1/2023-02-16T20:17:50+00:00/data.json new file mode 100644 index 00000000..5094caba --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:524ff8a75f21fd886ec7ed82387766df386671e8b77e898d05786118d5b7880b/grype@v0.12.1/2023-02-16T20:17:50+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:731514e3cbffa326b67b5449b571a18fc7d7d7bb64e3ee0c4d4599d01c636bac +size 2553889 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:524ff8a75f21fd886ec7ed82387766df386671e8b77e898d05786118d5b7880b/grype@v0.12.1/2023-02-16T20:17:50+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:524ff8a75f21fd886ec7ed82387766df386671e8b77e898d05786118d5b7880b/grype@v0.12.1/2023-02-16T20:17:50+00:00/metadata.json new file mode 100644 index 00000000..2dd0b43e --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:524ff8a75f21fd886ec7ed82387766df386671e8b77e898d05786118d5b7880b/grype@v0.12.1/2023-02-16T20:17:50+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:197f4fa88fddc6772bcea362ae7e426490b5014453046e7c30599583959f20ea +size 871 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:524ff8a75f21fd886ec7ed82387766df386671e8b77e898d05786118d5b7880b/grype@v0.40.1/2023-02-16T20:21:05+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:524ff8a75f21fd886ec7ed82387766df386671e8b77e898d05786118d5b7880b/grype@v0.40.1/2023-02-16T20:21:05+00:00/data.json new file mode 100644 index 00000000..d2c08604 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:524ff8a75f21fd886ec7ed82387766df386671e8b77e898d05786118d5b7880b/grype@v0.40.1/2023-02-16T20:21:05+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cb2c3730beb7dfbc680f01c88b84377b55867e51c74291d143bbda48eab563d6 +size 4882420 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:524ff8a75f21fd886ec7ed82387766df386671e8b77e898d05786118d5b7880b/grype@v0.40.1/2023-02-16T20:21:05+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:524ff8a75f21fd886ec7ed82387766df386671e8b77e898d05786118d5b7880b/grype@v0.40.1/2023-02-16T20:21:05+00:00/metadata.json new file mode 100644 index 00000000..4b060846 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:524ff8a75f21fd886ec7ed82387766df386671e8b77e898d05786118d5b7880b/grype@v0.40.1/2023-02-16T20:21:05+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:908394b78f4c3501e1d3c68edc7061e66563c6a958d01d636994b1c397a4cd2f +size 871 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:524ff8a75f21fd886ec7ed82387766df386671e8b77e898d05786118d5b7880b/grype@v0.50.2/2023-02-16T20:24:18+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:524ff8a75f21fd886ec7ed82387766df386671e8b77e898d05786118d5b7880b/grype@v0.50.2/2023-02-16T20:24:18+00:00/data.json new file mode 100644 index 00000000..0dfc54dc --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:524ff8a75f21fd886ec7ed82387766df386671e8b77e898d05786118d5b7880b/grype@v0.50.2/2023-02-16T20:24:18+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a79cae6b67c7efd05b9d134b33052671ecaadeccefbd2dcab182bcc03a49fde0 +size 4938842 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:524ff8a75f21fd886ec7ed82387766df386671e8b77e898d05786118d5b7880b/grype@v0.50.2/2023-02-16T20:24:18+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:524ff8a75f21fd886ec7ed82387766df386671e8b77e898d05786118d5b7880b/grype@v0.50.2/2023-02-16T20:24:18+00:00/metadata.json new file mode 100644 index 00000000..a0953afc --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:524ff8a75f21fd886ec7ed82387766df386671e8b77e898d05786118d5b7880b/grype@v0.50.2/2023-02-16T20:24:18+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c1b47eb656e341c1bf52a704a03dc6fa03c20842b15c08fadfe37baed320eef6 +size 871 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:524ff8a75f21fd886ec7ed82387766df386671e8b77e898d05786118d5b7880b/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:28:08+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:524ff8a75f21fd886ec7ed82387766df386671e8b77e898d05786118d5b7880b/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:28:08+00:00/data.json new file mode 100644 index 00000000..947ce834 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:524ff8a75f21fd886ec7ed82387766df386671e8b77e898d05786118d5b7880b/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:28:08+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e4a0ec725828ce45aef0142b505a5fb41605829dacb0256381cae292832738c3 +size 3550630 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:524ff8a75f21fd886ec7ed82387766df386671e8b77e898d05786118d5b7880b/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:28:08+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:524ff8a75f21fd886ec7ed82387766df386671e8b77e898d05786118d5b7880b/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:28:08+00:00/metadata.json new file mode 100644 index 00000000..22f216ed --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:524ff8a75f21fd886ec7ed82387766df386671e8b77e898d05786118d5b7880b/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:28:08+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5c44f637c31b17de5dee64f3488662aed542b3bdc58c0e17f0ae40bafc77d578 +size 949 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:524ff8a75f21fd886ec7ed82387766df386671e8b77e898d05786118d5b7880b/grype@v0.7.0/2023-02-16T20:14:29+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:524ff8a75f21fd886ec7ed82387766df386671e8b77e898d05786118d5b7880b/grype@v0.7.0/2023-02-16T20:14:29+00:00/data.json new file mode 100644 index 00000000..f9ee60e2 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:524ff8a75f21fd886ec7ed82387766df386671e8b77e898d05786118d5b7880b/grype@v0.7.0/2023-02-16T20:14:29+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:13cf3f05d69989b1224fd10cc9281631b3873c89aa336859fdf1cc238a9971c7 +size 2812029 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:524ff8a75f21fd886ec7ed82387766df386671e8b77e898d05786118d5b7880b/grype@v0.7.0/2023-02-16T20:14:29+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:524ff8a75f21fd886ec7ed82387766df386671e8b77e898d05786118d5b7880b/grype@v0.7.0/2023-02-16T20:14:29+00:00/metadata.json new file mode 100644 index 00000000..aa5d13b0 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:524ff8a75f21fd886ec7ed82387766df386671e8b77e898d05786118d5b7880b/grype@v0.7.0/2023-02-16T20:14:29+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1a6584d0d1ac466175e5993bd157777ea46f68226a7abb186c660a904a5d64eb +size 616 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:55c9ba4e24e15c0467a071d93fead0990b8f04bb60b359b4056a997598aa56a1/grype@v0.12.1/2023-02-16T20:15:39+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:55c9ba4e24e15c0467a071d93fead0990b8f04bb60b359b4056a997598aa56a1/grype@v0.12.1/2023-02-16T20:15:39+00:00/data.json new file mode 100644 index 00000000..83c9dcd4 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:55c9ba4e24e15c0467a071d93fead0990b8f04bb60b359b4056a997598aa56a1/grype@v0.12.1/2023-02-16T20:15:39+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c0c70fffa590a5589342c44e3da110936f5c8825359cac243edaf144f18d466e +size 72364 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:55c9ba4e24e15c0467a071d93fead0990b8f04bb60b359b4056a997598aa56a1/grype@v0.12.1/2023-02-16T20:15:39+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:55c9ba4e24e15c0467a071d93fead0990b8f04bb60b359b4056a997598aa56a1/grype@v0.12.1/2023-02-16T20:15:39+00:00/metadata.json new file mode 100644 index 00000000..66890488 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:55c9ba4e24e15c0467a071d93fead0990b8f04bb60b359b4056a997598aa56a1/grype@v0.12.1/2023-02-16T20:15:39+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4f604c7582e066a9306d1fad305e6c679e40bdc6b741be76905d722cbba45e7b +size 881 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:55c9ba4e24e15c0467a071d93fead0990b8f04bb60b359b4056a997598aa56a1/grype@v0.40.1/2023-02-16T20:18:43+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:55c9ba4e24e15c0467a071d93fead0990b8f04bb60b359b4056a997598aa56a1/grype@v0.40.1/2023-02-16T20:18:43+00:00/data.json new file mode 100644 index 00000000..4441b359 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:55c9ba4e24e15c0467a071d93fead0990b8f04bb60b359b4056a997598aa56a1/grype@v0.40.1/2023-02-16T20:18:43+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dfbe3fb1833449d126b185c493a3776b329ba7dc1b6ddc53a600b878dfb4e03a +size 46070 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:55c9ba4e24e15c0467a071d93fead0990b8f04bb60b359b4056a997598aa56a1/grype@v0.40.1/2023-02-16T20:18:43+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:55c9ba4e24e15c0467a071d93fead0990b8f04bb60b359b4056a997598aa56a1/grype@v0.40.1/2023-02-16T20:18:43+00:00/metadata.json new file mode 100644 index 00000000..6ff8dd3a --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:55c9ba4e24e15c0467a071d93fead0990b8f04bb60b359b4056a997598aa56a1/grype@v0.40.1/2023-02-16T20:18:43+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7efbe6b68ea47f8a56e0c8b851527c9d3aea08b324dd784c013defba03d41e05 +size 881 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:55c9ba4e24e15c0467a071d93fead0990b8f04bb60b359b4056a997598aa56a1/grype@v0.50.2/2023-02-16T20:21:52+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:55c9ba4e24e15c0467a071d93fead0990b8f04bb60b359b4056a997598aa56a1/grype@v0.50.2/2023-02-16T20:21:52+00:00/data.json new file mode 100644 index 00000000..80c118b0 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:55c9ba4e24e15c0467a071d93fead0990b8f04bb60b359b4056a997598aa56a1/grype@v0.50.2/2023-02-16T20:21:52+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:82e6a3ab4d3532bb4c912397b9f97ece7e7c615caee953fdac830a09817f6772 +size 46700 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:55c9ba4e24e15c0467a071d93fead0990b8f04bb60b359b4056a997598aa56a1/grype@v0.50.2/2023-02-16T20:21:52+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:55c9ba4e24e15c0467a071d93fead0990b8f04bb60b359b4056a997598aa56a1/grype@v0.50.2/2023-02-16T20:21:52+00:00/metadata.json new file mode 100644 index 00000000..95cbebea --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:55c9ba4e24e15c0467a071d93fead0990b8f04bb60b359b4056a997598aa56a1/grype@v0.50.2/2023-02-16T20:21:52+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ffece5c67d21476a01eb18faf0f6a6fbb5c828d69531dd17a24e0806f32d5feb +size 881 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:55c9ba4e24e15c0467a071d93fead0990b8f04bb60b359b4056a997598aa56a1/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:05+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:55c9ba4e24e15c0467a071d93fead0990b8f04bb60b359b4056a997598aa56a1/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:05+00:00/data.json new file mode 100644 index 00000000..31c0d095 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:55c9ba4e24e15c0467a071d93fead0990b8f04bb60b359b4056a997598aa56a1/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:05+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ee14bc41a5015716b95fac4b6014815117da09af10b8ff535cdccc0d3f9137ca +size 50317 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:55c9ba4e24e15c0467a071d93fead0990b8f04bb60b359b4056a997598aa56a1/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:05+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:55c9ba4e24e15c0467a071d93fead0990b8f04bb60b359b4056a997598aa56a1/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:05+00:00/metadata.json new file mode 100644 index 00000000..c453d5e0 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:55c9ba4e24e15c0467a071d93fead0990b8f04bb60b359b4056a997598aa56a1/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:05+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e94a8c302580cb168e610d8884fdaad1d08916cb7408451383d8ba592e8ca7eb +size 960 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:55c9ba4e24e15c0467a071d93fead0990b8f04bb60b359b4056a997598aa56a1/grype@v0.7.0/2023-02-16T20:09:54+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:55c9ba4e24e15c0467a071d93fead0990b8f04bb60b359b4056a997598aa56a1/grype@v0.7.0/2023-02-16T20:09:54+00:00/data.json new file mode 100644 index 00000000..5f68c5f0 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:55c9ba4e24e15c0467a071d93fead0990b8f04bb60b359b4056a997598aa56a1/grype@v0.7.0/2023-02-16T20:09:54+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ebcf29aecc05d97eff40f4afb2674baf878f9ea6c89f8102eda6e8bb81272456 +size 17126 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:55c9ba4e24e15c0467a071d93fead0990b8f04bb60b359b4056a997598aa56a1/grype@v0.7.0/2023-02-16T20:09:54+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:55c9ba4e24e15c0467a071d93fead0990b8f04bb60b359b4056a997598aa56a1/grype@v0.7.0/2023-02-16T20:09:54+00:00/metadata.json new file mode 100644 index 00000000..b70b0dcf --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:55c9ba4e24e15c0467a071d93fead0990b8f04bb60b359b4056a997598aa56a1/grype@v0.7.0/2023-02-16T20:09:54+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b44d112ad55d4dc53692f03a25c3c616da262981d3ded9fce5161dd2df0c77ec +size 626 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:58637f273108e3e9eb4df4d73f7b6b1da303cbbf64f65e65fb7762482f2de63d/grype@v0.12.1/2023-02-16T20:15:46+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:58637f273108e3e9eb4df4d73f7b6b1da303cbbf64f65e65fb7762482f2de63d/grype@v0.12.1/2023-02-16T20:15:46+00:00/data.json new file mode 100644 index 00000000..86c51e5a --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:58637f273108e3e9eb4df4d73f7b6b1da303cbbf64f65e65fb7762482f2de63d/grype@v0.12.1/2023-02-16T20:15:46+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8b2d12d23c565c75a05e0fc712e0f5e865cb487fa95713637f28d7008ca65dde +size 54056 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:58637f273108e3e9eb4df4d73f7b6b1da303cbbf64f65e65fb7762482f2de63d/grype@v0.12.1/2023-02-16T20:15:46+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:58637f273108e3e9eb4df4d73f7b6b1da303cbbf64f65e65fb7762482f2de63d/grype@v0.12.1/2023-02-16T20:15:46+00:00/metadata.json new file mode 100644 index 00000000..e22a8fdf --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:58637f273108e3e9eb4df4d73f7b6b1da303cbbf64f65e65fb7762482f2de63d/grype@v0.12.1/2023-02-16T20:15:46+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:143deb5e72daf3d0a75eb599383e9664b285934d0c9e38fa1bf60c661b3a9e92 +size 880 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:58637f273108e3e9eb4df4d73f7b6b1da303cbbf64f65e65fb7762482f2de63d/grype@v0.40.1/2023-02-16T20:18:50+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:58637f273108e3e9eb4df4d73f7b6b1da303cbbf64f65e65fb7762482f2de63d/grype@v0.40.1/2023-02-16T20:18:50+00:00/data.json new file mode 100644 index 00000000..22a7e67d --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:58637f273108e3e9eb4df4d73f7b6b1da303cbbf64f65e65fb7762482f2de63d/grype@v0.40.1/2023-02-16T20:18:50+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:02b4edd1ee7b270c64ac121da7395e1724a9240e5d0de83fa214500b6c0ad6ff +size 101600 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:58637f273108e3e9eb4df4d73f7b6b1da303cbbf64f65e65fb7762482f2de63d/grype@v0.40.1/2023-02-16T20:18:50+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:58637f273108e3e9eb4df4d73f7b6b1da303cbbf64f65e65fb7762482f2de63d/grype@v0.40.1/2023-02-16T20:18:50+00:00/metadata.json new file mode 100644 index 00000000..91e9cf3a --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:58637f273108e3e9eb4df4d73f7b6b1da303cbbf64f65e65fb7762482f2de63d/grype@v0.40.1/2023-02-16T20:18:50+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fd71f9bca9223055b9ee05594cc200dcceb4b69499676c2824fc3f32263c2ce6 +size 880 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:58637f273108e3e9eb4df4d73f7b6b1da303cbbf64f65e65fb7762482f2de63d/grype@v0.50.2/2023-02-16T20:21:59+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:58637f273108e3e9eb4df4d73f7b6b1da303cbbf64f65e65fb7762482f2de63d/grype@v0.50.2/2023-02-16T20:21:59+00:00/data.json new file mode 100644 index 00000000..8f3f53a2 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:58637f273108e3e9eb4df4d73f7b6b1da303cbbf64f65e65fb7762482f2de63d/grype@v0.50.2/2023-02-16T20:21:59+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:51dd152c7c20c1c128987a0d7d888910e30d91125b10570fb9eeb8d8ff72e9e3 +size 102326 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:58637f273108e3e9eb4df4d73f7b6b1da303cbbf64f65e65fb7762482f2de63d/grype@v0.50.2/2023-02-16T20:21:59+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:58637f273108e3e9eb4df4d73f7b6b1da303cbbf64f65e65fb7762482f2de63d/grype@v0.50.2/2023-02-16T20:21:59+00:00/metadata.json new file mode 100644 index 00000000..ffe0dfb6 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:58637f273108e3e9eb4df4d73f7b6b1da303cbbf64f65e65fb7762482f2de63d/grype@v0.50.2/2023-02-16T20:21:59+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:31bc58c35c95dd8ffa6fc6abb7ed18e734e56a8b27489360e709c1ecbdaba45d +size 880 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:58637f273108e3e9eb4df4d73f7b6b1da303cbbf64f65e65fb7762482f2de63d/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:14+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:58637f273108e3e9eb4df4d73f7b6b1da303cbbf64f65e65fb7762482f2de63d/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:14+00:00/data.json new file mode 100644 index 00000000..3c2a4837 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:58637f273108e3e9eb4df4d73f7b6b1da303cbbf64f65e65fb7762482f2de63d/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:14+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aa040b978ee6ff5d47b8a69214fbe77838846909097491f906d0c9e6d196cd6b +size 105256 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:58637f273108e3e9eb4df4d73f7b6b1da303cbbf64f65e65fb7762482f2de63d/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:14+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:58637f273108e3e9eb4df4d73f7b6b1da303cbbf64f65e65fb7762482f2de63d/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:14+00:00/metadata.json new file mode 100644 index 00000000..4ddb7314 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:58637f273108e3e9eb4df4d73f7b6b1da303cbbf64f65e65fb7762482f2de63d/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:14+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8f153e1f4f09d59e337d0828f9d6a07d47fed118f13d045b2e7936e5f322f64f +size 958 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:58637f273108e3e9eb4df4d73f7b6b1da303cbbf64f65e65fb7762482f2de63d/grype@v0.7.0/2023-02-16T20:10:06+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:58637f273108e3e9eb4df4d73f7b6b1da303cbbf64f65e65fb7762482f2de63d/grype@v0.7.0/2023-02-16T20:10:06+00:00/data.json new file mode 100644 index 00000000..20a727fd --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:58637f273108e3e9eb4df4d73f7b6b1da303cbbf64f65e65fb7762482f2de63d/grype@v0.7.0/2023-02-16T20:10:06+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:873d9e53d191e21a67986b2dfb12f2180f5339cf29c0434c20c87c578f39c2be +size 7997 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:58637f273108e3e9eb4df4d73f7b6b1da303cbbf64f65e65fb7762482f2de63d/grype@v0.7.0/2023-02-16T20:10:06+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:58637f273108e3e9eb4df4d73f7b6b1da303cbbf64f65e65fb7762482f2de63d/grype@v0.7.0/2023-02-16T20:10:06+00:00/metadata.json new file mode 100644 index 00000000..7f9cd2d4 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:58637f273108e3e9eb4df4d73f7b6b1da303cbbf64f65e65fb7762482f2de63d/grype@v0.7.0/2023-02-16T20:10:06+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:828af208d8653318911d24df7e6f12c48073c3962439536d29e2b5f58b1da669 +size 625 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:6749b1509fc4dd3f2b4e8688325fc5d447751bc9ae3be10c0f1fb92ec062b798/grype@v0.12.1/2023-02-16T20:15:41+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:6749b1509fc4dd3f2b4e8688325fc5d447751bc9ae3be10c0f1fb92ec062b798/grype@v0.12.1/2023-02-16T20:15:41+00:00/data.json new file mode 100644 index 00000000..2e84cd6f --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:6749b1509fc4dd3f2b4e8688325fc5d447751bc9ae3be10c0f1fb92ec062b798/grype@v0.12.1/2023-02-16T20:15:41+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9627937ea471b516fcaca7fcc570ee5cbaefe6df274d80d43a33a6f8e7359887 +size 23505 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:6749b1509fc4dd3f2b4e8688325fc5d447751bc9ae3be10c0f1fb92ec062b798/grype@v0.12.1/2023-02-16T20:15:41+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:6749b1509fc4dd3f2b4e8688325fc5d447751bc9ae3be10c0f1fb92ec062b798/grype@v0.12.1/2023-02-16T20:15:41+00:00/metadata.json new file mode 100644 index 00000000..3040ebbb --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:6749b1509fc4dd3f2b4e8688325fc5d447751bc9ae3be10c0f1fb92ec062b798/grype@v0.12.1/2023-02-16T20:15:41+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:04d3aafa2aeab94972fc4f9ab3b8b74365ab14cec8305b90d810fd79a6c17f41 +size 881 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:6749b1509fc4dd3f2b4e8688325fc5d447751bc9ae3be10c0f1fb92ec062b798/grype@v0.40.1/2023-02-16T20:18:44+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:6749b1509fc4dd3f2b4e8688325fc5d447751bc9ae3be10c0f1fb92ec062b798/grype@v0.40.1/2023-02-16T20:18:44+00:00/data.json new file mode 100644 index 00000000..dec40b4d --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:6749b1509fc4dd3f2b4e8688325fc5d447751bc9ae3be10c0f1fb92ec062b798/grype@v0.40.1/2023-02-16T20:18:44+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1e9310c9645453f5a38d7eb5f5d68651dcb4f57b91ed4e29f7effbb0df2bdedb +size 31767 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:6749b1509fc4dd3f2b4e8688325fc5d447751bc9ae3be10c0f1fb92ec062b798/grype@v0.40.1/2023-02-16T20:18:44+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:6749b1509fc4dd3f2b4e8688325fc5d447751bc9ae3be10c0f1fb92ec062b798/grype@v0.40.1/2023-02-16T20:18:44+00:00/metadata.json new file mode 100644 index 00000000..3651a572 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:6749b1509fc4dd3f2b4e8688325fc5d447751bc9ae3be10c0f1fb92ec062b798/grype@v0.40.1/2023-02-16T20:18:44+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3695856e3cf4fa9f7dcede200b040c730be7493a52535f1b04b132902ac9f744 +size 880 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:6749b1509fc4dd3f2b4e8688325fc5d447751bc9ae3be10c0f1fb92ec062b798/grype@v0.50.2/2023-02-16T20:21:54+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:6749b1509fc4dd3f2b4e8688325fc5d447751bc9ae3be10c0f1fb92ec062b798/grype@v0.50.2/2023-02-16T20:21:54+00:00/data.json new file mode 100644 index 00000000..c5cee3f5 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:6749b1509fc4dd3f2b4e8688325fc5d447751bc9ae3be10c0f1fb92ec062b798/grype@v0.50.2/2023-02-16T20:21:54+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:76a8c6916b94760dfec74816bcafd848c594aac5cd1ae36b9529920f5553235b +size 32397 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:6749b1509fc4dd3f2b4e8688325fc5d447751bc9ae3be10c0f1fb92ec062b798/grype@v0.50.2/2023-02-16T20:21:54+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:6749b1509fc4dd3f2b4e8688325fc5d447751bc9ae3be10c0f1fb92ec062b798/grype@v0.50.2/2023-02-16T20:21:54+00:00/metadata.json new file mode 100644 index 00000000..a7b32c41 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:6749b1509fc4dd3f2b4e8688325fc5d447751bc9ae3be10c0f1fb92ec062b798/grype@v0.50.2/2023-02-16T20:21:54+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7d3f12918745eb946ee79befde133f75a2ed0121173f49bcbfc99a83d145f501 +size 881 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:6749b1509fc4dd3f2b4e8688325fc5d447751bc9ae3be10c0f1fb92ec062b798/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:07+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:6749b1509fc4dd3f2b4e8688325fc5d447751bc9ae3be10c0f1fb92ec062b798/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:07+00:00/data.json new file mode 100644 index 00000000..0de970d6 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:6749b1509fc4dd3f2b4e8688325fc5d447751bc9ae3be10c0f1fb92ec062b798/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:07+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:86bec01f533482a2b0a6dceaf6c7f03371f602a0b33e2e641c3522690ee62d17 +size 35034 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:6749b1509fc4dd3f2b4e8688325fc5d447751bc9ae3be10c0f1fb92ec062b798/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:07+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:6749b1509fc4dd3f2b4e8688325fc5d447751bc9ae3be10c0f1fb92ec062b798/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:07+00:00/metadata.json new file mode 100644 index 00000000..ee1531b8 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:6749b1509fc4dd3f2b4e8688325fc5d447751bc9ae3be10c0f1fb92ec062b798/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:07+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6d89d6b8e03b8642ff6adf47dfbd40c5eb4d0048847b4a1bfc4f1c9be5990105 +size 960 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:6749b1509fc4dd3f2b4e8688325fc5d447751bc9ae3be10c0f1fb92ec062b798/grype@v0.7.0/2023-02-16T20:09:57+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:6749b1509fc4dd3f2b4e8688325fc5d447751bc9ae3be10c0f1fb92ec062b798/grype@v0.7.0/2023-02-16T20:09:57+00:00/data.json new file mode 100644 index 00000000..b334cf13 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:6749b1509fc4dd3f2b4e8688325fc5d447751bc9ae3be10c0f1fb92ec062b798/grype@v0.7.0/2023-02-16T20:09:57+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e355d64f62249120ba4d1143393e41ee2ed6f82f0c605ff770ba040d19a04602 +size 14397 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:6749b1509fc4dd3f2b4e8688325fc5d447751bc9ae3be10c0f1fb92ec062b798/grype@v0.7.0/2023-02-16T20:09:57+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:6749b1509fc4dd3f2b4e8688325fc5d447751bc9ae3be10c0f1fb92ec062b798/grype@v0.7.0/2023-02-16T20:09:57+00:00/metadata.json new file mode 100644 index 00000000..072a5504 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:6749b1509fc4dd3f2b4e8688325fc5d447751bc9ae3be10c0f1fb92ec062b798/grype@v0.7.0/2023-02-16T20:09:57+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d65590c1d463ba1dc1c830c713bd27fa0623e2407b243bc687deb27f0616884d +size 626 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:746d31247006cc06434ce91ccf3523b2c230ff6c378ffed7ca1c60bbb48ea86f/grype@v0.12.1/2023-02-16T20:16:08+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:746d31247006cc06434ce91ccf3523b2c230ff6c378ffed7ca1c60bbb48ea86f/grype@v0.12.1/2023-02-16T20:16:08+00:00/data.json new file mode 100644 index 00000000..b80a1d18 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:746d31247006cc06434ce91ccf3523b2c230ff6c378ffed7ca1c60bbb48ea86f/grype@v0.12.1/2023-02-16T20:16:08+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b018eab756949e997574452005b5cc60cddc573b6862c6146a0f1101622396c1 +size 1174749 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:746d31247006cc06434ce91ccf3523b2c230ff6c378ffed7ca1c60bbb48ea86f/grype@v0.12.1/2023-02-16T20:16:08+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:746d31247006cc06434ce91ccf3523b2c230ff6c378ffed7ca1c60bbb48ea86f/grype@v0.12.1/2023-02-16T20:16:08+00:00/metadata.json new file mode 100644 index 00000000..281df9c3 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:746d31247006cc06434ce91ccf3523b2c230ff6c378ffed7ca1c60bbb48ea86f/grype@v0.12.1/2023-02-16T20:16:08+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6e5eb8d142ef922808a37a77a78b9231edca54b05c4ba820413d2814345e2c58 +size 868 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:746d31247006cc06434ce91ccf3523b2c230ff6c378ffed7ca1c60bbb48ea86f/grype@v0.40.1/2023-02-16T20:19:12+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:746d31247006cc06434ce91ccf3523b2c230ff6c378ffed7ca1c60bbb48ea86f/grype@v0.40.1/2023-02-16T20:19:12+00:00/data.json new file mode 100644 index 00000000..f118e6c3 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:746d31247006cc06434ce91ccf3523b2c230ff6c378ffed7ca1c60bbb48ea86f/grype@v0.40.1/2023-02-16T20:19:12+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:866a692cd7f58237615582cd5690b6ddbd9082bd4a0246f38e1006101a76a632 +size 2943447 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:746d31247006cc06434ce91ccf3523b2c230ff6c378ffed7ca1c60bbb48ea86f/grype@v0.40.1/2023-02-16T20:19:12+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:746d31247006cc06434ce91ccf3523b2c230ff6c378ffed7ca1c60bbb48ea86f/grype@v0.40.1/2023-02-16T20:19:12+00:00/metadata.json new file mode 100644 index 00000000..e7d3aeb5 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:746d31247006cc06434ce91ccf3523b2c230ff6c378ffed7ca1c60bbb48ea86f/grype@v0.40.1/2023-02-16T20:19:12+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7f477804796685a6af0c91a5b82110de0164c9a6a6dc42e9e2218b5a48e224f0 +size 868 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:746d31247006cc06434ce91ccf3523b2c230ff6c378ffed7ca1c60bbb48ea86f/grype@v0.50.2/2023-02-16T20:22:20+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:746d31247006cc06434ce91ccf3523b2c230ff6c378ffed7ca1c60bbb48ea86f/grype@v0.50.2/2023-02-16T20:22:20+00:00/data.json new file mode 100644 index 00000000..d908b28e --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:746d31247006cc06434ce91ccf3523b2c230ff6c378ffed7ca1c60bbb48ea86f/grype@v0.50.2/2023-02-16T20:22:20+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:05de8d67e9e99b1d67a8a269c07a06b921f357afdedf7a57bb19b4e10f1d444a +size 2969133 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:746d31247006cc06434ce91ccf3523b2c230ff6c378ffed7ca1c60bbb48ea86f/grype@v0.50.2/2023-02-16T20:22:20+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:746d31247006cc06434ce91ccf3523b2c230ff6c378ffed7ca1c60bbb48ea86f/grype@v0.50.2/2023-02-16T20:22:20+00:00/metadata.json new file mode 100644 index 00000000..6315b301 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:746d31247006cc06434ce91ccf3523b2c230ff6c378ffed7ca1c60bbb48ea86f/grype@v0.50.2/2023-02-16T20:22:20+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:237a086081c1e2373faf4ff5f3a30122d343511b51e5217e7fd8f025786acafd +size 868 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:746d31247006cc06434ce91ccf3523b2c230ff6c378ffed7ca1c60bbb48ea86f/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:42+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:746d31247006cc06434ce91ccf3523b2c230ff6c378ffed7ca1c60bbb48ea86f/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:42+00:00/data.json new file mode 100644 index 00000000..d9262fd9 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:746d31247006cc06434ce91ccf3523b2c230ff6c378ffed7ca1c60bbb48ea86f/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:42+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce470f8fd1cb40437f26404ebb5b87c430065c4d8e710cd3626d28f3e69568a3 +size 3075620 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:746d31247006cc06434ce91ccf3523b2c230ff6c378ffed7ca1c60bbb48ea86f/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:42+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:746d31247006cc06434ce91ccf3523b2c230ff6c378ffed7ca1c60bbb48ea86f/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:42+00:00/metadata.json new file mode 100644 index 00000000..8ec871f4 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:746d31247006cc06434ce91ccf3523b2c230ff6c378ffed7ca1c60bbb48ea86f/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:42+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2c507e86408edfd07832d40933c07e9758187b510e115baacc22500e30248d6c +size 947 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:746d31247006cc06434ce91ccf3523b2c230ff6c378ffed7ca1c60bbb48ea86f/grype@v0.7.0/2023-02-16T20:10:49+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:746d31247006cc06434ce91ccf3523b2c230ff6c378ffed7ca1c60bbb48ea86f/grype@v0.7.0/2023-02-16T20:10:49+00:00/data.json new file mode 100644 index 00000000..68b23080 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:746d31247006cc06434ce91ccf3523b2c230ff6c378ffed7ca1c60bbb48ea86f/grype@v0.7.0/2023-02-16T20:10:49+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d3bab95ae815d56756199397b6b0ea9c7e706b18dbdffbad02d8aec23fead038 +size 1388877 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:746d31247006cc06434ce91ccf3523b2c230ff6c378ffed7ca1c60bbb48ea86f/grype@v0.7.0/2023-02-16T20:10:49+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:746d31247006cc06434ce91ccf3523b2c230ff6c378ffed7ca1c60bbb48ea86f/grype@v0.7.0/2023-02-16T20:10:49+00:00/metadata.json new file mode 100644 index 00000000..57119978 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:746d31247006cc06434ce91ccf3523b2c230ff6c378ffed7ca1c60bbb48ea86f/grype@v0.7.0/2023-02-16T20:10:49+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:26e00d0d7cbd2a67ff2b750bd8bcb340b0bbba0bf29b826c52fe1d7b3b1a0a92 +size 613 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:7790691e5efae8bfe9cf4a4447312318d8daaf05ffd5f265ae913edf660f4653/grype@v0.12.1/2023-02-16T20:15:44+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:7790691e5efae8bfe9cf4a4447312318d8daaf05ffd5f265ae913edf660f4653/grype@v0.12.1/2023-02-16T20:15:44+00:00/data.json new file mode 100644 index 00000000..e58b5e5b --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:7790691e5efae8bfe9cf4a4447312318d8daaf05ffd5f265ae913edf660f4653/grype@v0.12.1/2023-02-16T20:15:44+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ad7aa10ca06caa4c4d6925b6403a4795835c742548686fb83e60e403f5900394 +size 215129 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:7790691e5efae8bfe9cf4a4447312318d8daaf05ffd5f265ae913edf660f4653/grype@v0.12.1/2023-02-16T20:15:44+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:7790691e5efae8bfe9cf4a4447312318d8daaf05ffd5f265ae913edf660f4653/grype@v0.12.1/2023-02-16T20:15:44+00:00/metadata.json new file mode 100644 index 00000000..35cb94a1 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:7790691e5efae8bfe9cf4a4447312318d8daaf05ffd5f265ae913edf660f4653/grype@v0.12.1/2023-02-16T20:15:44+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:59a4656fdc8d57b6bac19fd981f1ba6cf751fd6a62dbc9f8f81f34f1f3227c4a +size 881 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:7790691e5efae8bfe9cf4a4447312318d8daaf05ffd5f265ae913edf660f4653/grype@v0.40.1/2023-02-16T20:18:48+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:7790691e5efae8bfe9cf4a4447312318d8daaf05ffd5f265ae913edf660f4653/grype@v0.40.1/2023-02-16T20:18:48+00:00/data.json new file mode 100644 index 00000000..2be87cc6 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:7790691e5efae8bfe9cf4a4447312318d8daaf05ffd5f265ae913edf660f4653/grype@v0.40.1/2023-02-16T20:18:48+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:be21f9a74296cfac516f2d275475eb234c3f238bba6dac3c46a36d80efa81787 +size 567962 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:7790691e5efae8bfe9cf4a4447312318d8daaf05ffd5f265ae913edf660f4653/grype@v0.40.1/2023-02-16T20:18:48+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:7790691e5efae8bfe9cf4a4447312318d8daaf05ffd5f265ae913edf660f4653/grype@v0.40.1/2023-02-16T20:18:48+00:00/metadata.json new file mode 100644 index 00000000..132383ab --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:7790691e5efae8bfe9cf4a4447312318d8daaf05ffd5f265ae913edf660f4653/grype@v0.40.1/2023-02-16T20:18:48+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:79da9d3357a931efca25182f4a2f6731d25e62b51f699f680fb59ab39b383500 +size 881 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:7790691e5efae8bfe9cf4a4447312318d8daaf05ffd5f265ae913edf660f4653/grype@v0.50.2/2023-02-16T20:21:57+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:7790691e5efae8bfe9cf4a4447312318d8daaf05ffd5f265ae913edf660f4653/grype@v0.50.2/2023-02-16T20:21:57+00:00/data.json new file mode 100644 index 00000000..d711e161 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:7790691e5efae8bfe9cf4a4447312318d8daaf05ffd5f265ae913edf660f4653/grype@v0.50.2/2023-02-16T20:21:57+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e7547aa1d135bf5fde6d185497517e45f3e73f37ce4fea0d309c40e42f44924a +size 571552 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:7790691e5efae8bfe9cf4a4447312318d8daaf05ffd5f265ae913edf660f4653/grype@v0.50.2/2023-02-16T20:21:57+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:7790691e5efae8bfe9cf4a4447312318d8daaf05ffd5f265ae913edf660f4653/grype@v0.50.2/2023-02-16T20:21:57+00:00/metadata.json new file mode 100644 index 00000000..4e51368f --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:7790691e5efae8bfe9cf4a4447312318d8daaf05ffd5f265ae913edf660f4653/grype@v0.50.2/2023-02-16T20:21:57+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:507144fef18cd9dfcdf58622c0fcb60b3e8658721df49981be6983a7ad7b08b7 +size 881 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:7790691e5efae8bfe9cf4a4447312318d8daaf05ffd5f265ae913edf660f4653/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:11+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:7790691e5efae8bfe9cf4a4447312318d8daaf05ffd5f265ae913edf660f4653/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:11+00:00/data.json new file mode 100644 index 00000000..a9b953a1 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:7790691e5efae8bfe9cf4a4447312318d8daaf05ffd5f265ae913edf660f4653/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:11+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e7d4770230b48f4b5b3083462a489ca5738a59f06afc0740f6afa400ee4a076f +size 599921 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:7790691e5efae8bfe9cf4a4447312318d8daaf05ffd5f265ae913edf660f4653/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:11+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:7790691e5efae8bfe9cf4a4447312318d8daaf05ffd5f265ae913edf660f4653/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:11+00:00/metadata.json new file mode 100644 index 00000000..d26f1158 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:7790691e5efae8bfe9cf4a4447312318d8daaf05ffd5f265ae913edf660f4653/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:11+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d50418f0f6566eb6e4c876169a10b6707fe7bdb8f0df02232463dac12eb4123d +size 960 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:7790691e5efae8bfe9cf4a4447312318d8daaf05ffd5f265ae913edf660f4653/grype@v0.7.0/2023-02-16T20:10:03+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:7790691e5efae8bfe9cf4a4447312318d8daaf05ffd5f265ae913edf660f4653/grype@v0.7.0/2023-02-16T20:10:03+00:00/data.json new file mode 100644 index 00000000..2355243d --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:7790691e5efae8bfe9cf4a4447312318d8daaf05ffd5f265ae913edf660f4653/grype@v0.7.0/2023-02-16T20:10:03+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:26f6d2db014b184e39155979e7e2ab82d5ae9dfedccd762f93c063e57f070d19 +size 100176 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:7790691e5efae8bfe9cf4a4447312318d8daaf05ffd5f265ae913edf660f4653/grype@v0.7.0/2023-02-16T20:10:03+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:7790691e5efae8bfe9cf4a4447312318d8daaf05ffd5f265ae913edf660f4653/grype@v0.7.0/2023-02-16T20:10:03+00:00/metadata.json new file mode 100644 index 00000000..720c6813 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:7790691e5efae8bfe9cf4a4447312318d8daaf05ffd5f265ae913edf660f4653/grype@v0.7.0/2023-02-16T20:10:03+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9510194cf673547896ef61b3704fb9fda92d80813c6076ca33309b0508d3487e +size 626 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9/grype@v0.12.1/2023-02-16T20:17:04+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9/grype@v0.12.1/2023-02-16T20:17:04+00:00/data.json new file mode 100644 index 00000000..0c5dd811 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9/grype@v0.12.1/2023-02-16T20:17:04+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:215d4ec797649398f894f85b68edced4dae4f848a01eccc7a30dff6a1986d8c3 +size 2620055 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9/grype@v0.12.1/2023-02-16T20:17:04+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9/grype@v0.12.1/2023-02-16T20:17:04+00:00/metadata.json new file mode 100644 index 00000000..2af7e466 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9/grype@v0.12.1/2023-02-16T20:17:04+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a1f794ea52c1b1cb1db787f896d25bc187f25c176fecf168d75869a551bbc972 +size 880 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9/grype@v0.40.1/2023-02-16T20:20:12+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9/grype@v0.40.1/2023-02-16T20:20:12+00:00/data.json new file mode 100644 index 00000000..65d29016 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9/grype@v0.40.1/2023-02-16T20:20:12+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:049c2f86369c0af580660a86ca918a22b715c3cf6d3d2bc7a29eeae00d65beaf +size 5152406 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9/grype@v0.40.1/2023-02-16T20:20:12+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9/grype@v0.40.1/2023-02-16T20:20:12+00:00/metadata.json new file mode 100644 index 00000000..0873f7fd --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9/grype@v0.40.1/2023-02-16T20:20:12+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:672243e6234bc539a707d53ecc5b3ae458aa685f148e6197c6347916699ff1e0 +size 880 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9/grype@v0.50.2/2023-02-16T20:23:24+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9/grype@v0.50.2/2023-02-16T20:23:24+00:00/data.json new file mode 100644 index 00000000..005840cd --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9/grype@v0.50.2/2023-02-16T20:23:24+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:804fb14fdeddd2028aebf160d16a312e899d6850175815f8c4de665758f4651e +size 5205547 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9/grype@v0.50.2/2023-02-16T20:23:24+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9/grype@v0.50.2/2023-02-16T20:23:24+00:00/metadata.json new file mode 100644 index 00000000..467c8dd4 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9/grype@v0.50.2/2023-02-16T20:23:24+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d60ea9f67019c54d8a5f9fe9245ad5d67f597f2392b3673ea6e198fddffba587 +size 880 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:27:01+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:27:01+00:00/data.json new file mode 100644 index 00000000..6f2f2daa --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:27:01+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9616678b586d558ec823fbd85d35c111099839d455bc29462940e0d076c7f407 +size 4086140 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:27:01+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:27:01+00:00/metadata.json new file mode 100644 index 00000000..532ca918 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:27:01+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:38df5d4e146dfc724d1a80c5c20e82d4a399104d9a139cdbec0e19154c87025f +size 959 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9/grype@v0.7.0/2023-02-16T20:12:41+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9/grype@v0.7.0/2023-02-16T20:12:41+00:00/data.json new file mode 100644 index 00000000..97328a53 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9/grype@v0.7.0/2023-02-16T20:12:41+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5c919042729e5158dff45160790faab8f043772192c26484dd3e3881d48b7c41 +size 2887698 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9/grype@v0.7.0/2023-02-16T20:12:41+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9/grype@v0.7.0/2023-02-16T20:12:41+00:00/metadata.json new file mode 100644 index 00000000..ffdaad4e --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:808f6cf3cf4473eb39ff9bb47ead639d2ed71255b75b9b140162b58c6102bcc9/grype@v0.7.0/2023-02-16T20:12:41+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:86c021f255d9a006e48d9f0793961f8a38b675018e0b57e53d042fe1fbfbc54c +size 625 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:835483c1a36f6cf50bbf84dcef135b4640ea7d8eb9cf15b9edc4f1734f8335d4/grype@v0.12.1/2023-02-16T20:16:53+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:835483c1a36f6cf50bbf84dcef135b4640ea7d8eb9cf15b9edc4f1734f8335d4/grype@v0.12.1/2023-02-16T20:16:53+00:00/data.json new file mode 100644 index 00000000..e4f3afd6 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:835483c1a36f6cf50bbf84dcef135b4640ea7d8eb9cf15b9edc4f1734f8335d4/grype@v0.12.1/2023-02-16T20:16:53+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:69012daa12ea37301d8300abf4e8c5476eb1dba9e3379efae40b90f288ca8c42 +size 348413 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:835483c1a36f6cf50bbf84dcef135b4640ea7d8eb9cf15b9edc4f1734f8335d4/grype@v0.12.1/2023-02-16T20:16:53+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:835483c1a36f6cf50bbf84dcef135b4640ea7d8eb9cf15b9edc4f1734f8335d4/grype@v0.12.1/2023-02-16T20:16:53+00:00/metadata.json new file mode 100644 index 00000000..9822b3ad --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:835483c1a36f6cf50bbf84dcef135b4640ea7d8eb9cf15b9edc4f1734f8335d4/grype@v0.12.1/2023-02-16T20:16:53+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d6f2e265a2f84326c948310ff9f8d185ef5ee5af9c408fd1d8244d0cf5a9106b +size 882 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:835483c1a36f6cf50bbf84dcef135b4640ea7d8eb9cf15b9edc4f1734f8335d4/grype@v0.40.1/2023-02-16T20:20:00+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:835483c1a36f6cf50bbf84dcef135b4640ea7d8eb9cf15b9edc4f1734f8335d4/grype@v0.40.1/2023-02-16T20:20:00+00:00/data.json new file mode 100644 index 00000000..63fe0b5e --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:835483c1a36f6cf50bbf84dcef135b4640ea7d8eb9cf15b9edc4f1734f8335d4/grype@v0.40.1/2023-02-16T20:20:00+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b8443e21ee8ff5ae5dcca03bddccb709fabb22096e0d91a2f8890029e431d984 +size 742947 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:835483c1a36f6cf50bbf84dcef135b4640ea7d8eb9cf15b9edc4f1734f8335d4/grype@v0.40.1/2023-02-16T20:20:00+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:835483c1a36f6cf50bbf84dcef135b4640ea7d8eb9cf15b9edc4f1734f8335d4/grype@v0.40.1/2023-02-16T20:20:00+00:00/metadata.json new file mode 100644 index 00000000..f8dab2c0 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:835483c1a36f6cf50bbf84dcef135b4640ea7d8eb9cf15b9edc4f1734f8335d4/grype@v0.40.1/2023-02-16T20:20:00+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:281fddcf8bf3e5dee28c242537d6acf9ad3aae5d532459ff866b20b3e23e63c8 +size 882 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:835483c1a36f6cf50bbf84dcef135b4640ea7d8eb9cf15b9edc4f1734f8335d4/grype@v0.50.2/2023-02-16T20:23:10+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:835483c1a36f6cf50bbf84dcef135b4640ea7d8eb9cf15b9edc4f1734f8335d4/grype@v0.50.2/2023-02-16T20:23:10+00:00/data.json new file mode 100644 index 00000000..73c3d054 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:835483c1a36f6cf50bbf84dcef135b4640ea7d8eb9cf15b9edc4f1734f8335d4/grype@v0.50.2/2023-02-16T20:23:10+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5bdee52ab90a9faf79135f7d56eea01fce8ad4593ddad0d4ccb96c4b689bed34 +size 727436 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:835483c1a36f6cf50bbf84dcef135b4640ea7d8eb9cf15b9edc4f1734f8335d4/grype@v0.50.2/2023-02-16T20:23:10+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:835483c1a36f6cf50bbf84dcef135b4640ea7d8eb9cf15b9edc4f1734f8335d4/grype@v0.50.2/2023-02-16T20:23:10+00:00/metadata.json new file mode 100644 index 00000000..ee2b308f --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:835483c1a36f6cf50bbf84dcef135b4640ea7d8eb9cf15b9edc4f1734f8335d4/grype@v0.50.2/2023-02-16T20:23:10+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:453088bf88a5161649a521ec2b6c37063f2d7b0d4f71af7e783fa812dd226750 +size 882 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:835483c1a36f6cf50bbf84dcef135b4640ea7d8eb9cf15b9edc4f1734f8335d4/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:26:44+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:835483c1a36f6cf50bbf84dcef135b4640ea7d8eb9cf15b9edc4f1734f8335d4/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:26:44+00:00/data.json new file mode 100644 index 00000000..663dd9f8 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:835483c1a36f6cf50bbf84dcef135b4640ea7d8eb9cf15b9edc4f1734f8335d4/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:26:44+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:145950040ddc608608e543c2073aff74e1342c35557781f4507e3c211306e1d5 +size 734815 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:835483c1a36f6cf50bbf84dcef135b4640ea7d8eb9cf15b9edc4f1734f8335d4/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:26:44+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:835483c1a36f6cf50bbf84dcef135b4640ea7d8eb9cf15b9edc4f1734f8335d4/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:26:44+00:00/metadata.json new file mode 100644 index 00000000..aec07555 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:835483c1a36f6cf50bbf84dcef135b4640ea7d8eb9cf15b9edc4f1734f8335d4/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:26:44+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ec764320914dcdf337d03e9130d84cbab964b34c414db7039f4f2df6535c3d35 +size 960 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:835483c1a36f6cf50bbf84dcef135b4640ea7d8eb9cf15b9edc4f1734f8335d4/grype@v0.7.0/2023-02-16T20:12:17+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:835483c1a36f6cf50bbf84dcef135b4640ea7d8eb9cf15b9edc4f1734f8335d4/grype@v0.7.0/2023-02-16T20:12:17+00:00/data.json new file mode 100644 index 00000000..50fdb69a --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:835483c1a36f6cf50bbf84dcef135b4640ea7d8eb9cf15b9edc4f1734f8335d4/grype@v0.7.0/2023-02-16T20:12:17+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:943eabd80f4ea748fb17a5091475d72bbc029245ab3fa278380ab5ae4159144f +size 422267 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:835483c1a36f6cf50bbf84dcef135b4640ea7d8eb9cf15b9edc4f1734f8335d4/grype@v0.7.0/2023-02-16T20:12:17+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:835483c1a36f6cf50bbf84dcef135b4640ea7d8eb9cf15b9edc4f1734f8335d4/grype@v0.7.0/2023-02-16T20:12:17+00:00/metadata.json new file mode 100644 index 00000000..628d6801 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:835483c1a36f6cf50bbf84dcef135b4640ea7d8eb9cf15b9edc4f1734f8335d4/grype@v0.7.0/2023-02-16T20:12:17+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3a1f09cc58a77993fd778653643e520b5130717942dc5c70fc9d615ead1e1ece +size 627 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:88e3684e2284fd61531cafd61a5fe3ce1258bcad2b7d4038bc0116abe59cb358/grype@v0.12.1/2023-02-16T20:16:19+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:88e3684e2284fd61531cafd61a5fe3ce1258bcad2b7d4038bc0116abe59cb358/grype@v0.12.1/2023-02-16T20:16:19+00:00/data.json new file mode 100644 index 00000000..a6972b68 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:88e3684e2284fd61531cafd61a5fe3ce1258bcad2b7d4038bc0116abe59cb358/grype@v0.12.1/2023-02-16T20:16:19+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6375e476dd6095b2d896310515e308aa16a148a105ebb95d1f956d5d58300e29 +size 712861 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:88e3684e2284fd61531cafd61a5fe3ce1258bcad2b7d4038bc0116abe59cb358/grype@v0.12.1/2023-02-16T20:16:19+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:88e3684e2284fd61531cafd61a5fe3ce1258bcad2b7d4038bc0116abe59cb358/grype@v0.12.1/2023-02-16T20:16:19+00:00/metadata.json new file mode 100644 index 00000000..1439463c --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:88e3684e2284fd61531cafd61a5fe3ce1258bcad2b7d4038bc0116abe59cb358/grype@v0.12.1/2023-02-16T20:16:19+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:42776ad746c35863c3e83a70aa2e85e8524c7f824a909af1183f20cb2a2200dd +size 868 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:88e3684e2284fd61531cafd61a5fe3ce1258bcad2b7d4038bc0116abe59cb358/grype@v0.40.1/2023-02-16T20:19:23+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:88e3684e2284fd61531cafd61a5fe3ce1258bcad2b7d4038bc0116abe59cb358/grype@v0.40.1/2023-02-16T20:19:23+00:00/data.json new file mode 100644 index 00000000..8029d085 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:88e3684e2284fd61531cafd61a5fe3ce1258bcad2b7d4038bc0116abe59cb358/grype@v0.40.1/2023-02-16T20:19:23+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:71896a34925d23c6bfb487d27e853261000cd5770b9908c0195bc03cd570b4a6 +size 1475826 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:88e3684e2284fd61531cafd61a5fe3ce1258bcad2b7d4038bc0116abe59cb358/grype@v0.40.1/2023-02-16T20:19:23+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:88e3684e2284fd61531cafd61a5fe3ce1258bcad2b7d4038bc0116abe59cb358/grype@v0.40.1/2023-02-16T20:19:23+00:00/metadata.json new file mode 100644 index 00000000..fa9f51ab --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:88e3684e2284fd61531cafd61a5fe3ce1258bcad2b7d4038bc0116abe59cb358/grype@v0.40.1/2023-02-16T20:19:23+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:50ff9388689cbfb025ac0814b057f79b53b0adc0f187cc02569b7aa0306672fd +size 868 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:88e3684e2284fd61531cafd61a5fe3ce1258bcad2b7d4038bc0116abe59cb358/grype@v0.50.2/2023-02-16T20:22:31+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:88e3684e2284fd61531cafd61a5fe3ce1258bcad2b7d4038bc0116abe59cb358/grype@v0.50.2/2023-02-16T20:22:31+00:00/data.json new file mode 100644 index 00000000..442c7f15 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:88e3684e2284fd61531cafd61a5fe3ce1258bcad2b7d4038bc0116abe59cb358/grype@v0.50.2/2023-02-16T20:22:31+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0d7ee7d573f6170a4685bcedaab1d35c89f8d390f34b2ef8dd3df5f17acff152 +size 1476303 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:88e3684e2284fd61531cafd61a5fe3ce1258bcad2b7d4038bc0116abe59cb358/grype@v0.50.2/2023-02-16T20:22:31+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:88e3684e2284fd61531cafd61a5fe3ce1258bcad2b7d4038bc0116abe59cb358/grype@v0.50.2/2023-02-16T20:22:31+00:00/metadata.json new file mode 100644 index 00000000..ce2a4941 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:88e3684e2284fd61531cafd61a5fe3ce1258bcad2b7d4038bc0116abe59cb358/grype@v0.50.2/2023-02-16T20:22:31+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:07e5a298f6c6784cd372766814a618cf5f4135c8746e1411b3af0222ede2da4e +size 867 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:88e3684e2284fd61531cafd61a5fe3ce1258bcad2b7d4038bc0116abe59cb358/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:54+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:88e3684e2284fd61531cafd61a5fe3ce1258bcad2b7d4038bc0116abe59cb358/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:54+00:00/data.json new file mode 100644 index 00000000..734a46be --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:88e3684e2284fd61531cafd61a5fe3ce1258bcad2b7d4038bc0116abe59cb358/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:54+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9f43b6cac9635f0d67753647b26b2f5b7e7112b5692a26ad4dd3872e1a3275b7 +size 1490731 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:88e3684e2284fd61531cafd61a5fe3ce1258bcad2b7d4038bc0116abe59cb358/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:54+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:88e3684e2284fd61531cafd61a5fe3ce1258bcad2b7d4038bc0116abe59cb358/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:54+00:00/metadata.json new file mode 100644 index 00000000..ac1d1d58 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:88e3684e2284fd61531cafd61a5fe3ce1258bcad2b7d4038bc0116abe59cb358/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:54+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:96fea43e3bb74027eb008526397080ae91698393e6c98e16cb35d761b3e251cc +size 947 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:88e3684e2284fd61531cafd61a5fe3ce1258bcad2b7d4038bc0116abe59cb358/grype@v0.7.0/2023-02-16T20:11:04+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:88e3684e2284fd61531cafd61a5fe3ce1258bcad2b7d4038bc0116abe59cb358/grype@v0.7.0/2023-02-16T20:11:04+00:00/data.json new file mode 100644 index 00000000..daf21044 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:88e3684e2284fd61531cafd61a5fe3ce1258bcad2b7d4038bc0116abe59cb358/grype@v0.7.0/2023-02-16T20:11:04+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:949537d2d9176c3ba7b5d9f4d5a4c1337b72320ad5e3f96eadbd716059e3db80 +size 806064 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:88e3684e2284fd61531cafd61a5fe3ce1258bcad2b7d4038bc0116abe59cb358/grype@v0.7.0/2023-02-16T20:11:04+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:88e3684e2284fd61531cafd61a5fe3ce1258bcad2b7d4038bc0116abe59cb358/grype@v0.7.0/2023-02-16T20:11:04+00:00/metadata.json new file mode 100644 index 00000000..db12b153 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:88e3684e2284fd61531cafd61a5fe3ce1258bcad2b7d4038bc0116abe59cb358/grype@v0.7.0/2023-02-16T20:11:04+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7c58beda698972ca7584a85cd82da55f85ed1b51a0ffd6a01e7ba21435b006ce +size 613 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:92f1981518e92bf3712ff95cf342f7f4d5fc83fb93a30a36d7d1204e64342199/grype@v0.12.1/2023-02-16T20:16:58+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:92f1981518e92bf3712ff95cf342f7f4d5fc83fb93a30a36d7d1204e64342199/grype@v0.12.1/2023-02-16T20:16:58+00:00/data.json new file mode 100644 index 00000000..c3666ee3 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:92f1981518e92bf3712ff95cf342f7f4d5fc83fb93a30a36d7d1204e64342199/grype@v0.12.1/2023-02-16T20:16:58+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fa25028982ede08ebba3f69fafbe2738f782c11d51290084da974aaa4eb4f4f5 +size 53853 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:92f1981518e92bf3712ff95cf342f7f4d5fc83fb93a30a36d7d1204e64342199/grype@v0.12.1/2023-02-16T20:16:58+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:92f1981518e92bf3712ff95cf342f7f4d5fc83fb93a30a36d7d1204e64342199/grype@v0.12.1/2023-02-16T20:16:58+00:00/metadata.json new file mode 100644 index 00000000..5ca117e5 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:92f1981518e92bf3712ff95cf342f7f4d5fc83fb93a30a36d7d1204e64342199/grype@v0.12.1/2023-02-16T20:16:58+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:75640ed401936cd61bb76f1cfa2d28cac19d432f646adf960b8f167002f5d2d9 +size 896 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:92f1981518e92bf3712ff95cf342f7f4d5fc83fb93a30a36d7d1204e64342199/grype@v0.40.1/2023-02-16T20:20:06+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:92f1981518e92bf3712ff95cf342f7f4d5fc83fb93a30a36d7d1204e64342199/grype@v0.40.1/2023-02-16T20:20:06+00:00/data.json new file mode 100644 index 00000000..fce619b9 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:92f1981518e92bf3712ff95cf342f7f4d5fc83fb93a30a36d7d1204e64342199/grype@v0.40.1/2023-02-16T20:20:06+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d1d95f7c3abbc927604b3437de2fc67e4fcd9b676a0ca8927391ff4398e708c5 +size 51974 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:92f1981518e92bf3712ff95cf342f7f4d5fc83fb93a30a36d7d1204e64342199/grype@v0.40.1/2023-02-16T20:20:06+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:92f1981518e92bf3712ff95cf342f7f4d5fc83fb93a30a36d7d1204e64342199/grype@v0.40.1/2023-02-16T20:20:06+00:00/metadata.json new file mode 100644 index 00000000..163d11f5 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:92f1981518e92bf3712ff95cf342f7f4d5fc83fb93a30a36d7d1204e64342199/grype@v0.40.1/2023-02-16T20:20:06+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:25938969775bda45e296825f2a9f6441f962c269627bff74d6f75990c07a1c9b +size 896 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:92f1981518e92bf3712ff95cf342f7f4d5fc83fb93a30a36d7d1204e64342199/grype@v0.50.2/2023-02-16T20:23:17+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:92f1981518e92bf3712ff95cf342f7f4d5fc83fb93a30a36d7d1204e64342199/grype@v0.50.2/2023-02-16T20:23:17+00:00/data.json new file mode 100644 index 00000000..47ca190a --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:92f1981518e92bf3712ff95cf342f7f4d5fc83fb93a30a36d7d1204e64342199/grype@v0.50.2/2023-02-16T20:23:17+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:552d417f9f74064a7d16b46734e3997d60b0bb49720c5ffc53593bd669e7561f +size 84540 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:92f1981518e92bf3712ff95cf342f7f4d5fc83fb93a30a36d7d1204e64342199/grype@v0.50.2/2023-02-16T20:23:17+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:92f1981518e92bf3712ff95cf342f7f4d5fc83fb93a30a36d7d1204e64342199/grype@v0.50.2/2023-02-16T20:23:17+00:00/metadata.json new file mode 100644 index 00000000..60404ce0 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:92f1981518e92bf3712ff95cf342f7f4d5fc83fb93a30a36d7d1204e64342199/grype@v0.50.2/2023-02-16T20:23:17+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b2d0f6089f13f53cdfaf381465a1f248984fef3743732d777cd01265ec74347 +size 896 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:92f1981518e92bf3712ff95cf342f7f4d5fc83fb93a30a36d7d1204e64342199/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:26:52+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:92f1981518e92bf3712ff95cf342f7f4d5fc83fb93a30a36d7d1204e64342199/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:26:52+00:00/data.json new file mode 100644 index 00000000..df636f01 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:92f1981518e92bf3712ff95cf342f7f4d5fc83fb93a30a36d7d1204e64342199/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:26:52+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:581a0df86c60e27f67730f6c76c2daedb818f20d9384866fdc2e9fd588089fa6 +size 85426 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:92f1981518e92bf3712ff95cf342f7f4d5fc83fb93a30a36d7d1204e64342199/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:26:52+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:92f1981518e92bf3712ff95cf342f7f4d5fc83fb93a30a36d7d1204e64342199/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:26:52+00:00/metadata.json new file mode 100644 index 00000000..0176016c --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:92f1981518e92bf3712ff95cf342f7f4d5fc83fb93a30a36d7d1204e64342199/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:26:52+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c9d36c26a7722c38bead3acbca0f5ded3240b7a124a28fd59b09721ebab68c25 +size 975 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:92f1981518e92bf3712ff95cf342f7f4d5fc83fb93a30a36d7d1204e64342199/grype@v0.7.0/2023-02-16T20:12:27+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:92f1981518e92bf3712ff95cf342f7f4d5fc83fb93a30a36d7d1204e64342199/grype@v0.7.0/2023-02-16T20:12:27+00:00/data.json new file mode 100644 index 00000000..0b5c957b --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:92f1981518e92bf3712ff95cf342f7f4d5fc83fb93a30a36d7d1204e64342199/grype@v0.7.0/2023-02-16T20:12:27+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e097ac30ad7d920cb0b01d53fbed9ae87930fa69bf0d05c6ebc387c86083017f +size 41936 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:92f1981518e92bf3712ff95cf342f7f4d5fc83fb93a30a36d7d1204e64342199/grype@v0.7.0/2023-02-16T20:12:27+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:92f1981518e92bf3712ff95cf342f7f4d5fc83fb93a30a36d7d1204e64342199/grype@v0.7.0/2023-02-16T20:12:27+00:00/metadata.json new file mode 100644 index 00000000..d11c6321 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:92f1981518e92bf3712ff95cf342f7f4d5fc83fb93a30a36d7d1204e64342199/grype@v0.7.0/2023-02-16T20:12:27+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d934403c0f4c3f4e163fe8999c7c93d57dd35d7af97c1ef0b056d1048233661a +size 641 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:a287a0ff98ac343aa710f4f4258d7198e240e9d416d5c7274663564202f832fb/grype@v0.12.1/2023-02-16T20:15:47+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:a287a0ff98ac343aa710f4f4258d7198e240e9d416d5c7274663564202f832fb/grype@v0.12.1/2023-02-16T20:15:47+00:00/data.json new file mode 100644 index 00000000..e934884d --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:a287a0ff98ac343aa710f4f4258d7198e240e9d416d5c7274663564202f832fb/grype@v0.12.1/2023-02-16T20:15:47+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9a4f9291d34e1b5679239887b544a1622c057a6c56c2fef3272e4bb988bdf1bb +size 140138 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:a287a0ff98ac343aa710f4f4258d7198e240e9d416d5c7274663564202f832fb/grype@v0.12.1/2023-02-16T20:15:47+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:a287a0ff98ac343aa710f4f4258d7198e240e9d416d5c7274663564202f832fb/grype@v0.12.1/2023-02-16T20:15:47+00:00/metadata.json new file mode 100644 index 00000000..1c3dc058 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:a287a0ff98ac343aa710f4f4258d7198e240e9d416d5c7274663564202f832fb/grype@v0.12.1/2023-02-16T20:15:47+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8586326d9dbd45d93646c365556883ec7d41a08bca865aeffcfd5cd7e2cf025c +size 880 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:a287a0ff98ac343aa710f4f4258d7198e240e9d416d5c7274663564202f832fb/grype@v0.40.1/2023-02-16T20:18:51+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:a287a0ff98ac343aa710f4f4258d7198e240e9d416d5c7274663564202f832fb/grype@v0.40.1/2023-02-16T20:18:51+00:00/data.json new file mode 100644 index 00000000..c4a1f4e0 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:a287a0ff98ac343aa710f4f4258d7198e240e9d416d5c7274663564202f832fb/grype@v0.40.1/2023-02-16T20:18:51+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eeefc5db7fceb49e77b221b55e5b0c12100c091f0f56d1cb2ed56afb1a2ccc02 +size 141730 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:a287a0ff98ac343aa710f4f4258d7198e240e9d416d5c7274663564202f832fb/grype@v0.40.1/2023-02-16T20:18:51+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:a287a0ff98ac343aa710f4f4258d7198e240e9d416d5c7274663564202f832fb/grype@v0.40.1/2023-02-16T20:18:51+00:00/metadata.json new file mode 100644 index 00000000..bbeec2a5 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:a287a0ff98ac343aa710f4f4258d7198e240e9d416d5c7274663564202f832fb/grype@v0.40.1/2023-02-16T20:18:51+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b5061e8d035da9a8644e64f950a447d4418b6c0fb6c89f63f5bf79cd6380368c +size 880 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:a287a0ff98ac343aa710f4f4258d7198e240e9d416d5c7274663564202f832fb/grype@v0.50.2/2023-02-16T20:22:00+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:a287a0ff98ac343aa710f4f4258d7198e240e9d416d5c7274663564202f832fb/grype@v0.50.2/2023-02-16T20:22:00+00:00/data.json new file mode 100644 index 00000000..1f0b2ab8 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:a287a0ff98ac343aa710f4f4258d7198e240e9d416d5c7274663564202f832fb/grype@v0.50.2/2023-02-16T20:22:00+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:33c065ca2260e8d3db4e02bf5a30df5562090e29f5e5f4a97f40c15193214691 +size 142816 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:a287a0ff98ac343aa710f4f4258d7198e240e9d416d5c7274663564202f832fb/grype@v0.50.2/2023-02-16T20:22:00+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:a287a0ff98ac343aa710f4f4258d7198e240e9d416d5c7274663564202f832fb/grype@v0.50.2/2023-02-16T20:22:00+00:00/metadata.json new file mode 100644 index 00000000..6d97e92a --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:a287a0ff98ac343aa710f4f4258d7198e240e9d416d5c7274663564202f832fb/grype@v0.50.2/2023-02-16T20:22:00+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3dbbeeb9eed89a6b6f14261aa1516a66fcd93c6ae0f23f29f3d1b68d43ab10dc +size 880 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:a287a0ff98ac343aa710f4f4258d7198e240e9d416d5c7274663564202f832fb/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:16+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:a287a0ff98ac343aa710f4f4258d7198e240e9d416d5c7274663564202f832fb/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:16+00:00/data.json new file mode 100644 index 00000000..fcec69e8 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:a287a0ff98ac343aa710f4f4258d7198e240e9d416d5c7274663564202f832fb/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:16+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f2a5a50a6febc9e56f48f78d2b81555d763117d05a1fd1abe59e1296b9a06d05 +size 150939 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:a287a0ff98ac343aa710f4f4258d7198e240e9d416d5c7274663564202f832fb/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:16+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:a287a0ff98ac343aa710f4f4258d7198e240e9d416d5c7274663564202f832fb/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:16+00:00/metadata.json new file mode 100644 index 00000000..cfd01cb6 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:a287a0ff98ac343aa710f4f4258d7198e240e9d416d5c7274663564202f832fb/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:16+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:77e276531fa513b241d8f5512fcf5973b6c4151c1fff5030e1e54654bedcf64d +size 959 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:a287a0ff98ac343aa710f4f4258d7198e240e9d416d5c7274663564202f832fb/grype@v0.7.0/2023-02-16T20:10:08+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:a287a0ff98ac343aa710f4f4258d7198e240e9d416d5c7274663564202f832fb/grype@v0.7.0/2023-02-16T20:10:08+00:00/data.json new file mode 100644 index 00000000..6c1b126d --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:a287a0ff98ac343aa710f4f4258d7198e240e9d416d5c7274663564202f832fb/grype@v0.7.0/2023-02-16T20:10:08+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b922c6175816a510a647d70e587f5b68ee5ab8b1c3f0512bec6187872708622d +size 27594 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:a287a0ff98ac343aa710f4f4258d7198e240e9d416d5c7274663564202f832fb/grype@v0.7.0/2023-02-16T20:10:08+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:a287a0ff98ac343aa710f4f4258d7198e240e9d416d5c7274663564202f832fb/grype@v0.7.0/2023-02-16T20:10:08+00:00/metadata.json new file mode 100644 index 00000000..03c6f8f7 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:a287a0ff98ac343aa710f4f4258d7198e240e9d416d5c7274663564202f832fb/grype@v0.7.0/2023-02-16T20:10:08+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f44a88a7343ff947359d912f1998d270eb7ccf2f07a2db5d0757aea9da8ce9a1 +size 625 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:c8d664b0e728d52f57eeb98ed1899c16d3b265f02ddfb41303d7a16c31e0b0f1/grype@v0.12.1/2023-02-16T20:17:30+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:c8d664b0e728d52f57eeb98ed1899c16d3b265f02ddfb41303d7a16c31e0b0f1/grype@v0.12.1/2023-02-16T20:17:30+00:00/data.json new file mode 100644 index 00000000..f0b813f7 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:c8d664b0e728d52f57eeb98ed1899c16d3b265f02ddfb41303d7a16c31e0b0f1/grype@v0.12.1/2023-02-16T20:17:30+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:06f33025e2738ab1e6f545273f9a83d884d1555489b53d922f532a2a005709b2 +size 947184 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:c8d664b0e728d52f57eeb98ed1899c16d3b265f02ddfb41303d7a16c31e0b0f1/grype@v0.12.1/2023-02-16T20:17:30+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:c8d664b0e728d52f57eeb98ed1899c16d3b265f02ddfb41303d7a16c31e0b0f1/grype@v0.12.1/2023-02-16T20:17:30+00:00/metadata.json new file mode 100644 index 00000000..9a027302 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:c8d664b0e728d52f57eeb98ed1899c16d3b265f02ddfb41303d7a16c31e0b0f1/grype@v0.12.1/2023-02-16T20:17:30+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:35771adbc96dd8776d0ac2f0ada4c67494f7398b252056536782bd21a4fd62ed +size 878 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:c8d664b0e728d52f57eeb98ed1899c16d3b265f02ddfb41303d7a16c31e0b0f1/grype@v0.40.1/2023-02-16T20:20:44+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:c8d664b0e728d52f57eeb98ed1899c16d3b265f02ddfb41303d7a16c31e0b0f1/grype@v0.40.1/2023-02-16T20:20:44+00:00/data.json new file mode 100644 index 00000000..f00afc86 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:c8d664b0e728d52f57eeb98ed1899c16d3b265f02ddfb41303d7a16c31e0b0f1/grype@v0.40.1/2023-02-16T20:20:44+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4f79e205dfd2f88bccc0dffdb82bf78769bdb020f682336c5b3c8c1b84050941 +size 1600659 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:c8d664b0e728d52f57eeb98ed1899c16d3b265f02ddfb41303d7a16c31e0b0f1/grype@v0.40.1/2023-02-16T20:20:44+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:c8d664b0e728d52f57eeb98ed1899c16d3b265f02ddfb41303d7a16c31e0b0f1/grype@v0.40.1/2023-02-16T20:20:44+00:00/metadata.json new file mode 100644 index 00000000..e2532b1c --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:c8d664b0e728d52f57eeb98ed1899c16d3b265f02ddfb41303d7a16c31e0b0f1/grype@v0.40.1/2023-02-16T20:20:44+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:34fc19491bcdffc30351eb8ec70e9b822e50424e0889852b85e2476b7687f383 +size 878 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:c8d664b0e728d52f57eeb98ed1899c16d3b265f02ddfb41303d7a16c31e0b0f1/grype@v0.50.2/2023-02-16T20:23:56+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:c8d664b0e728d52f57eeb98ed1899c16d3b265f02ddfb41303d7a16c31e0b0f1/grype@v0.50.2/2023-02-16T20:23:56+00:00/data.json new file mode 100644 index 00000000..9e02f4e9 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:c8d664b0e728d52f57eeb98ed1899c16d3b265f02ddfb41303d7a16c31e0b0f1/grype@v0.50.2/2023-02-16T20:23:56+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8f7347bbe06e6f7cf84dc96a875864eafd6eeba3d08b5124016f6e0e3a82830a +size 1615144 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:c8d664b0e728d52f57eeb98ed1899c16d3b265f02ddfb41303d7a16c31e0b0f1/grype@v0.50.2/2023-02-16T20:23:56+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:c8d664b0e728d52f57eeb98ed1899c16d3b265f02ddfb41303d7a16c31e0b0f1/grype@v0.50.2/2023-02-16T20:23:56+00:00/metadata.json new file mode 100644 index 00000000..3b084d55 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:c8d664b0e728d52f57eeb98ed1899c16d3b265f02ddfb41303d7a16c31e0b0f1/grype@v0.50.2/2023-02-16T20:23:56+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9b7541d03898267173761e7593503b631bfdf4077456edaf7b022460da481ba1 +size 877 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:c8d664b0e728d52f57eeb98ed1899c16d3b265f02ddfb41303d7a16c31e0b0f1/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:27:41+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:c8d664b0e728d52f57eeb98ed1899c16d3b265f02ddfb41303d7a16c31e0b0f1/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:27:41+00:00/data.json new file mode 100644 index 00000000..182e4434 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:c8d664b0e728d52f57eeb98ed1899c16d3b265f02ddfb41303d7a16c31e0b0f1/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:27:41+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cbd29b8e54d5eb8da713d8f2b5c412d5ef2d7c422bd2405cf7556ecd650a566a +size 1268084 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:c8d664b0e728d52f57eeb98ed1899c16d3b265f02ddfb41303d7a16c31e0b0f1/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:27:41+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:c8d664b0e728d52f57eeb98ed1899c16d3b265f02ddfb41303d7a16c31e0b0f1/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:27:41+00:00/metadata.json new file mode 100644 index 00000000..ec0882ea --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:c8d664b0e728d52f57eeb98ed1899c16d3b265f02ddfb41303d7a16c31e0b0f1/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:27:41+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8866501e85a6869a71c8bd2233d7615efad3ef86ec16ab0fb1d836228889819c +size 957 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:c8d664b0e728d52f57eeb98ed1899c16d3b265f02ddfb41303d7a16c31e0b0f1/grype@v0.7.0/2023-02-16T20:13:50+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:c8d664b0e728d52f57eeb98ed1899c16d3b265f02ddfb41303d7a16c31e0b0f1/grype@v0.7.0/2023-02-16T20:13:50+00:00/data.json new file mode 100644 index 00000000..2f3bc042 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:c8d664b0e728d52f57eeb98ed1899c16d3b265f02ddfb41303d7a16c31e0b0f1/grype@v0.7.0/2023-02-16T20:13:50+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c5e2a743e24e6b03fa5ee8dff79c7476c50ad8423c429ffc914e9d3457549fdb +size 1115683 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:c8d664b0e728d52f57eeb98ed1899c16d3b265f02ddfb41303d7a16c31e0b0f1/grype@v0.7.0/2023-02-16T20:13:50+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:c8d664b0e728d52f57eeb98ed1899c16d3b265f02ddfb41303d7a16c31e0b0f1/grype@v0.7.0/2023-02-16T20:13:50+00:00/metadata.json new file mode 100644 index 00000000..2b515178 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:c8d664b0e728d52f57eeb98ed1899c16d3b265f02ddfb41303d7a16c31e0b0f1/grype@v0.7.0/2023-02-16T20:13:50+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bd892769590e5cfeedd1787edd8f38b872249526dc8ca7f7ac407e79fceb9e7e +size 623 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:cf742eca189b02902a0a7926ac3fbb423e799937bf4358b0d2acc6cc36ab82aa/grype@v0.12.1/2023-02-16T20:15:49+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:cf742eca189b02902a0a7926ac3fbb423e799937bf4358b0d2acc6cc36ab82aa/grype@v0.12.1/2023-02-16T20:15:49+00:00/data.json new file mode 100644 index 00000000..187a48bc --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:cf742eca189b02902a0a7926ac3fbb423e799937bf4358b0d2acc6cc36ab82aa/grype@v0.12.1/2023-02-16T20:15:49+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ddf6420fa44fa58f1bd29c35c10665126358fe27851d89b48b178f4fbe8f6327 +size 1020237 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:cf742eca189b02902a0a7926ac3fbb423e799937bf4358b0d2acc6cc36ab82aa/grype@v0.12.1/2023-02-16T20:15:49+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:cf742eca189b02902a0a7926ac3fbb423e799937bf4358b0d2acc6cc36ab82aa/grype@v0.12.1/2023-02-16T20:15:49+00:00/metadata.json new file mode 100644 index 00000000..c02cbdcb --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:cf742eca189b02902a0a7926ac3fbb423e799937bf4358b0d2acc6cc36ab82aa/grype@v0.12.1/2023-02-16T20:15:49+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:59b19110d21543d44fb06baeca249d0eca60f31e00da0603476c2b2f928b4860 +size 883 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:cf742eca189b02902a0a7926ac3fbb423e799937bf4358b0d2acc6cc36ab82aa/grype@v0.40.1/2023-02-16T20:18:52+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:cf742eca189b02902a0a7926ac3fbb423e799937bf4358b0d2acc6cc36ab82aa/grype@v0.40.1/2023-02-16T20:18:52+00:00/data.json new file mode 100644 index 00000000..167b3db9 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:cf742eca189b02902a0a7926ac3fbb423e799937bf4358b0d2acc6cc36ab82aa/grype@v0.40.1/2023-02-16T20:18:52+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:25b9e1916776d45d132f4fd03dd507c1a5c649f679b9faf2b76100ee77c4fa19 +size 2166948 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:cf742eca189b02902a0a7926ac3fbb423e799937bf4358b0d2acc6cc36ab82aa/grype@v0.40.1/2023-02-16T20:18:52+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:cf742eca189b02902a0a7926ac3fbb423e799937bf4358b0d2acc6cc36ab82aa/grype@v0.40.1/2023-02-16T20:18:52+00:00/metadata.json new file mode 100644 index 00000000..94b35df4 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:cf742eca189b02902a0a7926ac3fbb423e799937bf4358b0d2acc6cc36ab82aa/grype@v0.40.1/2023-02-16T20:18:52+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a8aa45643f44fcef32814cea94e3993d5e0232b5ccb6889098fb61d2e9841ae5 +size 883 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:cf742eca189b02902a0a7926ac3fbb423e799937bf4358b0d2acc6cc36ab82aa/grype@v0.50.2/2023-02-16T20:22:01+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:cf742eca189b02902a0a7926ac3fbb423e799937bf4358b0d2acc6cc36ab82aa/grype@v0.50.2/2023-02-16T20:22:01+00:00/data.json new file mode 100644 index 00000000..6ff8dc6c --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:cf742eca189b02902a0a7926ac3fbb423e799937bf4358b0d2acc6cc36ab82aa/grype@v0.50.2/2023-02-16T20:22:01+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a1c410f00f92130a9515073b96c8b10bb82500b8085ea1472607726e8bcda80e +size 2180881 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:cf742eca189b02902a0a7926ac3fbb423e799937bf4358b0d2acc6cc36ab82aa/grype@v0.50.2/2023-02-16T20:22:01+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:cf742eca189b02902a0a7926ac3fbb423e799937bf4358b0d2acc6cc36ab82aa/grype@v0.50.2/2023-02-16T20:22:01+00:00/metadata.json new file mode 100644 index 00000000..7eb0c973 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:cf742eca189b02902a0a7926ac3fbb423e799937bf4358b0d2acc6cc36ab82aa/grype@v0.50.2/2023-02-16T20:22:01+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:68212657894167c0eb4e7f4d649e4bbedc42af1d9a7ad93236cf51cd639064a2 +size 881 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:cf742eca189b02902a0a7926ac3fbb423e799937bf4358b0d2acc6cc36ab82aa/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:17+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:cf742eca189b02902a0a7926ac3fbb423e799937bf4358b0d2acc6cc36ab82aa/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:17+00:00/data.json new file mode 100644 index 00000000..86912dc4 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:cf742eca189b02902a0a7926ac3fbb423e799937bf4358b0d2acc6cc36ab82aa/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:17+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4f9da948ceaed2064719b43d249cfc0d2063aafcf4d317cad5599a1592b91e2e +size 2295793 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:cf742eca189b02902a0a7926ac3fbb423e799937bf4358b0d2acc6cc36ab82aa/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:17+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:cf742eca189b02902a0a7926ac3fbb423e799937bf4358b0d2acc6cc36ab82aa/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:17+00:00/metadata.json new file mode 100644 index 00000000..bc993368 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:cf742eca189b02902a0a7926ac3fbb423e799937bf4358b0d2acc6cc36ab82aa/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:17+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a54bdbd1a61b25501814941e6f9d2666a56ca32e281bb0a291112b88aa9b0dd2 +size 962 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:cf742eca189b02902a0a7926ac3fbb423e799937bf4358b0d2acc6cc36ab82aa/grype@v0.7.0/2023-02-16T20:10:12+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:cf742eca189b02902a0a7926ac3fbb423e799937bf4358b0d2acc6cc36ab82aa/grype@v0.7.0/2023-02-16T20:10:12+00:00/data.json new file mode 100644 index 00000000..1b50428e --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:cf742eca189b02902a0a7926ac3fbb423e799937bf4358b0d2acc6cc36ab82aa/grype@v0.7.0/2023-02-16T20:10:12+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1c2a4a31601def06787f79867352b650fcc8b4fb07f4cf0c0f514c2c7c7cedbc +size 1201153 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:cf742eca189b02902a0a7926ac3fbb423e799937bf4358b0d2acc6cc36ab82aa/grype@v0.7.0/2023-02-16T20:10:12+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:cf742eca189b02902a0a7926ac3fbb423e799937bf4358b0d2acc6cc36ab82aa/grype@v0.7.0/2023-02-16T20:10:12+00:00/metadata.json new file mode 100644 index 00000000..5bfd73c9 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:cf742eca189b02902a0a7926ac3fbb423e799937bf4358b0d2acc6cc36ab82aa/grype@v0.7.0/2023-02-16T20:10:12+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4c947b67bcfe99b56876075475d82172d7bd63c1defc6ba0b26af7ea83b80643 +size 628 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:fe242a3a63699425317fba0a749253bceb700fb3d63e7a0f6497f53a587e38c5/grype@v0.12.1/2023-02-16T20:15:42+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:fe242a3a63699425317fba0a749253bceb700fb3d63e7a0f6497f53a587e38c5/grype@v0.12.1/2023-02-16T20:15:42+00:00/data.json new file mode 100644 index 00000000..97061c84 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:fe242a3a63699425317fba0a749253bceb700fb3d63e7a0f6497f53a587e38c5/grype@v0.12.1/2023-02-16T20:15:42+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d138b59fe5ac7d4b7b4ec91a77d7a2add6d178582be101728935e0c8524d9cde +size 215129 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:fe242a3a63699425317fba0a749253bceb700fb3d63e7a0f6497f53a587e38c5/grype@v0.12.1/2023-02-16T20:15:42+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:fe242a3a63699425317fba0a749253bceb700fb3d63e7a0f6497f53a587e38c5/grype@v0.12.1/2023-02-16T20:15:42+00:00/metadata.json new file mode 100644 index 00000000..28ae93fc --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:fe242a3a63699425317fba0a749253bceb700fb3d63e7a0f6497f53a587e38c5/grype@v0.12.1/2023-02-16T20:15:42+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:141b9dda0f156cdb625ff6f70a469b5098a57ad0678876867e39252df89226d2 +size 881 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:fe242a3a63699425317fba0a749253bceb700fb3d63e7a0f6497f53a587e38c5/grype@v0.40.1/2023-02-16T20:18:45+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:fe242a3a63699425317fba0a749253bceb700fb3d63e7a0f6497f53a587e38c5/grype@v0.40.1/2023-02-16T20:18:45+00:00/data.json new file mode 100644 index 00000000..52bd5bdf --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:fe242a3a63699425317fba0a749253bceb700fb3d63e7a0f6497f53a587e38c5/grype@v0.40.1/2023-02-16T20:18:45+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c7055dfa957839c73267672ecedf78d47300f5703b0305909a1ba464f20c225f +size 567962 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:fe242a3a63699425317fba0a749253bceb700fb3d63e7a0f6497f53a587e38c5/grype@v0.40.1/2023-02-16T20:18:45+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:fe242a3a63699425317fba0a749253bceb700fb3d63e7a0f6497f53a587e38c5/grype@v0.40.1/2023-02-16T20:18:45+00:00/metadata.json new file mode 100644 index 00000000..3464abc8 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:fe242a3a63699425317fba0a749253bceb700fb3d63e7a0f6497f53a587e38c5/grype@v0.40.1/2023-02-16T20:18:45+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3a0bb7b3fee7f6c58c67fe7a9d104ffa572737b0e4a4944b5d0a34083431ff09 +size 881 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:fe242a3a63699425317fba0a749253bceb700fb3d63e7a0f6497f53a587e38c5/grype@v0.50.2/2023-02-16T20:21:55+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:fe242a3a63699425317fba0a749253bceb700fb3d63e7a0f6497f53a587e38c5/grype@v0.50.2/2023-02-16T20:21:55+00:00/data.json new file mode 100644 index 00000000..4849f9c9 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:fe242a3a63699425317fba0a749253bceb700fb3d63e7a0f6497f53a587e38c5/grype@v0.50.2/2023-02-16T20:21:55+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4502fba8c5dda59f07b4f19ac5d7d94a6ce0a2c4d752687a3317a4f147abdd83 +size 571552 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:fe242a3a63699425317fba0a749253bceb700fb3d63e7a0f6497f53a587e38c5/grype@v0.50.2/2023-02-16T20:21:55+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:fe242a3a63699425317fba0a749253bceb700fb3d63e7a0f6497f53a587e38c5/grype@v0.50.2/2023-02-16T20:21:55+00:00/metadata.json new file mode 100644 index 00000000..ba3037a6 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:fe242a3a63699425317fba0a749253bceb700fb3d63e7a0f6497f53a587e38c5/grype@v0.50.2/2023-02-16T20:21:55+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f3da449c12c5f2a0f7c9813fb0c0ae3a70bae26ba970bf585181b327c374a94b +size 881 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:fe242a3a63699425317fba0a749253bceb700fb3d63e7a0f6497f53a587e38c5/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:08+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:fe242a3a63699425317fba0a749253bceb700fb3d63e7a0f6497f53a587e38c5/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:08+00:00/data.json new file mode 100644 index 00000000..45699857 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:fe242a3a63699425317fba0a749253bceb700fb3d63e7a0f6497f53a587e38c5/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:08+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:68091cb99a2df908407cb0667614d51aeca0245009c42e74ec9c45a777c8917d +size 599921 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:fe242a3a63699425317fba0a749253bceb700fb3d63e7a0f6497f53a587e38c5/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:08+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:fe242a3a63699425317fba0a749253bceb700fb3d63e7a0f6497f53a587e38c5/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:08+00:00/metadata.json new file mode 100644 index 00000000..378ed910 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:fe242a3a63699425317fba0a749253bceb700fb3d63e7a0f6497f53a587e38c5/grype@v0.55.0-1-g3a8b2e3/2023-02-16T20:25:08+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0544df2b36c05d29676022c492970fffe6deb14603d822a7858707249b74e183 +size 960 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:fe242a3a63699425317fba0a749253bceb700fb3d63e7a0f6497f53a587e38c5/grype@v0.7.0/2023-02-16T20:09:59+00:00/data.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:fe242a3a63699425317fba0a749253bceb700fb3d63e7a0f6497f53a587e38c5/grype@v0.7.0/2023-02-16T20:09:59+00:00/data.json new file mode 100644 index 00000000..d6db3b8c --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:fe242a3a63699425317fba0a749253bceb700fb3d63e7a0f6497f53a587e38c5/grype@v0.7.0/2023-02-16T20:09:59+00:00/data.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:96cdd0b16f651fd0497bb131e09214bf8419a34943e27bda0e15a296438f7949 +size 100176 diff --git a/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:fe242a3a63699425317fba0a749253bceb700fb3d63e7a0f6497f53a587e38c5/grype@v0.7.0/2023-02-16T20:09:59+00:00/metadata.json b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:fe242a3a63699425317fba0a749253bceb700fb3d63e7a0f6497f53a587e38c5/grype@v0.7.0/2023-02-16T20:09:59+00:00/metadata.json new file mode 100644 index 00000000..feeaf9f0 --- /dev/null +++ b/test/acceptance/test-fixtures/result/store/anchore+test_images@sha256:fe242a3a63699425317fba0a749253bceb700fb3d63e7a0f6497f53a587e38c5/grype@v0.7.0/2023-02-16T20:09:59+00:00/metadata.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:abed88a1e575432a067e9e3e19e281fd7d988feabbbaf1bd44dcce0aa2eac1a3 +size 626